Skip to content

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 consumes slice.end - ctx.cursor bytes.
  • 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 next push() or at flush().

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 to createTextNode).
  • 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 MdFlow instance, before the first push(). Plugin MAY allocate instance-local state.
  • tokenize / render / finalize. Called per parse event.
  • dispose. Called when the MdFlow instance 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.