Appearance
Writing Plugins
This guide walks through creating a custom Generative DOM plugin from scratch.
Plugin Structure
Every plugin is a factory function that returns an GenerativeDomPlugin object:
ts
import type { GenerativeDomPlugin, Token, RenderContext, BlockMatch, PluginContext } from '@generative-dom/core';
export function myPlugin(): GenerativeDomPlugin {
return {
name: 'my-plugin',
priority: 150,
render(token: Token, ctx: RenderContext): HTMLElement | Text | null {
// Required: render a token into a DOM node
return null;
},
};
}The name must be unique across all registered plugins. The priority determines matching order (lower = matched first).
Step 1: Define the Syntax
Decide what your plugin will match. For example, a callout box:
markdown
!!! note
This is a callout.
!!!Step 2: Implement matchBlock
The matchBlock method receives the buffer and a position. Return a BlockMatch if your plugin can handle the content at that position, or null to pass:
ts
matchBlock(buffer: string, pos: number): BlockMatch | null {
const rest = buffer.slice(pos);
const match = rest.match(/^!!! (\w+)\n([\s\S]*?)\n!!!\n/);
if (!match) return null;
return {
type: 'callout',
raw: match[0],
consumed: match[0].length,
content: match[2],
meta: { kind: match[1] },
};
}Key rules:
consumedmust exactly match how many characters the core should advancerawis used for AST diffing -- if it has not changed, the DOM node is reused- Return
nullif the content might be incomplete (waiting for more stream input)
Step 3: Implement render
The render method turns a token into a DOM node. Always use the render context's helpers:
ts
render(token: Token, ctx: RenderContext): HTMLElement {
const container = ctx.createElement('div');
container.className = `callout callout-${token.meta?.kind ?? 'note'}`;
const label = ctx.createElement('strong');
label.textContent = String(token.meta?.kind ?? 'Note');
container.appendChild(label);
const body = ctx.createElement('p');
// Use renderInline for content that may contain bold, italic, etc.
body.appendChild(ctx.renderInline(token.content ?? ''));
container.appendChild(body);
return container;
}Rules:
- Always use
ctx.createElement(pool-aware) instead ofdocument.createElement - Always use
ctx.createTextfor text nodes - Never use
innerHTMLorinsertAdjacentHTML - Use
ctx.renderInlineif inner content should support inline formatting
Step 4: Implement cleanup (Optional)
If your plugin creates event listeners or holds references, clean them up when the element is recycled:
ts
cleanup(element: HTMLElement): void {
// Remove any custom event listeners or references
element.removeEventListener('click', this.handleClick);
}Step 5: Implement init and destroy (Optional)
Use init for one-time setup and destroy for plugin-level cleanup:
ts
init(ctx: PluginContext): void {
// Store context for later use
this.ctx = ctx;
// Register a cleanup callback for destroy
ctx.onDestroy(() => {
// Clean up intervals, observers, etc.
});
// Check for other plugins
const codePlugin = ctx.getPlugin('markdown-code');
if (codePlugin) {
// Adjust behavior based on other plugins
}
}
destroy(): void {
// Release plugin-level resources
}Step 6: Implement matchInline (Optional)
For inline-level plugins, implement matchInline instead of (or in addition to) matchBlock:
ts
matchInline(text: string, pos: number): InlineMatch | null {
const rest = text.slice(pos);
const match = rest.match(/^==(.+?)==/);
if (!match) return null;
return {
type: 'highlight-text',
raw: match[0],
consumed: match[0].length,
content: match[1],
};
}Complete Example
A plugin that renders ==highlighted text== as <mark>:
ts
import type { GenerativeDomPlugin, Token, RenderContext, InlineMatch } from '@generative-dom/core';
export function markHighlight(): GenerativeDomPlugin {
return {
name: 'mark-highlight',
priority: 210,
matchInline(text: string, pos: number): InlineMatch | null {
const rest = text.slice(pos);
const match = rest.match(/^==(.+?)==/);
if (!match) return null;
return {
type: 'mark-highlight',
raw: match[0],
consumed: match[0].length,
content: match[1],
};
},
render(token: Token, ctx: RenderContext): HTMLElement {
const mark = ctx.createElement('mark');
mark.appendChild(ctx.renderInline(token.content ?? ''));
return mark;
},
};
}Usage:
ts
const md = new GenerativeDom({
container: el,
plugins: [markdownBase(), markdownInline(), markHighlight()],
});
md.push('This is ==highlighted== text.');
md.flush();Tips
- Choose a priority that makes sense for your syntax. Block-level custom syntax should be in the 0--99 range. Inline extensions should be near 200.
- Always handle the case where the stream might be incomplete. If you are not sure you have the full syntax, return
nulland wait for more input. - Use
metato pass extra data from match to render without modifying the token structure. - Test your plugin with single-character streaming to verify it handles all split points.