zfb
GitHub repository

Type to search...

to open search from anywhere

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

ExportSignatureDescription
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) => RootValidates and converts a raw parseToAst tree into a strict, ecosystem-compatible mdast Root. The one synchronous export.
ZfbMdWasmTrapErrorclass extends ErrorThrown when a wasm trap (Rust panic / internal fault) occurred; the instance is auto-reinstantiated first.
ZfbMdWasmTrapRecoveryLimitErrorclass extends ErrorThrown once the bounded trap-recovery budget (16 recoveries) is exhausted.
MdastAdapterErrorclass extends TypeErrorThrown 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 compiled WebAssembly.Module before 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 of ZfbMdWasmTrapError once 16 automatic trap recoveries have been exhausted for the process. Constructed with (maxRecoveries: number, cause: unknown), cause exposed via .cause. Further recovery stays disabled (to avoid unbounded ES module record growth) until the JS realm is reloaded.

  • MdastAdapterError extends TypeError — thrown by toMdastRoot. Carries path: string (a JSON-path-like pointer into the offending node, e.g. "$.children[2].value") and nodeType: string | null (the node's observed type, or null when 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.

FieldTypeDescription
filename?stringMust 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?booleanConsumed only by compile; renderHtml accepts and ignores it.
pipeline?PipelineOptionsShared pipeline configuration.

PipelineOptions

Mirrors zfb_content::facade::PipelineOptions verbatim.

FieldTypeDescription
theme?string | nullA 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?GfmOptionsPer-extension GFM toggles.
cjkFriendly?boolean
hardBreaks?boolean
codeHighlight?CodeHighlightOptions | nullOutput mode + class-mode knobs for fenced-code highlighting. Absent, null, or { mode: "inline" } reproduce the pre-existing inline-color behavior byte-for-byte.
features?MarkdownFeaturesConfigRecord<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

FieldType
strikethrough?boolean
table?boolean
autolinkLiteral?boolean
taskListItem?boolean
footnoteDefinition?boolean

CodeHighlightOptions

zfb_content::facade::CodeHighlightOptions, verbatim.

FieldTypeDescription
mode?"inline" | "class"Output mode for fenced-code highlighting. Defaults to "inline".
classPrefix?stringClass-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>> | nullPer-role class overrides for class mode. Absent or null uses {classPrefix}{role} for every role. Only meaningful when mode is "class".

CompileResult / RenderHtmlResult

FieldTypeDescription
code (CompileResult) / html (RenderHtmlResult)string | nullES-module JS source, or HTML fragment, on success; null on failure.
frontmatterunknownParsed YAML frontmatter as JSON, null when absent or unextractable.
diagnosticsDiagnostic[]Empty on success.

Diagnostic

One diagnostic entry. line/column are the sole supported diagnostic location and are 1-based.

FieldTypeDescription
severity"error"
source"options" | "frontmatter" | "markdown" | "compile"
messagestringOpaque 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.
linenumber | nullnull 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.
columnnumber | nullSame 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.

FieldTypeDescription
filename?stringMust 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?booleanParse 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

FieldTypeDescription
astMdastRoot | nullSerialized raw mdast root on success, null on failure.
frontmatterunknownSame contract as CompileResult.frontmatter.
diagnosticsDiagnostic[]Empty on success.

HighlightCodeOptions

Options for direct arbitrary-code semantic class highlighting.

FieldTypeDescription
languagestringRequired syntax token, for example "html", "css", or "javascript".
mode?"class"The only supported direct output mode. Defaults to "class".
classPrefix?stringSemantic role class prefix. Defaults to "hi-".
roleClasses?Partial<Record<HighlightRole, string>>Full-name role overrides, for example { keyword: "text-violet-600" }.

HighlightCodeResult and HighlightDiagnostic

FieldTypeDescription
htmlstring | nullComplete semantic <pre><code> wrapper, or null for invalid options/internal errors.
diagnosticsHighlightDiagnostic[]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):

RoleDefault classRoleDefault 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, :::name

  • leafDirective — block form, ::name

  • textDirective — 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 of ZfbMdWasmOptions (top level and pipeline). ParseToAstOptions is a separate, stricter closed document: its pipeline sub-object accepts only gfm — passing theme, cjkFriendly, hardBreaks, codeHighlight, or features there is rejected, even though those are valid PipelineOptions fields for compile/renderHtml.

  • Diagnostic.line/.column (from compile/renderHtml) and AstPoint.column/.offset (from parseToAst) 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 ./highlight imports require no migration. The additive ./render and ./parse 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.

Subpathgzip-9 wasm (2.15.0)Exact runtime valuesExact type surfaceUse when
.1,516,383 Binit, compile, renderHtml, parseToAst, highlightCode, version, toMdastRoot, MdastAdapterError, ZfbMdWasmTrapError, ZfbMdWasmTrapRecoveryLimitError, __forceTrapForTests, __getTrapRecoveryStateForTestsFull current compile/render/parse/raw-mdast/highlight typesCompile MDX or use several current APIs
./highlight817,951 Binit, highlightCode, version, ZfbMdWasmTrapError, ZfbMdWasmTrapRecoveryLimitError, __forceTrapForTests, __getTrapRecoveryStateForTestsHighlightRole, HighlightCodeOptions, HighlightCodeResult, HighlightDiagnostic, HighlightDiagnosticSourceHighlight code only; public API/resources stay compatible
./render1,091,678 Binit, renderHtml, version, ZfbMdWasmTrapError, ZfbMdWasmTrapRecoveryLimitError, __forceTrapForTests, __getTrapRecoveryStateForTestsRenderHtmlResult, Diagnostic, DiagnosticSource, ZfbMdWasmOptions, ParseDialect, PipelineOptions, GfmOptions, CodeHighlightMode, CodeHighlightOptions, MarkdownFeaturesConfig, JsxRuntime, HighlightRoleRender Markdown to HTML without compiler bytes
./parse283,991 Binit, parseToAst, toMdastRoot, MdastAdapterError, version, ZfbMdWasmTrapError, ZfbMdWasmTrapRecoveryLimitError, __forceTrapForTests, __getTrapRecoveryStateForTestsParseToAstResult, 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, MdxJsxExpressionAttributeParse 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.wasm

Their 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/graphfinal wasmgzip-9glueglue gzip-9
root (full)3,399,954 B1,516,383 B14,998 B4,199 B
highlight1,539,334 B817,951 B8,758 B2,637 B
render2,196,095 B1,091,678 B8,772 B2,661 B
parse700,364 B283,991 B11,159 B3,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 .wasm module into an SSR route at build time.

  • Syntax Highlighting — native zfb's syntect setup, for comparison with this package's fancy-regex backend.

Revision History

CreatedUpdated