@illusions-lab/mdi-to-docx 2.0.21 → 2.0.23

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
@@ -22,8 +22,30 @@ await writeFile("book.docx", document);
22
22
  ```
23
23
 
24
24
  For complete `.mdi` source, use `renderDocx(source)` from
25
- [`@illusions-lab/mdi`](https://www.npmjs.com/package/@illusions-lab/mdi).
26
- The MDI CLI uses that Rust route directly.
25
+ [`@illusions-lab/mdi`](https://www.npmjs.com/package/@illusions-lab/mdi), or
26
+ its profile-aware `renderDocx(source, profile)` overload when page and
27
+ typesetting settings are required. The CLI uses the same profile-aware route
28
+ when passed `--config`.
29
+
30
+ ## MDI construct mapping
31
+
32
+ DOCX is an OOXML print target, not a lossless CSS layout engine. The adapter
33
+ uses native Word constructs where they exist and intentionally documents the
34
+ remaining fallbacks:
35
+
36
+ | MDI construct | DOCX output |
37
+ | --- | --- |
38
+ | ruby | Native `<w:ruby>` run |
39
+ | tate-chu-yoko | Native East Asian combined-character run |
40
+ | `[[em:...]]` | Native Word dot emphasis; Word cannot retain arbitrary MDI mark glyphs |
41
+ | `[[kern:+/-Nem:...]]` | Signed run character spacing, calculated from the current run size |
42
+ | `[[warichu:...]]` | Preserved text at 60% size; OOXML has no portable two-line warichu primitive |
43
+ | `[[no-break:...]]` | Preserved text; OOXML cannot prohibit a line break for an arbitrary run |
44
+ | blank paragraph / pagebreak | Empty paragraph / native page break |
45
+
46
+ 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.
27
49
 
28
50
  ## Documentation
29
51
 
package/dist/index.cjs CHANGED
@@ -45,11 +45,15 @@ async function mdiToDocx(tree, profile) {
45
45
  const heightMm = options.pagination.landscape ? dimensions.width : dimensions.height;
46
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
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 = primary / options.pagination.charactersPerLine;
48
+ const fontSizeMm = options.typesetting.fontSize / 72 * 25.4;
49
49
  const fontSizeHalfPoints = Math.round(fontSizeMm / 25.4 * 72 * 2);
50
- const lineTwips = Math.round(
51
- cross / options.pagination.linesPerPage / 25.4 * 1440
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)
52
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);
53
57
  const document = new import_docx.Document({
54
58
  title: options.metadata.title ?? tree.data?.frontmatter?.title,
55
59
  creator: options.metadata.author ?? tree.data?.frontmatter?.author,
@@ -71,22 +75,22 @@ async function mdiToDocx(tree, profile) {
71
75
  size: fontSizeHalfPoints,
72
76
  color: "000000"
73
77
  },
74
- paragraph: { spacing: { line: lineTwips, after: 120 } }
78
+ paragraph: { spacing: { line: lineTwips, after: strictGrid ? 0 : 120 } }
75
79
  },
76
- heading1: headingStyle(options, 1),
77
- heading2: headingStyle(options, 2),
78
- heading3: headingStyle(options, 3),
79
- heading4: headingStyle(options, 4),
80
- heading5: headingStyle(options, 5),
81
- heading6: headingStyle(options, 6),
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),
82
86
  listParagraph: {
83
87
  run: { font: options.typesetting.fontFamily, color: "000000" }
84
88
  }
85
89
  },
86
90
  paragraphStyles: [
87
- customHeadingStyle(options, 7),
88
- customHeadingStyle(options, 8),
89
- customHeadingStyle(options, 9),
91
+ customHeadingStyle(options, 7, fontSizeHalfPoints),
92
+ customHeadingStyle(options, 8, fontSizeHalfPoints),
93
+ customHeadingStyle(options, 9, fontSizeHalfPoints),
90
94
  {
91
95
  id: "MdiQuote",
92
96
  name: "MDI Quote",
@@ -94,7 +98,7 @@ async function mdiToDocx(tree, profile) {
94
98
  run: { font: options.typesetting.fontFamily, color: "000000", italics: true },
95
99
  paragraph: {
96
100
  indent: { left: 720, right: 360 },
97
- spacing: { before: 120, after: 120, line: lineTwips }
101
+ spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 120, line: lineTwips }
98
102
  }
99
103
  },
100
104
  {
@@ -102,7 +106,7 @@ async function mdiToDocx(tree, profile) {
102
106
  name: "MDI List",
103
107
  basedOn: "ListParagraph",
104
108
  run: { font: options.typesetting.fontFamily, color: "000000" },
105
- paragraph: { spacing: { after: 60, line: lineTwips } }
109
+ paragraph: { spacing: { after: strictGrid ? 0 : 60, line: lineTwips } }
106
110
  },
107
111
  {
108
112
  id: "MdiCode",
@@ -111,7 +115,7 @@ async function mdiToDocx(tree, profile) {
111
115
  run: { font: "Courier New", color: "000000" },
112
116
  paragraph: {
113
117
  indent: { left: 360, right: 360 },
114
- spacing: { before: 120, after: 120, line: lineTwips }
118
+ spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 120, line: lineTwips }
115
119
  }
116
120
  },
117
121
  {
@@ -119,7 +123,7 @@ async function mdiToDocx(tree, profile) {
119
123
  name: "MDI Thematic Break",
120
124
  basedOn: "Normal",
121
125
  run: { font: options.typesetting.fontFamily, color: "000000" },
122
- paragraph: { spacing: { before: 120, after: 180 } }
126
+ paragraph: { spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 180, line: lineTwips } }
123
127
  }
124
128
  ]
125
129
  },
@@ -139,37 +143,61 @@ async function mdiToDocx(tree, profile) {
139
143
  ...options.typesetting.writingMode === "vertical" ? {
140
144
  textDirection: import_docx.PageTextDirectionType.TOP_TO_BOTTOM_RIGHT_TO_LEFT
141
145
  } : {}
142
- }
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
+ } : {}
143
158
  },
144
159
  children
145
160
  }
146
161
  ]
147
162
  });
163
+ addBookMarginSettings(document, options);
148
164
  return import_docx.Packer.toBuffer(document);
149
165
  }
150
- function headingStyle(options, level) {
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) {
151
178
  const before = [360, 300, 240, 180, 150, 120][level - 1] ?? 120;
179
+ const strictGrid = options.pagination.gridMode === "strict";
152
180
  return {
153
181
  run: {
154
182
  font: options.typesetting.fontFamily,
155
183
  color: "000000",
156
184
  bold: true,
157
- size: headingFontSize(level)
185
+ size: strictGrid ? bodyFontSizeHalfPoints2 : headingFontSize(level, bodyFontSizeHalfPoints2)
158
186
  },
159
187
  paragraph: {
160
- spacing: { before, after: 120 },
188
+ spacing: { before: strictGrid ? 0 : before, after: strictGrid ? 0 : 120 },
161
189
  keepNext: true,
162
190
  keepLines: true,
163
191
  outlineLevel: level - 1
164
192
  }
165
193
  };
166
194
  }
167
- function headingFontSize(level) {
195
+ function headingFontSize(level, bodyFontSizeHalfPoints2 = 22) {
168
196
  const scale = [1.8, 1.55, 1.35, 1.2, 1.1, 1][level - 1] ?? 1;
169
- return Math.round(22 * scale);
197
+ return Math.round(bodyFontSizeHalfPoints2 * scale);
170
198
  }
171
- function customHeadingStyle(options, level) {
172
- const style = headingStyle(options, level);
199
+ function customHeadingStyle(options, level, bodyFontSizeHalfPoints2 = 22) {
200
+ const style = headingStyle(options, level, bodyFontSizeHalfPoints2);
173
201
  return {
174
202
  id: `Heading${level}`,
175
203
  name: `Heading ${level}`,
@@ -197,7 +225,8 @@ function pageNumberSection(options, size) {
197
225
  return page.position.startsWith("top-") ? { headers: { default: new import_docx.Header({ children: [paragraph2] }) } } : { footers: { default: new import_docx.Footer({ children: [paragraph2] }) } };
198
226
  }
199
227
  function block(node, options, context) {
200
- if (node.type === "heading")
228
+ if (node.type === "heading") {
229
+ const runSize = options.pagination.gridMode === "strict" ? bodyFontSizeHalfPoints(options) : headingFontSize(node.depth);
201
230
  return [
202
231
  new import_docx.Paragraph({
203
232
  heading: [
@@ -208,9 +237,10 @@ function block(node, options, context) {
208
237
  import_docx.HeadingLevel.HEADING_5,
209
238
  import_docx.HeadingLevel.HEADING_6
210
239
  ][node.depth - 1],
211
- children: inline(node.children, context, headingFontSize(node.depth))
240
+ children: inline(node.children, context, runSize)
212
241
  })
213
242
  ];
243
+ }
214
244
  if (node.type === "paragraph") return [paragraph(node.children, options, void 0, context)];
215
245
  if (node.type === "list")
216
246
  return node.children.flatMap(
@@ -283,11 +313,7 @@ function tableBlock(node, options, context) {
283
313
  });
284
314
  }
285
315
  function paragraph(nodes, options, style, context) {
286
- const fontSizeMm = (() => {
287
- const dimensions = import_mdi_export_profile.PAGE_DIMENSIONS[options.pagination.pageSize];
288
- const primary = options.typesetting.writingMode === "vertical" ? (options.pagination.landscape ? dimensions.width : dimensions.height) - options.pagination.margins.top - options.pagination.margins.bottom : (options.pagination.landscape ? dimensions.height : dimensions.width) - options.pagination.margins.left - options.pagination.margins.right;
289
- return primary / options.pagination.charactersPerLine;
290
- })();
316
+ const fontSizeMm = options.typesetting.fontSize / 72 * 25.4;
291
317
  const prefix = options.typesetting.fullwidthSpaceIndent ? [new import_docx.TextRun("\u3000".repeat(Math.round(options.typesetting.textIndentEm)))] : [];
292
318
  return new import_docx.Paragraph({
293
319
  style,
@@ -327,6 +353,13 @@ function inline(nodes, context, rubyBaseTextSize = 24) {
327
353
  const id = context.footnoteIds.get(node.identifier);
328
354
  return id ? [new import_docx.FootnoteReferenceRun(id)] : [];
329
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
+ ];
330
363
  if (node.type === "mdiRuby") return [rawRun(rubyXml(node.base, node.ruby, rubyBaseTextSize))];
331
364
  if (node.type === "mdiTcy")
332
365
  return [
@@ -336,16 +369,42 @@ function inline(nodes, context, rubyBaseTextSize = 24) {
336
369
  )}</w:t></w:r>`
