@illusions-lab/mdi-to-docx 2.0.9 → 2.0.11

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.
Files changed (3) hide show
  1. package/dist/index.cjs +155 -16
  2. package/dist/index.js +161 -17
  3. package/package.json +4 -4
package/dist/index.cjs CHANGED
@@ -51,17 +51,60 @@ async function mdiToDocx(tree, profile) {
51
51
  document: {
52
52
  run: {
53
53
  font: options.typesetting.fontFamily,
54
- size: fontSizeHalfPoints
54
+ size: fontSizeHalfPoints,
55
+ color: "000000"
55
56
  },
56
- paragraph: { spacing: { line: lineTwips } }
57
+ paragraph: { spacing: { line: lineTwips, after: 120 } }
57
58
  },
58
- heading1: { run: headingRun(options) },
59
- heading2: { run: headingRun(options) },
60
- heading3: { run: headingRun(options) },
61
- heading4: { run: headingRun(options) },
62
- heading5: { run: headingRun(options) },
63
- heading6: { run: headingRun(options) }
64
- }
59
+ heading1: headingStyle(options, 1),
60
+ heading2: headingStyle(options, 2),
61
+ heading3: headingStyle(options, 3),
62
+ heading4: headingStyle(options, 4),
63
+ heading5: headingStyle(options, 5),
64
+ heading6: headingStyle(options, 6),
65
+ listParagraph: {
66
+ run: { font: options.typesetting.fontFamily, color: "000000" }
67
+ }
68
+ },
69
+ paragraphStyles: [
70
+ customHeadingStyle(options, 7),
71
+ customHeadingStyle(options, 8),
72
+ customHeadingStyle(options, 9),
73
+ {
74
+ id: "MdiQuote",
75
+ name: "MDI Quote",
76
+ basedOn: "Normal",
77
+ run: { font: options.typesetting.fontFamily, color: "000000", italics: true },
78
+ paragraph: {
79
+ indent: { left: 720, right: 360 },
80
+ spacing: { before: 120, after: 120, line: lineTwips }
81
+ }
82
+ },
83
+ {
84
+ id: "MdiList",
85
+ name: "MDI List",
86
+ basedOn: "ListParagraph",
87
+ run: { font: options.typesetting.fontFamily, color: "000000" },
88
+ paragraph: { spacing: { after: 60, line: lineTwips } }
89
+ },
90
+ {
91
+ id: "MdiCode",
92
+ name: "MDI Code Block",
93
+ basedOn: "Normal",
94
+ run: { font: "Courier New", color: "000000" },
95
+ paragraph: {
96
+ indent: { left: 360, right: 360 },
97
+ spacing: { before: 120, after: 120, line: lineTwips }
98
+ }
99
+ },
100
+ {
101
+ id: "MdiThematicBreak",
102
+ name: "MDI Thematic Break",
103
+ basedOn: "Normal",
104
+ run: { font: options.typesetting.fontFamily, color: "000000" },
105
+ paragraph: { spacing: { before: 120, after: 180 } }
106
+ }
107
+ ]
65
108
  },
66
109
  sections: [
67
110
  {
@@ -87,8 +130,34 @@ async function mdiToDocx(tree, profile) {
87
130
  });
88
131
  return import_docx.Packer.toBuffer(document);
89
132
  }
