@illusions-lab/mdi 2.0.8 → 2.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,15 +62,74 @@ unsupported version.
62
62
 
63
63
  ## Rendering
64
64
 
65
- Rendering starts from the same Rust IR. Canonical MDI, plain text, HTML, EPUB,
66
- and DOCX renderers execute in Rust and are exposed through this package. PDF
67
- uses Rust HTML as its input to a host layout adapter such as
65
+ Rendering starts from the same Rust IR. Canonical MDI, plain text, HTML, and
66
+ the one-argument baseline EPUB/DOCX renderers execute in Rust and are exposed
67
+ through this package. PDF uses Rust HTML as its input to a host layout adapter such as
68
68
  `@illusions-lab/mdi-to-pdf`; the adapter may control Chromium, but it never
69
69
  parses MDI or produces semantic HTML.
70
70
 
71
71
  Browser WebAssembly cannot start Chromium. Browser code sends Rust-rendered
72
72
  HTML to a server or desktop host when it needs PDF output.
73
73
 
74
+ ### HTML, diagnostics, and host workflows
75
+
76
+ `renderHtml(source)` returns a standalone HTML document with the stable MDI
77
+ classes emitted by Rust. Pass `{ bodyOnly: true }` to embed its semantic body
78
+ in an application shell; this changes only the outer document wrapper, never
79
+ the MDI-to-HTML semantics.
80
+
81
+ ```ts
82
+ import { renderHtmlWithDiagnostics } from "@illusions-lab/mdi";
83
+
84
+ const result = renderHtmlWithDiagnostics(source, { bodyOnly: true });
85
+ preview.replaceChildren(htmlToDom(result.output));
86
+ showDiagnostics(result.diagnostics); // stable codes and UTF-8 source spans
87
+ buildOutline(result.headings); // source-backed heading nodes, not HTML scraping
88
+ ```
89
+
90
+ For a parse-first flow, call `prepareRender(source)` (or `parse(source)`) and
91
+ display `diagnostics` before choosing an exporter. The public Rust ABI accepts
92
+ source text for renderer calls today, so renderers re-enter the same
93
+ Rust-authoritative parser rather than accepting mutable JavaScript IR. This
94
+ keeps the source spans and error codes predictable and prevents JavaScript from
95
+ becoming a second syntax implementation.
96
+
97
+ Configuration ownership is deliberately split: Rust owns MDI parsing and
98
+ semantic HTML; publication profiles own EPUB/DOCX metadata and typesetting;
99
+ the host owns Chromium/Electron, paper-printer integration, and application UI
100
+ preferences. This keeps platform-specific pagination controls out of the
101
+ parser and lets Electron supply its own PDF adapter.
102
+
103
+ ### Configured EPUB and DOCX
104
+
105
+ For publication output, pass an export profile to the overloads (or use the
106
+ explicit `WithProfile` functions). These Node.js-only async paths map the
107
+ Rust-owned IR through the publication adapters and retain configuration for
108
+ metadata, chapter splitting, vertical writing, font selection, paper size,
109
+ margins, and page numbers. EPUB also accepts in-memory PNG or JPEG cover art.
110
+
111
+ ```ts
112
+ import { renderDocxWithProfile, renderEpubWithProfile } from "@illusions-lab/mdi";
113
+
114
+ const epub = await renderEpubWithProfile(source, {
115
+ profile: {
116
+ metadata: { title: "Book", author: "Author" },
117
+ typesetting: { writingMode: "vertical", fontFamily: "Noto Serif JP" },
118
+ epub: { chapterSplitLevel: "h1" },
119
+ },
120
+ cover: { data: coverBytes, mediaType: "image/png" },
121
+ });
122
+
123
+ const docx = await renderDocxWithProfile(source, {
124
+ pagination: { pageSize: "A5", margins: { top: 12, bottom: 12, left: 14, right: 14 } },
125
+ });
126
+ ```
127
+
128
+ `renderEpub(source)` and `renderDocx(source)` remain synchronous,
129
+ backward-compatible Rust baseline exports. `renderEpub(source, options)` and
130
+ `renderDocx(source, profile)` are equivalent async overloads for configured
131
+ publication output.
132
+
74
133
  ## Remark compatibility
