Skip to content

JavaScript / TypeScript

Prerequisites: Getting Started and Document IR.

Terminal window
npm install @illusions-lab/mdi

The package runs the MDI parser in prebuilt WebAssembly. It works in Node.js and bundlers with WASM support; it does not need a native build on the consumer machine. Parse before an export UI makes a warning visible instead of silently rendering through it:

import { parse, renderHtmlWithDiagnostics } from "@illusions-lab/mdi";
const source = "# 第一章\n{東京|とうきょう}は雨だった。";
const parsed = parse(source);
if (parsed.diagnostics.some((item) => item.severity === "error")) {
// Use item.code and its UTF-8 byte span to mark the editor.
}
const html = renderHtmlWithDiagnostics(source, { bodyOnly: true });
console.log(html.output); // semantic contents of <body>
console.log(html.headings); // source-order heading text, depth, and spans

parse() returns Rust-owned document, diagnostics, and UTF-8-byte source spans. Malformed inline notation normally uses its literal fallback rather than throwing; see Diagnostics for the complete warning list. prepareRender(source) is the same parse-first convenience for a host workflow. The *WithDiagnostics helpers return that parser result alongside output; they do not make an invalid document fail automatically.

renderHtml(source) returns a standalone document with the MDI stylesheet. Pass { bodyOnly: true } when the application owns the outer page. renderHtmlWithDiagnostics also exposes headings, so navigation and chapter controls need not scrape generated HTML. The stable MDI classes (mdi-ruby, mdi-tcy, mdi-em, mdi-pagebreak, and related classes) are part of that semantic HTML.

In a browser, await the WebAssembly runtime once before calling the synchronous API. initializeMdi() is single-flight and idempotent: concurrent calls share one initialization, and Node.js resolves it immediately because its runtime is loaded eagerly.

import { initializeMdi, parse, serializeMdi } from "@illusions-lab/mdi";
await initializeMdi();
const parsed = parse("{東京|とうきょう} ^12^");
const canonical = serializeMdi("{東京|とうきょう} ^12^");

Vite and other bundlers that honor the browser export condition select the web facade and emit its private WASM asset automatically. Do not import the generated wasm-pack loader directly. A failed browser initialization can be retried safely by calling initializeMdi() again.

getMdiTextBlocks(source) parses once in Rust and returns source-order text blocks alongside the complete document IR and diagnostics. A position such as 3:18 means the eighteenth one-based Unicode grapheme in the third block. Ruby readings remain searchable annotations whose anchor points back to the base-text range.

import { getMdiTextBlocks, resolveMdiSourceSpan, resolveMdiSourceSpans, sourceSpansForTextRange } from "@illusions-lab/mdi";
const result = getMdiTextBlocks("# 題\n\n{東京|とうきょう}");
const paragraph = result.blocks[1];
const match = { start: "2:1", end: "2:3" } as const;
console.log(paragraph.text); // 東京
console.log(paragraph.annotations[0].text); // とうきょう
console.log(sourceSpansForTextRange(paragraph, match)); // UTF-8 source spans
console.log(resolveMdiSourceSpan("# 題\n\n{東京|とうきょう}", { startByte: 8, endByte: 14 }));
console.log(resolveMdiSourceSpans("same same", [{ startByte: 0, endByte: 4 }, { startByte: 5, endByte: 9 }]));

sourceMap.synthetic identifies separators added by the projection, such as table tabs and row newlines; these deliberately produce no source span. parseMdiTextPosition, formatMdiTextPosition, and formatMdiTextRange are stateless helpers for the canonical coordinate spelling.

resolveMdiSourceSpan(source, span) performs the inverse lookup in Rust. The span is half-open UTF-8 bytes and must use uint32 values in source order, stay within the source, and end on code-point boundaries. It returns ordered blockText and zero-based annotation matches plus complete, partial, or none coverage. Ruby base/readings are independent channels. A match is exact only when its full forward coverage equals the input; otherwise it is overlap. Empty spans return no caret-like neighbor. Structural delimiters, synthetic separators, and unmapped text produce no invented canonical range, so forward/reverse mapping is not promised to be bijective across annotations, multi-to-one tokens, partial graphemes, discontinuities, or unmapped text.

Use resolveMdiSourceSpans(source, spans) for diagnostics or decoration batches. Rust validates all spans, parses/projects the document once, and returns one resolution per input span in the same order. Each separate call to the singular convenience API performs its own parse.

The one-argument EPUB and DOCX calls are synchronous Rust baseline exports:

import { renderEpub, renderDocx } from "@illusions-lab/mdi";
import { writeFile } from "node:fs/promises";
await writeFile("book.epub", renderEpub(source));
await writeFile("book.docx", renderDocx(source));

Use the two-argument overloads (or their explicit WithProfile names) for a publication export. Their Promise-shaped API is kept for compatibility, while profile validation and EPUB/DOCX generation run in Rust. JavaScript neither reparses MDI nor keeps another document generator.

