@illusions-lab/mdi-to-docx 2.0.24 → 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
@@ -1,7 +1,8 @@
1
1
  # @illusions-lab/mdi-to-docx
2
2
 
3
- Creates a DOCX file from an MDI-flavoured mdast tree. It is a profile-aware
4
- compatibility adapter for unified applications and does not parse MDI source.
3
+ Creates a DOCX file from an MDI-flavoured mdast tree. It is a compatibility
4
+ entry point for unified applications: the tree is serialized to MDI, then the
5
+ same Rust OOXML renderer used by every binding creates the document.
5
6
 
6
7
  ## Install
7
8
 
@@ -44,12 +45,13 @@ remaining fallbacks:
44
45
  | blank paragraph / pagebreak | Empty paragraph / native page break |
45
46
 
46
47
  The source mdast remains available to the host application, so a UI can retain
47
- source spans and render diagnostics before choosing to export. This converter
48
- does not invent diagnostics or silently claim pixel-equivalent Japanese layout.
48
+ source spans and render diagnostics before choosing to export. JavaScript does
49
+ not contain a second DOCX generator, and the package does not claim
50
+ pixel-equivalent Japanese layout across Word-compatible readers.
49
51
 
50
52
  ## Documentation
51
53
 
52
54
  - [Output model](https://mdi.illusions.app/ecosystem/outputs/)
53
- - [Export-profile guide](https://mdi.illusions.app/guides/export-profiles/)
55
+ - [Export-profile guide](https://mdi.illusions.app/ecosystem/export-profiles/)
54
56
  - [API reference](https://mdi.illusions.app/api/to-docx/)
55
57
  - [JavaScript documentation](https://mdi.illusions.app/bindings/javascript/)
package/dist/index.cjs CHANGED
@@ -24,407 +24,17 @@ __export(index_exports, {
24
24
  mdiToDocx: () => mdiToDocx
25
25
  });
26
26
  module.exports = __toCommonJS(index_exports);
27
- var import_docx = require("docx");
28
- var import_mdi_export_profile = require("@illusions-lab/mdi-export-profile");
27
+ var import_mdi_core = require("@illusions-lab/mdi-core");
28
+ var import_mdast_util_mdi = require("mdast-util-mdi");
29
29
  var MDI_SPEC_VERSION = "2.0";
30
30
  async function mdiToDocx(tree, profile) {
31
- const options = (0, import_mdi_export_profile.resolvePrintProfile)(
32
- profile,
33
- tree.data?.frontmatter?.writingMode
31
+ if (!tree || tree.type !== "root" || !Array.isArray(tree.children))
32
+ throw new TypeError("tree must be an mdast root");
33
+ if (profile !== void 0 && (!profile || typeof profile !== "object" || Array.isArray(profile)))
34
+ throw new TypeError("profile must be an object");
35
+ return Buffer.from(
36
+ (0, import_mdi_core.renderDocxWithProfile)((0, import_mdast_util_mdi.mdastToMdiSource)(tree), JSON.stringify(profile ?? {}))
34
37
  );
35
- const definitions = tree.children.filter(
36
- (node) => node.type === "footnoteDefinition"
37
- );
38
- const footnoteIds = new Map(
39
- definitions.map((definition, index) => [definition.identifier, index + 1])
40
- );
41
- const context = { footnoteIds };
42
- const children = tree.children.flatMap((node) => block(node, options, context));
43
- const dimensions = import_mdi_export_profile.PAGE_DIMENSIONS[options.pagination.pageSize];
44
- const widthMm = options.pagination.landscape ? dimensions.height : dimensions.width;
45
- const heightMm = options.pagination.landscape ? dimensions.width : dimensions.height;
46
- const primary = options.typesetting.writingMode === "vertical" ? heightMm - options.pagination.margins.top - options.pagination.margins.bottom : widthMm - options.pagination.margins.left - options.pagination.margins.right;
47
- const cross = options.typesetting.writingMode === "vertical" ? widthMm - options.pagination.margins.left - options.pagination.margins.right : heightMm - options.pagination.margins.top - options.pagination.margins.bottom;
48
- const fontSizeMm = options.typesetting.fontSize / 72 * 25.4;
49
- const fontSizeHalfPoints = Math.round(fontSizeMm / 25.4 * 72 * 2);
50
- const strictGrid = options.pagination.gridMode === "strict";
51
- const characterPitchPoints = primary / options.pagination.charactersPerLine / 25.4 * 72;
52
- const characterSpace = Math.max(
53
- 0,
54
- Math.round((characterPitchPoints - fontSizeHalfPoints / 2) * 4096)
55
- );
56
- const lineTwips = options.typesetting.lineSpacing === void 0 ? Math.round(cross / options.pagination.linesPerPage / 25.4 * 1440) : Math.round(fontSizeHalfPoints * 10 * options.typesetting.lineSpacing);
57
- const document = new import_docx.Document({
58
- title: options.metadata.title ?? tree.data?.frontmatter?.title,
59
- creator: options.metadata.author ?? tree.data?.frontmatter?.author,
60
- footnotes: Object.fromEntries(
61
- definitions.map((definition, index) => [
62
- String(index + 1),
63
- {
64
- children: definition.children.flatMap(
65
- (child) => child.type === "paragraph" ? [new import_docx.Paragraph({ children: inline(child.children, context) })] : []
66
- )
67
- }
68
- ])
69
- ),
70
- styles: {
71
- default: {
72
- document: {
73
- run: {
74
- font: options.typesetting.fontFamily,
75
- size: fontSizeHalfPoints,
76
- color: "000000"
77
- },
78
- paragraph: { spacing: { line: lineTwips, after: strictGrid ? 0 : 120 } }
79
- },
80
- heading1: headingStyle(options, 1, fontSizeHalfPoints),
81
- heading2: headingStyle(options, 2, fontSizeHalfPoints),
82
- heading3: headingStyle(options, 3, fontSizeHalfPoints),
83
- heading4: headingStyle(options, 4, fontSizeHalfPoints),
84
- heading5: headingStyle(options, 5, fontSizeHalfPoints),
85
- heading6: headingStyle(options, 6, fontSizeHalfPoints),
86
- listParagraph: {
87
- run: { font: options.typesetting.fontFamily, color: "000000" }
88
- }
89
- },
90
- paragraphStyles: [
91
- customHeadingStyle(options, 7, fontSizeHalfPoints),
92
- customHeadingStyle(options, 8, fontSizeHalfPoints),
93
- customHeadingStyle(options, 9, fontSizeHalfPoints),
94
- {
95
- id: "MdiQuote",
96
- name: "MDI Quote",
97
- basedOn: "Normal",
98
- run: { font: options.typesetting.fontFamily, color: "000000", italics: true },
99
- paragraph: {
100
- indent: { left: 720, right: 360 },
101
- spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 120, line: lineTwips }
102
- }
103
- },
104
- {
105
- id: "MdiList",
106
- name: "MDI List",
107
- basedOn: "ListParagraph",
108
- run: { font: options.typesetting.fontFamily, color: "000000" },
109
- paragraph: { spacing: { after: strictGrid ? 0 : 60, line: lineTwips } }
110
- },
111
- {
112
- id: "MdiCode",
113
- name: "MDI Code Block",
114
- basedOn: "Normal",
115
- run: { font: "Courier New", color: "000000" },
116
- paragraph: {
117
- indent: { left: 360, right: 360 },
118
- spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 120, line: lineTwips }
119
- }
120
- },
121
- {
122
- id: "MdiThematicBreak",
123
- name: "MDI Thematic Break",
124
- basedOn: "Normal",
125
- run: { font: options.typesetting.fontFamily, color: "000000" },
126
- paragraph: { spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 180, line: lineTwips } }
127
- }
128
- ]
129
- },
130
- sections: [
131
- {
132
- ...pageNumberSection(options, fontSizeHalfPoints),
133
- properties: {
134
- page: {
135
- size: { width: mmToTwips(widthMm), height: mmToTwips(heightMm) },
136
- margin: Object.fromEntries(
137
- Object.entries(options.pagination.margins).map(([key, value]) => [
138
- key,
139
- mmToTwips(value)
140
- ])
141
- ),
142
- ...options.pagination.pageNumbers.enabled ? { pageNumbers: { start: 1 } } : {},
143
- ...options.typesetting.writingMode === "vertical" ? {
144
- textDirection: import_docx.PageTextDirectionType.TOP_TO_BOTTOM_RIGHT_TO_LEFT
145
- } : {}
146
- },
147
- // Word's document grid is the portable OOXML mechanism that records
148
- // a publisher's characters × lines contract. `linePitch` comes from
149
- // the resolved physical block extent; the matching PDF profile uses
150
- // the same resolved page geometry.
151
- ...strictGrid ? {
152
- grid: {
153
- type: "linesAndChars",
154
- ...characterSpace === 0 ? {} : { charSpace: characterSpace },
155
- linePitch: lineTwips
156
- }
157
- } : {}
158
- },
159
- children
160
- }
161
- ]
162
- });
163
- addBookMarginSettings(document, options);
164
- return import_docx.Packer.toBuffer(document);
165
- }
166
- function addBookMarginSettings(document, options) {
167
- if (options.layout.marginMode !== "mirror") return;
168
- const namespace = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"';
169
- document.Settings.addChildElement(
170
- import_docx.ImportedXmlComponent.fromXmlString(`<w:mirrorMargins ${namespace}/>`)
171
- );
172
- if (options.layout.bindingSide === "right")
173
- document.Settings.addChildElement(
174
- import_docx.ImportedXmlComponent.fromXmlString(`<w:rtlGutter ${namespace}/>`)
175
- );
176
- }
177
- function headingStyle(options, level, bodyFontSizeHalfPoints2 = 22) {
178
- const before = [360, 300, 240, 180, 150, 120][level - 1] ?? 120;
179
- const strictGrid = options.pagination.gridMode === "strict";
180
- return {
181
- run: {
182
- font: options.typesetting.fontFamily,
183
- color: "000000",
184
- bold: true,
185
- size: strictGrid ? bodyFontSizeHalfPoints2 : headingFontSize(level, bodyFontSizeHalfPoints2)
186
- },
187
- paragraph: {
188
- spacing: { before: strictGrid ? 0 : before, after: strictGrid ? 0 : 120 },
189
- keepNext: true,
190
- keepLines: true,
191
- outlineLevel: level - 1
192
- }
193
- };
194
- }
195
- function headingFontSize(level, bodyFontSizeHalfPoints2 = 22) {
196
- const scale = [1.8, 1.55, 1.35, 1.2, 1.1, 1][level - 1] ?? 1;
197
- return Math.round(bodyFontSizeHalfPoints2 * scale);
198
- }
199
- function customHeadingStyle(options, level, bodyFontSizeHalfPoints2 = 22) {
200
- const style = headingStyle(options, level, bodyFontSizeHalfPoints2);
201
- return {
202
- id: `Heading${level}`,
203
- name: `Heading ${level}`,
204
- basedOn: "Normal",
205
- next: "Normal",
206
- quickFormat: true,
207
- ...style
208
- };
209
- }
210
- function pageNumberSection(options, size) {
211
- const page = options.pagination.pageNumbers;
212
- if (!page.enabled) return {};
213
- const alignment = page.position.endsWith("left") ? import_docx.AlignmentType.LEFT : page.position.endsWith("right") ? import_docx.AlignmentType.RIGHT : import_docx.AlignmentType.CENTER;
214
- const current = new import_docx.TextRun({ children: [import_docx.PageNumber.CURRENT], size });
215
- const runs = page.format === "dash" ? [
216
- new import_docx.TextRun({ text: "\u2014 ", size }),
217
- current,
218
- new import_docx.TextRun({ text: " \u2014", size })
219
- ] : page.format === "fraction" ? [
220
- current,
221
- new import_docx.TextRun({ text: " / ", size }),
222
- new import_docx.TextRun({ children: [import_docx.PageNumber.TOTAL_PAGES], size })
223
- ] : [current];
224
- const paragraph2 = new import_docx.Paragraph({ alignment, children: runs });
225
- return page.position.startsWith("top-") ? { headers: { default: new import_docx.Header({ children: [paragraph2] }) } } : { footers: { default: new import_docx.Footer({ children: [paragraph2] }) } };
226
- }
227
- function block(node, options, context) {
228
- if (node.type === "heading") {
229
- const runSize = options.pagination.gridMode === "strict" ? bodyFontSizeHalfPoints(options) : headingFontSize(node.depth);
230
- return [
231
- new import_docx.Paragraph({
232
- heading: [
233
- import_docx.HeadingLevel.HEADING_1,
234
- import_docx.HeadingLevel.HEADING_2,
235
- import_docx.HeadingLevel.HEADING_3,
236
- import_docx.HeadingLevel.HEADING_4,
237
- import_docx.HeadingLevel.HEADING_5,
238
- import_docx.HeadingLevel.HEADING_6
239
- ][node.depth - 1],
240
- children: inline(node.children, context, runSize)
241
- })
242
- ];
243
- }
244
- if (node.type === "paragraph") return [paragraph(node.children, options, void 0, context)];
245
- if (node.type === "list")
246
- return node.children.flatMap(
247
- (item) => item.children.filter((n) => n.type === "paragraph").map(
248
- (n) => new import_docx.Paragraph({
249
- style: "MdiList",
250
- children: inline(n.children, context),
251
- bullet: { level: 0 }
252
- })
253
- )
254
- );
255
- if (node.type === "blockquote")
256
- return node.children.flatMap(
257
- (child) => child.type === "paragraph" ? [paragraph(child.children, options, "MdiQuote", context)] : block(child, options, context)
258
- );
259
- if (node.type === "code")
260
- return [
261
- new import_docx.Paragraph({
262
- style: "MdiCode",
263
- children: node.value.split("\n").map(
264
- (line, index) => new import_docx.TextRun({
265
- text: line,
266
- break: index === 0 ? void 0 : 1,
267
- font: "Courier New"
268
- })
269
- )
270
- })
271
- ];
272
- if (node.type === "thematicBreak")
273
- return [
274
- new import_docx.Paragraph({
275
- style: "MdiThematicBreak",
276
- alignment: import_docx.AlignmentType.CENTER,
277
- children: [new import_docx.TextRun("\u2014 \u2014 \u2014")]
278
- })
279
- ];
280
- if (node.type === "table") return [tableBlock(node, options, context)];
281
- if (node.type === "mdiPagebreak")
282
- return [new import_docx.Paragraph({ children: [new import_docx.PageBreak()] })];
283
- if (node.type === "mdiBlank") return [new import_docx.Paragraph("")];
284
- return [];
285
- }
286
- function tableBlock(node, options, context) {
287
- const columns = Math.max(1, ...node.children.map((row) => row.children.length));
288
- const dimensions = import_mdi_export_profile.PAGE_DIMENSIONS[options.pagination.pageSize];
289
- const pageWidth = options.pagination.landscape ? dimensions.height : dimensions.width;
290
- const usableWidth = mmToTwips(
291
- pageWidth - options.pagination.margins.left - options.pagination.margins.right
292
- );
293
- const columnWidth = Math.floor(usableWidth / columns);
294
- return new import_docx.Table({
295
- width: { size: usableWidth, type: import_docx.WidthType.DXA },
296
- columnWidths: Array.from({ length: columns }, () => columnWidth),
297
- layout: import_docx.TableLayoutType.FIXED,
298
- margins: { top: 80, bottom: 80, left: 100, right: 100 },
299
- rows: node.children.map(
300
- (row, rowIndex) => new import_docx.TableRow({
301
- children: row.children.map(
302
- (cell) => new import_docx.TableCell({
303
- width: { size: columnWidth, type: import_docx.WidthType.DXA },
304
- children: [
305
- new import_docx.Paragraph({
306
- children: rowIndex === 0 ? [new import_docx.TextRun({ text: inlineText(cell.children), bold: true })] : inline(cell.children, context)
307
- })
308
- ]
309
- })
310
- )
311
- })
312
- )
313
- });
314
- }
315
- function paragraph(nodes, options, style, context) {
316
- const fontSizeMm = options.typesetting.fontSize / 72 * 25.4;
317
- const prefix = options.typesetting.fullwidthSpaceIndent ? [new import_docx.TextRun("\u3000".repeat(Math.round(options.typesetting.textIndentEm)))] : [];
318
- return new import_docx.Paragraph({
319
- style,
320
- indent: options.typesetting.fullwidthSpaceIndent ? void 0 : {
321
- ...style ? {} : {
322
- firstLine: Math.round(
323
- options.typesetting.textIndentEm * fontSizeMm / 25.4 * 1440
324
- )
325
- }
326
- },
327
- children: [...prefix, ...inline(nodes, context)]
328
- });
329
- }
330
- function inline(nodes, context, rubyBaseTextSize = 24) {
331
- return nodes.flatMap((node) => {
332
- if (node.type === "text") return [new import_docx.TextRun(node.value)];
333
- if (node.type === "break" || node.type === "mdiBreak")
334
- return [new import_docx.TextRun({ break: 1 })];
335
- if (node.type === "emphasis")
336
- return [new import_docx.TextRun({ italics: true, children: inline(node.children, context, rubyBaseTextSize) })];
337
- if (node.type === "strong")
338
- return [new import_docx.TextRun({ bold: true, children: inline(node.children, context, rubyBaseTextSize) })];
339
- if (node.type === "delete")
340
- return [new import_docx.TextRun({ strike: true, children: inline(node.children, context, rubyBaseTextSize) })];
341
- if (node.type === "inlineCode")
342
- return [new import_docx.TextRun({ text: node.value, font: "Courier New" })];
343
- if (node.type === "image")
344
- return [new import_docx.TextRun({ text: node.alt ? `[Image: ${node.alt}]` : "[Image]" })];
345
- if (node.type === "link")
346
- return [
347
- new import_docx.ExternalHyperlink({
348
- link: node.url,
349
- children: inline(node.children, context, rubyBaseTextSize)
350
- })
351
- ];
352
- if (node.type === "footnoteReference") {
353
- const id = context.footnoteIds.get(node.identifier);
354
- return id ? [new import_docx.FootnoteReferenceRun(id)] : [];
355
- }
356
- if (node.type === "mdiEm")
357
- return [
358
- new import_docx.TextRun({
359
- emphasisMark: { type: "dot" },
360
- children: inline(node.children, context, rubyBaseTextSize)
361
- })
362
- ];
363
- if (node.type === "mdiRuby") return [rawRun(rubyXml(node.base, node.ruby, rubyBaseTextSize))];
364
- if (node.type === "mdiTcy")
365
- return [
366
- rawRun(
367
- `<w:r><w:rPr><w:eastAsianLayout w:combine="1" w:combineBrackets="none"/></w:rPr><w:t>${xml(
368
- node.value
369
- )}</w:t></w:r>`
370
- )
371
- ];
372
- if (node.type === "mdiWarichu")
373
- return [
374
- new import_docx.TextRun({
375
- size: Math.max(10, Math.round(rubyBaseTextSize * 0.6)),
376
- children: inline(node.children, context, rubyBaseTextSize)
377
- })
378
- ];
379
- if (node.type === "mdiKern") {
380
- const spacing = kernTwips(node.amount, rubyBaseTextSize);
381
- return [
382
- new import_docx.TextRun({
383
- ...spacing === 0 ? {} : { characterSpacing: spacing },
384
- children: inline(node.children, context, rubyBaseTextSize)
385
- })
386
- ];
387
- }
388
- if (node.type === "mdiNoBreak")
389
- return inline(node.children, context, rubyBaseTextSize);
390
- if ("children" in node) return inline(node.children, context, rubyBaseTextSize);
391
- return [];
392
- });
393
- }
394
- function kernTwips(amount, baseTextSize) {
395
- const value = Number.parseFloat(amount);
396
- if (!Number.isFinite(value) || !amount.endsWith("em")) return 0;
397
- return Math.round(value * baseTextSize * 10);
398
- }
399
- function inlineText(nodes) {
400
- return nodes.map((node) => node.type === "text" ? node.value : "children" in node ? inlineText(node.children) : "").join("");
401
- }
402
- function mmToTwips(mm) {
403
- return Math.round(mm / 25.4 * 1440);
404
- }
405
- function bodyFontSizeHalfPoints(options) {
406
- return Math.round(options.typesetting.fontSize * 2);
407
- }
408
- function rubyXml(base, reading, baseTextSize = 24) {
409
- const text = Array.isArray(reading) ? reading.join(".") : reading;
410
- const rubySize = Math.max(10, Math.round(baseTextSize / 2));
411
- const rubyRaise = baseTextSize === 24 ? 18 : Math.max(18, Math.round(baseTextSize * 0.8));
412
- return `<w:ruby><w:rubyPr><w:rubyAlign w:val="center"/><w:hps w:val="${rubySize}"/><w:hpsRaise w:val="${rubyRaise}"/><w:hpsBaseText w:val="${baseTextSize}"/></w:rubyPr><w:rt><w:r><w:t>${xml(
413
- text
414
- )}</w:t></w:r></w:rt><w:rubyBase><w:r><w:t>${xml(
415
- base
416
- )}</w:t></w:r></w:rubyBase></w:ruby>`;
417
- }
418
- function rawRun(xmlString) {
419
- const namespaced = xmlString.replace(
420
- /^<w:([\w-]+)/,
421
- '<w:$1 xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
422
- );
423
- const imported = import_docx.ImportedXmlComponent.fromXmlString(namespaced);
424
- return imported.root[0];
425
- }
426
- function xml(value) {
427
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;");
428
38
  }
429
39
  // Annotate the CommonJS export names for ESM import in node:
430
40
  0 && (module.exports = {
package/dist/index.d.cts CHANGED
@@ -1,17 +1,11 @@
1
- import { Root } from 'mdast';
2
1
  import { ExportProfile } from '@illusions-lab/mdi-export-profile';
2
+ import { Root } from 'mdast';
3
3
 
4
- declare module "mdast" {
5
- interface RootData {
6
- frontmatter?: {
7
- title?: string;
8
- author?: string;
9
- writingMode: "horizontal" | "vertical";
10
- };
11
- }
12
- }
13
4
  declare const MDI_SPEC_VERSION = "2.0";
14
- /** Creates a DOCX file using the same profile fields as PDF and EPUB exports. */
5
+ /**
6
+ * Preserve the mdast-facing package API while delegating OOXML generation to
7
+ * the single Rust implementation used by every language binding.
8
+ */
15
9
  declare function mdiToDocx(tree: Root, profile?: ExportProfile): Promise<Buffer>;
16
10
 
17
11
  export { MDI_SPEC_VERSION, mdiToDocx };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,11 @@
1
- import { Root } from 'mdast';
2
1
  import { ExportProfile } from '@illusions-lab/mdi-export-profile';
2
+ import { Root } from 'mdast';
3
3
 
4
- declare module "mdast" {
5
- interface RootData {
6
- frontmatter?: {
7
- title?: string;
8
- author?: string;
9
- writingMode: "horizontal" | "vertical";
10
- };
11
- }
12
- }
13
4
  declare const MDI_SPEC_VERSION = "2.0";
14
- /** Creates a DOCX file using the same profile fields as PDF and EPUB exports. */
5
+ /**
6
+ * Preserve the mdast-facing package API while delegating OOXML generation to
7
+ * the single Rust implementation used by every language binding.
8
+ */
15
9
  declare function mdiToDocx(tree: Root, profile?: ExportProfile): Promise<Buffer>;
16
10
 
17
11
  export { MDI_SPEC_VERSION, mdiToDocx };
package/dist/index.js CHANGED
@@ -1,428 +1,15 @@
1
1
  // src/index.ts
2
- import {
3
- AlignmentType,
4
- Document,
5
- ExternalHyperlink,
6
- Footer,
7
- FootnoteReferenceRun,
8
- HeadingLevel,
9
- Header,
10
- ImportedXmlComponent,
11
- Packer,
12
- PageBreak,
13
- PageNumber,
14
- PageTextDirectionType,
15
- Paragraph,
16
- Table as DocxTable,
17
- TableCell,
18
- TableLayoutType,
19
- TableRow,
20
- TextRun,
21
- WidthType
22
- } from "docx";
23
- import {
24
- PAGE_DIMENSIONS,
25
- resolvePrintProfile
26
- } from "@illusions-lab/mdi-export-profile";
2
+ import { renderDocxWithProfile } from "@illusions-lab/mdi-core";
3
+ import { mdastToMdiSource } from "mdast-util-mdi";
27
4
  var MDI_SPEC_VERSION = "2.0";
28
5
  async function mdiToDocx(tree, profile) {
29
- const options = resolvePrintProfile(
30
- profile,
31
- tree.data?.frontmatter?.writingMode
6
+ if (!tree || tree.type !== "root" || !Array.isArray(tree.children))
7
+ throw new TypeError("tree must be an mdast root");
8
+ if (profile !== void 0 && (!profile || typeof profile !== "object" || Array.isArray(profile)))
9
+ throw new TypeError("profile must be an object");
10
+ return Buffer.from(
11
+ renderDocxWithProfile(mdastToMdiSource(tree), JSON.stringify(profile ?? {}))
32
12
  );
33
- const definitions = tree.children.filter(
34
- (node) => node.type === "footnoteDefinition"
35
- );
36
- const footnoteIds = new Map(
37
- definitions.map((definition, index) => [definition.identifier, index + 1])
38
- );
39
- const context = { footnoteIds };
40
- const children = tree.children.flatMap((node) => block(node, options, context));
41
- const dimensions = PAGE_DIMENSIONS[options.pagination.pageSize];
42
- const widthMm = options.pagination.landscape ? dimensions.height : dimensions.width;
43
- const heightMm = options.pagination.landscape ? dimensions.width : dimensions.height;
44
- const primary = options.typesetting.writingMode === "vertical" ? heightMm - options.pagination.margins.top - options.pagination.margins.bottom : widthMm - options.pagination.margins.left - options.pagination.margins.right;
45
- const cross = options.typesetting.writingMode === "vertical" ? widthMm - options.pagination.margins.left - options.pagination.margins.right : heightMm - options.pagination.margins.top - options.pagination.margins.bottom;
46
- const fontSizeMm = options.typesetting.fontSize / 72 * 25.4;
47
- const fontSizeHalfPoints = Math.round(fontSizeMm / 25.4 * 72 * 2);
48
- const strictGrid = options.pagination.gridMode === "strict";
49
- const characterPitchPoints = primary / options.pagination.charactersPerLine / 25.4 * 72;
50
- const characterSpace = Math.max(
51
- 0,
52
- Math.round((characterPitchPoints - fontSizeHalfPoints / 2) * 4096)
53
- );
54
- const lineTwips = options.typesetting.lineSpacing === void 0 ? Math.round(cross / options.pagination.linesPerPage / 25.4 * 1440) : Math.round(fontSizeHalfPoints * 10 * options.typesetting.lineSpacing);
55
- const document = new Document({
56
- title: options.metadata.title ?? tree.data?.frontmatter?.title,
57
- creator: options.metadata.author ?? tree.data?.frontmatter?.author,
58
- footnotes: Object.fromEntries(
59
- definitions.map((definition, index) => [
60
- String(index + 1),
61
- {
62
- children: definition.children.flatMap(
63
- (child) => child.type === "paragraph" ? [new Paragraph({ children: inline(child.children, context) })] : []
64
- )
65
- }
66
- ])
67
- ),
68
- styles: {
69
- default: {
70
- document: {
71
- run: {
72
- font: options.typesetting.fontFamily,
73
- size: fontSizeHalfPoints,
74
- color: "000000"
75
- },
76
- paragraph: { spacing: { line: lineTwips, after: strictGrid ? 0 : 120 } }
77
- },
78
- heading1: headingStyle(options, 1, fontSizeHalfPoints),
79
- heading2: headingStyle(options, 2, fontSizeHalfPoints),
80
- heading3: headingStyle(options, 3, fontSizeHalfPoints),
81
- heading4: headingStyle(options, 4, fontSizeHalfPoints),
82
- heading5: headingStyle(options, 5, fontSizeHalfPoints),
83
- heading6: headingStyle(options, 6, fontSizeHalfPoints),
84
- listParagraph: {
85
- run: { font: options.typesetting.fontFamily, color: "000000" }
86
- }
87
- },
88
- paragraphStyles: [
89
- customHeadingStyle(options, 7, fontSizeHalfPoints),
90
- customHeadingStyle(options, 8, fontSizeHalfPoints),
91
- customHeadingStyle(options, 9, fontSizeHalfPoints),
92
- {
93
- id: "MdiQuote",
94
- name: "MDI Quote",
95
- basedOn: "Normal",
96
- run: { font: options.typesetting.fontFamily, color: "000000", italics: true },
97
- paragraph: {
98
- indent: { left: 720, right: 360 },
99
- spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 120, line: lineTwips }
100
- }
101
- },
102
- {
103
- id: "MdiList",
104
- name: "MDI List",
105
- basedOn: "ListParagraph",
106
- run: { font: options.typesetting.fontFamily, color: "000000" },
107
- paragraph: { spacing: { after: strictGrid ? 0 : 60, line: lineTwips } }
108
- },
109
- {
110
- id: "MdiCode",
111
- name: "MDI Code Block",
112
- basedOn: "Normal",
113
- run: { font: "Courier New", color: "000000" },
114
- paragraph: {
115
- indent: { left: 360, right: 360 },
116
- spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 120, line: lineTwips }
117
- }
118
- },
119
- {
120
- id: "MdiThematicBreak",
121
- name: "MDI Thematic Break",
122
- basedOn: "Normal",
123
- run: { font: options.typesetting.fontFamily, color: "000000" },
124
- paragraph: { spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 180, line: lineTwips } }
125
- }
126
- ]
127
- },
128
- sections: [
129
- {
130
- ...pageNumberSection(options, fontSizeHalfPoints),
131
- properties: {
132
- page: {
133
- size: { width: mmToTwips(widthMm), height: mmToTwips(heightMm) },
134
- margin: Object.fromEntries(
135
- Object.entries(options.pagination.margins).map(([key, value]) => [
136
- key,
137
- mmToTwips(value)
138
- ])
139
- ),
140
- ...options.pagination.pageNumbers.enabled ? { pageNumbers: { start: 1 } } : {},
141
- ...options.typesetting.writingMode === "vertical" ? {
142
- textDirection: PageTextDirectionType.TOP_TO_BOTTOM_RIGHT_TO_LEFT
143
- } : {}
144
- },
145
- // Word's document grid is the portable OOXML mechanism that records
146
- // a publisher's characters × lines contract. `linePitch` comes from
147
- // the resolved physical block extent; the matching PDF profile uses
148
- // the same resolved page geometry.
149
- ...strictGrid ? {
150
- grid: {
151
- type: "linesAndChars",
152
- ...characterSpace === 0 ? {} : { charSpace: characterSpace },
153
- linePitch: lineTwips
154
- }
155
- } : {}
156
- },
157
- children
158
- }
159
- ]
160
- });
161
- addBookMarginSettings(document, options);
162
- return Packer.toBuffer(document);
163
- }
164
- function addBookMarginSettings(document, options) {
165
- if (options.layout.marginMode !== "mirror") return;
166
- const namespace = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"';
167
- document.Settings.addChildElement(
168
- ImportedXmlComponent.fromXmlString(`<w:mirrorMargins ${namespace}/>`)
169
- );
170
- if (options.layout.bindingSide === "right")
171
- document.Settings.addChildElement(
172
- ImportedXmlComponent.fromXmlString(`<w:rtlGutter ${namespace}/>`)
173
- );
174
- }
175
- function headingStyle(options, level, bodyFontSizeHalfPoints2 = 22) {
176
- const before = [360, 300, 240, 180, 150, 120][level - 1] ?? 120;
177
- const strictGrid = options.pagination.gridMode === "strict";
178
- return {
179
- run: {
180
- font: options.typesetting.fontFamily,
181
- color: "000000",
182
- bold: true,
183
- size: strictGrid ? bodyFontSizeHalfPoints2 : headingFontSize(level, bodyFontSizeHalfPoints2)
184
- },
185
- paragraph: {
186
- spacing: { before: strictGrid ? 0 : before, after: strictGrid ? 0 : 120 },
187
- keepNext: true,
188
- keepLines: true,
189
- outlineLevel: level - 1
190
- }
191
- };
192
- }
193
- function headingFontSize(level, bodyFontSizeHalfPoints2 = 22) {
194
- const scale = [1.8, 1.55, 1.35, 1.2, 1.1, 1][level - 1] ?? 1;
195
- return Math.round(bodyFontSizeHalfPoints2 * scale);
196
- }
197
- function customHeadingStyle(options, level, bodyFontSizeHalfPoints2 = 22) {
198
- const style = headingStyle(options, level, bodyFontSizeHalfPoints2);
199
- return {
200
- id: `Heading${level}`,
201
- name: `Heading ${level}`,
202
- basedOn: "Normal",
203
- next: "Normal",
204
- quickFormat: true,
205
- ...style
206
- };
207
- }
208
- function pageNumberSection(options, size) {
209
- const page = options.pagination.pageNumbers;
210
- if (!page.enabled) return {};
211
- const alignment = page.position.endsWith("left") ? AlignmentType.LEFT : page.position.endsWith("right") ? AlignmentType.RIGHT : AlignmentType.CENTER;
212
- const current = new TextRun({ children: [PageNumber.CURRENT], size });
213
- const runs = page.format === "dash" ? [
214
- new TextRun({ text: "\u2014 ", size }),
215
- current,
216
- new TextRun({ text: " \u2014", size })
217
- ] : page.format === "fraction" ? [
218
- current,
219
- new TextRun({ text: " / ", size }),
220
- new TextRun({ children: [PageNumber.TOTAL_PAGES], size })
221
- ] : [current];
222
- const paragraph2 = new Paragraph({ alignment, children: runs });
223
- return page.position.startsWith("top-") ? { headers: { default: new Header({ children: [paragraph2] }) } } : { footers: { default: new Footer({ children: [paragraph2] }) } };
224
- }
225
- function block(node, options, context) {
226
- if (node.type === "heading") {
227
- const runSize = options.pagination.gridMode === "strict" ? bodyFontSizeHalfPoints(options) : headingFontSize(node.depth);
228
- return [
229
- new Paragraph({
230
- heading: [
231
- HeadingLevel.HEADING_1,
232
- HeadingLevel.HEADING_2,
233
- HeadingLevel.HEADING_3,
234
- HeadingLevel.HEADING_4,
235
- HeadingLevel.HEADING_5,
236
- HeadingLevel.HEADING_6
237
- ][node.depth - 1],
238
- children: inline(node.children, context, runSize)
239
- })
240
- ];
241
- }
242
- if (node.type === "paragraph") return [paragraph(node.children, options, void 0, context)];
243
- if (node.type === "list")
244
- return node.children.flatMap(
245
- (item) => item.children.filter((n) => n.type === "paragraph").map(
246
- (n) => new Paragraph({
247
- style: "MdiList",
248
- children: inline(n.children, context),
249
- bullet: { level: 0 }
250
- })
251
- )
252
- );
253
- if (node.type === "blockquote")
254
- return node.children.flatMap(
255
- (child) => child.type === "paragraph" ? [paragraph(child.children, options, "MdiQuote", context)] : block(child, options, context)
256
- );
257
- if (node.type === "code")
258
- return [
259
- new Paragraph({
260
- style: "MdiCode",
261
- children: node.value.split("\n").map(
262
- (line, index) => new TextRun({
263
- text: line,
264
- break: index === 0 ? void 0 : 1,
265
- font: "Courier New"
266
- })
267
- )
268
- })
269
- ];
270
- if (node.type === "thematicBreak")
271
- return [
272
- new Paragraph({
273
- style: "MdiThematicBreak",
274
- alignment: AlignmentType.CENTER,
275
- children: [new TextRun("\u2014 \u2014 \u2014")]
276
- })
277
- ];
278
- if (node.type === "table") return [tableBlock(node, options, context)];
279
- if (node.type === "mdiPagebreak")
280
- return [new Paragraph({ children: [new PageBreak()] })];
281
- if (node.type === "mdiBlank") return [new Paragraph("")];
282
- return [];
283
- }
284
- function tableBlock(node, options, context) {
285
- const columns = Math.max(1, ...node.children.map((row) => row.children.length));
286
- const dimensions = PAGE_DIMENSIONS[options.pagination.pageSize];
287
- const pageWidth = options.pagination.landscape ? dimensions.height : dimensions.width;
288
- const usableWidth = mmToTwips(
289
- pageWidth - options.pagination.margins.left - options.pagination.margins.right
290
- );
291
- const columnWidth = Math.floor(usableWidth / columns);
292
- return new DocxTable({
293
- width: { size: usableWidth, type: WidthType.DXA },
294
- columnWidths: Array.from({ length: columns }, () => columnWidth),
295
- layout: TableLayoutType.FIXED,
296
- margins: { top: 80, bottom: 80, left: 100, right: 100 },
297
- rows: node.children.map(
298
- (row, rowIndex) => new TableRow({
299
- children: row.children.map(
300
- (cell) => new TableCell({
301
- width: { size: columnWidth, type: WidthType.DXA },
302
- children: [
303
- new Paragraph({
304
- children: rowIndex === 0 ? [new TextRun({ text: inlineText(cell.children), bold: true })] : inline(cell.children, context)
305
- })
306
- ]
307
- })
308
- )
309
- })
310
- )
311
- });
312
- }
313
- function paragraph(nodes, options, style, context) {
314
- const fontSizeMm = options.typesetting.fontSize / 72 * 25.4;
315
- const prefix = options.typesetting.fullwidthSpaceIndent ? [new TextRun("\u3000".repeat(Math.round(options.typesetting.textIndentEm)))] : [];
316
- return new Paragraph({
317
- style,
318
- indent: options.typesetting.fullwidthSpaceIndent ? void 0 : {
319
- ...style ? {} : {
320
- firstLine: Math.round(
321
- options.typesetting.textIndentEm * fontSizeMm / 25.4 * 1440
322
- )
323
- }
324
- },
325
- children: [...prefix, ...inline(nodes, context)]
326
- });
327
- }
328
- function inline(nodes, context, rubyBaseTextSize = 24) {
329
- return nodes.flatMap((node) => {
330
- if (node.type === "text") return [new TextRun(node.value)];
331
- if (node.type === "break" || node.type === "mdiBreak")
332
- return [new TextRun({ break: 1 })];
333
- if (node.type === "emphasis")
334
- return [new TextRun({ italics: true, children: inline(node.children, context, rubyBaseTextSize) })];
335
- if (node.type === "strong")
336
- return [new TextRun({ bold: true, children: inline(node.children, context, rubyBaseTextSize) })];
337
- if (node.type === "delete")
338
- return [new TextRun({ strike: true, children: inline(node.children, context, rubyBaseTextSize) })];
339
- if (node.type === "inlineCode")
340
- return [new TextRun({ text: node.value, font: "Courier New" })];
341
- if (node.type === "image")
342
- return [new TextRun({ text: node.alt ? `[Image: ${node.alt}]` : "[Image]" })];
343
- if (node.type === "link")
344
- return [
345
- new ExternalHyperlink({
346
- link: node.url,
347
- children: inline(node.children, context, rubyBaseTextSize)
348
- })
349
- ];
350
- if (node.type === "footnoteReference") {
351
- const id = context.footnoteIds.get(node.identifier);
352
- return id ? [new FootnoteReferenceRun(id)] : [];
353
- }
354
- if (node.type === "mdiEm")
355
- return [
356
- new TextRun({
357
- emphasisMark: { type: "dot" },
358
- children: inline(node.children, context, rubyBaseTextSize)
359
- })
360
- ];
361
- if (node.type === "mdiRuby") return [rawRun(rubyXml(node.base, node.ruby, rubyBaseTextSize))];
362
- if (node.type === "mdiTcy")
363
- return [
364
- rawRun(
365
- `<w:r><w:rPr><w:eastAsianLayout w:combine="1" w:combineBrackets="none"/></w:rPr><w:t>${xml(
366
- node.value
367
- )}</w:t></w:r>`
368
- )
369
- ];
370
- if (node.type === "mdiWarichu")
371
- return [
372
- new TextRun({
373
- size: Math.max(10, Math.round(rubyBaseTextSize * 0.6)),
374
- children: inline(node.children, context, rubyBaseTextSize)
375
- })
376
- ];
377
- if (node.type === "mdiKern") {
378
- const spacing = kernTwips(node.amount, rubyBaseTextSize);
379
- return [
380
- new TextRun({
381
- ...spacing === 0 ? {} : { characterSpacing: spacing },
382
- children: inline(node.children, context, rubyBaseTextSize)
383
- })
384
- ];
385
- }
386
- if (node.type === "mdiNoBreak")
387
- return inline(node.children, context, rubyBaseTextSize);
388
- if ("children" in node) return inline(node.children, context, rubyBaseTextSize);
389
- return [];
390
- });
391
- }
392
- function kernTwips(amount, baseTextSize) {
393
- const value = Number.parseFloat(amount);
394
- if (!Number.isFinite(value) || !amount.endsWith("em")) return 0;
395
- return Math.round(value * baseTextSize * 10);
396
- }
397
- function inlineText(nodes) {
398
- return nodes.map((node) => node.type === "text" ? node.value : "children" in node ? inlineText(node.children) : "").join("");
399
- }
400
- function mmToTwips(mm) {
401
- return Math.round(mm / 25.4 * 1440);
402
- }
403
- function bodyFontSizeHalfPoints(options) {
404
- return Math.round(options.typesetting.fontSize * 2);
405
- }
406
- function rubyXml(base, reading, baseTextSize = 24) {
407
- const text = Array.isArray(reading) ? reading.join(".") : reading;
408
- const rubySize = Math.max(10, Math.round(baseTextSize / 2));
409
- const rubyRaise = baseTextSize === 24 ? 18 : Math.max(18, Math.round(baseTextSize * 0.8));
410
- return `<w:ruby><w:rubyPr><w:rubyAlign w:val="center"/><w:hps w:val="${rubySize}"/><w:hpsRaise w:val="${rubyRaise}"/><w:hpsBaseText w:val="${baseTextSize}"/></w:rubyPr><w:rt><w:r><w:t>${xml(
411
- text
412
- )}</w:t></w:r></w:rt><w:rubyBase><w:r><w:t>${xml(
413
- base
414
- )}</w:t></w:r></w:rubyBase></w:ruby>`;
415
- }
416
- function rawRun(xmlString) {
417
- const namespaced = xmlString.replace(
418
- /^<w:([\w-]+)/,
419
- '<w:$1 xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
420
- );
421
- const imported = ImportedXmlComponent.fromXmlString(namespaced);
422
- return imported.root[0];
423
- }
424
- function xml(value) {
425
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;");
426
13
  }
427
14
  export {
428
15
  MDI_SPEC_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illusions-lab/mdi-to-docx",
3
- "version": "2.0.24",
3
+ "version": "2.0.26",
4
4
  "description": "Render MDI (.mdi) documents to DOCX",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,9 +28,9 @@
28
28
  "typecheck": "tsc --noEmit"
29
29
  },
30
30
  "dependencies": {
31
- "@illusions-lab/mdi-export-profile": "^2.0.16",
32
- "mdast-util-mdi": "^2.0.16",
33
- "docx": "^9.0.0",
31
+ "@illusions-lab/mdi-core": "^2.0.13",
32
+ "@illusions-lab/mdi-export-profile": "^2.0.18",
33
+ "mdast-util-mdi": "^2.0.21",
34
34
  "@types/mdast": "^4.0.0"
35
35
  },
36
36
  "devDependencies": {