75
134
 
76
135
  Remark support is an optional adapter between Rust IR and mdast. It exists for
@@ -0,0 +1,383 @@
1
+ // src/index.ts
2
+ import {
3
+ parseMdiSyntaxJson,
4
+ renderHtml as renderHtmlFromRust,
5
+ renderEpub as renderEpubFromRust,
6
+ renderDocx as renderDocxFromRust,
7
+ renderText as renderTextFromRust,
8
+ renderTextFormat as renderTextFormatFromRust,
9
+ serializeMdi as serializeMdiFromRust
10
+ } from "@illusions-lab/mdi-core";
11
+ import {
12
+ requireLayoutSystem
13
+ } from "@illusions-lab/mdi-export-profile";
14
+ import { parse as parseYaml } from "yaml";
15
+ var MDI_SPEC_VERSION = "2.0";
16
+ var MDI_IR_VERSION = "1.0";
17
+ function parse(source) {
18
+ if (typeof source !== "string") throw new TypeError("source must be a string");
19
+ const result = JSON.parse(parseMdiSyntaxJson(source));
20
+ if (result.irVersion !== MDI_IR_VERSION) {
21
+ throw new Error(`Unsupported MDI IR version: ${String(result.irVersion)}`);
22
+ }
23
+ return result;
24
+ }
25
+ function renderHtml(source, options) {
26
+ assertSource(source);
27
+ assertHtmlOptions(options);
28
+ const html = renderHtmlFromRust(source);
29
+ return options?.bodyOnly ? htmlBody(html) : html;
30
+ }
31
+ function renderHtmlWithDiagnostics(source, options) {
32
+ assertSource(source);
33
+ assertHtmlOptions(options);
34
+ return renderWithDiagnostics(source, () => renderHtml(source, options));
35
+ }
36
+ function prepareRender(source) {
37
+ return parse(source);
38
+ }
39
+ function renderEpub(source, options) {
40
+ if (typeof source !== "string") throw new TypeError("source must be a string");
41
+ if (options !== void 0) {
42
+ assertEpubOptions(options);
43
+ return renderEpubWithProfile(source, options);
44
+ }
45
+ return renderEpubFromRust(source);
46
+ }
47
+ function renderEpubWithDiagnostics(source, options) {
48
+ return options === void 0 ? renderWithDiagnostics(source, () => renderEpub(source)) : renderWithDiagnosticsAsync(source, () => renderEpub(source, options));
49
+ }
50
+ function renderDocx(source, profile) {
51
+ if (typeof source !== "string") throw new TypeError("source must be a string");
52
+ if (profile !== void 0) {
53
+ assertPlainObject(profile, "profile");
54
+ return renderDocxWithProfile(source, profile);
55
+ }
56
+ return renderDocxFromRust(source);
57
+ }
58
+ function renderDocxWithDiagnostics(source, profile) {
59
+ return profile === void 0 ? renderWithDiagnostics(source, () => renderDocx(source)) : renderWithDiagnosticsAsync(source, () => renderDocx(source, profile));
60
+ }
61
+ async function renderEpubWithProfile(source, options = {}) {
62
+ assertSource(source);
63
+ assertEpubOptions(options);
64
+ const { mdiToEpub } = await import("@illusions-lab/mdi-to-epub");
65
+ const normalized = normalizeEpubOptions(options);
66
+ requireLayoutSystem(normalized.profile);
67
+ return mdiToEpub(toPublicationMdast(parse(source).document), normalized);
68
+ }
69
+ async function renderDocxWithProfile(source, profile = {}) {
70
+ assertSource(source);
71
+ assertPlainObject(profile, "profile");
72
+ prepareNodeDocxImport();
73
+ const { mdiToDocx } = await import("@illusions-lab/mdi-to-docx");
74
+ const normalized = normalizeDocxProfile(profile);
75
+ requireLayoutSystem(normalized);
76
+ return mdiToDocx(toPublicationMdast(parse(source).document), normalized);
77
+ }
78
+ function prepareNodeDocxImport() {
79
+ const nodeProcess = globalThis.process;
80
+ if (nodeProcess?.release?.name === "node" && !nodeProcess.execArgv?.some((argument) => argument.startsWith("--localstorage-file="))) {
81
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
82
+ if (descriptor?.configurable && descriptor.get)
83
+ Object.defineProperty(globalThis, "localStorage", {
84
+ value: void 0,
85
+ configurable: true
86
+ });
87
+ }
88
+ }
89
+ function toPublicationMdast(document) {
90
+ const children = document.children.map(toPublicationMdastNode);
91
+ const tree = { type: "root", children };
92
+ if (document.frontmatter) {
93
+ children.unshift({ type: "yaml", value: document.frontmatter.raw });
94
+ const frontmatter = publicationFrontmatter(document.frontmatter.raw);
95
+ tree.data ??= {};
96
+ tree.data.frontmatter = frontmatter;
97
+ }
98
+ return tree;
99
+ }
100
+ function toPublicationMdastNode(node) {
101
+ const { span: _span, children, ...rest } = node;
102
+ const mapped = { ...rest };
103
+ if (children) mapped.children = children.map(toPublicationMdastNode);
104
+ switch (node.type) {
105
+ case "ruby": {
106
+ const ruby = node.ruby;
107
+ return { ...mapped, type: "mdiRuby", ruby: ruby.value };
108
+ }
109
+ case "tcy":
110
+ return { ...mapped, type: "mdiTcy" };
111
+ case "break":
112
+ return { ...mapped, type: "mdiBreak" };
113
+ case "em":
114
+ return { ...mapped, type: "mdiEm" };
115
+ case "noBreak":
116
+ return { ...mapped, type: "mdiNoBreak" };
117
+ case "warichu":
118
+ return { ...mapped, type: "mdiWarichu" };
119
+ case "kern":
120
+ return { ...mapped, type: "mdiKern" };
121
+ case "blank":
122
+ return { ...mapped, type: "mdiBlank" };
123
+ case "pagebreak": {
124
+ if (mapped.variant === null) delete mapped.variant;
125
+ return { ...mapped, type: "mdiPagebreak" };
126
+ }
127
+ case "paragraph": {
128
+ const data = {};
129
+ if (typeof mapped.indent === "number") data.mdiIndent = mapped.indent;
130
+ if (typeof mapped.bottom === "number") data.mdiBottom = mapped.bottom;
131
+ delete mapped.indent;
132
+ delete mapped.bottom;
133
+ if (Object.keys(data).length) mapped.data = data;
134
+ return mapped;
135
+ }
136
+ default:
137
+ return mapped;
138
+ }
139
+ }
140
+ function publicationFrontmatter(raw) {
141
+ let value;
142
+ try {
143
+ value = parseYaml(raw);
144
+ } catch {
145
+ value = void 0;
146
+ }
147
+ const source = isRecord(value) ? value : {};
148
+ const writingMode = source["writing-mode"] === "vertical" ? "vertical" : "horizontal";
149
+ return {
150
+ mdi: stringValue(source.mdi) ?? "2.0",
151
+ title: stringValue(source.title),
152
+ author: stringValue(source.author),
153
+ lang: stringValue(source.lang) ?? "ja",
154
+ date: stringValue(source.date),
155
+ writingMode,
156
+ pageProgression: source["page-progression"] === "ltr" || source["page-progression"] === "rtl" ? source["page-progression"] : writingMode === "vertical" ? "rtl" : "ltr"
157
+ };
158
+ }
159
+ function isRecord(value) {
160
+ return typeof value === "object" && value !== null && !Array.isArray(value);
161
+ }
162
+ function stringValue(value) {
163
+ return typeof value === "string" ? value : void 0;
164
+ }
165
+ function renderWithDiagnostics(source, render) {
166
+ const parsed = parse(source);
167
+ return {
168
+ output: render(),
169
+ document: parsed.document,
170
+ diagnostics: parsed.diagnostics,
171
+ headings: headingsFromDocument(parsed.document)
172
+ };
173
+ }
174
+ async function renderWithDiagnosticsAsync(source, render) {
175
+ const parsed = parse(source);
176
+ return {
177
+ output: await render(),
178
+ document: parsed.document,
179
+ diagnostics: parsed.diagnostics,
180
+ headings: headingsFromDocument(parsed.document)
181
+ };
182
+ }
183
+ function assertSource(source) {
184
+ if (typeof source !== "string") throw new TypeError("source must be a string");
185
+ }
186
+ function assertHtmlOptions(options) {
187
+ if (options === void 0) return;
188
+ assertPlainObject(options, "options");
189
+ if (options.bodyOnly !== void 0 && typeof options.bodyOnly !== "boolean") {
190
+ throw new TypeError("options.bodyOnly must be a boolean");
191
+ }
192
+ }
193
+ function htmlBody(html) {
194
+ const match = /<body(?:\s[^>]*)?>([\s\S]*)<\/body>/i.exec(html);
195
+ if (!match) throw new Error("Rust HTML renderer returned a document without a body");
196
+ return match[1];
197
+ }
198
+ function headingsFromDocument(document) {
199
+ const headings = [];
200
+ visitNodes(document.children, (node) => {
201
+ if (node.type !== "heading" || !isHeadingDepth(node.depth)) return;
202
+ headings.push({
203
+ depth: node.depth,
204
+ text: plainNodeText(node),
205
+ span: node.span,
206
+ node
207
+ });
208
+ });
209
+ return headings;
210
+ }
211
+ function visitNodes(nodes, visit) {
212
+ for (const node of nodes) {
213
+ visit(node);
214
+ if (node.children) visitNodes(node.children, visit);
215
+ }
216
+ }
217
+ function isHeadingDepth(value) {
218
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 6;
219
+ }
220
+ function plainNodeText(node) {
221
+ if (node.type === "ruby" && typeof node.base === "string") return node.base;
222
+ const value = node.value;
223
+ const ownText = typeof value === "string" ? value : "";
224
+ return ownText + (node.children?.map(plainNodeText).join("") ?? "");
225
+ }
226
+ function assertPlainObject(value, label) {
227
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
228
+ throw new TypeError(`${label} must be an object`);
229
+ }
230
+ }
231
+ function assertEpubOptions(options) {
232
+ assertPlainObject(options, "options");
233
+ if (options.cover !== void 0) {
234
+ assertPlainObject(options.cover, "options.cover");
235
+ if (!(options.cover.data instanceof Uint8Array)) {
236
+ throw new TypeError("options.cover.data must be a Uint8Array");
237
+ }
238
+ if (options.cover.mediaType !== "image/jpeg" && options.cover.mediaType !== "image/png") {
239
+ throw new TypeError("options.cover.mediaType must be image/jpeg or image/png");
240
+ }
241
+ }
242
+ if (options.coverImage !== void 0 && !(options.coverImage instanceof Uint8Array)) {
243
+ throw new TypeError("options.coverImage must be a Uint8Array");
244
+ }
245
+ if (options.coverMediaType !== void 0 && options.coverMediaType !== "image/jpeg" && options.coverMediaType !== "image/png") {
246
+ throw new TypeError("options.coverMediaType must be image/jpeg or image/png");
247
+ }
248
+ }
249
+ function normalizeEpubOptions(options) {
250
+ const {
251
+ title,
252
+ author,
253
+ publisher,
254
+ identifier,
255
+ language,
256
+ date,
257
+ verticalWriting,
258
+ fontFamily,
259
+ fontSize,
260
+ lineSpacing,
261
+ textIndent,
262
+ fullwidthSpaceIndent,
263
+ gridMode,
264
+ chapterSplitLevel,
265
+ coverImage,
266
+ coverMediaType,
267
+ cover,
268
+ profile
269
+ } = options;
270
+ const metadata = { ...profile?.metadata, ...defined({ title, author, publisher, identifier, language, date }) };
271
+ const writingMode = verticalWriting === void 0 ? void 0 : verticalWriting ? "vertical" : "horizontal";
272
+ const typesetting = {
273
+ ...profile?.typesetting,
274
+ ...defined({
275
+ writingMode,
276
+ fontFamily,
277
+ fontSize,
278
+ lineSpacing,
279
+ textIndentEm: textIndent,
280
+ fullwidthSpaceIndent
281
+ })
282
+ };
283
+ const epub = { ...profile?.epub, ...defined({ chapterSplitLevel }) };
284
+ return {
285
+ profile: {
286
+ ...profile,
287
+ metadata,
288
+ typesetting,
289
+ epub,
290
+ pagination: { ...profile?.pagination, ...defined({ gridMode }) }
291
+ },
292
+ cover: cover ?? (coverImage ? { data: coverImage, mediaType: coverMediaType ?? "image/png" } : void 0)
293
+ };
294
+ }
295
+ function normalizeDocxProfile(profile) {
296
+ const {
297
+ title,
298
+ author,
299
+ publisher,
300
+ identifier,
301
+ language,
302
+ date,
303
+ verticalWriting,
304
+ fontFamily,
305
+ fontSize,
306
+ lineSpacing,
307
+ textIndent,
308
+ pageSize,
309
+ landscape,
310
+ gridMode,
311
+ charactersPerLine,
312
+ linesPerPage,
313
+ margins,
314
+ fullwidthSpaceIndent,
315
+ showPageNumbers,
316
+ pageNumberPosition,
317
+ pageNumberFormat,
318
+ ...nested
319
+ } = profile;
320
+ const writingMode = verticalWriting === void 0 ? void 0 : verticalWriting ? "vertical" : "horizontal";
321
+ return {
322
+ ...nested,
323
+ metadata: { ...nested.metadata, ...defined({ title, author, publisher, identifier, language, date }) },
324
+ typesetting: {
325
+ ...nested.typesetting,
326
+ ...defined({ writingMode, fontFamily, fontSize, lineSpacing, textIndentEm: textIndent, fullwidthSpaceIndent })
327
+ },
328
+ pagination: {
329
+ ...nested.pagination,
330
+ ...defined({ pageSize, landscape, gridMode, charactersPerLine, linesPerPage, margins }),
331
+ pageNumbers: {
332
+ ...nested.pagination?.pageNumbers,
333
+ ...defined({ enabled: showPageNumbers, position: pageNumberPosition, format: pageNumberFormat })
334
+ }
335
+ }
336
+ };
337
+ }
338
+ function defined(value) {
339
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
340
+ }
341
+ function serializeMdi(source) {
342
+ if (typeof source !== "string") throw new TypeError("source must be a string");
343
+ return serializeMdiFromRust(source);
344
+ }
345
+ function renderText(source) {
346
+ if (typeof source !== "string") throw new TypeError("source must be a string");
347
+ return renderTextFromRust(source);
348
+ }
349
+ function renderTextWithDiagnostics(source) {
350
+ return renderWithDiagnostics(source, () => renderText(source));
351
+ }
352
+ function renderTextFormat(source, format, indentPrefix = "") {
353
+ if (typeof source !== "string" || typeof indentPrefix !== "string") {
354
+ throw new TypeError("source and indentPrefix must be strings");
355
+ }
356
+ return renderTextFormatFromRust(source, format, indentPrefix);
357
+ }
358
+ function renderTextFormatWithDiagnostics(source, format, indentPrefix = "") {
359
+ return renderWithDiagnostics(source, () => renderTextFormat(source, format, indentPrefix));
360
+ }
361
+ var parseMdiSyntax = parse;
362
+
363
+ export {
364
+ MDI_SPEC_VERSION,
365
+ MDI_IR_VERSION,
366
+ parse,
367
+ renderHtml,
368
+ renderHtmlWithDiagnostics,
369
+ prepareRender,
370
+ renderEpub,
371
+ renderEpubWithDiagnostics,
372
+ renderDocx,
373
+ renderDocxWithDiagnostics,
374
+ renderEpubWithProfile,
375
+ renderDocxWithProfile,
376
+ toPublicationMdast,
377
+ serializeMdi,
378
+ renderText,
379
+ renderTextWithDiagnostics,
380
+ renderTextFormat,
381
+ renderTextFormatWithDiagnostics,
382
+ parseMdiSyntax
383
+ };