zfb-md-wasm
Reference for @takazudo/zfb-md-wasm — the WebAssembly build of zfb's markdown/MDX pipeline. Exports, async signatures, option/result types (ZfbMdWasmOptions, ParseDialect, PipelineOptions, Diagnostic, ParseToAstOptions, HighlightCodeOptions, HighlightRole), the raw mdast shape, and package entry points.
Overview
@takazudo/zfb-md-wasm ships zfb's markdown/MDX → JS/HTML pipeline compiled to WebAssembly. This page is the signature/type reference. For the narrative walkthrough — installing the package, evaluating a compiled module in a browser, the JSX-runtime import-map setup, and the parity guarantee's limitations — see Browser Markdown Preview.
This package is unrelated to importing a .wasm module into an SSR route at build time — that is a separate feature covered in Importing Wasm.
Exports
| Export | Signature | Description |
|---|---|---|
init | () => Promise<void> | Eagerly instantiate the wasm module. Optional — every other function initializes the module lazily on first use. |
compile | (source: string, options?: ZfbMdWasmOptions) => Promise<CompileResult> | MDX → JSX → SWC → ES-module JS. |
renderHtml | (source: string, options?: ZfbMdWasmOptions) => Promise<RenderHtmlResult> | Markdown → mdast → visitors → hast → HTML string. |
parseToAst | (source: string, options?: ParseToAstOptions) => Promise<ParseToAstResult> | Markdown/MDX → raw mdast tree, pre-zfb-visitors. |
highlightCode | (code: string, options: HighlightCodeOptions) => Promise<HighlightCodeResult> | Direct semantic-class syntax highlighting for an arbitrary source string. |
version | () => Promise<string> | Release-stamped package version string. |
toMdastRoot | (ast: MdastRoot | null) => Root | Validates and converts a raw parseToAst tree into a strict, ecosystem-compatible mdast Root. The one synchronous export. |
ZfbMdWasmTrapError | class extends Error | Thrown when a wasm trap (Rust panic / internal fault) occurred; the instance is auto-reinstantiated first. |
ZfbMdWasmTrapRecoveryLimitError | class extends Error | Thrown once the bounded trap-recovery budget (16 recoveries) is exhausted. |
MdastAdapterError | class extends TypeError | Thrown by toMdastRoot for an unsupported or malformed node. |
init, compile, renderHtml, parseToAst, highlightCode, and version are all async — every call returns a Promise, whether or not the underlying wasm module is already instantiated.
Note
Expected failures never throw. Markdown parse errors, malformed options JSON, unknown syntect theme names, and similar problems all come back as a diagnostics entry in the resolved result — never as a thrown error or a rejected Promise. Only a genuine wasm trap rejects, with ZfbMdWasmTrapError or ZfbMdWasmTrapRecoveryLimitError (see Error classes).
init()
function init(): Promise<void>Eagerly instantiate the wasm module. Calling it up front only controls when the fetch/compile cost happens — every other function initializes the module lazily on first use if init() was never called.
compile(source, options?)
function compile(source: string, options?: ZfbMdWasmOptions): Promise<CompileResult>MDX → JSX → SWC → ES-module JS, the same emitter zfb's own build uses. options defaults to {}. jsxRuntime and development are consumed only here — renderHtml accepts and silently ignores both. Conversely, dialect is consumed only by renderHtml; compile accepts and ignores it, so one options object can serve either call.
renderHtml(source, options?)
function renderHtml(source: string, options?: ZfbMdWasmOptions): Promise<RenderHtmlResult>Markdown/MDX → mdast → visitors → hast → HTML string, skipping SWC entirely. options defaults to {}. Without an explicit dialect, a .md filename selects CommonMark and .mdx selects MDX; omitting filename uses "<anonymous>.md", hence CommonMark. dialect: "markdown" | "mdx" overrides either valid extension without waiving the extension gate.
parseToAst(source, options?)
function parseToAst(source: string, options?: ParseToAstOptions): Promise<ParseToAstResult>Markdown/MDX → raw mdast tree. Takes the distinct, closed ParseToAstOptions document, not ZfbMdWasmOptions — its pipeline sub-object only accepts gfm (see Validation and position contract). options defaults to {}.
highlightCode(code, options)
function highlightCode(code: string, options: HighlightCodeOptions): Promise<HighlightCodeResult>Direct semantic-class syntax highlighting for an arbitrary source string — no Markdown fence involved, and no options default: language is required.
version()
function version(): Promise<string>Release-stamped package version string. Published artifacts stamp this with the package semver during release; local development builds fall back to the Rust manifest version placeholder.
toMdastRoot(ast)
function toMdastRoot(ast: MdastRoot | null): Root // ecosystem "mdast" Root (@types/mdast)Validates a raw parseToAst().ast tree and returns a detached, @types/mdast-compatible Root, typed against the same mdast/mdast-util-directive/mdast-util-mdx content-model registries the ecosystem uses. This strict adapter is intentionally separate from the forward-compatible raw tier parseToAst returns: an unsupported or malformed node throws MdastAdapterError instead of being silently dropped or coerced. Synchronous — it does not touch the wasm boundary at all.
Error classes
ZfbMdWasmTrapError extends Error— thrown by the async call that observed a wasm trap. Constructed with(cause: unknown), exposed via.cause. The trapped instance is dropped and a fresh one started from the cached compiledWebAssembly.Modulebefore this error is thrown, so the very next call already uses the replacement — no caches or globals survive between calls, so re-instantiation loses nothing but time.ZfbMdWasmTrapRecoveryLimitError extends Error— thrown instead ofZfbMdWasmTrapErroronce 16 automatic trap recoveries have been exhausted for the process. Constructed with(maxRecoveries: number, cause: unknown),causeexposed via.cause. Further recovery stays disabled (to avoid unbounded ES module record growth) until the JS realm is reloaded.MdastAdapterError extends TypeError— thrown bytoMdastRoot. Carriespath: string(a JSON-path-like pointer into the offending node, e.g."$.children[2].value") andnodeType: string | null(the node's observedtype, ornullwhen the value wasn't even a typed record).
Type reference
ZfbMdWasmOptions
The options document shared by compile and renderHtml. Every field is optional; {} selects all defaults.
| Field | Type | Description |
|---|---|---|
filename? | string | Must end in .md or .mdx. Defaults to "<anonymous>.mdx" for compile, "<anonymous>.md" for renderHtml. |
dialect? | "markdown" | "mdx" | Consumed only by renderHtml; overrides filename inference. compile accepts and ignores it. |
jsxRuntime? | "preact" | "react" | Consumed only by compile; renderHtml accepts and ignores it. |
development? | boolean | Consumed only by compile; renderHtml accepts and ignores it. |
pipeline? | PipelineOptions | Shared pipeline configuration. |
PipelineOptions
Mirrors zfb_content::facade::PipelineOptions verbatim.
| Field | Type | Description |
|---|---|---|
theme? | string | null | A syntect theme name. Absent or explicit null keeps the built-in default ("base16-ocean.dark") — fenced code is always highlighted; there is no "no syntax highlighting" value. Mutually exclusive with codeHighlight.mode: "class". |
gfm? | GfmOptions | Per-extension GFM toggles. |
cjkFriendly? | boolean | — |
hardBreaks? | boolean | — |
codeHighlight? | CodeHighlightOptions | null | Output mode + class-mode knobs for fenced-code highlighting. Absent, null, or { mode: "inline" } reproduce the pre-existing inline-color behavior byte-for-byte. |
features? | MarkdownFeaturesConfig | Record<string, unknown> — left open on the TypeScript side; the wasm boundary passes it through verbatim to Rust's own deny_unknown_fields deserializer, which is the authoritative validator for its keys. |
GfmOptions
| Field | Type |
|---|---|
strikethrough? | boolean |
table? | boolean |
autolinkLiteral? | boolean |
taskListItem? | boolean |
footnoteDefinition? | boolean |
CodeHighlightOptions
zfb_content::facade::CodeHighlightOptions, verbatim.
| Field | Type | Description |
|---|---|---|
mode? | "inline" | "class" | Output mode for fenced-code highlighting. Defaults to "inline". |
classPrefix? | string | Class-name prefix for class-mode role classes (e.g. default "hi-" yields hi-kw, hi-str, …). Only meaningful when mode is "class". |
roleClasses? | Partial<Record<HighlightRole, string>> | null | Per-role class overrides for class mode. Absent or null uses {classPrefix}{role} for every role. Only meaningful when mode is "class". |
CompileResult / RenderHtmlResult
| Field | Type | Description |
|---|---|---|
code (CompileResult) / html (RenderHtmlResult) | string | null | ES-module JS source, or HTML fragment, on success; null on failure. |
frontmatter | unknown | Parsed YAML frontmatter as JSON, null when absent or unextractable. |
diagnostics | Diagnostic[] | Empty on success. |
Diagnostic
One diagnostic entry. line/column are the sole supported diagnostic location and are 1-based.
| Field | Type | Description |
|---|---|---|
severity | "error" | — |
source | "options" | "frontmatter" | "markdown" | "compile" | — |
message | string | Opaque display text from this package or an upstream dependency. Do not parse or rewrite it — embedded coordinates, when present, use the dependency's own coordinate space. Use structured line/column instead. |
line | number | null | null when the underlying error carries no location. For "markdown"/"frontmatter" it points into the original source; for "options" it points into the options JSON document. |
column | number | null | Same location rules as line. For "markdown"/"frontmatter" this is JavaScript UTF-16 code units — see Validation and position contract. |
ParseToAstOptions
The distinct, closed options document consumed by parseToAst. Visitor/serializer options are not accepted.
| Field | Type | Description |
|---|---|---|
filename? | string | Must end in .md or .mdx. With no explicit dialect, .md selects CommonMark and .mdx selects MDX; omitting filename uses "<anonymous>.mdx", hence MDX. |
dialect? | "markdown" | "mdx" | Overrides either valid extension but does not waive the extension gate. |
directives? | boolean | Parse generic remark-directive syntax. Default false. |
frontmatter? | "extract" | "node" | "none" | YAML handling policy. Default "extract". "extract" parses the stripped body and returns YAML as JSON (no YAML node). "node" parses the full logical source and returns JSON plus a canonical YAML node. "none" parses every logical-source byte as Markdown/MDX and always returns null. Malformed/unterminated YAML fails "extract"/"node" with one frontmatter diagnostic; "none" never produces one. |
pipeline? | ParsePipelineOptions | { gfm?: GfmOptions } — the only pipeline knob parseToAst accepts; theme, cjkFriendly, hardBreaks, codeHighlight, and features are not part of this closed document. |
ParseToAstResult
| Field | Type | Description |
|---|---|---|
ast | MdastRoot | null | Serialized raw mdast root on success, null on failure. |
frontmatter | unknown | Same contract as CompileResult.frontmatter. |
diagnostics | Diagnostic[] | Empty on success. |
HighlightCodeOptions
Options for direct arbitrary-code semantic class highlighting.
| Field | Type | Description |
|---|---|---|
language | string | Required syntax token, for example "html", "css", or "javascript". |
mode? | "class" | The only supported direct output mode. Defaults to "class". |
classPrefix? | string | Semantic role class prefix. Defaults to "hi-". |
roleClasses? | Partial<Record<HighlightRole, string>> | Full-name role overrides, for example { keyword: "text-violet-600" }. |
HighlightCodeResult and HighlightDiagnostic
| Field | Type | Description |
|---|---|---|
html | string | null | Complete semantic <pre><code> wrapper, or null for invalid options/internal errors. |
diagnostics | HighlightDiagnostic[] | Sources used by direct highlighting are a distinct, smaller set than Diagnostic. |
HighlightDiagnostic fields: severity: "error" | "warning", source: "options" | "highlight" | "internal", message: string, line: number | null (always null — JSON option parse locations are 1-based but this call never reports one), column: number | null (same).
Invalid options (missing/empty language, an unsupported mode, a bad classPrefix, an unknown role key, or extra fields) return { html: null, diagnostics: [{ severity: "error", source: "options", … }] }. An unknown but non-empty language is different: it succeeds with escaped fallback markup and a { severity: "warning", source: "highlight" } diagnostic.
HighlightRole
The fixed 18-role semantic taxonomy highlightCode emits (mechanically checked against Rust's canonical HiRole::FULL_NAMES):
| Role | Default class | Role | Default class |
|---|---|---|---|
"escape" | hi-esc | "variable" | hi-var |
"operator" | hi-op | "tag" | hi-tag |
"comment" | hi-com | "attribute" | hi-attr |
"string" | hi-str | "punctuation" | hi-punct |
"number" | hi-num | "inserted" | hi-ins |
"constant" | hi-const | "deleted" | hi-del |
"keyword" | hi-kw | "heading" | hi-hd |
"function" | hi-fn | "type" | hi-ty |
"namespace" | hi-ns | "property" | hi-prop |
The default class for each role is ${classPrefix}${suffix} (default classPrefix is "hi-"); roleClasses overrides use the full name as the key (keyword), never the class suffix (kw).
Raw mdast shape (parseToAst / toMdastRoot)
parseToAst's ast field is a serialized raw markdown-rs mdast tree (MdastRoot, always a root-typed Root node when present) — the parser's own node shape converted through its serde representation into an open, unist-shaped carrier, and deliberately pre-zfb-visitors. Every node in the tree always carries a position: { start: AstPosition; end: AstPosition }; AstPoint's column and offset (0-based) are JavaScript UTF-16 code units, matching String.prototype.slice, mdast-util-to-hast, and remark/unist's own convention — not Unicode scalar values, so a surrogate-pair character (most emoji) advances column/offset by 2, not 1.
The full core-mdast and MDX node set (Root, Paragraph, Heading, List, Code, Link, MdxJsxFlowElement, …) plus a forward-compatible UnknownMdastNode catch-all are all re-exported as named types from the package for callers who want to narrow on node.type. Two documented divergences from remark-parse/remark-mdx: mdxJsxAttribute (and its value/expression-attribute siblings) carry no position — markdown-rs does not model attribute positions; and top-level MDX import/export degrade to paragraphs (no mdxjsEsm nodes, no estree data) since the wasm boundary hosts no JS/acorn parser. _markdownRsStops (on MDX expression/ESM nodes) is internal, unstable, markdown-rs re-parse bookkeeping and — unlike position — is UTF-8 byte-based; never slice a string with it.
With directives: true (see ParseToAstOptions, default false), the tree can also carry the three generic remark-directive node types, all sharing the same DirectiveNodeBase shape (position, name, attributes, children):
containerDirective— block form,:::nameleafDirective— block form,::nametextDirective— inline form,:name
DirectiveNodeBase's name is the directive's name string, and attributes is a Record<string, string> — a boolean attribute is represented as an empty string, never true or an omitted key. With directives left at its default false, directive-looking text survives exactly as markdown-rs parsed it and none of these three node types ever appear.
toMdastRoot (see above) converts this raw tier into a strict, ecosystem-compatible tree for consumers that want ordinary mdast/unist-util-visit tooling instead of the raw carrier.
Validation and position contract
Unknown fields are rejected, on the Rust side,
deny_unknown_fields-style, at both nesting levels ofZfbMdWasmOptions(top level andpipeline).ParseToAstOptionsis a separate, stricter closed document: itspipelinesub-object accepts onlygfm— passingtheme,cjkFriendly,hardBreaks,codeHighlight, orfeaturesthere is rejected, even though those are validPipelineOptionsfields forcompile/renderHtml.Diagnostic.line/.column(fromcompile/renderHtml) andAstPoint.column/.offset(fromparseToAst) share the same JavaScript UTF-16 code unit convention described above. Pure-ASCII sources have byte offsets equal to UTF-16 offsets, so this only matters once a source has non-ASCII content.
Package entry points
The root (.) remains the complete compatibility API. Existing root and . imports require no migration. The additive . and . entries are isolated SWC-free graphs: they omit swc_core and zfb-render while intentionally retaining zfb-content and syntect-fancy. Parse is not syntect-free.
| Subpath | gzip-9 wasm (2.15.0) | Exact runtime values | Exact type surface | Use when |
|---|---|---|---|---|
. | 1,516,383 B | init, compile, renderHtml, parseToAst, highlightCode, version, toMdastRoot, MdastAdapterError, ZfbMdWasmTrapError, ZfbMdWasmTrapRecoveryLimitError, __forceTrapForTests, __getTrapRecoveryStateForTests | Full current compile/render/parse/raw-mdast/highlight types | Compile MDX or use several current APIs |
. | 817,951 B | init, highlightCode, version, ZfbMdWasmTrapError, ZfbMdWasmTrapRecoveryLimitError, __forceTrapForTests, __getTrapRecoveryStateForTests | HighlightRole, HighlightCodeOptions, HighlightCodeResult, HighlightDiagnostic, HighlightDiagnosticSource | Highlight code only; public API/resources stay compatible |
. | 1,091,678 B | init, renderHtml, version, ZfbMdWasmTrapError, ZfbMdWasmTrapRecoveryLimitError, __forceTrapForTests, __getTrapRecoveryStateForTests | RenderHtmlResult, Diagnostic, DiagnosticSource, ZfbMdWasmOptions, ParseDialect, PipelineOptions, GfmOptions, CodeHighlightMode, CodeHighlightOptions, MarkdownFeaturesConfig, JsxRuntime, HighlightRole | Render Markdown to HTML without compiler bytes |
. | 283,991 B | init, parseToAst, toMdastRoot, MdastAdapterError, version, ZfbMdWasmTrapError, ZfbMdWasmTrapRecoveryLimitError, __forceTrapForTests, __getTrapRecoveryStateForTests | ParseToAstResult, ParseToAstOptions, ParseDialect, FrontmatterPolicy, ParsePipelineOptions, Diagnostic, DiagnosticSource, AstPoint, AstPosition, RawMdastData, MarkdownRsStop, MdastNode, MdastRoot, UnknownMdastNode, Root, Paragraph, Heading, ThematicBreak, Blockquote, List, ListItem, Html, Code, Definition, Text, DirectiveNodeBase, ContainerDirective, LeafDirective, TextDirective, Emphasis, Strong, InlineCode, Break, Link, Image, ReferenceKind, LinkReference, ImageReference, FootnoteDefinition, FootnoteReference, TableAlign, Table, TableRow, TableCell, Delete, Yaml, MdxFlowExpression, MdxTextExpression, MdxJsxFlowElement, MdxJsxTextElement, MdxJsxAttributeContent, MdxJsxAttribute, MdxJsxAttributeValueExpression, MdxJsxExpressionAttribute | Parse and optionally interpret an AST in controlled consumer code |
The focused resource pairs are private and non-interchangeable:
wasm-render/zfb_md_wasm_render_glue.zfb-resource.mjs
wasm-render/zfb_md_wasm_render_bg.wasm
wasm-parse/zfb_md_wasm_parse_glue.zfb-resource.mjs
wasm-parse/zfb_md_wasm_parse_bg.wasmTheir declaration sidecars stay in their matching directories. Each entry creates one independent createWasmApi state (compiled module, instance, generation, retry state, and terminal state). Importing multiple entries intentionally loads independent pairs and instances.
Direct Node and browser-aware imports use the same subpath:
import { renderHtml } from "@takazudo/zfb-md-wasm/render";
import { parseToAst, toMdastRoot } from "@takazudo/zfb-md-wasm/parse";For lazy browser loading, import from a user action. The browser condition uses static ?url edges to only the selected entry's pair:
renderButton.addEventListener("click", async () => {
const { renderHtml } = await import("@takazudo/zfb-md-wasm/render");
const { html } = await renderHtml(source, { filename: "preview.md" });
preview.innerHTML = html ?? "";
});
parseButton.addEventListener("click", async () => {
const { parseToAst, toMdastRoot } = await import("@takazudo/zfb-md-wasm/parse");
const parsed = await parseToAst(source, { filename: "preview.md" });
const root = parsed.ast === null ? null : toMdastRoot(parsed.ast);
inspect(root);
});compile is root-only and returns module source requiring host evaluation, the JSX runtime, and components. toMdastRoot is a controlled consumer-side adapter; an AST-to-React renderer may interpret the resulting data. No slim entry evaluates author JavaScript. renderHtml is not a sanitizer and raw HTML remains untrusted; MDX JSX/expression/ESM-shaped AST nodes remain inert data.
Shipped artifact sizes and locked ceilings
These are the shipped 2.15.0 artifact rows — optimized final wasm after wasm-bindgen and wasm-opt, Node gzipSync(..., { level: 9 }), and glue bytes/gzip:
| Entry/graph | final wasm | gzip-9 | glue | glue gzip-9 |
|---|---|---|---|---|
| root (full) | 3,399,954 B | 1,516,383 B | 14,998 B | 4,199 B |
| highlight | 1,539,334 B | 817,951 B | 8,758 B | 2,637 B |
| render | 2,196,095 B | 1,091,678 B | 8,772 B | 2,661 B |
| parse | 700,364 B | 283,991 B | 11,159 B | 3,797 B |
The #2447 decision snapshot found the split package layout at 3,638,607 B versus 2,314,818 B for root plus highlight, a 210 s clean production ceiling, and a selected-snapshot median of 155.015 s [153.496, 165.977]. Locked gzip-9 ceilings are root 1,600,000 B, highlight 880,000 B, render 1,100,000 B, and parse 325,000 B; the complete packed tarball ceiling is 3,900,000 B. All four ship inside their ceilings, with 83,617 B (root), 62,049 B (highlight), 8,322 B (render), and 41,009 B (parse) of headroom. These are 2.15.0 measurements, not permanent promises — re-measure against the version you actually install.
Sizes are guarded; content digests are not. The byte sizes above are held by shipped-sizes.json and asserted in CI, so they move only on a deliberate artifact change. SHA-256 digests are a different matter: every release stamps its own version string into each .wasm — the value version() returns — so all four digests change on every release, including a documentation-only patch whose compiled code is identical and whose byte sizes do not move at all. If you verify these artifacts by content digest rather than by semver, re-pin on every upgrade; never read "sizes unchanged" as "nothing to re-verify".
Gating swc_core out of the highlight graph (#2449/#2450) was a provability win, not a size win. The #2447 SWC-retaining baseline was 1,484,705 B raw and 767,009 B gzip-9; the #2450 result was 7,965 B smaller raw and 8,765 B smaller gzip-9 (758,244 B) — wasm-opt was already dead-stripping the unreachable swc_core, and #2450's exact-parity and no-swc_core assertions turned that emergent property into a guaranteed one. The delta that matters to a highlight-only consumer is root versus highlight: the highlight artifact is 1,860,620 B smaller raw and 698,432 B smaller gzip-9, landing at about 45% of root's raw bytes and 54% of its gzipped bytes.
The package is ESM-only ("type": "module", no require support) and requires Node ≥ 20.0.0. Call version() (above) at runtime, or check the installed package's own package.json, rather than trusting a version number baked into this page — it drifts release to release.
See also
Playground — try the browser-based Rust pipeline interactively.
Browser Markdown Preview — the narrative guide: installation, evaluating a compiled module, the JSX-runtime import-map footgun, lazy resource loading, and the parity guarantee's explicit limitations.
Importing Wasm — the separate, unrelated feature of importing a
.wasmmodule into an SSR route at build time.Syntax Highlighting — native zfb's syntect setup, for comparison with this package's
fancy-regexbackend.