ブラウザでの Markdown プレビュー
@takazudo/zfb-md-wasm パッケージを使い、MDX を実行可能な ES モジュールへコンパイルしたり、Markdown を HTML へレンダリングしたりを、zfb のビルド時パイプラインと出力パリティを保ちながら、すべてブラウザ内で行います。
このページで扱う内容
@takazudo/zfb-md-wasm、zfb の md/mdx → JS/HTML パイプラインの ブラウザ側での動的変換向け WebAssembly ビルド。完全なルート API(compile() と renderHtml())、parseToAst()による生の mdast ツリーへのパース(および検証付きの toMdastRoot() アダプタ)、直接的な セマンティックな highlightCode()、ブラウザでコンパイル済みモジュールを評価する方法、 Vite でのパッケージの使い方、テストとツールでの Node 利用、そしてパリティ保証の明示的な 制限を扱います。
これは何か、なぜ存在するのか
@takazudo/zfb-md-wasm は、zfb 自身の markdown/MDX → JS 変換パイプラインを WebAssembly にコンパイルし、あなたのマシンでのビルド時だけでなく、ブラウザのタブ内でも実行できる ようにします。
代表的なユースケースは CMS のライブプレビュー です: 編集者が CMS に Markdown や MDX を 入力するとき、プレビューペインは zfb build が最終的に生成するものを — 近似ではなく — そのまま表示すべきです。これを @mdx-js/mdx をブラウザで実行して 行うと、独自のパース、プラグイン、JSX 出力の挙動を持つ 別の パイプラインを使うことに なり、そのプレビューは zfb の実際のビルド出力からずれる可能性があります。zfb-md-wasm は、 zfb 自身がビルド時に使うのと 同じ Rust パイプライン をコンパイルすることでそのずれを 避けるため、プレビューは実際に出荷されるものに忠実であり続けます。パリティこそが、単に @mdx-js/mdx に手を伸ばすのではなくこのパッケージが存在する、そのすべての理由です。
Node ≥ 20 でもこのパッケージをロードできます — パッケージ自身のテストスイートがそう しています — が、ターゲットはブラウザです。zfb バイナリを直接実行するシェルアクセスを 持つサーバーは、このパッケージをロードするのではなく、引き続きそうすべきです。
Note
pnpm add @takazudo/zfb-md-wasm(またはお好みのパッケージマネージャ)でインストール します。
最小のエントリを選ぶ
ルート(@takazudo/zfb-md-wasm)は完全な後方互換の面で、compile を export する唯一の エントリです。既存のルートと @takazudo/ のインポートに移行は不要です。 追加された / と / は独立した SWC-free グラフで、swc_core と zfb-render を除外しながら zfb-content と syntect-fancy を意図的に保持します。parse は syntect-free ではありません。
| インポート | gzip-9 wasm(2.15.0) | 正確なランタイム値 | 型の面 | 使う場面 |
|---|---|---|---|---|
@takazudo/zfb-md-wasm | 1,516,383 B | init、compile、renderHtml、parseToAst、highlightCode、version、toMdastRoot、MdastAdapterError、ZfbMdWasmTrapError、ZfbMdWasmTrapRecoveryLimitError、__forceTrapForTests、__getTrapRecoveryStateForTests | compile/render/parse/raw-mdast/highlight の現在の完全な型セット | MDX をコンパイル、または複数の API を使う場合 |
@takazudo/ | 817,951 B | init、highlightCode、version、ZfbMdWasmTrapError、ZfbMdWasmTrapRecoveryLimitError、__forceTrapForTests、__getTrapRecoveryStateForTests | HighlightRole、HighlightCodeOptions、HighlightCodeResult、HighlightDiagnostic、HighlightDiagnosticSource | コードだけをハイライトする場合。公開 API / リソースは互換 |
@takazudo/ | 1,091,678 B | init、renderHtml、version、ZfbMdWasmTrapError、ZfbMdWasmTrapRecoveryLimitError、__forceTrapForTests、__getTrapRecoveryStateForTests | RenderHtmlResult、Diagnostic、DiagnosticSource、ZfbMdWasmOptions、ParseDialect、PipelineOptions、GfmOptions、CodeHighlightMode、CodeHighlightOptions、MarkdownFeaturesConfig、JsxRuntime、HighlightRole | コンパイラのバイトを含めず Markdown を HTML にする場合 |
@takazudo/ | 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 | AST をパースし、必要なら利用側の制御されたコードで解釈する場合 |
focused エントリは、対応する private リソースペアだけを持ちます:
wasm-render/zfb_md_wasm_render_glue.zfb-resource.mjs + zfb_md_wasm_render_bg.wasm
wasm-parse/zfb_md_wasm_parse_glue.zfb-resource.mjs + zfb_md_wasm_parse_bg.wasm宣言サイドカーも対応ディレクトリに閉じています。各エントリは独立したコンパイル済みモジュール、 インスタンス、世代、リトライ状態、終端状態を持ちます。複数のエントリをインポートすると、独立した ペアとインスタンスが意図的にロードされます。
Node の直接利用とブラウザ対応バンドラでは、focused サブパスを選びます:
import { renderHtml } from "@takazudo/zfb-md-wasm/render";
import { parseToAst, toMdastRoot } from "@takazudo/zfb-md-wasm/parse";ブラウザのユーザー操作から遅延インポートすると、選択したエントリのペアだけが取得されます:
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() はルートだけにあり、ホストによる評価、JSX ランタイム、コンポーネントを必要とする モジュールソースを返します。toMdastRoot() は、パース済みデータを利用側の制御された AST-to-React レンダラーで 解釈するためのアダプタです。slim エントリは author JavaScript を評価しません。renderHtml は サニタイザーではなく、生 HTML は信頼できないままです。MDX JSX / 式 / ESM 形状の AST ノードは 不活性なデータです。
完全なルート API
compile() と renderHtml() はどちらも、markdown/MDX の source 文字列とオプション オブジェクト — すべてのフィールドは任意で、{} はすべてのデフォルトを選びます — を受け 取り、どちらも diagnostics 配列を持つ結果オブジェクトに解決します。想定される失敗は 決してスローしません。 パースエラー、不正なオプション JSON、未知のテーマなどの問題は、 すべて code / html が null に設定された Diagnostic[] エントリとして返され、 スローされるエラーになることはありません。(スローされる ZfbMdWasmTrapError は wasm ビルドの実際のバグを意味します — その、より限定的なケースについては、パッケージ自身の README「Error / trap / re-init contract」を参照してください。)
compile() — MDX から ES モジュール JS へ
完全な MDX → JSX → SWC → ES モジュール。結果の code は、自動 JSX ランタイムを使う MDXContent のデフォルト export(コンポーネント関数)を持つ ES モジュールソースです。
import { compile } from "@takazudo/zfb-md-wasm";
const { code, frontmatter, diagnostics } = await compile(
"---\ntitle: Hello\n---\n\n# Welcome\n\n<Callout>Sum is {1 + 2}</Callout>\n",
{ filename: "post.mdx", jsxRuntime: "preact" },
);
// code -> ES-module JS source (string), or null on failure
// frontmatter -> { title: "Hello" }
// diagnostics -> []frontmatter の値は結果の frontmatter フィールドで返されます — コンテンツ内のバインディング としては公開 されません。ソース内の {frontmatter.title} ではなく、ホスト側のコード (上記の frontmatter オブジェクト)から参照してください: コンパイル済みモジュールの スコープには frontmatter 変数がないため、コンテンツ内での参照はモジュール実行時に ReferenceError をスローします。
ブラウザでコンパイル済みモジュールを評価する
compile() は、実行可能なモジュールではなく ES モジュールの ソーステキスト を返します。 blob URL と動的な import() でそれを実行可能なモジュールに変換します:
const { code, diagnostics } = await compile(source, { filename: "preview.mdx" });
if (code === null) {
// Compilation failed — render the `diagnostics` array instead of a module.
return;
}
const url = URL.createObjectURL(new Blob([code], { type: "text/javascript" }));
const { default: MDXContent } = await import(/* @vite-ignore */ url);
URL.revokeObjectURL(url);
// render <MDXContent components={{ Callout }} /> with your frameworkPascalCase のコンポーネント(上記ソースの <Callout>)は、モジュールの components prop を 通して渡します — zfb が自身の MDX Components で使うのと 同じ規約です。
JSX ランタイムを供給する
コンパイル済みモジュールは JSX ランタイムを裸の指定子 — preact/jsx-runtime または react/jsx-runtime — でインポートするため、それを評価するページは、 import map やバンドラを通じて、それらの指定子を自分で解決しなければなりません。zfb-md-wasm は どちらのランタイムも同梱・バンドルしません。
Fragment の癖 — preact 利用者はエイリアスが 1 つ必要
zfb のエミッタは JSX の ファクトリ をあなたが選んだ jsxRuntime から取りますが、 どちらのランタイムを選んだかにかかわらず、Fragment は常に react/jsx-runtime から インポートします。これは zfb の本番エミッタの形であり — パリティとして正しく、バグでは ありません — しかしこれは、preact 利用者が react/jsx-runtime を preact 自身の ランタイムへエイリアスしなければならないことを意味します。さもなければ Fragment は 実行時に解決に失敗します:
<script type="importmap">
{
"imports": {
"preact/jsx-runtime": "https://esm.sh/preact/jsx-runtime",
"react/jsx-runtime": "https://esm.sh/preact/jsx-runtime"
}
}
</script>react 利用者は react/jsx-runtime を React 自身のランタイムにマッピングするだけでよく、 エイリアスは不要です。
renderHtml() — Markdown から HTML へ
Markdown → hast → HTML 文字列で、実行時に SWC をスキップします。プレーンな markdown の プレビューだけが必要で、コンポーネントモジュールを評価する必要がないときに使ってください。
import { renderHtml } from "@takazudo/zfb-md-wasm";
const { html, frontmatter, diagnostics } = await renderHtml("Budget <8 ms\n", {
filename: "post.md",
});
// html -> "<p>Budget <8 ms</p>"renderHtml は .md から CommonMark、.mdx から MDX を推論し、明示的な dialect: "markdown" | "mdx" はどちらの有効な拡張子も上書きします。ファイル名を 省略すると <anonymous>.md が使われるため CommonMark になります。compile は MDX 専用のままで dialect を受け取って無視し、renderHtml は jsxRuntime / development を受け取って無視するため、オプションオブジェクトを動的に組み立てれば 1 つで両方の階層に 使えます。
AST へのパース(parseToAst)
parseToAst() は 3 つ目のパイプライン階層で、renderHtml() より 1 段階手前に位置します: パースの直後で止まり、HTML や JS へ進む代わりに、生の mdast ツリーを JSON として返します。 ホストがツリー自体を走査または変換する必要があるとき — 目次、カスタムレンダラ、単語数 / 読了時間のメタデータ、あるいは完成した文字列ではなくドキュメントへの構造化されたアクセスを 必要とするその他のもの — に使ってください。
import { parseToAst, type ParseToAstOptions, type ParseToAstResult } from "@takazudo/zfb-md-wasm";
const result: ParseToAstResult = await parseToAst(
"# Welcome\n\nHello **world**.\n",
{ filename: "post.md" } satisfies ParseToAstOptions,
);
// result.ast -> { type: "root", position: {...}, children: [...] }
// result.frontmatter -> null (no frontmatter in this source)
// result.diagnostics -> []ParseToAstOptions
parseToAst は独自の、独立した閉じたオプションドキュメントを受け取ります — compile / renderHtml のオプションの形ではありません:
filename— 小文字の.mdまたは.mdxで終わる必要があります。明示的なdialectが ない場合、.mdは CommonMark を、.mdxは MDX を選びます。filenameを省略すると<anonymous>.mdxが使われ、したがって MDX になります。明示的なdialectはどちらの 有効な拡張子も上書きしますが、拡張子のゲート自体は免除しません。dialect—"markdown"または"mdx"。省略時は上記のとおりfilenameから推論されます。directives— 汎用の remark-directive 構文(:::name/::name/:name)をパース します。デフォルトはfalse。オフのときは、ディレクティブのように見えるテキストは markdown-rs がパースしたそのままで残ります。frontmatter— YAML の処理ポリシー。デフォルトは"extract":ポリシー パースされるソース AST の yamlノードfrontmatterの値不正な YAML extract(デフォルト)frontmatter を除去した本文 なし パースされた JSON / nullfrontmatter診断が 1 つnode論理ソース全体 正規の yamlノードパースされた JSON / nullfrontmatter診断が 1 つnone論理ソース全体、全バイトを Markdown/MDX として なし 常に nullfrontmatter診断を生成しないpipeline.gfm— 5 つの真偽値GfmOptionsスイッチ(strikethrough、table、autolinkLiteral、taskListItem、footnoteDefinition)のみ。parseToAstのパイプ ラインオプションはcompile/renderHtmlのものより厳格です: 適用できないビジター / シリアライザ専用のつまみを黙って受け入れて無視するのではなく、theme、cjkFriendly、hardBreaks、codeHighlight、featuresをきっぱり拒否します(deny_unknown_fields)。
ParseToAstResult
interface ParseToAstResult {
ast: MdastRoot | null; // serialized raw mdast root on success, null on failure
frontmatter: unknown; // parsed YAML frontmatter as JSON, per the policy table above
diagnostics: Diagnostic[]; // same Diagnostic shape as compile()/renderHtml()
}ast は markdown-rs の生の mdast ツリーを、その serde の形を通して、開いた unist 形状の キャリアへ変換したものです — 文書化されたすべてのノードは type、position、ノード固有の フィールドを保持しますが、これは zfb ビジター適用前 の生の出力であり、レンダリング されたパイプラインが生成する HTML/JS ではありません。
UTF-16 の位置
ツリー内のすべてのノードは position: { start, end } を持ち、それぞれ line(1 始まり)、 column(1 始まり)、offset(0 始まり)を持つ AstPoint です。column と offset は UTF-16 コードユニット — String.prototype.slice や remark/unist 自身の規約が使うのと 同じインデックス — であり、Unicode スカラー値では ありません: BMP 外のスカラー(多くの 絵文字)はサロゲートペアであるため、offset / column を 1 ではなく 2 進めます。位置は 元のソース に対して報告され、frontmatter の行も含まれます。これは、markdown-rs が実際に パースする本文からそれらの行が除去されている frontmatter: "extract" の下でも同様です。
開いたノードのユニオンは `type` だけでは絞り込めない
MdastNode は、文書化されたセットにないあらゆるノード型のキャッチオールとしてUnknownMdastNode を含み、そのキャッチオールの type フィールドは、リテラルではなく 一般的な string です。これにより、認識されないノード型が unknown にフォールバックせず 型付けされたまま保たれますが、同時に、単純な等価チェックではキャッチオールを 絞り込んで 除外できない ことも意味します:
// child: MdastNode
if (child.type === "heading") {
child.depth; // still a type error — TypeScript can't rule out UnknownMdastNode here
}実行時に形を確認したうえでノードの種類を明示的にアサートするか、下記の toMdastRoot() を 使ってください。これはキャストなしで正しく絞り込めるツリーを返します。
toMdastRoot() — 検証付きで型を絞り込むアダプタ
parseToAst の生の ast は設計上、前方互換です: 認識されないノードは、呼び出しを壊すの ではなく UnknownMdastNode として残ります。toMdastRoot() はその逆の、より厳格なツール です — 生のツリーを検証し、切り離されたエコシステム形状の mdast の Root を返します。これは @types/mdast の利用者が期待するとおりに type で絞り込めます:
import { parseToAst, toMdastRoot, MdastAdapterError } from "@takazudo/zfb-md-wasm";
const { ast } = await parseToAst("# Welcome\n\nHello **world**.\n");
try {
const root = toMdastRoot(ast);
const heading = root.children[0];
if (heading.type === "heading") {
heading.depth; // narrows correctly — no assertion needed
}
} catch (error) {
if (error instanceof MdastAdapterError) {
// error.path -> e.g. "$.children[0]"
// error.nodeType -> the offending node's `type`, or null for a null ast
}
}toMdastRoot() は診断する代わりに MdastAdapterError(.path と .nodeType を持つ TypeError のサブクラス)を スロー します — 結果オブジェクトは返しません。次の場合に スローします:
nullのast(path は"$")、および未知またはサポートされていないノード型(
math、toml、mdxjsEsmを明示的に含む)。
スローするため、パース失敗が想定される場合は必ず先に diagnostics を確認してください — toMdastRoot() は、既に成功した ast を絞り込み可能なツリーに変換するためのものであり、 パース失敗とアダプタ失敗を区別するためのものではありません。
remark からの文書化された相違点
mdxJsxAttribute(およびその値属性 / 式属性の兄弟)はpositionを持ちません — markdown-rs は属性の位置をモデル化しません。トップレベルの
import/exportはプレーンな段落へと格下げされます(mdxjsEsmノード なし) — wasm の境界は JS の ESM/acorn パーサをホストできません。remark-mdx 相当の ESM/estree データが必要な利用者は、それらのドキュメントには remark を使い続けてください。_markdownRsStops(MDX の式 / ESM 形状のノードが持つ)は markdown-rs 内部の管理情報で、 不安定であり、UTF-8 バイト ベースです —positionとは異なり、上記の UTF-16 コード ユニットの契約を共有しません。これで文字列をスライスしては決していけません。
Vite で使う
parseToAst()(および他のすべての @takazudo/zfb-md-wasm エクスポート)は、動作に Vite の プラグイン、エイリアス、設定を必要としません — 素の vite devDependency で十分です。 パッケージの browser export 条件と、グルー / wasm リソースに対する ?url アセット インポートの契約は、どちらも Vite のデフォルトの dev サーバーと本番ビルドの下で(そして zfb 自身の esbuild パイプラインの下でも同様に)自動的に解決されます。本番では サーバー MIME の要件 が引き続き適用 されます: 出力される .mjs を application/javascript として、.wasm を application/wasm として配信してください。
直接的なセマンティックコードハイライト
highlightCode() は、任意の HTML、CSS、JavaScript、またはその他の同梱された構文に対する、 直接的なルート API です。Markdown のフェンスを必要とせず、インラインの色や Shiki のクラスを 出力することもありません。エスケープされた、セマンティックなクラスモードの HTML を返します。
import {
highlightCode,
type HighlightCodeOptions,
type HighlightCodeResult,
} from "@takazudo/zfb-md-wasm";
const output: HighlightCodeResult = await highlightCode("const answer = 42;", {
language: "javascript", // required
mode: "class", // optional; the only accepted mode
classPrefix: "hi-", // optional; defaults to "hi-"
roleClasses: { keyword: "text-violet-600 dark:text-violet-400" },
} satisfies HighlightCodeOptions);type HighlightRole =
| "escape"
| "operator"
| "comment"
| "string"
| "number"
| "constant"
| "keyword"
| "function"
| "type"
| "namespace"
| "property"
| "variable"
| "tag"
| "attribute"
| "punctuation"
| "inserted"
| "deleted"
| "heading";
interface HighlightCodeOptions {
language: string;
mode?: "class";
classPrefix?: string;
roleClasses?: Partial<Record<HighlightRole, string>>;
}
interface HighlightCodeResult {
html: string | null;
diagnostics: HighlightDiagnostic[];
}デフォルトの出力は <pre class="hi-root"><code>…</code></pre> で、空でない各行に <span class="line"> が付きます。固定されたフルネームのロールキーとデフォルトの hi- クラスは次のとおりです: escape → hi-esc、operator → hi-op、comment → hi-com、 string → hi-str、number → hi-num、constant → hi-const、keyword → hi-kw、 function → hi-fn、type → hi-ty、namespace → hi-ns、property → hi-prop、 variable → hi-var、tag → hi-tag、attribute → hi-attr、punctuation → hi-punct、inserted → hi-ins、deleted → hi-del、heading → hi-hd。
roleClasses のキーは、そのリストのフルネームです。例えば { keyword: "my-keyword" } は hi-kw を置き換えます。kw は有効な上書きキーではありません。classPrefix は、ルート ("token-" の場合は token-root)と、上書きされていないすべてのトークンクラスの両方を 変更します。
無効なオプション(例えば language の欠落、サポートされないモード、無効なプレフィックス、 認識されないロールキー、余分なプロパティ)は、source: "options" の error 診断とともに html: null を返します。スローはしません。未知だが空でない language は、source: "highlight" の warning 診断 1 つと null の位置フィールドを伴う、成功したエスケープ済みフォールバックに なります。不完全なエディタ入力は受け入れられ、診断なしで通常の markup を返すことがあります。
遅延ロードされるブラウザリソースとサーバー MIME
すべてのパッケージエントリは、自分自身の glue/wasm ペアだけを静的に宣言します。ルートは wasm/ と wasm/ を保持し、focused ペアは次のとおりです:
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.wasmzfb の本番ビルドは、選択したペアをハッシュ付き island アセットとして出力します:
assets/islands-resource-zfb_md_wasm_glue.zfb-resource-<hash>.mjs
assets/islands-resource-zfb_md_wasm_bg-<hash>.wasmfocused アセットは、stem に render または parse を含む同じ命名規則になります (例: islands-resource-zfb_md_wasm_render_bg-<hash>.wasm)。
. サブパス は代わりに、独自の別のリソースのペアを宣言します — zfb_md_wasm_highlight_glue.zfb-resource.mjs と zfb_md_wasm_highlight_bg.wasm — で、 同じ方法でハッシュ化されます:
assets/islands-resource-zfb_md_wasm_highlight_glue.zfb-resource-<hash>.mjs
assets/islands-resource-zfb_md_wasm_highlight_bg-<hash>.wasm同じバンドルで複数のエントリをインポートすると、各 private ペアと独立したコンパイル済み モジュール / インスタンス状態が意図どおりロードされます。可能ならバンドルごとに 1 つを選び、 両方の機能が必要な場合は複数利用できます。
初期ページロードから除外するには、ユーザー操作からのみ選択したエントリを動的にインポートします。 その最初の公開 API 呼び出しで、グルーと wasm がフェッチされます:
button.addEventListener("click", async () => {
const { renderHtml } = await import("@takazudo/zfb-md-wasm/render");
const result = await renderHtml(editor.value, { filename: "preview.md" });
preview.innerHTML = result.html ?? "";
});
parseButton.addEventListener("click", async () => {
const { parseToAst, toMdastRoot } = await import("@takazudo/zfb-md-wasm/parse");
const parsed = await parseToAst(editor.value, { filename: "preview.md" });
const root = parsed.ast === null ? null : toMdastRoot(parsed.ast);
inspect(root);
});出力される .mjs を application/javascript として、.wasm を application/wasm として 配信してください。リソースを手動でコピーしたり、パッケージのソースパスをインポートしたり しないでください: zfb がハッシュ付きの URL グラフを維持できるよう、パックされたブラウザ エントリを使ってください。
出荷アーティファクトのサイズと上限
出荷された 2.15.0 のアーティファクトの行は次のとおりです。final wasm は wasm-bindgen と wasm-opt を通した最適化後の値、gzip-9 は Node の gzipSync(..., { level: 9 })、glue は グルーコードのバイト数とその gzip です:
| エントリ / グラフ | 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 |
#2447 の決定スナップショットでは、split パッケージの構成が 3,638,607 B、root + highlight が 2,314,818 B、クリーンな本番の参照上限は 210 秒、選択されたスナップショットの中央値は 155.015 秒 [153.496, 165.977] でした。固定された gzip-9 の上限は root 1,600,000 B、highlight 880,000 B、 render 1,100,000 B、parse 325,000 B、完全な packed tarball は 3,900,000 B です。4 つのエントリは いずれも上限の内側で出荷されており、上限までの余裕は root 83,617 B、highlight 62,049 B、 render 8,322 B、parse 41,009 B です。これらは永続的な保証ではなく 2.15.0 の測定値です — 実際にインストールしたバージョンで測り直してください。
サイズは保証されますが、コンテンツダイジェストは保証されません。 上記のバイトサイズは shipped-sizes.json が保持し CI がアサートしているため、意図的なアーティファクト変更が あったときにしか動きません。SHA-256 ダイジェストは事情が異なります。各リリースは自身の バージョン文字列を個々の .wasm に焼き込む(version() が返す値です)ため、コンパイル結果が 同一でバイトサイズがまったく動かないドキュメントのみのパッチであっても、4 つのダイジェストは 毎リリース変わります。semver ではなくコンテンツダイジェストでこれらのアーティファクトを 検証している場合は、アップグレードのたびに再ピン留めしてください。「サイズが変わっていない」を 「再検証は不要」と読み替えてはいけません。
highlight グラフから swc_core をゲートで外したこと(#2449 / #2450)は、 証明可能性の勝利であって、サイズの勝利ではありません。#2447 の SWC 保持ベースラインは raw 1,484,705 B、gzip-9 767,009 B で、#2450 の結果は raw で 7,965 B、gzip-9 で 8,765 B(結果は 758,244 B)小さくなりました。wasm-opt は 到達不能な swc_core を既にデッドストリップしていたのであり、#2450 の完全パリティと swc_core 不在のアサーションが、その創発的な性質を保証されたものへと変えました。 highlight だけを使う利用者にとって意味のある差は、root と highlight の差のほうです: raw で 1,860,620 B、gzip-9 で 698,432 B 小さく、highlight は root の raw バイトの約 45%、 gzip 後のバイトの約 54% に収まります。
Node での利用(テストとツール)
エントリは追加のセットアップなしで Node ≥ 20 の下で動作します。ツールやスナップショット テストでは、処理に対応するリソースペアを直接選びます:
import { renderHtml } from "@takazudo/zfb-md-wasm/render";
import { parseToAst, toMdastRoot } from "@takazudo/zfb-md-wasm/parse";
const { html } = await renderHtml("# Hello from Node\n");
const parsed = await parseToAst("# Hello from Node\n", { filename: "post.md" });
const root = parsed.ast === null ? null : toMdastRoot(parsed.ast);同じ利用者が compile も必要とする場合はルートを使います。ルートと highlight のインポートは後方互換です。
パリティ保証と制限
出力は、固定されたフィクスチャコーパスにおいて zfb のネイティブパイプラインと一致します — パリティテストスイートは完全一致でゲートします。この保証には、ブラウザビルドに対する意図 的な制限が伴います:
ファイルシステムに依存するプラグインは不活性です。
transclude、imageDimensions、linkValidationは登録されますが、ファイルシステムには一切触れません — ブラウザのタブ にはファイルシステムがありません — これは、zfb 自身の MDX ローダがビルドコンテキストの ルートを無効化した状態でそれらを実行するのとまったく同じです。ホストコールバック版 (ブラウザのホストが必要に応じてファイル内容を供給できるようにするもの)は将来の epic の 可能性であり、ここでは実装されていません。設定は
zfb.config.tsではなく、解決済みの JSON です。 TypeScript の設定を評価する には JS エンジンが必要であり、それはビルド側にとどまります。まず設定を JSON へ解決し — zfb がビルド時にzfb.config.tsから導出するのと同じ形、Markdown のカスタマイズ を参照 — それをpipelineオプションとして渡してください。クロスファイル機能はありません。 ルートテーブルのリンク解決とクロスファイルの アンカー解決には、プロジェクト全体のルートグラフが必要ですが、単一ドキュメントのブラウザ 呼び出しはそれを決して持ちません。
ルートは互換性 / コンパイラのエントリです。
compileは author JavaScript を実行し得る 唯一の経路なので、完全なグラフを保持します。SWC-free な処理には/またはrender /、 直接的なハイライトにはparse /を選んでください。focused エントリは意図的にhighlight zfb-content/syntect-fancyを保持し、parse は syntect-free ではありません。renderHtmlはサニタイザーではありません。 生 HTML は信頼できないままです。MDX の JSX / 式 / ESM 形状の AST ノードは不活性なデータであり、既にパースされた AST を解釈できるのは 利用側の制御されたコードだけです。slim エントリは author JavaScript を評価しません。renderHtmlはファイル名から構文を選びます。.mdは CommonMark、.mdxは MDX を 使い、明示的なdialectはどちらの有効な拡張子も上書きします。compileは MDX 専用の ままです。シンタックスハイライトは syntect の
fancy-regexバックエンドを使います — ネイティブ zfb のonigurumaバックエンドではありません(onigurumaは wasm にコンパイルできません)。 ネイティブ側の挙動については シンタックスハイライト を参照 してください。2 つのバックエンドは zfb のフィクスチャコーパスにおいてバイト単位で同一 です。文法レベルの相違は、クレート自身の情報提供用のバックエンド相違テストによって追跡 されます。
バンドルサイズ
出荷された 2.15.0 のアーティファクトの行は次のとおりです。final wasm は wasm-bindgen と wasm-opt を通した最適化後の値、gzip-9 は Node の gzipSync(..., { level: 9 })、glue は グルーコードのバイト数とその gzip です:
| エントリ / グラフ | 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 |
#2447 の決定スナップショットでは、split パッケージの構成が 3,638,607 B、root + highlight が 2,314,818 B、クリーンな本番の上限は 210 秒、決定スナップショットの中央値は 155.015 秒 [153.496, 165.977] でした。固定された gzip-9 の上限は root 1,600,000 B、highlight 880,000 B、 render 1,100,000 B、parse 325,000 B、完全な packed tarball は 3,900,000 B です。4 つのエントリは いずれも上限の内側で出荷されており、上限までの余裕は root 83,617 B、highlight 62,049 B、 render 8,322 B、parse 41,009 B です。これらは永続的な保証ではなく 2.15.0 の測定値です — 実際にインストールしたバージョンで測り直してください。
出荷アーティファクトのサイズと上限 にあるダイジェストの注意書きは、この表にも当てはまります。ここに挙げたバイトサイズは 保証されますが、アーティファクトの SHA-256 ダイジェストは毎リリース変わります。
highlight グラフから swc_core をゲートで外したこと(#2449 / #2450)は、 証明可能性の勝利であって、サイズの勝利ではありません。#2447 の SWC 保持ベースラインは raw 1,484,705 B、gzip-9 767,009 B で、#2450 の結果は raw で 7,965 B、gzip-9 で 8,765 B(結果は 758,244 B)小さくなりました。wasm-opt は 到達不能な swc_core を既にデッドストリップしていたのであり、#2450 の完全パリティと swc_core 不在のアサーションが、その創発的な性質を保証されたものへと変えました。 highlight だけを使う利用者にとって意味のある差は、root と highlight の差のほうです: raw で 1,860,620 B、gzip-9 で 698,432 B 小さく、highlight は root の raw バイトの約 45%、 gzip 後のバイトの約 54% に収まります。
トラップからの回復
想定される highlightCode のオプションエラーや未知の language のフォールバックは、通常の 結果診断です。本物の wasm トラップは別です: 該当インスタンスは汚染されるため、その呼び出しは ZfbMdWasmTrapError をスローします。ラッパーは、キャッシュ済みのコンパイル済み wasm モジュールから直ちに新しいインスタンスを起動するため、次の compile、renderHtml、 highlightCode、version の呼び出しは、2 回目の wasm の fetch / コンパイルなしに置き換え後 のインスタンスを使います。ブラウザでの回復は、新しいグルーモジュールの世代 (?zfbMdWasmGen=N)をインポートし、無制限なモジュールレコードを避けるため 16 回の回復に 上限が設けられています。トラップは常にパッケージのバグです。それを引き起こした入力を報告 してください。
関連項目
Playground — ブラウザで Rust パイプラインをインタラクティブに試せます。
MDX Components —
compile()の出力が従うcomponentsprop の規約。Markdown のカスタマイズ —
pipelineオプションが反映する パイプライン設定を zfb がどう解決するか。シンタックスハイライト — wasm ビルドの
fancy-regexバックエンドと比較するための、ネイティブ zfb の syntect のセットアップ。@takazudo/zfb-md-wasmAPI リファレンス —compile、renderHtml、parseToAst、toMdastRoot、highlightCodeの完全なオプション / 結果の型リファレンス。Wasm のインポート — このページのブラウザ側の使い方と比較する ための、SSR 側での Wasm モジュールのロード。