337
370
  )
338
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);
339
390
  if ("children" in node) return inline(node.children, context, rubyBaseTextSize);
340
391
  return [];
341
392
  });
342
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
+ }
343
399
  function inlineText(nodes) {
344
400
  return nodes.map((node) => node.type === "text" ? node.value : "children" in node ? inlineText(node.children) : "").join("");
345
401
  }
346
402
  function mmToTwips(mm) {
347
403
  return Math.round(mm / 25.4 * 1440);
348
404
  }
405
+ function bodyFontSizeHalfPoints(options) {
406
+ return Math.round(options.typesetting.fontSize * 2);
407
+ }
349
408
  function rubyXml(base, reading, baseTextSize = 24) {
350
409
  const text = Array.isArray(reading) ? reading.join(".") : reading;
351
410
  const rubySize = Math.max(10, Math.round(baseTextSize / 2));
package/dist/index.d.cts CHANGED
@@ -1,6 +1,15 @@
1
1
  import { Root } from 'mdast';
2
2
  import { ExportProfile } from '@illusions-lab/mdi-export-profile';
3
3
 
4
+ declare module "mdast" {
5
+ interface RootData {
6
+ frontmatter?: {
7
+ title?: string;
8
+ author?: string;
9
+ writingMode: "horizontal" | "vertical";
10
+ };
11
+ }
12
+ }
4
13
  declare const MDI_SPEC_VERSION = "2.0";
5
14
  /** Creates a DOCX file using the same profile fields as PDF and EPUB exports. */
6
15
  declare function mdiToDocx(tree: Root, profile?: ExportProfile): Promise<Buffer>;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  import { Root } from 'mdast';
2
2
  import { ExportProfile } from '@illusions-lab/mdi-export-profile';
3
3
 
4
+ declare module "mdast" {
5
+ interface RootData {
6
+ frontmatter?: {
7
+ title?: string;
8
+ author?: string;
9
+ writingMode: "horizontal" | "vertical";
10
+ };
11
+ }
12
+ }
4
13
  declare const MDI_SPEC_VERSION = "2.0";
5
14
  /** Creates a DOCX file using the same profile fields as PDF and EPUB exports. */
6
15
  declare function mdiToDocx(tree: Root, profile?: ExportProfile): Promise<Buffer>;
package/dist/index.js CHANGED
@@ -43,11 +43,15 @@ async function mdiToDocx(tree, profile) {
43
43
  const heightMm = options.pagination.landscape ? dimensions.width : dimensions.height;
44
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
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 = primary / options.pagination.charactersPerLine;
46
+ const fontSizeMm = options.typesetting.fontSize / 72 * 25.4;
47
47
  const fontSizeHalfPoints = Math.round(fontSizeMm / 25.4 * 72 * 2);
48
- const lineTwips = Math.round(
49
- cross / options.pagination.linesPerPage / 25.4 * 1440
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)
50
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);
51
55
  const document = new Document({
52
56
  title: options.metadata.title ?? tree.data?.frontmatter?.title,
53
57
  creator: options.metadata.author ?? tree.data?.frontmatter?.author,
@@ -69,22 +73,22 @@ async function mdiToDocx(tree, profile) {
69
73
  size: fontSizeHalfPoints,
70
74
  color: "000000"
71
75
  },
72
- paragraph: { spacing: { line: lineTwips, after: 120 } }
76
+ paragraph: { spacing: { line: lineTwips, after: strictGrid ? 0 : 120 } }
73
77
  },
74
- heading1: headingStyle(options, 1),
75
- heading2: headingStyle(options, 2),
76
- heading3: headingStyle(options, 3),
77
- heading4: headingStyle(options, 4),
78
- heading5: headingStyle(options, 5),
79
- heading6: headingStyle(options, 6),
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),
80
84
  listParagraph: {
81
85
  run: { font: options.typesetting.fontFamily, color: "000000" }
82
86
  }
83
87
  },
