@bendyline/squisq-formats 0.1.1 → 1.1.0

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 (119) hide show
  1. package/dist/__tests__/container.test.d.ts +8 -0
  2. package/dist/__tests__/container.test.d.ts.map +1 -0
  3. package/dist/__tests__/container.test.js +147 -0
  4. package/dist/__tests__/container.test.js.map +1 -0
  5. package/dist/__tests__/docxExport.test.d.ts +8 -0
  6. package/dist/__tests__/docxExport.test.d.ts.map +1 -0
  7. package/dist/__tests__/docxExport.test.js +383 -0
  8. package/dist/__tests__/docxExport.test.js.map +1 -0
  9. package/dist/__tests__/docxImport.test.d.ts +8 -0
  10. package/dist/__tests__/docxImport.test.d.ts.map +1 -0
  11. package/dist/__tests__/docxImport.test.js +309 -0
  12. package/dist/__tests__/docxImport.test.js.map +1 -0
  13. package/dist/__tests__/html.test.d.ts +8 -0
  14. package/dist/__tests__/html.test.d.ts.map +1 -0
  15. package/dist/__tests__/html.test.js +243 -0
  16. package/dist/__tests__/html.test.js.map +1 -0
  17. package/dist/__tests__/ooxml.test.d.ts +5 -0
  18. package/dist/__tests__/ooxml.test.d.ts.map +1 -0
  19. package/dist/__tests__/ooxml.test.js +152 -0
  20. package/dist/__tests__/ooxml.test.js.map +1 -0
  21. package/dist/__tests__/pdfExport.test.d.ts +8 -0
  22. package/dist/__tests__/pdfExport.test.d.ts.map +1 -0
  23. package/dist/__tests__/pdfExport.test.js +275 -0
  24. package/dist/__tests__/pdfExport.test.js.map +1 -0
  25. package/dist/__tests__/pdfImport.test.d.ts +11 -0
  26. package/dist/__tests__/pdfImport.test.d.ts.map +1 -0
  27. package/dist/__tests__/pdfImport.test.js +254 -0
  28. package/dist/__tests__/pdfImport.test.js.map +1 -0
  29. package/dist/__tests__/roundTrip.test.d.ts +7 -0
  30. package/dist/__tests__/roundTrip.test.d.ts.map +1 -0
  31. package/dist/__tests__/roundTrip.test.js +173 -0
  32. package/dist/__tests__/roundTrip.test.js.map +1 -0
  33. package/dist/container/index.d.ts +36 -0
  34. package/dist/container/index.d.ts.map +1 -0
  35. package/dist/container/index.js +74 -0
  36. package/dist/container/index.js.map +1 -0
  37. package/dist/docx/export.d.ts +54 -0
  38. package/dist/docx/export.d.ts.map +1 -0
  39. package/dist/docx/export.js +729 -0
  40. package/dist/docx/export.js.map +1 -0
  41. package/dist/docx/import.d.ts +52 -0
  42. package/dist/docx/import.d.ts.map +1 -0
  43. package/dist/docx/import.js +687 -0
  44. package/dist/docx/import.js.map +1 -0
  45. package/dist/docx/index.d.ts +15 -100
  46. package/dist/docx/index.d.ts.map +1 -0
  47. package/dist/docx/index.js +22 -13
  48. package/dist/docx/index.js.map +1 -1
  49. package/dist/docx/styles.d.ts +68 -0
  50. package/dist/docx/styles.d.ts.map +1 -0
  51. package/dist/docx/styles.js +125 -0
  52. package/dist/docx/styles.js.map +1 -0
  53. package/dist/html/htmlTemplate.d.ts +64 -0
  54. package/dist/html/htmlTemplate.d.ts.map +1 -0
  55. package/dist/html/htmlTemplate.js +235 -0
  56. package/dist/html/htmlTemplate.js.map +1 -0
  57. package/dist/html/imageUtils.d.ts +29 -0
  58. package/dist/html/imageUtils.d.ts.map +1 -0
  59. package/dist/html/imageUtils.js +63 -0
  60. package/dist/html/imageUtils.js.map +1 -0
  61. package/dist/html/index.d.ts +9 -77
  62. package/dist/html/index.d.ts.map +1 -0
  63. package/dist/html/index.js +137 -16
  64. package/dist/html/index.js.map +1 -1
  65. package/dist/index.d.ts +36 -7
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +35 -53
  68. package/dist/index.js.map +1 -1
  69. package/dist/ooxml/index.d.ts +14 -343
  70. package/dist/ooxml/index.d.ts.map +1 -0
  71. package/dist/ooxml/index.js +22 -98
  72. package/dist/ooxml/index.js.map +1 -1
  73. package/dist/ooxml/namespaces.d.ts +67 -0
  74. package/dist/ooxml/namespaces.d.ts.map +1 -0
  75. package/dist/ooxml/namespaces.js +94 -0
  76. package/dist/ooxml/namespaces.js.map +1 -0
  77. package/dist/ooxml/reader.d.ts +52 -0
  78. package/dist/ooxml/reader.d.ts.map +1 -0
  79. package/dist/ooxml/reader.js +190 -0
  80. package/dist/ooxml/reader.js.map +1 -0
  81. package/dist/ooxml/types.d.ts +82 -0
  82. package/dist/ooxml/types.d.ts.map +1 -0
  83. package/dist/ooxml/types.js +9 -0
  84. package/dist/ooxml/types.js.map +1 -0
  85. package/dist/ooxml/writer.d.ts +71 -0
  86. package/dist/ooxml/writer.d.ts.map +1 -0
  87. package/dist/ooxml/writer.js +200 -0
  88. package/dist/ooxml/writer.js.map +1 -0
  89. package/dist/ooxml/xmlUtils.d.ts +76 -0
  90. package/dist/ooxml/xmlUtils.d.ts.map +1 -0
  91. package/dist/ooxml/xmlUtils.js +108 -0
  92. package/dist/ooxml/xmlUtils.js.map +1 -0
  93. package/dist/pdf/export.d.ts +50 -0
  94. package/dist/pdf/export.d.ts.map +1 -0
  95. package/dist/pdf/export.js +740 -0
  96. package/dist/pdf/export.js.map +1 -0
  97. package/dist/pdf/import.d.ts +78 -0
  98. package/dist/pdf/import.d.ts.map +1 -0
  99. package/dist/pdf/import.js +619 -0
  100. package/dist/pdf/import.js.map +1 -0
  101. package/dist/pdf/index.d.ts +17 -121
  102. package/dist/pdf/index.d.ts.map +1 -0
  103. package/dist/pdf/index.js +25 -14
  104. package/dist/pdf/index.js.map +1 -1
  105. package/dist/pdf/styles.d.ts +142 -0
  106. package/dist/pdf/styles.d.ts.map +1 -0
  107. package/dist/pdf/styles.js +145 -0
  108. package/dist/pdf/styles.js.map +1 -0
  109. package/dist/pptx/index.d.ts +9 -12
  110. package/dist/pptx/index.d.ts.map +1 -0
  111. package/dist/pptx/index.js +44 -12
  112. package/dist/pptx/index.js.map +1 -1
  113. package/dist/xlsx/index.d.ts +9 -12
  114. package/dist/xlsx/index.d.ts.map +1 -0
  115. package/dist/xlsx/index.js +44 -12
  116. package/dist/xlsx/index.js.map +1 -1
  117. package/package.json +8 -4
  118. package/src/__tests__/container.test.ts +189 -0
  119. package/src/container/index.ts +89 -0
