@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 +24 -2
- package/dist/index.cjs +91 -32
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +91 -32
- package/package.json +43 -44
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
|
-
|
|
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 =
|
|
48
|
+
const fontSizeMm = options.typesetting.fontSize / 72 * 25.4;
|
|
49
49
|
const fontSizeHalfPoints = Math.round(fontSizeMm / 25.4 * 72 * 2);
|
|
50
|
-
const
|
|
51
|
-
|
|
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
|
|
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(
|
|
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,
|
|
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 =
|
|
46
|
+
const fontSizeMm = options.typesetting.fontSize / 72 * 25.4;
|
|
47
47
|
const fontSizeHalfPoints = Math.round(fontSizeMm / 25.4 * 72 * 2);
|
|
48
|
-
const
|
|
49
|
-
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
}
|