84
88
  paragraphStyles: [
85
- customHeadingStyle(options, 7),
86
- customHeadingStyle(options, 8),
87
- customHeadingStyle(options, 9),
89
+ customHeadingStyle(options, 7, fontSizeHalfPoints),
90
+ customHeadingStyle(options, 8, fontSizeHalfPoints),
91
+ customHeadingStyle(options, 9, fontSizeHalfPoints),
88
92
  {
89
93
  id: "MdiQuote",
90
94
  name: "MDI Quote",
@@ -92,7 +96,7 @@ async function mdiToDocx(tree, profile) {
92
96
  run: { font: options.typesetting.fontFamily, color: "000000", italics: true },
93
97
  paragraph: {
94
98
  indent: { left: 720, right: 360 },
95
- spacing: { before: 120, after: 120, line: lineTwips }
99
+ spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 120, line: lineTwips }
96
100
  }
97
101
  },
98
102
  {
@@ -100,7 +104,7 @@ async function mdiToDocx(tree, profile) {
100
104
  name: "MDI List",
101
105
  basedOn: "ListParagraph",
102
106
  run: { font: options.typesetting.fontFamily, color: "000000" },
103
- paragraph: { spacing: { after: 60, line: lineTwips } }
107
+ paragraph: { spacing: { after: strictGrid ? 0 : 60, line: lineTwips } }
104
108
  },
105
109
  {
106
110
  id: "MdiCode",
@@ -109,7 +113,7 @@ async function mdiToDocx(tree, profile) {
109
113
  run: { font: "Courier New", color: "000000" },
110
114
  paragraph: {
111
115
  indent: { left: 360, right: 360 },
112
- spacing: { before: 120, after: 120, line: lineTwips }
116
+ spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 120, line: lineTwips }
113
117
  }
114
118
  },
115
119
  {
@@ -117,7 +121,7 @@ async function mdiToDocx(tree, profile) {
117
121
  name: "MDI Thematic Break",
118
122
  basedOn: "Normal",
119
123
  run: { font: options.typesetting.fontFamily, color: "000000" },
120
- paragraph: { spacing: { before: 120, after: 180 } }
124
+ paragraph: { spacing: { before: strictGrid ? 0 : 120, after: strictGrid ? 0 : 180, line: lineTwips } }
121
125
  }
122
126
  ]
123
127
  },
@@ -137,37 +141,61 @@ async function mdiToDocx(tree, profile) {
137
141
  ...options.typesetting.writingMode === "vertical" ? {
138
142
  textDirection: PageTextDirectionType.TOP_TO_BOTTOM_RIGHT_TO_LEFT
139
143
  } : {}
140
- }
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
+ } : {}
141
156
  },