@@ -0,0 +1,729 @@
1
+ /**
2
+ * DOCX Export
3
+ *
4
+ * Converts a squisq MarkdownDocument (or Doc) into a .docx file
5
+ * by generating WordprocessingML XML and assembling the OOXML package.
6
+ *
7
+ * No third-party docx library — all XML is generated directly using
8
+ * the shared ooxml/ infrastructure of this package.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { parseMarkdown } from '@bendyline/squisq/markdown';
13
+ * import { markdownDocToDocx } from '@bendyline/squisq-formats/docx';
14
+ *
15
+ * const md = parseMarkdown('# Hello\n\nWorld **bold** text');
16
+ * const blob = await markdownDocToDocx(md);
17
+ * ```
18
+ */
19
+ import { docToMarkdown } from '@bendyline/squisq/doc';
20
+ import { createPackage } from '../ooxml/writer.js';
21
+ import { xmlDeclaration, escapeXml } from '../ooxml/xmlUtils.js';
22
+ import { NS_WML, NS_R, NS_MC, REL_OFFICE_DOCUMENT, REL_STYLES, REL_NUMBERING, REL_SETTINGS, REL_FONT_TABLE, REL_HYPERLINK, REL_FOOTNOTES, CONTENT_TYPE_DOCX_DOCUMENT, CONTENT_TYPE_DOCX_STYLES, CONTENT_TYPE_DOCX_NUMBERING, CONTENT_TYPE_DOCX_SETTINGS, CONTENT_TYPE_DOCX_FONT_TABLE, CONTENT_TYPE_DOCX_FOOTNOTES, } from '../ooxml/namespaces.js';
23
+ import { DEPTH_TO_STYLE_ID, HEADING_FONT_SIZES, DEFAULT_FONT, DEFAULT_HEADING_FONT, DEFAULT_FONT_SIZE_HALF_POINTS, DEFAULT_CODE_FONT, DEFAULT_CODE_FONT_SIZE, HYPERLINK_COLOR, pointsToTwips, } from './styles.js';
24
+ /**
25
+ * Convert a MarkdownDocument to a .docx Blob.
26
+ *
27
+ * @param doc - The parsed markdown document
28
+ * @param options - Export options
29
+ * @returns An ArrayBuffer containing the .docx file
30
+ */
31
+ export async function markdownDocToDocx(doc, options = {}) {
32
+ const ctx = new ExportContext(options);
33
+ const bodyXml = convertBlocks(doc.children, ctx);
34
+ return buildDocxPackage(bodyXml, ctx, options);
35
+ }
36
+ /**
37
+ * Convert a squisq Doc to a .docx Blob.
38
+ *
39
+ * Convenience wrapper that converts Doc → MarkdownDocument → DOCX.
40
+ *
41
+ * @param doc - The squisq Doc
42
+ * @param options - Export options
43
+ * @returns An ArrayBuffer containing the .docx file
44
+ */
45
+ export async function docToDocx(doc, options = {}) {
46
+ const markdownDoc = docToMarkdown(doc);
47
+ return markdownDocToDocx(markdownDoc, options);
48
+ }
49
+ // ============================================
50
+ // Export Context
51
+ // ============================================
52
+ /**
53
+ * Tracks state during export: relationship IDs, numbering definitions,
54
+ * footnote bodies, and embedded images.
55
+ */
56
+ class ExportContext {
57
+ constructor(options) {
58
+ this.nextRelId = 1;
59
+ this.nextNumId = 1;
60
+ this.nextFootnoteId = 1; // 0 is separator, start user footnotes at 1
61
+ /** Relationships for word/_rels/document.xml.rels */
62
+ this.relationships = [];
63
+ /** Numbering definitions (abstract + num) */
64
+ this.numberingDefs = [];
65
+ /** Footnote XML bodies (keyed by footnote id) */
66
+ this.footnotes = new Map();
67
+ /** Footnote identifier → numeric id mapping */
68
+ this.footnoteIdMap = new Map();
69
+ /** Embedded images: rId → { path, data, contentType } */
70
+ this.images = [];
71
+ /** Whether we have any lists (determines if numbering.xml is needed) */
72
+ this.hasLists = false;
73
+ /** Whether we have any footnotes */
74
+ this.hasFootnotes = false;
75
+ this.font = options.defaultFont ?? DEFAULT_FONT;
76
+ this.fontSize = options.defaultFontSize
77
+ ? options.defaultFontSize * 2
78
+ : DEFAULT_FONT_SIZE_HALF_POINTS;
79
+ }
80
+ /** Allocate a new relationship ID */
81
+ allocRelId() {
82
+ return `rId${this.nextRelId++}`;
83
+ }
84
+ /** Add a hyperlink relationship and return the rId */
85
+ addHyperlink(url) {
86
+ const id = this.allocRelId();
87
+ this.relationships.push({
88
+ id,
89
+ type: REL_HYPERLINK,
90
+ target: url,
91
+ targetMode: 'External',
92
+ });
93
+ return id;
94
+ }
95
+ /** Allocate a numbering definition for a list */
96
+ allocNumbering(ordered) {
97
+ const numId = this.nextNumId++;
98
+ this.numberingDefs.push({ numId, ordered });
99
+ this.hasLists = true;
100
+ return numId;
101
+ }
102
+ /** Register or look up a footnote by its string identifier */
103
+ getFootnoteId(identifier) {
104
+ let id = this.footnoteIdMap.get(identifier);
105
+ if (id === undefined) {
106
+ id = this.nextFootnoteId++;
107
+ this.footnoteIdMap.set(identifier, id);
108
+ this.hasFootnotes = true;
109
+ }
110
+ return id;
111
+ }
112
+ }
113
+ // ============================================
114
+ // Block Conversion
115
+ // ============================================
116
+ function convertBlocks(nodes, ctx) {
117
+ const parts = [];
118
+ for (const node of nodes) {
119
+ parts.push(convertBlock(node, ctx, 0));
120
+ }
121
+ return parts.join('');
122
+ }
123
+ function convertBlock(node, ctx, listDepth) {
124
+ switch (node.type) {
125
+ case 'heading':
126
+ return convertHeading(node, ctx);
127
+ case 'paragraph':
128
+ return convertParagraph(node, ctx);
129
+ case 'blockquote':
130
+ return convertBlockquote(node, ctx);
131
+ case 'list':
132
+ return convertList(node, ctx, listDepth);
133
+ case 'code':
134
+ return convertCodeBlock(node);
135
+ case 'table':
136
+ return convertTable(node, ctx);
137
+ case 'thematicBreak':
138
+ return convertThematicBreak();
139
+ case 'htmlBlock':
140
+ return convertHtmlBlock(node);
141
+ case 'math':
142
+ return convertMathBlock(node);
143
+ case 'footnoteDefinition':
144
+ return convertFootnoteDefinition(node, ctx);
145
+ default:
146
+ // Definition lists, directives, link definitions — skip or emit as plain text
147
+ return '';
148
+ }
149
+ }
150
+ function convertHeading(node, ctx) {
151
+ const styleId = DEPTH_TO_STYLE_ID[node.depth] ?? 'Heading1';
152
+ const runs = convertInlines(node.children, ctx);
153
+ return `<w:p>` + `<w:pPr><w:pStyle w:val="${styleId}"/></w:pPr>` + runs + `</w:p>`;
154
+ }
155
+ function convertParagraph(node, ctx) {
156
+ const runs = convertInlines(node.children, ctx);
157
+ return `<w:p>${runs}</w:p>`;
158
+ }
159
+ function convertBlockquote(node, ctx) {
160
+ // Render each child block as a paragraph with Quote style
161
+ const parts = [];
162
+ for (const child of node.children) {
163
+ if (child.type === 'paragraph') {
164
+ const runs = convertInlines(child.children, ctx);
165
+ parts.push(`<w:p>` +
166
+ `<w:pPr><w:pStyle w:val="Quote"/>` +
167
+ `<w:ind w:left="${pointsToTwips(36)}"/>` +
168
+ `<w:pBdr><w:left w:val="single" w:sz="12" w:space="4" w:color="CCCCCC"/></w:pBdr>` +
169
+ `</w:pPr>` +
170
+ runs +
171
+ `</w:p>`);
172
+ }
173
+ else {
174
+ // Nested non-paragraph (e.g., nested blockquote, list) — recurse
175
+ parts.push(convertBlock(child, ctx, 0));
176
+ }
177
+ }
178
+ return parts.join('');
179
+ }
180
+ function convertList(node, ctx, depth) {
181
+ const numId = ctx.allocNumbering(node.ordered ?? false);
182
+ const parts = [];
183
+ for (const item of node.children) {
184
+ parts.push(convertListItem(item, ctx, numId, depth));
185
+ }
186
+ return parts.join('');
187
+ }
188
+ function convertListItem(item, ctx, numId, depth) {
189
+ const parts = [];
190
+ for (const child of item.children) {
191
+ if (child.type === 'paragraph') {
192
+ const runs = convertInlines(child.children, ctx);
193
+ parts.push(`<w:p>` +
194
+ `<w:pPr>` +
195
+ `<w:pStyle w:val="ListParagraph"/>` +
196
+ `<w:numPr><w:ilvl w:val="${depth}"/><w:numId w:val="${numId}"/></w:numPr>` +
197
+ `</w:pPr>` +
198
+ runs +
199
+ `</w:p>`);
200
+ }
201
+ else if (child.type === 'list') {
202
+ // Nested list — increase depth
203
+ parts.push(convertList(child, ctx, depth + 1));
204
+ }
205
+ else {
206
+ parts.push(convertBlock(child, ctx, depth));
207
+ }
208
+ }
209
+ return parts.join('');
210
+ }
211
+ function convertCodeBlock(node) {
212
+ // Emit each line as a separate paragraph with code styling
213
+ const lines = node.value.split('\n');
214
+ const parts = [];
215
+ for (const line of lines) {
216
+ parts.push(`<w:p>` +
217
+ `<w:pPr>` +
218
+ `<w:pStyle w:val="Code"/>` +
219
+ `<w:pBdr>` +
220
+ `<w:top w:val="single" w:sz="4" w:space="1" w:color="CCCCCC"/>` +
221
+ `<w:left w:val="single" w:sz="4" w:space="4" w:color="CCCCCC"/>` +
222
+ `<w:bottom w:val="single" w:sz="4" w:space="1" w:color="CCCCCC"/>` +
223
+ `<w:right w:val="single" w:sz="4" w:space="4" w:color="CCCCCC"/>` +
224
+ `</w:pBdr>` +
225
+ `<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>` +
226
+ `</w:pPr>` +
227
+ `<w:r>` +
228
+ `<w:rPr><w:rFonts w:ascii="${DEFAULT_CODE_FONT}" w:hAnsi="${DEFAULT_CODE_FONT}"/>` +
229
+ `<w:sz w:val="${DEFAULT_CODE_FONT_SIZE}"/></w:rPr>` +
230
+ `<w:t xml:space="preserve">${escapeXml(line)}</w:t>` +
231
+ `</w:r>` +
232
+ `</w:p>`);
233
+ }
234
+ return parts.join('');
235
+ }
236
+ function convertTable(node, ctx) {
237
+ const rows = [];
238
+ for (let ri = 0; ri < node.children.length; ri++) {
239
+ const row = node.children[ri];
240
+ rows.push(convertTableRow(row, ctx, ri === 0, node.align));
241
+ }
242
+ return (`<w:tbl>` +
243
+ `<w:tblPr>` +
244
+ `<w:tblStyle w:val="TableGrid"/>` +
245
+ `<w:tblW w:w="0" w:type="auto"/>` +
246
+ `<w:tblBorders>` +
247
+ `<w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
248
+ `<w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
249
+ `<w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
250
+ `<w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
251
+ `<w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
252
+ `<w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
253
+ `</w:tblBorders>` +
254
+ `</w:tblPr>` +
255
+ `<w:tblGrid/>` +
256
+ rows.join('') +
257
+ `</w:tbl>`);
258
+ }
259
+ function convertTableRow(row, ctx, isHeader, align) {
260
+ const cells = [];
261
+ for (let ci = 0; ci < row.children.length; ci++) {
262
+ const cell = row.children[ci];
263
+ const cellAlign = align?.[ci] ?? null;
264
+ cells.push(convertTableCell(cell, ctx, isHeader, cellAlign));
265
+ }
266
+ const trPr = isHeader ? '<w:trPr><w:tblHeader/></w:trPr>' : '';
267
+ return `<w:tr>${trPr}${cells.join('')}</w:tr>`;
268
+ }
269
+ function convertTableCell(cell, ctx, isHeader, align) {
270
+ const runs = convertInlines(cell.children, ctx);
271
+ const rPr = isHeader ? '<w:rPr><w:b/></w:rPr>' : '';
272
+ const jcMap = { left: 'left', center: 'center', right: 'right' };
273
+ const jc = align ? `<w:jc w:val="${jcMap[align]}"/>` : '';
274
+ const pPr = rPr || jc ? `<w:pPr>${jc}</w:pPr>` : '';
275
+ return `<w:tc><w:p>${pPr}${runs}</w:p></w:tc>`;
276
+ }
277
+ function convertThematicBreak() {
278
+ return (`<w:p>` +
279
+ `<w:pPr>` +
280
+ `<w:pBdr><w:bottom w:val="single" w:sz="6" w:space="1" w:color="auto"/></w:pBdr>` +
281
+ `</w:pPr>` +
282
+ `</w:p>`);
283
+ }
284
+ function convertHtmlBlock(node) {
285
+ // Best-effort: extract text content from the HTML
286
+ const text = stripHtmlTags(node.rawHtml);
287
+ if (!text.trim())
288
+ return '';
289
+ return `<w:p><w:r><w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r></w:p>`;
290
+ }
291
+ function convertMathBlock(node) {
292
+ // Emit as a styled paragraph with the raw LaTeX
293
+ return (`<w:p>` +
294
+ `<w:pPr><w:jc w:val="center"/></w:pPr>` +
295
+ `<w:r>` +
296
+ `<w:rPr><w:rFonts w:ascii="Cambria Math" w:hAnsi="Cambria Math"/><w:i/></w:rPr>` +
297
+ `<w:t xml:space="preserve">${escapeXml(node.value)}</w:t>` +
298
+ `</w:r>` +
299
+ `</w:p>`);
300
+ }
301
+ function convertFootnoteDefinition(node, ctx) {
302
+ const fnId = ctx.getFootnoteId(node.identifier);
303
+ // Build the footnote body XML
304
+ const bodyParts = [];
305
+ for (const child of node.children) {
306
+ if (child.type === 'paragraph') {
307
+ const runs = convertInlines(child.children, ctx);
308
+ bodyParts.push(`<w:p>${runs}</w:p>`);
309
+ }
310
+ }
311
+ const footnoteXml = `<w:footnote w:id="${fnId}">` +
312
+ (bodyParts.length > 0 ? bodyParts.join('') : `<w:p/>`) +
313
+ `</w:footnote>`;
314
+ ctx.footnotes.set(fnId, footnoteXml);
315
+ // Don't emit anything in the main body for the definition
316
+ return '';
317
+ }
318
+ function convertInlines(nodes, ctx, format = {}) {
319
+ const parts = [];
320
+ for (const node of nodes) {
321
+ parts.push(convertInline(node, ctx, format));
322
+ }
323
+ return parts.join('');
324
+ }
325
+ function convertInline(node, ctx, format) {
326
+ switch (node.type) {
327
+ case 'text':
328
+ return makeRun(node.value, format);
329
+ case 'strong':
330
+ return convertInlines(node.children, ctx, { ...format, bold: true });
331
+ case 'emphasis':
332
+ return convertInlines(node.children, ctx, { ...format, italic: true });
333
+ case 'delete':
334
+ return convertInlines(node.children, ctx, { ...format, strike: true });
335
+ case 'inlineCode':
336
+ return makeRun(node.value, { ...format, code: true });
337
+ case 'link':
338
+ return convertLink(node, ctx, format);
339
+ case 'image':
340
+ return convertImage(node);
341
+ case 'break':
342
+ return `<w:r><w:br/></w:r>`;
343
+ case 'htmlInline':
344
+ return makeRun(stripHtmlTags(node.rawHtml), format);
345
+ case 'inlineMath':
346
+ return makeRun(node.value, { ...format, code: true });
347
+ case 'footnoteReference':
348
+ return convertFootnoteRef(node, ctx);
349
+ default:
350
+ // linkReference, imageReference, textDirective — skip or emit plain
351
+ return '';
352
+ }
353
+ }
354
+ function makeRun(text, format) {
355
+ if (!text)
356
+ return '';
357
+ const rPrParts = [];
358
+ if (format.bold)
359
+ rPrParts.push('<w:b/>');
360
+ if (format.italic)
361
+ rPrParts.push('<w:i/>');
362
+ if (format.strike)
363
+ rPrParts.push('<w:strike/>');
364
+ if (format.code) {
365
+ rPrParts.push(`<w:rFonts w:ascii="${DEFAULT_CODE_FONT}" w:hAnsi="${DEFAULT_CODE_FONT}"/>`, `<w:sz w:val="${DEFAULT_CODE_FONT_SIZE}"/>`);
366
+ }
367
+ const rPr = rPrParts.length > 0 ? `<w:rPr>${rPrParts.join('')}</w:rPr>` : '';
368
+ // Use xml:space="preserve" to keep leading/trailing whitespace
369
+ return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r>`;
370
+ }
371
+ function convertLink(node, ctx, format) {
372
+ const rId = ctx.addHyperlink(node.url);
373
+ const _runs = convertInlines(node.children, ctx, { ...format });
374
+ // Wrap each run's rPr with hyperlink styling
375
+ // For simplicity, emit inline runs with hyperlink color + underline
376
+ const styledRuns = convertInlinesWithHyperlinkStyle(node.children, ctx, format);
377
+ return `<w:hyperlink r:id="${rId}">${styledRuns}</w:hyperlink>`;
378
+ }
379
+ function convertInlinesWithHyperlinkStyle(nodes, ctx, format) {
380
+ const parts = [];
381
+ for (const node of nodes) {
382
+ if (node.type === 'text') {
383
+ parts.push(makeHyperlinkRun(node.value, format));
384
+ }
385
+ else {
386
+ // For nested formatting inside links, add hyperlink style
387
+ parts.push(convertInline(node, ctx, format));
388
+ }
389
+ }
390
+ return parts.join('');
391
+ }
392
+ function makeHyperlinkRun(text, format) {
393
+ if (!text)
394
+ return '';
395
+ const rPrParts = [
396
+ '<w:rStyle w:val="Hyperlink"/>',
397
+ `<w:color w:val="${HYPERLINK_COLOR}"/>`,
398
+ '<w:u w:val="single"/>',
399
+ ];
400
+ if (format.bold)
401
+ rPrParts.push('<w:b/>');
402
+ if (format.italic)
403
+ rPrParts.push('<w:i/>');
404
+ return (`<w:r><w:rPr>${rPrParts.join('')}</w:rPr>` +
405
+ `<w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r>`);
406
+ }
407
+ function convertImage(_node) {
408
+ // Image embedding requires fetching the image data and packing it as
409
+ // a binary part. For now, emit a placeholder text run.
410
+ // Full image support can be added by fetching the URL and creating
411
+ // a DrawingML inline + binary media part.
412
+ const alt = _node.alt || _node.url;
413
+ return makeRun(`[Image: ${alt}]`, { italic: true });
414
+ }
415
+ function convertFootnoteRef(node, ctx) {
416
+ const fnId = ctx.getFootnoteId(node.identifier);
417
+ return (`<w:r>` +
418
+ `<w:rPr><w:rStyle w:val="FootnoteReference"/><w:vertAlign w:val="superscript"/></w:rPr>` +
419
+ `<w:footnoteReference w:id="${fnId}"/>` +
420
+ `</w:r>`);
421
+ }
422
+ // ============================================
423
+ // Package Assembly
424
+ // ============================================
425
+ async function buildDocxPackage(bodyXml, ctx, options) {
426
+ const pkg = createPackage();
427
+ // --- Register fixed relationships ---
428
+ let relCounter = 100; // Start high to avoid collisions with dynamic rels
429
+ const stylesRelId = `rId${relCounter++}`;
430
+ const numberingRelId = `rId${relCounter++}`;
431
+ const settingsRelId = `rId${relCounter++}`;
432
+ const fontTableRelId = `rId${relCounter++}`;
433
+ const footnotesRelId = `rId${relCounter++}`;
434
+ // --- word/document.xml ---
435
+ const documentXml = buildDocumentXml(bodyXml);
436
+ pkg.addPart('word/document.xml', documentXml, CONTENT_TYPE_DOCX_DOCUMENT);
437
+ // --- word/styles.xml ---
438
+ const stylesXml = buildStylesXml(options);
439
+ pkg.addPart('word/styles.xml', stylesXml, CONTENT_TYPE_DOCX_STYLES);
440
+ // --- word/settings.xml ---
441
+ const settingsXml = buildSettingsXml();
442
+ pkg.addPart('word/settings.xml', settingsXml, CONTENT_TYPE_DOCX_SETTINGS);
443
+ // --- word/fontTable.xml ---
444
+ const fontTableXml = buildFontTableXml(options);
445
+ pkg.addPart('word/fontTable.xml', fontTableXml, CONTENT_TYPE_DOCX_FONT_TABLE);
446
+ // --- word/numbering.xml (only if lists present) ---
447
+ if (ctx.hasLists) {
448
+ const numberingXml = buildNumberingXml(ctx);
449
+ pkg.addPart('word/numbering.xml', numberingXml, CONTENT_TYPE_DOCX_NUMBERING);
450
+ }
451
+ // --- word/footnotes.xml (only if footnotes present) ---
452
+ if (ctx.hasFootnotes) {
453
+ const footnotesXml = buildFootnotesXml(ctx);
454
+ pkg.addPart('word/footnotes.xml', footnotesXml, CONTENT_TYPE_DOCX_FOOTNOTES);
455
+ }
456
+ // --- Root relationship: this package contains a word document ---
457
+ pkg.addRelationship('', {
458
+ id: 'rId1',
459
+ type: REL_OFFICE_DOCUMENT,
460
+ target: 'word/document.xml',
461
+ });
462
+ // --- Document relationships ---
463
+ pkg.addRelationship('word/document.xml', {
464
+ id: stylesRelId,
465
+ type: REL_STYLES,
466
+ target: 'styles.xml',
467
+ });
468
+ pkg.addRelationship('word/document.xml', {
469
+ id: settingsRelId,
470
+ type: REL_SETTINGS,
471
+ target: 'settings.xml',
472
+ });
473
+ pkg.addRelationship('word/document.xml', {
474
+ id: fontTableRelId,
475
+ type: REL_FONT_TABLE,
476
+ target: 'fontTable.xml',
477
+ });
478
+ if (ctx.hasLists) {
479
+ pkg.addRelationship('word/document.xml', {
480
+ id: numberingRelId,
481
+ type: REL_NUMBERING,
482
+ target: 'numbering.xml',
483
+ });
484
+ }
485
+ if (ctx.hasFootnotes) {
486
+ pkg.addRelationship('word/document.xml', {
487
+ id: footnotesRelId,
488
+ type: REL_FOOTNOTES,
489
+ target: 'footnotes.xml',
490
+ });
491
+ }
492
+ // --- Dynamic relationships (hyperlinks, images) ---
493
+ for (const rel of ctx.relationships) {
494
+ pkg.addRelationship('word/document.xml', {
495
+ id: rel.id,
496
+ type: rel.type,
497
+ target: rel.target,
498
+ ...(rel.targetMode ? { targetMode: rel.targetMode } : {}),
499
+ });
500
+ }
501
+ // --- Embedded images ---
502
+ for (const img of ctx.images) {
503
+ pkg.addBinaryPart(img.path, img.data, img.contentType);
504
+ }
505
+ // --- Core properties ---
506
+ if (options.title || options.author || options.description) {
507
+ pkg.setCoreProperties({
508
+ title: options.title,
509
+ creator: options.author,
510
+ description: options.description,
511
+ created: new Date().toISOString(),
512
+ modified: new Date().toISOString(),
513
+ });
514
+ }
515
+ return pkg.toArrayBuffer();
516
+ }
517
+ // ============================================
518
+ // XML Part Generators
519
+ // ============================================
520
+ function buildDocumentXml(bodyXml) {
521
+ return (xmlDeclaration() +
522
+ `<w:document` +
523
+ ` xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"` +
524
+ ` xmlns:mc="${NS_MC}"` +
525
+ ` xmlns:o="urn:schemas-microsoft-com:office:office"` +
526
+ ` xmlns:r="${NS_R}"` +
527
+ ` xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"` +
528
+ ` xmlns:v="urn:schemas-microsoft-com:vml"` +
529
+ ` xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"` +
530
+ ` xmlns:w10="urn:schemas-microsoft-com:office:word"` +
531
+ ` xmlns:w="${NS_WML}"` +
532
+ ` xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml">` +
533
+ `<w:body>` +
534
+ bodyXml +
535
+ `<w:sectPr>` +
536
+ `<w:pgSz w:w="12240" w:h="15840"/>` +
537
+ `<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/>` +
538
+ `<w:cols w:space="720"/>` +
539
+ `</w:sectPr>` +
540
+ `</w:body>` +
541
+ `</w:document>`);
542
+ }
543
+ function buildStylesXml(options) {
544
+ const font = options.defaultFont ?? DEFAULT_FONT;
545
+ const headingFont = DEFAULT_HEADING_FONT;
546
+ return (xmlDeclaration() +
547
+ `<w:styles xmlns:w="${NS_WML}">` +
548
+ // Default run properties
549
+ `<w:docDefaults>` +
550
+ `<w:rPrDefault><w:rPr>` +
551
+ `<w:rFonts w:ascii="${escapeXml(font)}" w:hAnsi="${escapeXml(font)}" w:eastAsia="${escapeXml(font)}" w:cs="${escapeXml(font)}"/>` +
552
+ `<w:sz w:val="${DEFAULT_FONT_SIZE_HALF_POINTS}"/>` +
553
+ `<w:szCs w:val="${DEFAULT_FONT_SIZE_HALF_POINTS}"/>` +
554
+ `</w:rPr></w:rPrDefault>` +
555
+ `<w:pPrDefault/>` +
556
+ `</w:docDefaults>` +
557
+ // Normal style
558
+ `<w:style w:type="paragraph" w:default="1" w:styleId="Normal">` +
559
+ `<w:name w:val="Normal"/>` +
560
+ `<w:qFormat/>` +
561
+ `</w:style>` +
562
+ // Heading styles
563
+ buildHeadingStyles(headingFont) +
564
+ // Quote style
565
+ `<w:style w:type="paragraph" w:styleId="Quote">` +
566
+ `<w:name w:val="Quote"/>` +
567
+ `<w:basedOn w:val="Normal"/>` +
568
+ `<w:pPr><w:ind w:left="720"/></w:pPr>` +
569
+ `<w:rPr><w:i/><w:color w:val="404040"/></w:rPr>` +
570
+ `</w:style>` +
571
+ // Code style
572
+ `<w:style w:type="paragraph" w:styleId="Code">` +
573
+ `<w:name w:val="Code"/>` +
574
+ `<w:basedOn w:val="Normal"/>` +
575
+ `<w:rPr>` +
576
+ `<w:rFonts w:ascii="${DEFAULT_CODE_FONT}" w:hAnsi="${DEFAULT_CODE_FONT}"/>` +
577
+ `<w:sz w:val="${DEFAULT_CODE_FONT_SIZE}"/>` +
578
+ `</w:rPr>` +
579
+ `</w:style>` +
580
+ // ListParagraph style
581
+ `<w:style w:type="paragraph" w:styleId="ListParagraph">` +
582
+ `<w:name w:val="List Paragraph"/>` +
583
+ `<w:basedOn w:val="Normal"/>` +
584
+ `<w:pPr><w:ind w:left="720"/></w:pPr>` +
585
+ `</w:style>` +
586
+ // Hyperlink character style
587
+ `<w:style w:type="character" w:styleId="Hyperlink">` +
588
+ `<w:name w:val="Hyperlink"/>` +
589
+ `<w:rPr><w:color w:val="${HYPERLINK_COLOR}"/><w:u w:val="single"/></w:rPr>` +
590
+ `</w:style>` +
591
+ // FootnoteReference character style
592
+ `<w:style w:type="character" w:styleId="FootnoteReference">` +
593
+ `<w:name w:val="footnote reference"/>` +
594
+ `<w:rPr><w:vertAlign w:val="superscript"/></w:rPr>` +
595
+ `</w:style>` +
596
+ // Table Grid style
597
+ `<w:style w:type="table" w:styleId="TableGrid">` +
598
+ `<w:name w:val="Table Grid"/>` +
599
+ `<w:tblPr>` +
600
+ `<w:tblBorders>` +
601
+ `<w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
602
+ `<w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
603
+ `<w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
604
+ `<w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
605
+ `<w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
606
+ `<w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/>` +
607
+ `</w:tblBorders>` +
608
+ `</w:tblPr>` +
609
+ `</w:style>` +
610
+ `</w:styles>`);
611
+ }
612
+ function buildHeadingStyles(headingFont) {
613
+ let result = '';
614
+ for (let depth = 1; depth <= 6; depth++) {
615
+ const styleId = DEPTH_TO_STYLE_ID[depth];
616
+ const fontSize = HEADING_FONT_SIZES[depth] ?? 22;
617
+ result +=
618
+ `<w:style w:type="paragraph" w:styleId="${styleId}">` +
619
+ `<w:name w:val="heading ${depth}"/>` +
620
+ `<w:basedOn w:val="Normal"/>` +
621
+ `<w:next w:val="Normal"/>` +
622
+ `<w:qFormat/>` +
623
+ `<w:pPr><w:outlineLvl w:val="${depth - 1}"/><w:spacing w:before="240" w:after="60"/></w:pPr>` +
624
+ `<w:rPr>` +
625
+ `<w:rFonts w:ascii="${escapeXml(headingFont)}" w:hAnsi="${escapeXml(headingFont)}"/>` +
626
+ `<w:b/>` +
627
+ `<w:sz w:val="${fontSize}"/>` +
628
+ `<w:szCs w:val="${fontSize}"/>` +
629
+ `</w:rPr>` +
630
+ `</w:style>`;
631
+ }
632
+ return result;
633
+ }
634
+ function buildSettingsXml() {
635
+ return (xmlDeclaration() +
636
+ `<w:settings xmlns:w="${NS_WML}">` +
637
+ `<w:defaultTabStop w:val="720"/>` +
638
+ `<w:characterSpacingControl w:val="doNotCompress"/>` +
639
+ `</w:settings>`);
640
+ }
641
+ function buildFontTableXml(options) {
642
+ const font = options.defaultFont ?? DEFAULT_FONT;
643
+ return (xmlDeclaration() +
644
+ `<w:fonts xmlns:w="${NS_WML}">` +
645
+ `<w:font w:name="${escapeXml(font)}">` +
646
+ `<w:panose1 w:val="020F0502020204030204"/>` +
647
+ `<w:charset w:val="00"/>` +
648
+ `<w:family w:val="swiss"/>` +
649
+ `<w:pitch w:val="variable"/>` +
650
+ `</w:font>` +
651
+ `<w:font w:name="${DEFAULT_HEADING_FONT}">` +
652
+ `<w:panose1 w:val="020F0302020204030204"/>` +
653
+ `<w:charset w:val="00"/>` +
654
+ `<w:family w:val="swiss"/>` +
655
+ `<w:pitch w:val="variable"/>` +
656
+ `</w:font>` +
657
+ `<w:font w:name="${DEFAULT_CODE_FONT}">` +
658
+ `<w:charset w:val="00"/>` +
659
+ `<w:family w:val="modern"/>` +
660
+ `<w:pitch w:val="fixed"/>` +
661
+ `</w:font>` +
662
+ `</w:fonts>`);
663
+ }
664
+ function buildNumberingXml(ctx) {
665
+ const abstract = [];
666
+ const concrete = [];
667
+ for (const def of ctx.numberingDefs) {
668
+ const absId = def.numId;
669
+ const levels = [];
670
+ for (let lvl = 0; lvl < 9; lvl++) {
671
+ if (def.ordered) {
672
+ levels.push(`<w:lvl w:ilvl="${lvl}">` +
673
+ `<w:start w:val="1"/>` +
674
+ `<w:numFmt w:val="decimal"/>` +
675
+ `<w:lvlText w:val="%${lvl + 1}."/>` +
676
+ `<w:lvlJc w:val="left"/>` +
677
+ `<w:pPr><w:ind w:left="${720 * (lvl + 1)}" w:hanging="360"/></w:pPr>` +
678
+ `</w:lvl>`);
679
+ }
680
+ else {
681
+ const bullets = ['\u2022', '\u25E6', '\u25AA']; // •, ◦, ▪
682
+ const bullet = bullets[lvl % bullets.length];
683
+ levels.push(`<w:lvl w:ilvl="${lvl}">` +
684
+ `<w:start w:val="1"/>` +
685
+ `<w:numFmt w:val="bullet"/>` +
686
+ `<w:lvlText w:val="${bullet}"/>` +
687
+ `<w:lvlJc w:val="left"/>` +
688
+ `<w:pPr><w:ind w:left="${720 * (lvl + 1)}" w:hanging="360"/></w:pPr>` +
689
+ `<w:rPr><w:rFonts w:ascii="Symbol" w:hAnsi="Symbol"/></w:rPr>` +
690
+ `</w:lvl>`);
691
+ }
692
+ }
693
+ abstract.push(`<w:abstractNum w:abstractNumId="${absId}">` + levels.join('') + `</w:abstractNum>`);
694
+ concrete.push(`<w:num w:numId="${def.numId}">` + `<w:abstractNumId w:val="${absId}"/>` + `</w:num>`);
695
+ }
696
+ return (xmlDeclaration() +
697
+ `<w:numbering xmlns:w="${NS_WML}">` +
698
+ abstract.join('') +
699
+ concrete.join('') +
700
+ `</w:numbering>`);
701
+ }
702
+ function buildFootnotesXml(ctx) {
703
+ const footnotes = [];
704
+ // Separator and continuation separator (required by Word)
705
+ footnotes.push(`<w:footnote w:type="separator" w:id="-1">` +
706
+ `<w:p><w:r><w:separator/></w:r></w:p>` +
707
+ `</w:footnote>`);
708
+ footnotes.push(`<w:footnote w:type="continuationSeparator" w:id="0">` +
709
+ `<w:p><w:r><w:continuationSeparator/></w:r></w:p>` +
710
+ `</w:footnote>`);
711
+ // User footnotes
712
+ for (const [_id, xml] of ctx.footnotes) {
713
+ footnotes.push(xml);
714
+ }
715
+ return (xmlDeclaration() +
716
+ `<w:footnotes xmlns:w="${NS_WML}" xmlns:r="${NS_R}">` +
717
+ footnotes.join('') +
718
+ `</w:footnotes>`);
719
+ }
720
+ // ============================================
721
+ // Helpers
722
+ // ============================================
723
+ /**
724
+ * Strip HTML tags from a string, keeping only text content.
725
+ */
726
+ function stripHtmlTags(html) {
727
+ return html.replace(/<[^>]*>/g, '');
728
+ }
729
+ //# sourceMappingURL=export.js.map