Appearance
9. Extension Mechanism
9.1 Overview
MdFlow's parser and renderer are built from plugins. Every block kind, every inline kind, every custom-element tag, and every event type is provided by a plugin. The core defines no syntax directly; it orchestrates plugins according to the rules in this chapter.
A conforming Extension host (§2.3) MUST implement the plugin contract specified here.
9.2 Plugin shape
A plugin is a value satisfying the following abstract shape:
Plugin {
name: string, // globally unique identifier
version: string, // SemVer
priority: integer, // ordering within its phase
phase: 'block' | 'inline' | 'custom-element' | 'event',
blockTags?: string[], // for custom-element and event phases
inlineTags?: string[],
eventTags?: string[],
allowedChildren?: Kind[], // inline-kind restriction for blocks
tokenize?: (ctx, cursor) → MatchResult,
render?: (ctx, token, container) → void,
finalize?: (ctx, token) → void
}Fields not marked with ? are REQUIRED. The tokenize, render, and finalize callbacks are plugin-phase-dependent.
The exact names of fields are implementation-defined; this specification constrains the semantics, not the TypeScript signature.
9.3 Priority semantics
Within a phase, plugins are sorted by priority descending. For each parse position, the implementation attempts plugins in that order; the first to return a successful MatchResult wins.
- Ties are broken by plugin registration order (earlier wins).
- Core-bundled plugin priorities are listed in §6 and §7.
- Third-party plugins SHOULD choose priorities in the band 10–90 for block/inline constructs to avoid collision with core. Priorities above 200 are RESERVED for future core use.
9.4 Match result
A plugin's tokenize callback returns one of:
- Match.
{ kind, slice, subTokens? }— a tokenization that consumesslice.end - ctx.cursorbytes. - No match. A sentinel (e.g.,
null). The next plugin is tried. - Pending.
{ kind, slice, state: 'pending' }— the plugin has started a token but needs more source bytes. The implementation MUST retry the plugin after the nextpush()or atflush().
A Pending result commits the plugin: no other plugin at this position is tried until the pending token transitions to complete or is abandoned (via the plugin's own logic).
9.5 Plugin context
Plugins receive a context ctx exposing:
ctx.source— a read-only view of the current buffer (bytes, character-at, slice).ctx.cursor— the current parse position.ctx.createElement(name, attrs?)— DOM element factory (safe; forbidden tags throw).ctx.createText(str)— DOM text node factory (equivalent tocreateTextNode).ctx.inlineParse(range)— recursively parse inline content in the given source range.ctx.emit(event)— emit an event record (§12.1).ctx.log(level, message)— diagnostic sink.
Plugins MUST use ctx.createElement and ctx.createText rather than calling document.createElement directly, so the context can enforce the security model and attach ownership tags.
9.6 Plugin safety review
Every plugin, on registration, MUST be reviewed against the security model:
- It MUST NOT call
innerHTML-family APIs. - It MUST NOT register
on*attributes. - It MUST pass URL-valued attributes through
ctx.filterURL(...)(or the equivalent API) before attaching to DOM. - Its produced DOM MUST be constructible via the safe factories in §9.5.
The implementation's test suite MUST include a plugin safety audit that runs each registered plugin against the XSS-vector corpus (§13.4) and fails registration if any vector produces unsafe output.
9.7 Plugin lifecycle
register → initialize → (tokenize | render | finalize)* → dispose- register. The plugin is added to the relevant phase list in priority order. No parse activity yet.
- initialize. Called once per
MdFlowinstance, before the firstpush(). Plugin MAY allocate instance-local state. - tokenize / render / finalize. Called per parse event.
- dispose. Called when the
MdFlowinstance is destroyed. Plugin MUST release instance-local state.
9.8 Allowed-children constraint
A block plugin MAY declare allowedChildren — a set of inline kinds its block's inline parser is permitted to produce. The inline parser MUST skip plugins producing kinds not in the set.
For example, Heading.allowedChildren excludes LineBreak(hard=true): hard breaks inside headings become soft breaks or are dropped.
9.9 Plugin errors
Plugin errors are handled per §11. A plugin MUST NOT silently return malformed state; it SHOULD throw a descriptive error that the implementation can route to the error observer.
9.10 Plugin versioning
Plugin version follows SemVer. The MdFlow core declares a compatible range (e.g., ^1.0); plugins outside the range SHOULD refuse to load. Cross-plugin compatibility is NOT specified — extension authors are responsible for coordinating.
9.11 Multi-plugin coordination
When multiple plugins produce elements that interact (e.g., a syntax highlighter and a code-block renderer), the implementation MUST apply them in priority order. The first plugin may produce tokens that later plugins transform or extend via the finalize hook.
The core specification does not mandate a specific extension protocol beyond priority ordering and the shared context. Extension authors MAY define peer protocols atop this substrate.
9.12 Extension specifications
Each extension is a document in the spec/extensions/ directory that declares:
- Extension name and version.
- Dependencies (other extensions, core range).
- New tag names and whether they are block / inline / event.
- New block or inline kinds (if any).
- New attribute names and their value grammars.
- Security-relevant semantics (URL attributes, content model restrictions).
- Test vectors, added to the conformance suite under the extension's namespace.
The core specification does not define any specific extension in this document; extensions are separately authored.
9.13 Non-conforming plugins
A plugin that violates §9.6 MUST cause registration to fail. A plugin that throws during initialize MUST cause the MdFlow instance to fail construction. Other plugin errors are caught per §11.