import { renderEpub, renderDocx } from "@illusions-lab/mdi";
const epub = await renderEpub(source, {
profile: { layout: { system: "japanese-publisher" } },
title: "雨の東京",
author: "Illusions",
language: "ja",
publisher: "Illusions Lab",
identifier: "urn:isbn:example",
date: "2026-07-21",
verticalWriting: true,
fontFamily: "Yu Mincho",
textIndent: 1,
chapterSplitLevel: "h1",
coverImage: coverBytes,
coverMediaType: "image/png",
});
const docx = await renderDocx(source, {
layout: { system: "word" },
title: "雨の東京",
author: "Illusions",
verticalWriting: true,
fontFamily: "Yu Mincho",
fontSize: 11,
lineSpacing: 1.6,
textIndent: 1,
pagination: { gridMode: "typographic" },
pageSize: "A5",
landscape: false,
margins: { top: 18, right: 15, bottom: 18, left: 15 },
showPageNumbers: true,
pageNumberPosition: "bottom-center",
pageNumberFormat: "simple",
});

Both configured calls also accept the full nested ExportProfile schema through profile (EPUB) or directly (DOCX). The short fields above are aliases: EPUB supports metadata, writing direction, typeface, indent, chapter split, and a PNG/JPEG Uint8Array cover; DOCX supports metadata, direction, page size/orientation/margins, typeface/size/line spacing/indent, and page numbering (simple, dash, or fraction). See Export profiles for the complete JSON shape.

Every configured export must state layout.system. Choose "japanese-publisher" for a mirrored Japanese book: horizontal text defaults to 10 pt Mincho on Shirokuban, with a strict 27-character × 26-line left-bound grid; vertical text defaults to the A4-landscape novel manuscript, a strict 40-character × 30-line right-bound grid. Choose "word" for Word-style flowing pages: its default is A4 with 25.4 mm on every side, no mirror margins, and gridMode: "typographic"; "word" rejects "strict".

Rust owns parsing, diagnostics, source spans, profile validation, the canonical paper catalogue, and configured EPUB/DOCX generation. For PDF, Rust prepares the styled HTML, page geometry, and page-number templates; the host only controls Chromium and application UI. The configured DOCX exporter represents page breaks, vertical text, ordinary paragraph formatting, ruby/tate-chu-yoko/no-break/kern/blank constructs as far as OOXML permits, but it is not a byte-for-byte visual equivalent of browser HTML. Test the generated DOCX in the Word-compatible reader your users rely on when those Japanese composition details are critical.

PDF is deliberately in the Node-only entry point, so browser bundles do not acquire a browser launcher:

import { preparePdfExport, renderPdfWithChromium } from "@illusions-lab/mdi/node";
// Electron can take this HTML and call its own print-to-PDF API.
const request = preparePdfExport(source, profile);
// Node uses @illusions-lab/mdi-to-pdf when it is installed.
const pdf = await renderPdfWithChromium(source, profile);

Install @illusions-lab/mdi-to-pdf alongside @illusions-lab/mdi for the default Node/Playwright host. An Electron host may instead pass { renderHtmlToPdf(html, profile, sourceWritingMode) } to renderPdfWithChromium. Rust resolves PDF paper, landscape, margins, writing direction, font, font size/line spacing, character/line grids, indentation, and page-number settings. Browser/WASM consumers can create configured EPUB/DOCX locally; PDF alone must send preparePdfExport() to a Node, Electron, Tauri, or CLI host that can launch Chromium.

renderText, renderTextFormat, and serializeMdi are synchronous Rust functions. renderTextFormat accepts txt, txt-ruby, narou, kakuyomu, aozora, or note plus an optional indentation prefix. parseMdiSyntax is a deprecated alias for parse; MDI_SPEC_VERSION is "2.1" and MDI_IR_VERSION is "1.0".

Non-string source is a TypeError; invalid option objects are also rejected with TypeError. Treat diagnostics as document feedback, and reserve try/catch for programming, I/O, archive, or host-renderer failures. Source spans are UTF-8 byte offsets, not JavaScript string indices; see Diagnostics.

Automatic warichu uses two lines at 50% type. Use layoutMdiWarichu(children, { firstCapacity, continuationCapacity }) for Rust splitting, attachMdiWarichuLayout(container) for read-only HTML, and settleMdiPrintLayout(evaluate, { timeoutMs, signal, page: prepared.page }) before printing. Layout is presentation-only; canonical MDI is unchanged. Static EPUB/HTML preserve readable precomputed lines; reader reflow may vary.

parse(source, { includeComments: true })

MDI 2.1 recognizes <!-- note --> in all documents, including declared 2.0 and unversioned source. Comments can be empty, multiline or Unicode; the nearest --> closes them and their contents are not interpreted. Code, front matter, link destinations and plain-text MDI parameters remain literal. Escape an opener as \<!--.

Source saving retains comments. Default parse/prepare/mdast APIs omit them with IR 1.0; { includeComments: true } returns positional comment nodes and IR 1.1. Both report syntax 2.1. Existing front-matter declarations are retained. Public body projections and layout exclude comments even with an inclusive tree, and their source-map runs preserve the gaps.

Every publication format always omits valid comments. This intentionally changes old 2.0 output that displayed them as HTML text. Unterminated comments remain literal and return mdi.comment.unterminated: export is allowed, so intended private text may be visible.

Use source serialization or an inclusive IR for lossless comment retention. A filtered external IR cannot restore omitted comments.

MIT License© 2026 Iktahana