142
157
  children
143
158
  }
144
159
  ]
145
160
  });
161
+ addBookMarginSettings(document, options);
146
162
  return Packer.toBuffer(document);
147
163
  }
148
- function headingStyle(options, level) {
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) {
149
176
  const before = [360, 300, 240, 180, 150, 120][level - 1] ?? 120;
177
+ const strictGrid = options.pagination.gridMode === "strict";
150
178
  return {
151
179
  run: {
152
180
  font: options.typesetting.fontFamily,
153
181
  color: "000000",
154
182
  bold: true,
155
- size: headingFontSize(level)
183
+ size: strictGrid ? bodyFontSizeHalfPoints2 : headingFontSize(level, bodyFontSizeHalfPoints2)
156
184
  },
157
185
  paragraph: {
158
- spacing: { before, after: 120 },
186
+ spacing: { before: strictGrid ? 0 : before, after: strictGrid ? 0 : 120 },
159
187
  keepNext: true,
160
188
  keepLines: true,
161
189
  outlineLevel: level - 1
162
190
  }
163
191
  };
164
192
  }
165
- function headingFontSize(level) {
193
+ function headingFontSize(level, bodyFontSizeHalfPoints2 = 22) {
166
194
  const scale = [1.8, 1.55, 1.35, 1.2, 1.1, 1][level - 1] ?? 1;
167
- return Math.round(22 * scale);
195
+ return Math.round(bodyFontSizeHalfPoints2 * scale);
168
196
  }
169
- function customHeadingStyle(options, level) {
170
- const style = headingStyle(options, level);
197
+ function customHeadingStyle(options, level, bodyFontSizeHalfPoints2 = 22) {
198
+ const style = headingStyle(options, level, bodyFontSizeHalfPoints2);
171
199
  return {
172
200
  id: `Heading${level}`,
173
201
  name: `Heading ${level}`,
@@ -195,7 +223,8 @@ function pageNumberSection(options, size) {
195
223
  return page.position.startsWith("top-") ? { headers: { default: new Header({ children: [paragraph2] }) } } : { footers: { default: new Footer({ children: [paragraph2] }) } };
196
224
  }
197
225
  function block(node, options, context) {
198
- if (node.type === "heading")
226
+ if (node.type === "heading") {
227
+ const runSize = options.pagination.gridMode === "strict" ? bodyFontSizeHalfPoints(options) : headingFontSize(node.depth);
199
228
  return [
200
229
  new Paragraph({
201
230
  heading: [
@@ -206,9 +235,10 @@ function block(node, options, context) {
206
235
  HeadingLevel.HEADING_5,
207
236
  HeadingLevel.HEADING_6
208
237
  ][node.depth - 1],
209
- children: inline(node.children, context, headingFontSize(node.depth))
238
+ children: inline(node.children, context, runSize)
210
239
  })
211
240
  ];
241
+ }
212
242
  if (node.type === "paragraph") return [paragraph(node.children, options, void 0, context)];
213
243
  if (node.type === "list")
214
244
  return node.children.flatMap(
@@ -281,11 +311,7 @@ function tableBlock(node, options, context) {
281
311
  });
282
312
  }
283
313
  function paragraph(nodes, options, style, context) {
284
- const fontSizeMm = (() => {
285
- const dimensions = PAGE_DIMENSIONS[options.pagination.pageSize];
286
- const primary = options.typesetting.writingMode === "vertical" ? (options.pagination.landscape ? dimensions.width : dimensions.height) - options.pagination.margins.top - options.pagination.margins.bottom : (options.pagination.landscape ? dimensions.height : dimensions.width) - options.pagination.margins.left - options.pagination.margins.right;
287
- return primary / options.pagination.charactersPerLine;
288
- })();
314
+ const fontSizeMm = options.typesetting.fontSize / 72 * 25.4;
289
315
  const prefix = options.typesetting.fullwidthSpaceIndent ? [new TextRun("\u3000".repeat(Math.round(options.typesetting.textIndentEm)))] : [];
290
316
  return new Paragraph({
291
317
  style,
@@ -325,6 +351,13 @@ function inline(nodes, context, rubyBaseTextSize = 24) {
325
351
  const id = context.footnoteIds.get(node.identifier);
326
352
  return id ? [new FootnoteReferenceRun(id)] : [];
327
353
  }
354
+ if (node.type === "mdiEm")
355
+ return [
356
+ new TextRun({
357
+ emphasisMark: { type: "dot" },
358
+ children: inline(node.children, context, rubyBaseTextSize)
359
+ })
360
+ ];
328
361
  if (node.type === "mdiRuby") return [rawRun(rubyXml(node.base, node.ruby, rubyBaseTextSize))];
329
362
  if (node.type === "mdiTcy")
330
363
  return [
@@ -334,16 +367,42 @@ function inline(nodes, context, rubyBaseTextSize = 24) {
334
367
  )}</w:t></w:r>`
335
368
  )
336
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);
337
388
  if ("children" in node) return inline(node.children, context, rubyBaseTextSize);
338
389
  return [];
339
390
  });
340
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
+ }
341
397
  function inlineText(nodes) {
342
398
  return nodes.map((node) => node.type === "text" ? node.value : "children" in node ? inlineText(node.children) : "").join("");
343
399
  }
344
400
  function mmToTwips(mm) {
345
401
  return Math.round(mm / 25.4 * 1440);
346
402
  }
403
+ function bodyFontSizeHalfPoints(options) {
404
+ return Math.round(options.typesetting.fontSize * 2);
405
+ }
347
406
  function rubyXml(base, reading, baseTextSize = 24) {
348
407
  const text = Array.isArray(reading) ? reading.join(".") : reading;
349
408
  const rubySize = Math.max(10, Math.round(baseTextSize / 2));
package/package.json CHANGED
@@ -1,46 +1,45 @@
1
1
  {
2
- "name": "@illusions-lab/mdi-to-docx",
3
- "version": "2.0.21",
4
- "description": "Render MDI (.mdi) documents to DOCX",
5
- "license": "MIT",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/illusions-lab/MDI.git",
9
- "directory": "nodejs/packages/to-docx"
10
- },
11
- "homepage": "https://mdi.illusions.app/bindings/javascript/",
12
- "bugs": "https://github.com/illusions-lab/MDI/issues",
13
- "type": "module",
14
- "main": "./dist/index.js",
15
- "types": "./dist/index.d.ts",
16
- "exports": {
17
- ".": {
18
- "types": "./dist/index.d.ts",
19
- "default": "./dist/index.js"
20
- }
21
- },
22
- "files": [
23
- "dist"
24
- ],
25
- "scripts": {
26
- "build": "tsup src/index.ts --format esm,cjs --dts",
27
- "test": "vitest run --passWithNoTests",
28
- "typecheck": "tsc --noEmit"
29
- },
30
- "dependencies": {
31
- "@illusions-lab/mdi-export-profile": "workspace:*",
32
- "mdast-util-mdi": "workspace:*",
33
- "docx": "^9.0.0",
34
- "@types/mdast": "^4.0.0"
35
- },
36
- "devDependencies": {
37
- "@illusions-lab/mdi-remark": "workspace:*",
38
- "remark-parse": "^11.0.0",
39
- "unified": "^11.0.0",
40
- "jszip": "^3.10.0",
41
- "tsup": "^8.3.0",
42
- "typescript": "^5.6.0",
43
- "vitest": "^2.1.0",
44
- "@types/node": "^20.0.0"
45
- }
2
+ "name": "@illusions-lab/mdi-to-docx",
3
+ "version": "2.0.23",
4
+ "description": "Render MDI (.mdi) documents to DOCX",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/illusions-lab/MDI.git",
9
+ "directory": "nodejs/packages/to-docx"
10
+ },
11
+ "homepage": "https://mdi.illusions.app/bindings/javascript/",
12
+ "bugs": "https://github.com/illusions-lab/MDI/issues",
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup src/index.ts --format esm,cjs --dts",
27
+ "test": "vitest run --passWithNoTests",
28
+ "typecheck": "tsc --noEmit"
29
+ },
30
+ "dependencies": {
31
+ "@illusions-lab/mdi-export-profile": "^2.0.15",
32
+ "mdast-util-mdi": "^2.0.19",
33
+ "docx": "^9.0.0",
34
+ "@types/mdast": "^4.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "remark-parse": "^11.0.0",
38
+ "unified": "^11.0.0",
39
+ "jszip": "^3.10.0",
40
+ "tsup": "^8.3.0",
41
+ "typescript": "^5.6.0",
42
+ "vitest": "^2.1.0",
43
+ "@types/node": "^20.0.0"
44
+ }
46
45
  }