@illusions-lab/mdi 2.0.25 → 2.0.26

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
@@ -217,3 +217,32 @@ executable syntax authority is `mdi-core`.
217
217
  - [Document IR and diagnostics](https://mdi.illusions.app/core/document-ir/)
218
218
  - [Rendering model](https://mdi.illusions.app/core/rendering/)
219
219
  - [JavaScript documentation](https://mdi.illusions.app/bindings/javascript/)
220
+
221
+ ### Automatic two-line notes
222
+
223
+ `layoutMdiWarichu(children, capacity)` remains available. To account for the
224
+ space before a note, pass `{ firstCapacity, continuationCapacity }` instead.
225
+ Both capacities use half-em units at the note's 50% font size. Rust returns
226
+ `lines`, `widths`, `overflow`, `hardBreakAfter`, `html`, and `sources`. Source
227
+ paths address the original inline children; byte offsets are UTF-8 leaf
228
+ boundaries, and `group` identifies indivisible units. Repeated text must be
229
+ mapped by these positions, never by searching its value.
230
+
231
+ For read-only rendered HTML, initialize MDI and call
232
+ `attachMdiWarichuLayout(container)`. The controller exposes `configure()`,
233
+ `settled({ timeoutMs, signal })`, and `dispose()`. Resize, font, and ancestor
234
+ style changes trigger presentation updates. Editors should use the layout
235
+ result with their own selection-preserving presentation layer.
236
+
237
+ `settleMdiPrintLayout(evaluate, { timeoutMs, signal, page: prepared.page })` works with a host's
238
+ existing hidden print document. Pass the resolved `page` from `prepareChromiumPrintProfile` so layout uses the paper's printable dimensions instead of the host window's viewport. Supply `code => page.evaluate(code)` for
239
+ Playwright or `code => webContents.executeJavaScript(code)` for Electron.
240
+ The helper waits for fonts and a stable Rust layout; timeout, cancellation,
241
+ and nonconvergence reject the promise and must prevent printing.
242
+
243
+ The adapter does not change canonical MDI or insert generated `[[br]]`.
244
+ No-script HTML/EPUB retain precomputed two-line spans. Their reading system
245
+ may reflow differently; proportional-font balance is an estimate, and this
246
+ API does not imply testing in Word or every EPUB reader.
247
+
248
+ Browser measurement accounts for actual rendered advances and inherited text insets; Rust still owns every split. This retains configured tracking while avoiding clipped notes and Chromium shrink-to-fit.
@@ -1,6 +1,261 @@
1
1
  // src/index.ts
2
2
  import * as mdiCore from "@illusions-lab/mdi-core";
3
3
  import { parse as parseYaml } from "yaml";
4
+
5
+ // src/warichu-browser.ts
6
+ function measureMdiWarichu(container = document.body, affected, minimumUnits) {
7
+ return Array.from(container.querySelectorAll("[data-mdi-warichu-source]")).flatMap((note, index) => {
8
+ if (note.parentElement?.closest("[data-mdi-warichu-source]")) return [];
9
+ if (affected && !affected.some((element) => element.contains(note))) return [];
10
+ const paragraph = note.closest("p,li,td,th,h1,h2,h3,h4,h5,h6") ?? note.parentElement ?? container;
11
+ const view = container.ownerDocument.defaultView;
12
+ const style = view.getComputedStyle(paragraph);
13
+ const vertical = style.writingMode.startsWith("vertical");
14
+ const box = paragraph.getBoundingClientRect();
15
+ const range = container.ownerDocument.createRange();
16
+ range.selectNodeContents(paragraph);
17
+ range.setEndBefore(note);
18
+ const rects = Array.from(range.getClientRects());
19
+ const previous = rects.at(-1);
20
+ const size = parseFloat(view.getComputedStyle(note).fontSize) || parseFloat(style.fontSize) / 2;
21
+ const paddingStart = parseFloat(style.paddingInlineStart) || 0;
22
+ const paddingEnd = parseFloat(style.paddingInlineEnd) || 0;
23
+ const full = (vertical ? paragraph.clientHeight : paragraph.clientWidth) - paddingStart - paddingEnd;
24
+ const scale = (vertical ? box.height / paragraph.offsetHeight : box.width / paragraph.offsetWidth) || 1;
25
+ let unit = Math.max(size / 2, minimumUnits?.[index] ?? 0);
26
+ let inset = 0;
27
+ for (const line of note.querySelectorAll(":scope > .mdi-warichu-fragment > .mdi-warichu-line[data-mdi-width]")) {
28
+ const width = Number(line.dataset.mdiWidth);
29
+ if (!(width > 0)) continue;
30
+ const contents = container.ownerDocument.createRange();
31
+ contents.selectNodeContents(line);
32
+ const bounds = contents.getBoundingClientRect();
33
+ const lineBox = line.getBoundingClientRect();
34
+ inset = Math.max(inset, (vertical ? bounds.top - lineBox.top : style.direction === "rtl" ? lineBox.right - bounds.right : bounds.left - lineBox.left) / scale);
35
+ unit = Math.max(unit, (vertical ? bounds.height : bounds.width) / scale / width);
36
+ }
37
+ const remaining = !previous ? full : vertical ? (box.bottom - previous.bottom) / scale - paddingEnd : style.direction === "rtl" ? (previous.left - box.left) / scale - paddingStart : (box.right - previous.right) / scale - paddingEnd;
38
+ return [{ index, unit, children: JSON.parse(note.dataset.mdiWarichuSource), options: { firstCapacity: Math.max(1, Math.floor(((remaining <= 0 ? full : Math.min(full, remaining)) - inset) / unit)), continuationCapacity: Math.max(1, Math.floor((full - inset) / unit)) } }];
39
+ });
40
+ }
41
+ function layoutMeasuredMdiWarichu(measurement) {
42
+ const { children, options } = measurement;
43
+ const fragments = layoutMdiWarichu(children, options);
44
+ const first = fragments[0];
45
+ return first?.overflow && options.firstCapacity < options.continuationCapacity && Math.max(...first.widths) <= options.continuationCapacity ? layoutMdiWarichu(children, { ...options, firstCapacity: options.continuationCapacity }) : fragments;
46
+ }
47
+ function applyMdiWarichu(updates, container = document.body) {
48
+ const notes = container.querySelectorAll("[data-mdi-warichu-source]");
49
+ let changed = false;
50
+ for (const { index, fragments } of updates) {
51
+ const note = notes[index];
52
+ if (!note) continue;
53
+ const html = fragments.map((fragment) => `<span class="mdi-warichu-fragment" style="display:inline-flex;flex-direction:column;vertical-align:middle;text-align:start"${fragment.overflow ? ' data-mdi-overflow="true"' : ""}>${fragment.html.map((line, row) => `<span class="mdi-warichu-line" data-mdi-width="${fragment.widths[row]}" style="display:block;white-space:nowrap;min-block-size:1em">${line}</span>`).join("")}</span>${fragment.hardBreakAfter ? "<br>" : ""}`).join("");
54
+ if (note.dataset.mdiWarichuLayout === html) continue;
55
+ note.innerHTML = html;
56
+ note.dataset.mdiWarichuLayout = html;
57
+ changed = true;
58
+ }
59
+ return changed;
60
+ }
61
+ function attachMdiWarichuLayout(container) {
62
+ const win = container.ownerDocument.defaultView;
63
+ const realm = win;
64
+ let disposed = false, scheduled = false, pending = false;
65
+ let affected = null;
66
+ let rejectDisposal;
67
+ const disposal = new Promise((_, reject) => {
68
+ rejectDisposal = reject;
69
+ });
70
+ void disposal.catch(() => {
71
+ });
72
+ let ready = Promise.resolve();
73
+ const publish = () => {
74
+ win.__mdiWarichuLayoutReady = ready;
75
+ void ready.catch(() => {
76
+ });
77
+ };
78
+ const frame = async () => {
79
+ let id = 0;
80
+ try {
81
+ await Promise.race([new Promise((resolve) => {
82
+ id = win.requestAnimationFrame(() => resolve());
83
+ }), disposal]);
84
+ } finally {
85
+ win.cancelAnimationFrame?.(id);
86
+ }
87
+ };
88
+ const observe = () => mutation.observe(container.ownerDocument.documentElement, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ["style", "class", "dir", "data-mdi-warichu-source"] });
89
+ const mutation = new realm.MutationObserver((records) => {
90
+ for (const record of records) {
91
+ if (!container.contains(record.target) && !record.target.contains(container)) continue;
92
+ const element = record.target.nodeType === 1 ? record.target : record.target.parentElement;
93
+ const paragraph = element?.closest("p,li,td,th,h1,h2,h3,h4,h5,h6");
94
+ invalidate(paragraph && container.contains(paragraph) ? paragraph : void 0);
95
+ }
96
+ });
97
+ const configure = () => invalidate();
98
+ const invalidate = (paragraph) => {
99
+ if (paragraph) affected?.add(paragraph);
100
+ else affected = null;
101
+ if (disposed) return;
102
+ pending = true;
103
+ if (scheduled) return;
104
+ scheduled = true;
105
+ ready = (async () => {
106
+ try {
107
+ do {
108
+ syncObservedParagraphs();
109
+ pending = false;
110
+ const batch = affected === null ? void 0 : Array.from(affected);
111
+ affected = /* @__PURE__ */ new Set();
112
+ await Promise.race([container.ownerDocument.fonts.ready, disposal]);
113
+ await frame();
114
+ let stable = false;
115
+ const minimumUnits = {};
116
+ for (let pass = 0; pass < 12; pass++) {
117
+ if (disposed) return;
118
+ const updates = measureMdiWarichu(container, batch, minimumUnits).map((m) => {
119
+ if (m.unit) minimumUnits[m.index] = m.unit;
120
+ return { index: m.index, fragments: layoutMeasuredMdiWarichu(m) };
121
+ });
122
+ mutation.disconnect();
123
+ let changed;
124
+ try {
125
+ changed = applyMdiWarichu(updates, container);
126
+ } finally {
127
+ observe();
128
+ }
129
+ await frame();
130
+ if (!changed) {
131
+ stable = true;
132
+ break;
133
+ }
134
+ }
135
+ if (!stable) throw new Error("Warichu layout did not stabilize");
136
+ } while (pending && !disposed);
137
+ } finally {
138
+ scheduled = false;
139
+ }
140
+ })();
141
+ publish();
142
+ };
143
+ const resize = new realm.ResizeObserver((entries) => {
144
+ for (const entry of entries) invalidate(entry.target === container ? void 0 : entry.target);
145
+ });
146
+ resize.observe(container);
147
+ const observed = /* @__PURE__ */ new Set();
148
+ const syncObservedParagraphs = () => {
149
+ const paragraphs = new Set(container.querySelectorAll("p,li,td,th,h1,h2,h3,h4,h5,h6"));
150
+ for (const paragraph of observed) if (!paragraphs.has(paragraph)) {
151
+ resize.unobserve(paragraph);
152
+ observed.delete(paragraph);
153
+ }
154
+ for (const paragraph of paragraphs) if (!observed.has(paragraph)) {
155
+ resize.observe(paragraph);
156
+ observed.add(paragraph);
157
+ }
158
+ };
159
+ syncObservedParagraphs();
160
+ observe();
161
+ container.ownerDocument.fonts.addEventListener("loadingdone", configure);
162
+ win.addEventListener("resize", configure);
163
+ win.visualViewport?.addEventListener("resize", configure);
164
+ configure();
165
+ return { configure, settled: ({ timeoutMs = 1e4, signal } = {}) => new Promise((resolve, reject) => {
166
+ if (signal?.aborted) {
167
+ reject(signal.reason ?? new Error("Warichu layout cancelled"));
168
+ return;
169
+ }
170
+ const finish = (error) => {
171
+ clearTimeout(timer);
172
+ signal?.removeEventListener("abort", abort);
173
+ error ? reject(error) : resolve();
174
+ };
175
+ const abort = () => finish(signal?.reason ?? new Error("Warichu layout cancelled"));
176
+ const timer = setTimeout(() => finish(new Error("Warichu layout timed out")), timeoutMs);
177
+ signal?.addEventListener("abort", abort, { once: true });
178
+ const latest = async () => {
179
+ let current;
180
+ do {
181
+ current = ready;
182
+ await Promise.race([current, disposal]);
183
+ } while (current !== ready);
184
+ };
185
+ latest().then(() => finish(), finish);
186
+ }), dispose: () => {
187
+ disposed = true;
188
+ rejectDisposal(new Error("Warichu layout disposed"));
189
+ resize.disconnect();
190
+ mutation.disconnect();
191
+ container.ownerDocument.fonts.removeEventListener("loadingdone", configure);
192
+ win.removeEventListener("resize", configure);
193
+ win.visualViewport?.removeEventListener("resize", configure);
194
+ } };
195
+ }
196
+
197
+ // src/warichu-print.ts
198
+ async function settleMdiPrintLayout(evaluate, { timeoutMs = 1e4, signal, page } = {}) {
199
+ let contentWidthMm, contentHeightMm;
200
+ if (page) {
201
+ contentWidthMm = page.widthMm - page.marginsMm.left - page.marginsMm.right;
202
+ contentHeightMm = page.heightMm - page.marginsMm.top - page.marginsMm.bottom;
203
+ if (!Number.isFinite(contentWidthMm) || !Number.isFinite(contentHeightMm) || contentWidthMm <= 0 || contentHeightMm <= 0) {
204
+ throw new RangeError("MDI print page must have positive finite printable dimensions");
205
+ }
206
+ }
207
+ let timer;
208
+ let abort = () => {
209
+ };
210
+ let stopped = false;
211
+ const failure = new Promise((_, reject) => {
212
+ abort = () => {
213
+ stopped = true;
214
+ reject(signal?.reason ?? new Error("MDI print layout cancelled"));
215
+ };
216
+ if (signal?.aborted) {
217
+ abort();
218
+ return;
219
+ }
220
+ signal?.addEventListener("abort", abort, { once: true });
221
+ timer = setTimeout(() => {
222
+ stopped = true;
223
+ reject(new Error("MDI print layout timed out"));
224
+ }, timeoutMs);
225
+ });
226
+ const run = async () => {
227
+ if (stopped) return;
228
+ if (page) {
229
+ await evaluate(`document.body.style.setProperty("inline-size", (getComputedStyle(document.body).writingMode.startsWith("vertical") ? ${contentHeightMm} : ${contentWidthMm}) + "mm")`);
230
+ if (stopped) return;
231
+ }
232
+ await evaluate("document.fonts.ready.then(() => undefined)");
233
+ const minimumUnits = {};
234
+ for (let pass = 0; pass < 12; pass++) {
235
+ if (stopped) return;
236
+ const measurements = await evaluate(`(${measureMdiWarichu.toString()})(document.body,undefined,${JSON.stringify(minimumUnits)})`);
237
+ if (stopped) return;
238
+ const updates = measurements.map((m) => {
239
+ if (m.unit) minimumUnits[m.index] = m.unit;
240
+ return { index: m.index, fragments: layoutMeasuredMdiWarichu(m) };
241
+ });
242
+ const changed = await evaluate(`(${applyMdiWarichu.toString()})(${JSON.stringify(updates)})`);
243
+ if (stopped) return;
244
+ await evaluate("new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())))");
245
+ if (!changed) return;
246
+ }
247
+ throw new Error("MDI print layout did not stabilize");
248
+ };
249
+ try {
250
+ await Promise.race([failure, run()]);
251
+ } finally {
252
+ stopped = true;
253
+ clearTimeout(timer);
254
+ signal?.removeEventListener("abort", abort);
255
+ }
256
+ }
257
+
258
+ // src/index.ts
4
259
  var {
5
260
  getMdiTextBlocksJson,
6
261
  resolveMdiSourceSpansJson,
@@ -229,12 +484,12 @@ async function renderDocxWithProfile(source, profile = {}) {
229
484
  requireLayoutSystem(normalized);
230
485
  return renderDocxWithProfileFromRust(source, JSON.stringify(normalized));
231
486
  }
232
- function toPublicationMdast(document) {
233
- const children = document.children.map(toPublicationMdastNode);
487
+ function toPublicationMdast(document2) {
488
+ const children = document2.children.map(toPublicationMdastNode);
234
489
  const tree = { type: "root", children };
235
- if (document.frontmatter) {
236
- children.unshift({ type: "yaml", value: document.frontmatter.raw });
237
- const frontmatter = publicationFrontmatter(document.frontmatter.raw);
490
+ if (document2.frontmatter) {
491
+ children.unshift({ type: "yaml", value: document2.frontmatter.raw });
492
+ const frontmatter = publicationFrontmatter(document2.frontmatter.raw);
238
493
  tree.data ??= {};
239
494
  tree.data.frontmatter = frontmatter;
240
495
  }
@@ -338,9 +593,9 @@ function htmlBody(html) {
338
593
  if (!match) throw new Error("Rust HTML renderer returned a document without a body");
339
594
  return match[1];
340
595
  }
341
- function headingsFromDocument(document) {
596
+ function headingsFromDocument(document2) {
342
597
  const headings = [];
343
- visitNodes(document.children, (node) => {
598
+ visitNodes(document2.children, (node) => {
344
599
  if (node.type !== "heading" || !isHeadingDepth(node.depth)) return;
345
600
  headings.push({
346
601
  depth: node.depth,
@@ -502,8 +757,21 @@ function renderTextFormatWithDiagnostics(source, format, indentPrefix = "") {
502
757
  return renderWithDiagnostics(source, () => renderTextFormat(source, format, indentPrefix));
503
758
  }
504
759
  var parseMdiSyntax = parse;
760
+ function layoutMdiWarichu(children, capacity = 40) {
761
+ const options = typeof capacity === "number" ? { firstCapacity: capacity, continuationCapacity: capacity } : capacity;
762
+ for (const value of [options.firstCapacity, options.continuationCapacity]) {
763
+ if (!Number.isSafeInteger(value) || value < 1 || value > 4294967295) {
764
+ throw new RangeError("Warichu capacity must be a positive u32 integer");
765
+ }
766
+ }
767
+ return JSON.parse(mdiCore.layoutWarichuOptionsJson(JSON.stringify(children), JSON.stringify(options)));
768
+ }
505
769
 
506
770
  export {
771
+ measureMdiWarichu,
772
+ applyMdiWarichu,
773
+ attachMdiWarichuLayout,
774
+ settleMdiPrintLayout,
507
775
  initializeMdi,
508
776
  MDI_SPEC_VERSION,
509
777
  MDI_IR_VERSION,
@@ -531,5 +799,6 @@ export {
531
799
  renderTextWithDiagnostics,
532
800
  renderTextFormat,
533
801
  renderTextFormatWithDiagnostics,
534
- parseMdiSyntax
802
+ parseMdiSyntax,
803
+ layoutMdiWarichu
535
804
  };
package/dist/index.cjs CHANGED
@@ -33,10 +33,14 @@ __export(index_exports, {
33
33
  MDI_IR_VERSION: () => MDI_IR_VERSION,
34
34
  MDI_SPEC_VERSION: () => MDI_SPEC_VERSION,
35
35
  MDI_TEXT_PROJECTION_VERSION: () => MDI_TEXT_PROJECTION_VERSION,
36
+ applyMdiWarichu: () => applyMdiWarichu,
37
+ attachMdiWarichuLayout: () => attachMdiWarichuLayout,
36
38
  formatMdiTextPosition: () => formatMdiTextPosition,
37
39
  formatMdiTextRange: () => formatMdiTextRange,
38
40
  getMdiTextBlocks: () => getMdiTextBlocks,
39
41
  initializeMdi: () => initializeMdi,
42
+ layoutMdiWarichu: () => layoutMdiWarichu,
43
+ measureMdiWarichu: () => measureMdiWarichu,
40
44
  parse: () => parse,
41
45
  parseMdiSyntax: () => parseMdiSyntax,
42
46
  parseMdiTextPosition: () => parseMdiTextPosition,
@@ -56,12 +60,268 @@ __export(index_exports, {
56
60
  resolveMdiSourceSpan: () => resolveMdiSourceSpan,
57
61
  resolveMdiSourceSpans: () => resolveMdiSourceSpans,
58
62
  serializeMdi: () => serializeMdi,
63
+ settleMdiPrintLayout: () => settleMdiPrintLayout,
59
64
  sourceSpansForTextRange: () => sourceSpansForTextRange,
60
65
  toPublicationMdast: () => toPublicationMdast
61
66
  });
62
67
  module.exports = __toCommonJS(index_exports);
63
68
  var mdiCore = __toESM(require("@illusions-lab/mdi-core"), 1);
64
69
  var import_yaml = require("yaml");
70
+
71
+ // src/warichu-browser.ts
72
+ function measureMdiWarichu(container = document.body, affected, minimumUnits) {
73
+ return Array.from(container.querySelectorAll("[data-mdi-warichu-source]")).flatMap((note, index) => {
74
+ if (note.parentElement?.closest("[data-mdi-warichu-source]")) return [];
75
+ if (affected && !affected.some((element) => element.contains(note))) return [];
76
+ const paragraph = note.closest("p,li,td,th,h1,h2,h3,h4,h5,h6") ?? note.parentElement ?? container;
77
+ const view = container.ownerDocument.defaultView;
78
+ const style = view.getComputedStyle(paragraph);
79
+ const vertical = style.writingMode.startsWith("vertical");
80
+ const box = paragraph.getBoundingClientRect();
81
+ const range = container.ownerDocument.createRange();
82
+ range.selectNodeContents(paragraph);
83
+ range.setEndBefore(note);
84
+ const rects = Array.from(range.getClientRects());
85
+ const previous = rects.at(-1);
86
+ const size = parseFloat(view.getComputedStyle(note).fontSize) || parseFloat(style.fontSize) / 2;
87
+ const paddingStart = parseFloat(style.paddingInlineStart) || 0;
88
+ const paddingEnd = parseFloat(style.paddingInlineEnd) || 0;
89
+ const full = (vertical ? paragraph.clientHeight : paragraph.clientWidth) - paddingStart - paddingEnd;
90
+ const scale = (vertical ? box.height / paragraph.offsetHeight : box.width / paragraph.offsetWidth) || 1;
91
+ let unit = Math.max(size / 2, minimumUnits?.[index] ?? 0);
92
+ let inset = 0;
93
+ for (const line of note.querySelectorAll(":scope > .mdi-warichu-fragment > .mdi-warichu-line[data-mdi-width]")) {
94
+ const width = Number(line.dataset.mdiWidth);
95
+ if (!(width > 0)) continue;
96
+ const contents = container.ownerDocument.createRange();
97
+ contents.selectNodeContents(line);
98
+ const bounds = contents.getBoundingClientRect();
99
+ const lineBox = line.getBoundingClientRect();
100
+ inset = Math.max(inset, (vertical ? bounds.top - lineBox.top : style.direction === "rtl" ? lineBox.right - bounds.right : bounds.left - lineBox.left) / scale);
101
+ unit = Math.max(unit, (vertical ? bounds.height : bounds.width) / scale / width);
102
+ }
103
+ const remaining = !previous ? full : vertical ? (box.bottom - previous.bottom) / scale - paddingEnd : style.direction === "rtl" ? (previous.left - box.left) / scale - paddingStart : (box.right - previous.right) / scale - paddingEnd;
104
+ return [{ index, unit, children: JSON.parse(note.dataset.mdiWarichuSource), options: { firstCapacity: Math.max(1, Math.floor(((remaining <= 0 ? full : Math.min(full, remaining)) - inset) / unit)), continuationCapacity: Math.max(1, Math.floor((full - inset) / unit)) } }];
105
+ });
106
+ }
107
+ function layoutMeasuredMdiWarichu(measurement) {
108
+ const { children, options } = measurement;
109
+ const fragments = layoutMdiWarichu(children, options);
110
+ const first = fragments[0];
111
+ return first?.overflow && options.firstCapacity < options.continuationCapacity && Math.max(...first.widths) <= options.continuationCapacity ? layoutMdiWarichu(children, { ...options, firstCapacity: options.continuationCapacity }) : fragments;
112
+ }
113
+ function applyMdiWarichu(updates, container = document.body) {
114
+ const notes = container.querySelectorAll("[data-mdi-warichu-source]");
115
+ let changed = false;
116
+ for (const { index, fragments } of updates) {
117
+ const note = notes[index];
118
+ if (!note) continue;
119
+ const html = fragments.map((fragment) => `<span class="mdi-warichu-fragment" style="display:inline-flex;flex-direction:column;vertical-align:middle;text-align:start"${fragment.overflow ? ' data-mdi-overflow="true"' : ""}>${fragment.html.map((line, row) => `<span class="mdi-warichu-line" data-mdi-width="${fragment.widths[row]}" style="display:block;white-space:nowrap;min-block-size:1em">${line}</span>`).join("")}</span>${fragment.hardBreakAfter ? "<br>" : ""}`).join("");
120
+ if (note.dataset.mdiWarichuLayout === html) continue;
121
+ note.innerHTML = html;
122
+ note.dataset.mdiWarichuLayout = html;
123
+ changed = true;
124
+ }
125
+ return changed;
126
+ }
127
+ function attachMdiWarichuLayout(container) {
128
+ const win = container.ownerDocument.defaultView;
129
+ const realm = win;
130
+ let disposed = false, scheduled = false, pending = false;
131
+ let affected = null;
132
+ let rejectDisposal;
133
+ const disposal = new Promise((_, reject) => {
134
+ rejectDisposal = reject;
135
+ });
136
+ void disposal.catch(() => {
137
+ });
138
+ let ready = Promise.resolve();
139
+ const publish = () => {
140
+ win.__mdiWarichuLayoutReady = ready;
141
+ void ready.catch(() => {
142
+ });
143
+ };
144
+ const frame = async () => {
145
+ let id = 0;
146
+ try {
147
+ await Promise.race([new Promise((resolve) => {
148
+ id = win.requestAnimationFrame(() => resolve());
149
+ }), disposal]);
150
+ } finally {
151
+ win.cancelAnimationFrame?.(id);
152
+ }
153
+ };
154
+ const observe = () => mutation.observe(container.ownerDocument.documentElement, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ["style", "class", "dir", "data-mdi-warichu-source"] });
155
+ const mutation = new realm.MutationObserver((records) => {
156
+ for (const record of records) {
157
+ if (!container.contains(record.target) && !record.target.contains(container)) continue;
158
+ const element = record.target.nodeType === 1 ? record.target : record.target.parentElement;
159
+ const paragraph = element?.closest("p,li,td,th,h1,h2,h3,h4,h5,h6");
160
+ invalidate(paragraph && container.contains(paragraph) ? paragraph : void 0);
161
+ }
162
+ });
163
+ const configure = () => invalidate();
164
+ const invalidate = (paragraph) => {
165
+ if (paragraph) affected?.add(paragraph);
166
+ else affected = null;
167
+ if (disposed) return;
168
+ pending = true;
169
+ if (scheduled) return;
170
+ scheduled = true;
171
+ ready = (async () => {
172
+ try {
173
+ do {
174
+ syncObservedParagraphs();
175
+ pending = false;
176
+ const batch = affected === null ? void 0 : Array.from(affected);
177
+ affected = /* @__PURE__ */ new Set();
178
+ await Promise.race([container.ownerDocument.fonts.ready, disposal]);
179
+ await frame();
180
+ let stable = false;
181
+ const minimumUnits = {};
182
+ for (let pass = 0; pass < 12; pass++) {
183
+ if (disposed) return;
184
+ const updates = measureMdiWarichu(container, batch, minimumUnits).map((m) => {
185
+ if (m.unit) minimumUnits[m.index] = m.unit;
186
+ return { index: m.index, fragments: layoutMeasuredMdiWarichu(m) };
187
+ });
188
+ mutation.disconnect();
189
+ let changed;
190
+ try {
191
+ changed = applyMdiWarichu(updates, container);
192
+ } finally {
193
+ observe();
194
+ }
195
+ await frame();
196
+ if (!changed) {
197
+ stable = true;
198
+ break;
199
+ }
200
+ }
201
+ if (!stable) throw new Error("Warichu layout did not stabilize");
202
+ } while (pending && !disposed);
203
+ } finally {
204
+ scheduled = false;
205
+ }
206
+ })();
207
+ publish();
208
+ };
209
+ const resize = new realm.ResizeObserver((entries) => {
210
+ for (const entry of entries) invalidate(entry.target === container ? void 0 : entry.target);
211
+ });
212
+ resize.observe(container);
213
+ const observed = /* @__PURE__ */ new Set();
214
+ const syncObservedParagraphs = () => {
215
+ const paragraphs = new Set(container.querySelectorAll("p,li,td,th,h1,h2,h3,h4,h5,h6"));
216
+ for (const paragraph of observed) if (!paragraphs.has(paragraph)) {
217
+ resize.unobserve(paragraph);
218
+ observed.delete(paragraph);
219
+ }
220
+ for (const paragraph of paragraphs) if (!observed.has(paragraph)) {
221
+ resize.observe(paragraph);
222
+ observed.add(paragraph);
223
+ }
224
+ };
225
+ syncObservedParagraphs();
226
+ observe();
227
+ container.ownerDocument.fonts.addEventListener("loadingdone", configure);
228
+ win.addEventListener("resize", configure);
229
+ win.visualViewport?.addEventListener("resize", configure);
230
+ configure();
231
+ return { configure, settled: ({ timeoutMs = 1e4, signal } = {}) => new Promise((resolve, reject) => {
232
+ if (signal?.aborted) {
233
+ reject(signal.reason ?? new Error("Warichu layout cancelled"));
234
+ return;
235
+ }
236
+ const finish = (error) => {
237
+ clearTimeout(timer);
238
+ signal?.removeEventListener("abort", abort);
239
+ error ? reject(error) : resolve();
240
+ };
241
+ const abort = () => finish(signal?.reason ?? new Error("Warichu layout cancelled"));
242
+ const timer = setTimeout(() => finish(new Error("Warichu layout timed out")), timeoutMs);
243
+ signal?.addEventListener("abort", abort, { once: true });
244
+ const latest = async () => {
245
+ let current;
246
+ do {
247
+ current = ready;
248
+ await Promise.race([current, disposal]);
249
+ } while (current !== ready);
250
+ };
251
+ latest().then(() => finish(), finish);
252
+ }), dispose: () => {
253
+ disposed = true;
254
+ rejectDisposal(new Error("Warichu layout disposed"));
255
+ resize.disconnect();
256
+ mutation.disconnect();
257
+ container.ownerDocument.fonts.removeEventListener("loadingdone", configure);
258
+ win.removeEventListener("resize", configure);
259
+ win.visualViewport?.removeEventListener("resize", configure);
260
+ } };
261
+ }
262
+
263
+ // src/warichu-print.ts
264
+ async function settleMdiPrintLayout(evaluate, { timeoutMs = 1e4, signal, page } = {}) {
265
+ let contentWidthMm, contentHeightMm;
266
+ if (page) {
267
+ contentWidthMm = page.widthMm - page.marginsMm.left - page.marginsMm.right;
268
+ contentHeightMm = page.heightMm - page.marginsMm.top - page.marginsMm.bottom;
269
+ if (!Number.isFinite(contentWidthMm) || !Number.isFinite(contentHeightMm) || contentWidthMm <= 0 || contentHeightMm <= 0) {
270
+ throw new RangeError("MDI print page must have positive finite printable dimensions");
271
+ }
272
+ }
273
+ let timer;
274
+ let abort = () => {
275
+ };
276
+ let stopped = false;
277
+ const failure = new Promise((_, reject) => {
278
+ abort = () => {
279
+ stopped = true;
280
+ reject(signal?.reason ?? new Error("MDI print layout cancelled"));
281
+ };
282
+ if (signal?.aborted) {
283
+ abort();
284
+ return;
285
+ }
286
+ signal?.addEventListener("abort", abort, { once: true });
287
+ timer = setTimeout(() => {
288
+ stopped = true;
289
+ reject(new Error("MDI print layout timed out"));
290
+ }, timeoutMs);
291
+ });
292
+ const run = async () => {
293
+ if (stopped) return;
294
+ if (page) {
295
+ await evaluate(`document.body.style.setProperty("inline-size", (getComputedStyle(document.body).writingMode.startsWith("vertical") ? ${contentHeightMm} : ${contentWidthMm}) + "mm")`);
296
+ if (stopped) return;
297
+ }
298
+ await evaluate("document.fonts.ready.then(() => undefined)");
299
+ const minimumUnits = {};
300
+ for (let pass = 0; pass < 12; pass++) {
301
+ if (stopped) return;
302
+ const measurements = await evaluate(`(${measureMdiWarichu.toString()})(document.body,undefined,${JSON.stringify(minimumUnits)})`);
303
+ if (stopped) return;
304
+ const updates = measurements.map((m) => {
305
+ if (m.unit) minimumUnits[m.index] = m.unit;
306
+ return { index: m.index, fragments: layoutMeasuredMdiWarichu(m) };
307
+ });
308
+ const changed = await evaluate(`(${applyMdiWarichu.toString()})(${JSON.stringify(updates)})`);
309
+ if (stopped) return;
310
+ await evaluate("new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())))");
311
+ if (!changed) return;
312
+ }
313
+ throw new Error("MDI print layout did not stabilize");
314
+ };
315
+ try {
316
+ await Promise.race([failure, run()]);
317
+ } finally {
318
+ stopped = true;
319
+ clearTimeout(timer);
320
+ signal?.removeEventListener("abort", abort);
321
+ }
322
+ }
323
+
324
+ // src/index.ts
65
325
  var {
66
326
  getMdiTextBlocksJson,
67
327
  resolveMdiSourceSpansJson,
@@ -290,12 +550,12 @@ async function renderDocxWithProfile(source, profile = {}) {
290
550
  requireLayoutSystem(normalized);
291
551
  return renderDocxWithProfileFromRust(source, JSON.stringify(normalized));
292
552
  }
293
- function toPublicationMdast(document) {
294
- const children = document.children.map(toPublicationMdastNode);
553
+ function toPublicationMdast(document2) {
554
+ const children = document2.children.map(toPublicationMdastNode);
295
555
  const tree = { type: "root", children };
296
- if (document.frontmatter) {
297
- children.unshift({ type: "yaml", value: document.frontmatter.raw });
298
- const frontmatter = publicationFrontmatter(document.frontmatter.raw);
556
+ if (document2.frontmatter) {
557
+ children.unshift({ type: "yaml", value: document2.frontmatter.raw });
558
+ const frontmatter = publicationFrontmatter(document2.frontmatter.raw);
299
559
  tree.data ??= {};
300
560
  tree.data.frontmatter = frontmatter;
301
561
  }
@@ -399,9 +659,9 @@ function htmlBody(html) {
399
659
  if (!match) throw new Error("Rust HTML renderer returned a document without a body");
400
660
  return match[1];
401
661
  }
402
- function headingsFromDocument(document) {
662
+ function headingsFromDocument(document2) {
403
663
  const headings = [];
404
- visitNodes(document.children, (node) => {
664
+ visitNodes(document2.children, (node) => {
405
665
  if (node.type !== "heading" || !isHeadingDepth(node.depth)) return;
406
666
  headings.push({
407
667
  depth: node.depth,
@@ -563,15 +823,28 @@ function renderTextFormatWithDiagnostics(source, format, indentPrefix = "") {
563
823
  return renderWithDiagnostics(source, () => renderTextFormat(source, format, indentPrefix));
564
824
  }
565
825
  var parseMdiSyntax = parse;
826
+ function layoutMdiWarichu(children, capacity = 40) {
827
+ const options = typeof capacity === "number" ? { firstCapacity: capacity, continuationCapacity: capacity } : capacity;
828
+ for (const value of [options.firstCapacity, options.continuationCapacity]) {
829
+ if (!Number.isSafeInteger(value) || value < 1 || value > 4294967295) {
830
+ throw new RangeError("Warichu capacity must be a positive u32 integer");
831
+ }
832
+ }
833
+ return JSON.parse(mdiCore.layoutWarichuOptionsJson(JSON.stringify(children), JSON.stringify(options)));
834
+ }
566
835
  // Annotate the CommonJS export names for ESM import in node:
567
836
  0 && (module.exports = {
568
837
  MDI_IR_VERSION,
569
838
  MDI_SPEC_VERSION,
570
839
  MDI_TEXT_PROJECTION_VERSION,
840
+ applyMdiWarichu,
841
+ attachMdiWarichuLayout,
571
842
  formatMdiTextPosition,
572
843
  formatMdiTextRange,
573
844
  getMdiTextBlocks,
574
845
  initializeMdi,
846
+ layoutMdiWarichu,
847
+ measureMdiWarichu,
575
848
  parse,
576
849
  parseMdiSyntax,
577
850
  parseMdiTextPosition,
@@ -591,6 +864,7 @@ var parseMdiSyntax = parse;
591
864
  resolveMdiSourceSpan,
592
865
  resolveMdiSourceSpans,
593
866
  serializeMdi,
867
+ settleMdiPrintLayout,
594
868
  sourceSpansForTextRange,
595
869
  toPublicationMdast
596
870
  });
package/dist/index.d.cts CHANGED
@@ -4,6 +4,52 @@ import { ExportProfile } from '@illusions-lab/mdi-export-profile';
4
4
  export { ExportProfile } from '@illusions-lab/mdi-export-profile';
5
5
  import { Root } from 'mdast';
6
6
 
7
+ interface MdiWarichuMeasurement {
8
+ index: number;
9
+ children: Record<string, unknown>[];
10
+ options: MdiWarichuOptions;
11
+ /** Measured CSS pixels per Rust half-em unit for this layout pass. */
12
+ unit?: number;
13
+ }
14
+ /** Self-contained for execution in a browser hosted by Electron or Playwright. */
15
+ declare function measureMdiWarichu(container?: HTMLElement, affected?: readonly HTMLElement[], minimumUnits?: Readonly<Record<number, number>>): MdiWarichuMeasurement[];
16
+ /** Applies Rust-rendered presentation only; never modifies canonical source. */
17
+ declare function applyMdiWarichu(updates: {
18
+ index: number;
19
+ fragments: MdiWarichuFragment[];
20
+ }[], container?: HTMLElement): boolean;
21
+ interface MdiWarichuSettleOptions {
22
+ timeoutMs?: number;
23
+ signal?: AbortSignal;
24
+ }
25
+ interface MdiWarichuLayoutController {
26
+ configure(): void;
27
+ settled(options?: MdiWarichuSettleOptions): Promise<void>;
28
+ dispose(): void;
29
+ }
30
+ /** Attach to read-only Rust-rendered HTML. Editable editors consume layoutMdiWarichu directly. */
31
+ declare function attachMdiWarichuLayout(container: HTMLElement): MdiWarichuLayoutController;
32
+
33
+ /** Execute JavaScript in the print document. Electron and Playwright can supply their existing page. */
34
+ type MdiPrintEvaluate = (javascript: string) => Promise<unknown>;
35
+ /** Physical page dimensions resolved by the upstream Chromium print profile. */
36
+ interface MdiPrintPage {
37
+ widthMm: number;
38
+ heightMm: number;
39
+ marginsMm: {
40
+ top: number;
41
+ right: number;
42
+ bottom: number;
43
+ left: number;
44
+ };
45
+ }
46
+ interface MdiPrintLayoutOptions extends MdiWarichuSettleOptions {
47
+ /** Pass prepared.page so measurement uses paper space rather than the host window viewport. */
48
+ page?: MdiPrintPage;
49
+ }
50
+ /** Wait for fonts, then measure and apply the shared Rust layout before permitting printing. */
51
+ declare function settleMdiPrintLayout(evaluate: MdiPrintEvaluate, { timeoutMs, signal, page }?: MdiPrintLayoutOptions): Promise<void>;
52
+
7
53
  /**
8
54
  * Initialize the shared MDI core runtime exactly once.
9
55
  *
@@ -349,5 +395,27 @@ declare function renderTextFormat(source: string, format: MdiTextFormat, indentP
349
395
  declare function renderTextFormatWithDiagnostics(source: string, format: MdiTextFormat, indentPrefix?: string): MdiRenderResult<string>;
350
396
  /** @deprecated Use {@link parse}; it now parses the complete document. */
351
397
  declare const parseMdiSyntax: typeof parse;
398
+ /** UTF-8 boundaries in an original inline leaf; paths are relative to children. */
399
+ interface MdiWarichuSource {
400
+ path: number[];
401
+ startUtf8: number;
402
+ endUtf8: number;
403
+ group: number;
404
+ }
405
+ interface MdiWarichuOptions {
406
+ firstCapacity: number;
407
+ continuationCapacity: number;
408
+ }
409
+ /** Presentation-only two-line fragment; generated boundaries never belong in MDI. */
410
+ interface MdiWarichuFragment {
411
+ lines: [Record<string, unknown>[], Record<string, unknown>[]];
412
+ widths: [number, number];
413
+ overflow: boolean;
414
+ hardBreakAfter: boolean;
415
+ sources: [MdiWarichuSource[], MdiWarichuSource[]];
416
+ html: [string, string];
417
+ }
418
+ /** Rust owns splitting. Capacities are half-em units at the note's 50% font size. */
419
+ declare function layoutMdiWarichu(children: readonly Record<string, unknown>[], capacity?: number | MdiWarichuOptions): MdiWarichuFragment[];
352
420
 
353
- export { MDI_IR_VERSION, MDI_SPEC_VERSION, MDI_TEXT_PROJECTION_VERSION, type MdiAnnotationSourceMap, type MdiDiagnostic, type MdiDocument, type MdiDocxExportProfile, type MdiEpubExportOptions, type MdiFrontmatter, type MdiHeading, type MdiHtmlRenderOptions, type MdiNode, type MdiParserCapabilities, type MdiPublicationFrontmatter, type MdiPublicationRoot, type MdiRenderResult, type MdiRubyReading, type MdiSourceSpan, type MdiSourceSpanAnnotationMatch, type MdiSourceSpanBlockTextMatch, type MdiSourceSpanCoverage, type MdiSourceSpanRelation, type MdiSourceSpanTextMatch, type MdiSourceSpanTextResolution, type MdiSyntaxDocument, type MdiSyntaxParseResult, type MdiTextAnnotation, type MdiTextBlock, type MdiTextBlocksResult, type MdiTextFormat, type MdiTextPosition, type MdiTextPositionValue, type MdiTextRange, type MdiTextSourceMap, type MdiTextSourceRun, formatMdiTextPosition, formatMdiTextRange, getMdiTextBlocks, initializeMdi, parse, parseMdiSyntax, parseMdiTextPosition, prepareRender, renderDocx, renderDocxWithDiagnostics, renderDocxWithProfile, renderEpub, renderEpubWithDiagnostics, renderEpubWithProfile, renderHtml, renderHtmlWithDiagnostics, renderText, renderTextFormat, renderTextFormatWithDiagnostics, renderTextWithDiagnostics, resolveMdiSourceSpan, resolveMdiSourceSpans, serializeMdi, sourceSpansForTextRange, toPublicationMdast };
421
+ export { MDI_IR_VERSION, MDI_SPEC_VERSION, MDI_TEXT_PROJECTION_VERSION, type MdiAnnotationSourceMap, type MdiDiagnostic, type MdiDocument, type MdiDocxExportProfile, type MdiEpubExportOptions, type MdiFrontmatter, type MdiHeading, type MdiHtmlRenderOptions, type MdiNode, type MdiParserCapabilities, type MdiPrintEvaluate, type MdiPrintLayoutOptions, type MdiPrintPage, type MdiPublicationFrontmatter, type MdiPublicationRoot, type MdiRenderResult, type MdiRubyReading, type MdiSourceSpan, type MdiSourceSpanAnnotationMatch, type MdiSourceSpanBlockTextMatch, type MdiSourceSpanCoverage, type MdiSourceSpanRelation, type MdiSourceSpanTextMatch, type MdiSourceSpanTextResolution, type MdiSyntaxDocument, type MdiSyntaxParseResult, type MdiTextAnnotation, type MdiTextBlock, type MdiTextBlocksResult, type MdiTextFormat, type MdiTextPosition, type MdiTextPositionValue, type MdiTextRange, type MdiTextSourceMap, type MdiTextSourceRun, type MdiWarichuFragment, type MdiWarichuLayoutController, type MdiWarichuOptions, type MdiWarichuSettleOptions, type MdiWarichuSource, applyMdiWarichu, attachMdiWarichuLayout, formatMdiTextPosition, formatMdiTextRange, getMdiTextBlocks, initializeMdi, layoutMdiWarichu, measureMdiWarichu, parse, parseMdiSyntax, parseMdiTextPosition, prepareRender, renderDocx, renderDocxWithDiagnostics, renderDocxWithProfile, renderEpub, renderEpubWithDiagnostics, renderEpubWithProfile, renderHtml, renderHtmlWithDiagnostics, renderText, renderTextFormat, renderTextFormatWithDiagnostics, renderTextWithDiagnostics, resolveMdiSourceSpan, resolveMdiSourceSpans, serializeMdi, settleMdiPrintLayout, sourceSpansForTextRange, toPublicationMdast };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,52 @@ import { ExportProfile } from '@illusions-lab/mdi-export-profile';
4
4
  export { ExportProfile } from '@illusions-lab/mdi-export-profile';
5
5
  import { Root } from 'mdast';
6
6
 
7
+ interface MdiWarichuMeasurement {
8
+ index: number;
9
+ children: Record<string, unknown>[];
10
+ options: MdiWarichuOptions;
11
+ /** Measured CSS pixels per Rust half-em unit for this layout pass. */
12
+ unit?: number;
13
+ }
14
+ /** Self-contained for execution in a browser hosted by Electron or Playwright. */
15
+ declare function measureMdiWarichu(container?: HTMLElement, affected?: readonly HTMLElement[], minimumUnits?: Readonly<Record<number, number>>): MdiWarichuMeasurement[];
16
+ /** Applies Rust-rendered presentation only; never modifies canonical source. */
17
+ declare function applyMdiWarichu(updates: {
18
+ index: number;
19
+ fragments: MdiWarichuFragment[];
20
+ }[], container?: HTMLElement): boolean;
21
+ interface MdiWarichuSettleOptions {
22
+ timeoutMs?: number;
23
+ signal?: AbortSignal;
24
+ }
25
+ interface MdiWarichuLayoutController {
26
+ configure(): void;
27
+ settled(options?: MdiWarichuSettleOptions): Promise<void>;
28
+ dispose(): void;
29
+ }
30
+ /** Attach to read-only Rust-rendered HTML. Editable editors consume layoutMdiWarichu directly. */
31
+ declare function attachMdiWarichuLayout(container: HTMLElement): MdiWarichuLayoutController;
32
+
33
+ /** Execute JavaScript in the print document. Electron and Playwright can supply their existing page. */
34
+ type MdiPrintEvaluate = (javascript: string) => Promise<unknown>;
35
+ /** Physical page dimensions resolved by the upstream Chromium print profile. */
36
+ interface MdiPrintPage {
37
+ widthMm: number;
38
+ heightMm: number;
39
+ marginsMm: {
40
+ top: number;
41
+ right: number;
42
+ bottom: number;
43
+ left: number;
44
+ };
45
+ }
46
+ interface MdiPrintLayoutOptions extends MdiWarichuSettleOptions {
47
+ /** Pass prepared.page so measurement uses paper space rather than the host window viewport. */
48
+ page?: MdiPrintPage;
49
+ }
50
+ /** Wait for fonts, then measure and apply the shared Rust layout before permitting printing. */
51
+ declare function settleMdiPrintLayout(evaluate: MdiPrintEvaluate, { timeoutMs, signal, page }?: MdiPrintLayoutOptions): Promise<void>;
52
+
7
53
  /**
8
54
  * Initialize the shared MDI core runtime exactly once.
9
55
  *
@@ -349,5 +395,27 @@ declare function renderTextFormat(source: string, format: MdiTextFormat, indentP
349
395
  declare function renderTextFormatWithDiagnostics(source: string, format: MdiTextFormat, indentPrefix?: string): MdiRenderResult<string>;
350
396
  /** @deprecated Use {@link parse}; it now parses the complete document. */
351
397
  declare const parseMdiSyntax: typeof parse;
398
+ /** UTF-8 boundaries in an original inline leaf; paths are relative to children. */
399
+ interface MdiWarichuSource {
400
+ path: number[];
401
+ startUtf8: number;
402
+ endUtf8: number;
403
+ group: number;
404
+ }
405
+ interface MdiWarichuOptions {
406
+ firstCapacity: number;
407
+ continuationCapacity: number;
408
+ }
409
+ /** Presentation-only two-line fragment; generated boundaries never belong in MDI. */
410
+ interface MdiWarichuFragment {
411
+ lines: [Record<string, unknown>[], Record<string, unknown>[]];
412
+ widths: [number, number];
413
+ overflow: boolean;
414
+ hardBreakAfter: boolean;
415
+ sources: [MdiWarichuSource[], MdiWarichuSource[]];
416
+ html: [string, string];
417
+ }
418
+ /** Rust owns splitting. Capacities are half-em units at the note's 50% font size. */
419
+ declare function layoutMdiWarichu(children: readonly Record<string, unknown>[], capacity?: number | MdiWarichuOptions): MdiWarichuFragment[];
352
420
 
353
- export { MDI_IR_VERSION, MDI_SPEC_VERSION, MDI_TEXT_PROJECTION_VERSION, type MdiAnnotationSourceMap, type MdiDiagnostic, type MdiDocument, type MdiDocxExportProfile, type MdiEpubExportOptions, type MdiFrontmatter, type MdiHeading, type MdiHtmlRenderOptions, type MdiNode, type MdiParserCapabilities, type MdiPublicationFrontmatter, type MdiPublicationRoot, type MdiRenderResult, type MdiRubyReading, type MdiSourceSpan, type MdiSourceSpanAnnotationMatch, type MdiSourceSpanBlockTextMatch, type MdiSourceSpanCoverage, type MdiSourceSpanRelation, type MdiSourceSpanTextMatch, type MdiSourceSpanTextResolution, type MdiSyntaxDocument, type MdiSyntaxParseResult, type MdiTextAnnotation, type MdiTextBlock, type MdiTextBlocksResult, type MdiTextFormat, type MdiTextPosition, type MdiTextPositionValue, type MdiTextRange, type MdiTextSourceMap, type MdiTextSourceRun, formatMdiTextPosition, formatMdiTextRange, getMdiTextBlocks, initializeMdi, parse, parseMdiSyntax, parseMdiTextPosition, prepareRender, renderDocx, renderDocxWithDiagnostics, renderDocxWithProfile, renderEpub, renderEpubWithDiagnostics, renderEpubWithProfile, renderHtml, renderHtmlWithDiagnostics, renderText, renderTextFormat, renderTextFormatWithDiagnostics, renderTextWithDiagnostics, resolveMdiSourceSpan, resolveMdiSourceSpans, serializeMdi, sourceSpansForTextRange, toPublicationMdast };
421
+ export { MDI_IR_VERSION, MDI_SPEC_VERSION, MDI_TEXT_PROJECTION_VERSION, type MdiAnnotationSourceMap, type MdiDiagnostic, type MdiDocument, type MdiDocxExportProfile, type MdiEpubExportOptions, type MdiFrontmatter, type MdiHeading, type MdiHtmlRenderOptions, type MdiNode, type MdiParserCapabilities, type MdiPrintEvaluate, type MdiPrintLayoutOptions, type MdiPrintPage, type MdiPublicationFrontmatter, type MdiPublicationRoot, type MdiRenderResult, type MdiRubyReading, type MdiSourceSpan, type MdiSourceSpanAnnotationMatch, type MdiSourceSpanBlockTextMatch, type MdiSourceSpanCoverage, type MdiSourceSpanRelation, type MdiSourceSpanTextMatch, type MdiSourceSpanTextResolution, type MdiSyntaxDocument, type MdiSyntaxParseResult, type MdiTextAnnotation, type MdiTextBlock, type MdiTextBlocksResult, type MdiTextFormat, type MdiTextPosition, type MdiTextPositionValue, type MdiTextRange, type MdiTextSourceMap, type MdiTextSourceRun, type MdiWarichuFragment, type MdiWarichuLayoutController, type MdiWarichuOptions, type MdiWarichuSettleOptions, type MdiWarichuSource, applyMdiWarichu, attachMdiWarichuLayout, formatMdiTextPosition, formatMdiTextRange, getMdiTextBlocks, initializeMdi, layoutMdiWarichu, measureMdiWarichu, parse, parseMdiSyntax, parseMdiTextPosition, prepareRender, renderDocx, renderDocxWithDiagnostics, renderDocxWithProfile, renderEpub, renderEpubWithDiagnostics, renderEpubWithProfile, renderHtml, renderHtmlWithDiagnostics, renderText, renderTextFormat, renderTextFormatWithDiagnostics, renderTextWithDiagnostics, resolveMdiSourceSpan, resolveMdiSourceSpans, serializeMdi, settleMdiPrintLayout, sourceSpansForTextRange, toPublicationMdast };
package/dist/index.js CHANGED
@@ -2,10 +2,14 @@ import {
2
2
  MDI_IR_VERSION,
3
3
  MDI_SPEC_VERSION,
4
4
  MDI_TEXT_PROJECTION_VERSION,
5
+ applyMdiWarichu,
6
+ attachMdiWarichuLayout,
5
7
  formatMdiTextPosition,
6
8
  formatMdiTextRange,
7
9
  getMdiTextBlocks,
8
10
  initializeMdi,
11
+ layoutMdiWarichu,
12
+ measureMdiWarichu,
9
13
  parse,
10
14
  parseMdiSyntax,
11
15
  parseMdiTextPosition,
@@ -25,17 +29,22 @@ import {
25
29
  resolveMdiSourceSpan,
26
30
  resolveMdiSourceSpans,
27
31
  serializeMdi,
32
+ settleMdiPrintLayout,
28
33
  sourceSpansForTextRange,
29
34
  toPublicationMdast
30
- } from "./chunk-GMS5SFZO.js";
35
+ } from "./chunk-SC6NZVRY.js";
31
36
  export {
32
37
  MDI_IR_VERSION,
33
38
  MDI_SPEC_VERSION,
34
39
  MDI_TEXT_PROJECTION_VERSION,
40
+ applyMdiWarichu,
41
+ attachMdiWarichuLayout,
35
42
  formatMdiTextPosition,
36
43
  formatMdiTextRange,
37
44
  getMdiTextBlocks,
38
45
  initializeMdi,
46
+ layoutMdiWarichu,
47
+ measureMdiWarichu,
39
48
  parse,
40
49
  parseMdiSyntax,
41
50
  parseMdiTextPosition,
@@ -55,6 +64,7 @@ export {
55
64
  resolveMdiSourceSpan,
56
65
  resolveMdiSourceSpans,
57
66
  serializeMdi,
67
+ settleMdiPrintLayout,
58
68
  sourceSpansForTextRange,
59
69
  toPublicationMdast
60
70
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MDI_IR_VERSION
3
- } from "../chunk-GMS5SFZO.js";
3
+ } from "../chunk-SC6NZVRY.js";
4
4
 
5
5
  // src/internal/mdast.ts
6
6
  import { parseMdiMdastJson } from "@illusions-lab/mdi-core";
package/dist/node.cjs CHANGED
@@ -143,7 +143,7 @@ async function renderPdfWithChromiumWithDiagnostics(source, profile, adapter) {
143
143
  )
144
144
  };
145
145
  }
146
- function headingsFromDocument(document) {
146
+ function headingsFromDocument(document2) {
147
147
  const headings = [];
148
148
  const visit = (nodes) => {
149
149
  for (const node of nodes) {
@@ -153,7 +153,7 @@ function headingsFromDocument(document) {
153
153
  if (node.children) visit(node.children);
154
154
  }
155
155
  };
156
- visit(document.children);
156
+ visit(document2.children);
157
157
  return headings;
158
158
  }
159
159
  function isHeadingDepth(value) {
package/dist/node.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parse,
3
3
  renderHtml
4
- } from "./chunk-GMS5SFZO.js";
4
+ } from "./chunk-SC6NZVRY.js";
5
5
 
6
6
  // src/node.ts
7
7
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illusions-lab/mdi",
3
- "version": "2.0.25",
3
+ "version": "2.0.26",
4
4
  "description": "Thin JavaScript binding for the Rust-authoritative MDI parser",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,9 +36,9 @@
36
36
  "typecheck": "tsc --noEmit"
37
37
  },
38
38
  "dependencies": {
39
- "@illusions-lab/mdi-core": "^2.0.25",
40
- "@illusions-lab/mdi-export-profile": "^2.0.30",
41
- "@illusions-lab/mdi-to-epub": "^2.0.40",
39
+ "@illusions-lab/mdi-core": "^2.0.26",
40
+ "@illusions-lab/mdi-export-profile": "^2.0.31",
41
+ "@illusions-lab/mdi-to-epub": "^2.0.41",
42
42
  "@types/mdast": "^4.0.0",
43
43
  "yaml": "^2.0.0"
44
44
  },