90
- function headingRun(options) {
91
- return { font: options.typesetting.fontFamily, color: "000000" };
133
+ function headingStyle(options, level) {
134
+ const scale = [1.8, 1.55, 1.35, 1.2, 1.1, 1][level - 1] ?? 1;
135
+ const before = [360, 300, 240, 180, 150, 120][level - 1] ?? 120;
136
+ return {
137
+ run: {
138
+ font: options.typesetting.fontFamily,
139
+ color: "000000",
140
+ bold: true,
141
+ size: Math.round(22 * scale)
142
+ },
143
+ paragraph: {
144
+ spacing: { before, after: 120 },
145
+ keepNext: true,
146
+ keepLines: true,
147
+ outlineLevel: level - 1
148
+ }
149
+ };
150
+ }
151
+ function customHeadingStyle(options, level) {
152
+ const style = headingStyle(options, level);
153
+ return {
154
+ id: `Heading${level}`,
155
+ name: `Heading ${level}`,
156
+ basedOn: "Normal",
157
+ next: "Normal",
158
+ quickFormat: true,
159
+ ...style
160
+ };
92
161
  }
93
162
  function pageNumberSection(options, size) {
94
163
  const page = options.pagination.pageNumbers;
@@ -127,17 +196,73 @@ function block(node, options) {
127
196
  return node.children.flatMap(
128
197
  (item) => item.children.filter((n) => n.type === "paragraph").map(
129
198
  (n) => new import_docx.Paragraph({
199
+ style: "MdiList",
130
200
  children: inline(n.children),
131
201
  bullet: { level: 0 }
132
202
  })
133
203
  )
134
204
  );
205
+ if (node.type === "blockquote")
206
+ return node.children.flatMap(
207
+ (child) => child.type === "paragraph" ? [paragraph(child.children, options, "MdiQuote")] : block(child, options)
208
+ );
209
+ if (node.type === "code")
210
+ return [
211
+ new import_docx.Paragraph({
212
+ style: "MdiCode",
213
+ children: node.value.split("\n").map(
214
+ (line, index) => new import_docx.TextRun({
215
+ text: line,
216
+ break: index === 0 ? void 0 : 1,
217
+ font: "Courier New"
218
+ })
219
+ )
220
+ })
221
+ ];
222
+ if (node.type === "thematicBreak")
223
+ return [
224
+ new import_docx.Paragraph({
225
+ style: "MdiThematicBreak",
226
+ alignment: import_docx.AlignmentType.CENTER,
227
+ children: [new import_docx.TextRun("\u2014 \u2014 \u2014")]
228
+ })
229
+ ];
230
+ if (node.type === "table") return [tableBlock(node, options)];
135
231
  if (node.type === "mdiPagebreak")
136
232
  return [new import_docx.Paragraph({ children: [new import_docx.PageBreak()] })];
137
233
  if (node.type === "mdiBlank") return [new import_docx.Paragraph("")];
138
234
  return [];
139
235
  }
140
- function paragraph(nodes, options) {
236
+ function tableBlock(node, options) {
237
+ const columns = Math.max(1, ...node.children.map((row) => row.children.length));
238
+ const dimensions = import_mdi_export_profile.PAGE_DIMENSIONS[options.pagination.pageSize];
239
+ const pageWidth = options.pagination.landscape ? dimensions.height : dimensions.width;
240
+ const usableWidth = mmToTwips(
241
+ pageWidth - options.pagination.margins.left - options.pagination.margins.right
242
+ );
243
+ const columnWidth = Math.floor(usableWidth / columns);
244
+ return new import_docx.Table({
245
+ width: { size: usableWidth, type: import_docx.WidthType.DXA },
246
+ columnWidths: Array.from({ length: columns }, () => columnWidth),
247
+ layout: import_docx.TableLayoutType.FIXED,
248
+ margins: { top: 80, bottom: 80, left: 100, right: 100 },
249
+ rows: node.children.map(
250
+ (row, rowIndex) => new import_docx.TableRow({
251
+ children: row.children.map(
252
+ (cell) => new import_docx.TableCell({
253
+ width: { size: columnWidth, type: import_docx.WidthType.DXA },
254
+ children: [
255
+ new import_docx.Paragraph({
256
+ children: rowIndex === 0 ? [new import_docx.TextRun({ bold: true, children: inline(cell.children) })] : inline(cell.children)
257
+ })
258
+ ]
259
+ })
260
+ )
261
+ })
262
+ )
263
+ });
264
+ }
265
+ function paragraph(nodes, options, style) {
141
266
  const fontSizeMm = (() => {
142
267
  const dimensions = import_mdi_export_profile.PAGE_DIMENSIONS[options.pagination.pageSize];
143
268
  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;
@@ -145,10 +270,13 @@ function paragraph(nodes, options) {
145
270
  })();
146
271
  const prefix = options.typesetting.fullwidthSpaceIndent ? [new import_docx.TextRun("\u3000".repeat(Math.round(options.typesetting.textIndentEm)))] : [];
147
272
  return new import_docx.Paragraph({
273
+ style,
148
274
  indent: options.typesetting.fullwidthSpaceIndent ? void 0 : {
149
- firstLine: Math.round(
150
- options.typesetting.textIndentEm * fontSizeMm / 25.4 * 1440
151
- )
275
+ ...style ? {} : {
276
+ firstLine: Math.round(
277
+ options.typesetting.textIndentEm * fontSizeMm / 25.4 * 1440
278
+ )
279
+ }
152
280
  },
153
281
  children: [...prefix, ...inline(nodes)]
154
282
  });
@@ -164,6 +292,12 @@ function inline(nodes) {
164
292
  return [new import_docx.TextRun({ bold: true, children: inline(node.children) })];
165
293
  if (node.type === "delete")
166
294
  return [new import_docx.TextRun({ strike: true, children: inline(node.children) })];
295
+ if (node.type === "inlineCode")
296
+ return [new import_docx.TextRun({ text: node.value, font: "Courier New" })];
297
+ if (node.type === "image")
298
+ return [new import_docx.TextRun({ text: node.alt ? `[Image: ${node.alt}]` : "[Image]" })];
299
+ if (node.type === "footnoteReference")
300
+ return [new import_docx.TextRun({ text: `[${node.label ?? node.identifier}]`, superScript: true })];
167
301
  if (node.type === "mdiRuby") return [rawRun(rubyXml(node.base, node.ruby))];
168
302
  if (node.type === "mdiTcy")
169
303
  return [
@@ -189,7 +323,12 @@ function rubyXml(base, reading) {
189
323
  )}</w:t></w:r></w:rubyBase></w:ruby>`;
190
324
  }
191
325
  function rawRun(xmlString) {
192
- return import_docx.ImportedXmlComponent.fromXmlString(xmlString);
326
+ const namespaced = xmlString.replace(
327
+ /^<w:([\w-]+)/,
328
+ '<w:$1 xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
329
+ );
330
+ const imported = import_docx.ImportedXmlComponent.fromXmlString(namespaced);
331
+ return imported.root[0];
193
332
  }
194
333
  function xml(value) {
195
334
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;");
package/dist/index.js CHANGED
@@ -11,7 +11,12 @@ import {
11
11
  PageNumber,
12
12
  PageTextDirectionType,
13
13
  Paragraph,
14
- TextRun
14
+ Table as DocxTable,
15
+ TableCell,
16
+ TableLayoutType,
17
+ TableRow,
18
+ TextRun,
19
+ WidthType
15
20
  } from "docx";
16
21
  import {
17
22
  PAGE_DIMENSIONS,
@@ -42,17 +47,60 @@ async function mdiToDocx(tree, profile) {
42
47
  document: {
43
48
  run: {
44
49
  font: options.typesetting.fontFamily,
45
- size: fontSizeHalfPoints
50
+ size: fontSizeHalfPoints,
51
+ color: "000000"
46
52
  },
47
- paragraph: { spacing: { line: lineTwips } }
53
+ paragraph: { spacing: { line: lineTwips, after: 120 } }
48
54
  },
49
- heading1: { run: headingRun(options) },
50
- heading2: { run: headingRun(options) },
51
- heading3: { run: headingRun(options) },
52
- heading4: { run: headingRun(options) },
53
- heading5: { run: headingRun(options) },
54
- heading6: { run: headingRun(options) }
55
- }
55
+ heading1: headingStyle(options, 1),
56
+ heading2: headingStyle(options, 2),
57
+ heading3: headingStyle(options, 3),
58
+ heading4: headingStyle(options, 4),
59
+ heading5: headingStyle(options, 5),
60
+ heading6: headingStyle(options, 6),
61
+ listParagraph: {
62
+ run: { font: options.typesetting.fontFamily, color: "000000" }
63
+ }
64
+ },
65
+ paragraphStyles: [
66
+ customHeadingStyle(options, 7),
67
+ customHeadingStyle(options, 8),
68
+ customHeadingStyle(options, 9),
69
+ {
70
+ id: "MdiQuote",
71
+ name: "MDI Quote",
72
+ basedOn: "Normal",
73
+ run: { font: options.typesetting.fontFamily, color: "000000", italics: true },
74
+ paragraph: {
75
+ indent: { left: 720, right: 360 },
76
+ spacing: { before: 120, after: 120, line: lineTwips }
77
+ }
78
+ },
79
+ {
80
+ id: "MdiList",
81
+ name: "MDI List",
82
+ basedOn: "ListParagraph",
83
+ run: { font: options.typesetting.fontFamily, color: "000000" },
84
+ paragraph: { spacing: { after: 60, line: lineTwips } }
85
+ },
86
+ {
87
+ id: "MdiCode",
88
+ name: "MDI Code Block",
89
+ basedOn: "Normal",
90
+ run: { font: "Courier New", color: "000000" },
91
+ paragraph: {
92
+ indent: { left: 360, right: 360 },
93
+ spacing: { before: 120, after: 120, line: lineTwips }
94
+ }
95
+ },
96
+ {
97
+ id: "MdiThematicBreak",
98
+ name: "MDI Thematic Break",
99
+ basedOn: "Normal",
100
+ run: { font: options.typesetting.fontFamily, color: "000000" },
101
+ paragraph: { spacing: { before: 120, after: 180 } }
102
+ }
103
+ ]
56
104
  },
57
105
  sections: [
58
106
  {
@@ -78,8 +126,34 @@ async function mdiToDocx(tree, profile) {
78
126
  });
79
127
  return Packer.toBuffer(document);
80
128
  }
81
- function headingRun(options) {
82
- return { font: options.typesetting.fontFamily, color: "000000" };
129
+ function headingStyle(options, level) {
130
+ const scale = [1.8, 1.55, 1.35, 1.2, 1.1, 1][level - 1] ?? 1;
131
+ const before = [360, 300, 240, 180, 150, 120][level - 1] ?? 120;
132
+ return {
133
+ run: {
134
+ font: options.typesetting.fontFamily,
135
+ color: "000000",
136
+ bold: true,
137
+ size: Math.round(22 * scale)
138
+ },
139
+ paragraph: {
140
+ spacing: { before, after: 120 },
141
+ keepNext: true,
142
+ keepLines: true,
143
+ outlineLevel: level - 1
144
+ }
145
+ };
146
+ }
147
+ function customHeadingStyle(options, level) {
148
+ const style = headingStyle(options, level);
149
+ return {
150
+ id: `Heading${level}`,
151
+ name: `Heading ${level}`,
152
+ basedOn: "Normal",
153
+ next: "Normal",
154
+ quickFormat: true,
155
+ ...style
156
+ };
83
157
  }
84
158
  function pageNumberSection(options, size) {
85
159
  const page = options.pagination.pageNumbers;
@@ -118,17 +192,73 @@ function block(node, options) {
118
192
  return node.children.flatMap(
119
193
  (item) => item.children.filter((n) => n.type === "paragraph").map(
120
194
  (n) => new Paragraph({
195
+ style: "MdiList",
121
196
  children: inline(n.children),
122
197
  bullet: { level: 0 }
123
198
  })
124
199
  )
125
200
  );
201
+ if (node.type === "blockquote")
202
+ return node.children.flatMap(
203
+ (child) => child.type === "paragraph" ? [paragraph(child.children, options, "MdiQuote")] : block(child, options)
204
+ );
205
+ if (node.type === "code")
206
+ return [
207
+ new Paragraph({
208
+ style: "MdiCode",
209
+ children: node.value.split("\n").map(
210
+ (line, index) => new TextRun({
211
+ text: line,
212
+ break: index === 0 ? void 0 : 1,
213
+ font: "Courier New"
214
+ })
215
+ )
216
+ })
217
+ ];
218
+ if (node.type === "thematicBreak")
219
+ return [
220
+ new Paragraph({
221
+ style: "MdiThematicBreak",
222
+ alignment: AlignmentType.CENTER,
223
+ children: [new TextRun("\u2014 \u2014 \u2014")]
224
+ })
225
+ ];
226
+ if (node.type === "table") return [tableBlock(node, options)];
126
227
  if (node.type === "mdiPagebreak")
127
228
  return [new Paragraph({ children: [new PageBreak()] })];
128
229
  if (node.type === "mdiBlank") return [new Paragraph("")];
129
230
  return [];
130
231
  }
131
- function paragraph(nodes, options) {
232
+ function tableBlock(node, options) {
233
+ const columns = Math.max(1, ...node.children.map((row) => row.children.length));
234
+ const dimensions = PAGE_DIMENSIONS[options.pagination.pageSize];
235
+ const pageWidth = options.pagination.landscape ? dimensions.height : dimensions.width;
236
+ const usableWidth = mmToTwips(
237
+ pageWidth - options.pagination.margins.left - options.pagination.margins.right
238
+ );
239
+ const columnWidth = Math.floor(usableWidth / columns);
240
+ return new DocxTable({
241
+ width: { size: usableWidth, type: WidthType.DXA },
242
+ columnWidths: Array.from({ length: columns }, () => columnWidth),
243
+ layout: TableLayoutType.FIXED,
244
+ margins: { top: 80, bottom: 80, left: 100, right: 100 },
245
+ rows: node.children.map(
246
+ (row, rowIndex) => new TableRow({
247
+ children: row.children.map(
248
+ (cell) => new TableCell({
249
+ width: { size: columnWidth, type: WidthType.DXA },
250
+ children: [
251
+ new Paragraph({
252
+ children: rowIndex === 0 ? [new TextRun({ bold: true, children: inline(cell.children) })] : inline(cell.children)
253
+ })
254
+ ]
255
+ })
256
+ )
257
+ })
258
+ )
259
+ });
260
+ }
261
+ function paragraph(nodes, options, style) {
132
262
  const fontSizeMm = (() => {
133
263
  const dimensions = PAGE_DIMENSIONS[options.pagination.pageSize];
134
264
  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;
@@ -136,10 +266,13 @@ function paragraph(nodes, options) {
136
266
  })();
137
267
  const prefix = options.typesetting.fullwidthSpaceIndent ? [new TextRun("\u3000".repeat(Math.round(options.typesetting.textIndentEm)))] : [];
138
268
  return new Paragraph({
269
+ style,
139
270
  indent: options.typesetting.fullwidthSpaceIndent ? void 0 : {
140
- firstLine: Math.round(
141
- options.typesetting.textIndentEm * fontSizeMm / 25.4 * 1440
142
- )
271
+ ...style ? {} : {
272
+ firstLine: Math.round(
273
+ options.typesetting.textIndentEm * fontSizeMm / 25.4 * 1440
274
+ )
275
+ }
143
276
  },
144
277
  children: [...prefix, ...inline(nodes)]
145
278
  });
@@ -155,6 +288,12 @@ function inline(nodes) {
155
288
  return [new TextRun({ bold: true, children: inline(node.children) })];
156
289
  if (node.type === "delete")
157
290
  return [new TextRun({ strike: true, children: inline(node.children) })];
291
+ if (node.type === "inlineCode")
292
+ return [new TextRun({ text: node.value, font: "Courier New" })];
293
+ if (node.type === "image")
294
+ return [new TextRun({ text: node.alt ? `[Image: ${node.alt}]` : "[Image]" })];
295
+ if (node.type === "footnoteReference")
296
+ return [new TextRun({ text: `[${node.label ?? node.identifier}]`, superScript: true })];
158
297
  if (node.type === "mdiRuby") return [rawRun(rubyXml(node.base, node.ruby))];
159
298
  if (node.type === "mdiTcy")
160
299
  return [
@@ -180,7 +319,12 @@ function rubyXml(base, reading) {
180
319
  )}</w:t></w:r></w:rubyBase></w:ruby>`;
181
320
  }
182
321
  function rawRun(xmlString) {
183
- return ImportedXmlComponent.fromXmlString(xmlString);
322
+ const namespaced = xmlString.replace(
323
+ /^<w:([\w-]+)/,
324
+ '<w:$1 xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
325
+ );
326
+ const imported = ImportedXmlComponent.fromXmlString(namespaced);
327
+ return imported.root[0];
184
328
  }
185
329
  function xml(value) {
186
330
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illusions-lab/mdi-to-docx",
3
- "version": "2.0.9",
3
+ "version": "2.0.11",
4
4
  "description": "Render MDI (.mdi) documents to DOCX",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,8 +25,8 @@
25
25
  "dependencies": {
26
26
  "docx": "^9.0.0",
27
27
  "@types/mdast": "^4.0.0",
28
- "@illusions-lab/mdi-export-profile": "2.0.4",
29
- "mdast-util-mdi": "2.0.8"
28
+ "@illusions-lab/mdi-export-profile": "2.0.6",
29
+ "mdast-util-mdi": "2.0.10"
30
30
  },
31
31
  "devDependencies": {
32
32
  "remark-parse": "^11.0.0",
@@ -36,7 +36,7 @@
36
36
  "typescript": "^5.6.0",
37
37
  "vitest": "^2.1.0",
38
38
  "@types/node": "^20.0.0",
39
- "@illusions-lab/mdi-remark": "2.0.9"
39
+ "@illusions-lab/mdi-remark": "2.0.11"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsup src/index.ts --format esm,cjs --dts",