@bendyline/squisq-formats 1.2.3 → 1.3.1

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