@cj-tech-master/excelts 9.6.1 → 10.0.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 (207) hide show
  1. package/README.md +18 -3
  2. package/README_zh.md +18 -3
  3. package/dist/browser/modules/excel/cell.d.ts +4 -0
  4. package/dist/browser/modules/excel/note.js +5 -1
  5. package/dist/browser/modules/excel/row.js +35 -2
  6. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +8 -1
  7. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +22 -2
  8. package/dist/browser/modules/excel/types.d.ts +81 -0
  9. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +8 -0
  10. package/dist/browser/modules/excel/utils/drawing-utils.js +19 -2
  11. package/dist/browser/modules/excel/workbook.browser.d.ts +16 -0
  12. package/dist/browser/modules/excel/workbook.browser.js +32 -2
  13. package/dist/browser/modules/excel/worksheet.d.ts +31 -1
  14. package/dist/browser/modules/excel/worksheet.js +83 -0
  15. package/dist/browser/modules/excel/xlsx/xform/comment/vml-shape-xform.d.ts +7 -0
  16. package/dist/browser/modules/excel/xlsx/xform/comment/vml-shape-xform.js +42 -8
  17. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +3 -1
  18. package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +5 -0
  19. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +18 -1
  20. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +6 -0
  21. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.js +38 -11
  22. package/dist/browser/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.d.ts +1 -0
  23. package/dist/browser/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +5 -0
  24. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +2 -0
  25. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  26. package/dist/browser/modules/excel/xlsx/xform/drawing/shape-xform.d.ts +47 -0
  27. package/dist/browser/modules/excel/xlsx/xform/drawing/shape-xform.js +109 -0
  28. package/dist/browser/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +10 -1
  29. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +64 -1
  30. package/dist/browser/modules/pdf/builder/document-builder.js +22 -49
  31. package/dist/browser/modules/pdf/builder/pdf-editor.js +1 -1
  32. package/dist/browser/modules/pdf/core/pdf-stream.d.ts +28 -1
  33. package/dist/browser/modules/pdf/core/pdf-stream.js +38 -2
  34. package/dist/browser/modules/pdf/font/font-manager.d.ts +26 -0
  35. package/dist/browser/modules/pdf/font/font-manager.js +35 -18
  36. package/dist/browser/modules/pdf/render/page-renderer.d.ts +51 -3
  37. package/dist/browser/modules/pdf/render/page-renderer.js +111 -18
  38. package/dist/browser/modules/word/advanced/field-engine.js +45 -20
  39. package/dist/browser/modules/word/advanced/glossary.d.ts +10 -36
  40. package/dist/browser/modules/word/advanced/glossary.js +8 -9
  41. package/dist/browser/modules/word/advanced/math-convert.js +94 -12
  42. package/dist/browser/modules/word/advanced/ole-objects.d.ts +28 -0
  43. package/dist/browser/modules/word/advanced/ole-objects.js +122 -19
  44. package/dist/browser/modules/word/advanced/style-map.js +31 -10
  45. package/dist/browser/modules/word/builder/run-builders.d.ts +7 -1
  46. package/dist/browser/modules/word/builder/run-builders.js +7 -1
  47. package/dist/browser/modules/word/constants.d.ts +4 -0
  48. package/dist/browser/modules/word/constants.js +5 -1
  49. package/dist/browser/modules/word/convert/docx-to-semantic.d.ts +2 -1
  50. package/dist/browser/modules/word/convert/docx-to-semantic.js +135 -1
  51. package/dist/browser/modules/word/convert/html/html-import.d.ts +32 -1
  52. package/dist/browser/modules/word/convert/html/html-import.js +167 -14
  53. package/dist/browser/modules/word/convert/html/html.d.ts +2 -2
  54. package/dist/browser/modules/word/convert/html/html.js +1 -1
  55. package/dist/browser/modules/word/convert/markdown/markdown-import.d.ts +48 -18
  56. package/dist/browser/modules/word/convert/markdown/markdown-import.js +279 -69
  57. package/dist/browser/modules/word/convert/markdown/markdown.d.ts +1 -1
  58. package/dist/browser/modules/word/convert/odt/odt.js +407 -56
  59. package/dist/browser/modules/word/html.d.ts +2 -2
  60. package/dist/browser/modules/word/html.js +1 -1
  61. package/dist/browser/modules/word/index.base.d.ts +3 -3
  62. package/dist/browser/modules/word/index.base.js +1 -1
  63. package/dist/browser/modules/word/layout/layout-full.js +326 -19
  64. package/dist/browser/modules/word/layout/render-page.js +35 -8
  65. package/dist/browser/modules/word/markdown.d.ts +1 -1
  66. package/dist/browser/modules/word/query/compat.d.ts +10 -2
  67. package/dist/browser/modules/word/query/compat.js +29 -21
  68. package/dist/browser/modules/word/reader/docx-reader.js +105 -2
  69. package/dist/browser/modules/word/reader/math-parser.js +8 -2
  70. package/dist/browser/modules/word/security/cfb-reader.js +5 -5
  71. package/dist/browser/modules/word/types.d.ts +96 -1
  72. package/dist/browser/modules/word/writer/docx-packager.js +108 -2
  73. package/dist/browser/modules/word/writer/glossary-writer.d.ts +28 -0
  74. package/dist/browser/modules/word/writer/glossary-writer.js +121 -0
  75. package/dist/browser/modules/word/writer/header-footer-writer.js +105 -20
  76. package/dist/browser/modules/word/writer/math-writer.js +7 -2
  77. package/dist/browser/utils/font-metrics.d.ts +8 -0
  78. package/dist/browser/utils/font-metrics.js +43 -0
  79. package/dist/browser/utils/theme-colors.js +4 -1
  80. package/dist/cjs/modules/excel/note.js +5 -1
  81. package/dist/cjs/modules/excel/row.js +35 -2
  82. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +22 -2
  83. package/dist/cjs/modules/excel/utils/drawing-utils.js +19 -2
  84. package/dist/cjs/modules/excel/workbook.browser.js +31 -1
  85. package/dist/cjs/modules/excel/worksheet.js +83 -0
  86. package/dist/cjs/modules/excel/xlsx/xform/comment/vml-shape-xform.js +42 -8
  87. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +3 -1
  88. package/dist/cjs/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +5 -0
  89. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +18 -1
  90. package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-xform.js +38 -11
  91. package/dist/cjs/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +5 -0
  92. package/dist/cjs/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  93. package/dist/cjs/modules/excel/xlsx/xform/drawing/shape-xform.js +112 -0
  94. package/dist/cjs/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +10 -1
  95. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +64 -1
  96. package/dist/cjs/modules/pdf/builder/document-builder.js +21 -48
  97. package/dist/cjs/modules/pdf/builder/pdf-editor.js +1 -1
  98. package/dist/cjs/modules/pdf/core/pdf-stream.js +38 -2
  99. package/dist/cjs/modules/pdf/font/font-manager.js +35 -18
  100. package/dist/cjs/modules/pdf/render/page-renderer.js +112 -18
  101. package/dist/cjs/modules/word/advanced/field-engine.js +45 -20
  102. package/dist/cjs/modules/word/advanced/glossary.js +8 -9
  103. package/dist/cjs/modules/word/advanced/math-convert.js +94 -12
  104. package/dist/cjs/modules/word/advanced/ole-objects.js +123 -19
  105. package/dist/cjs/modules/word/advanced/style-map.js +31 -10
  106. package/dist/cjs/modules/word/builder/run-builders.js +7 -1
  107. package/dist/cjs/modules/word/constants.js +5 -1
  108. package/dist/cjs/modules/word/convert/docx-to-semantic.js +135 -1
  109. package/dist/cjs/modules/word/convert/html/html-import.js +168 -14
  110. package/dist/cjs/modules/word/convert/html/html.js +2 -1
  111. package/dist/cjs/modules/word/convert/markdown/markdown-import.js +279 -69
  112. package/dist/cjs/modules/word/convert/odt/odt.js +407 -56
  113. package/dist/cjs/modules/word/html.js +2 -1
  114. package/dist/cjs/modules/word/index.base.js +4 -3
  115. package/dist/cjs/modules/word/layout/layout-full.js +325 -18
  116. package/dist/cjs/modules/word/layout/render-page.js +35 -8
  117. package/dist/cjs/modules/word/query/compat.js +29 -21
  118. package/dist/cjs/modules/word/reader/docx-reader.js +104 -1
  119. package/dist/cjs/modules/word/reader/math-parser.js +8 -2
  120. package/dist/cjs/modules/word/security/cfb-reader.js +5 -5
  121. package/dist/cjs/modules/word/writer/docx-packager.js +108 -2
  122. package/dist/cjs/modules/word/writer/glossary-writer.js +124 -0
  123. package/dist/cjs/modules/word/writer/header-footer-writer.js +105 -20
  124. package/dist/cjs/modules/word/writer/math-writer.js +7 -2
  125. package/dist/cjs/utils/font-metrics.js +44 -0
  126. package/dist/cjs/utils/theme-colors.js +4 -1
  127. package/dist/esm/modules/excel/note.js +5 -1
  128. package/dist/esm/modules/excel/row.js +35 -2
  129. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +22 -2
  130. package/dist/esm/modules/excel/utils/drawing-utils.js +19 -2
  131. package/dist/esm/modules/excel/workbook.browser.js +32 -2
  132. package/dist/esm/modules/excel/worksheet.js +83 -0
  133. package/dist/esm/modules/excel/xlsx/xform/comment/vml-shape-xform.js +42 -8
  134. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +3 -1
  135. package/dist/esm/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +5 -0
  136. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +18 -1
  137. package/dist/esm/modules/excel/xlsx/xform/drawing/blip-xform.js +38 -11
  138. package/dist/esm/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +5 -0
  139. package/dist/esm/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  140. package/dist/esm/modules/excel/xlsx/xform/drawing/shape-xform.js +109 -0
  141. package/dist/esm/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +10 -1
  142. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +64 -1
  143. package/dist/esm/modules/pdf/builder/document-builder.js +22 -49
  144. package/dist/esm/modules/pdf/builder/pdf-editor.js +1 -1
  145. package/dist/esm/modules/pdf/core/pdf-stream.js +38 -2
  146. package/dist/esm/modules/pdf/font/font-manager.js +35 -18
  147. package/dist/esm/modules/pdf/render/page-renderer.js +111 -18
  148. package/dist/esm/modules/word/advanced/field-engine.js +45 -20
  149. package/dist/esm/modules/word/advanced/glossary.js +8 -9
  150. package/dist/esm/modules/word/advanced/math-convert.js +94 -12
  151. package/dist/esm/modules/word/advanced/ole-objects.js +122 -19
  152. package/dist/esm/modules/word/advanced/style-map.js +31 -10
  153. package/dist/esm/modules/word/builder/run-builders.js +7 -1
  154. package/dist/esm/modules/word/constants.js +5 -1
  155. package/dist/esm/modules/word/convert/docx-to-semantic.js +135 -1
  156. package/dist/esm/modules/word/convert/html/html-import.js +167 -14
  157. package/dist/esm/modules/word/convert/html/html.js +1 -1
  158. package/dist/esm/modules/word/convert/markdown/markdown-import.js +279 -69
  159. package/dist/esm/modules/word/convert/odt/odt.js +407 -56
  160. package/dist/esm/modules/word/html.js +1 -1
  161. package/dist/esm/modules/word/index.base.js +1 -1
  162. package/dist/esm/modules/word/layout/layout-full.js +326 -19
  163. package/dist/esm/modules/word/layout/render-page.js +35 -8
  164. package/dist/esm/modules/word/query/compat.js +29 -21
  165. package/dist/esm/modules/word/reader/docx-reader.js +105 -2
  166. package/dist/esm/modules/word/reader/math-parser.js +8 -2
  167. package/dist/esm/modules/word/security/cfb-reader.js +5 -5
  168. package/dist/esm/modules/word/writer/docx-packager.js +108 -2
  169. package/dist/esm/modules/word/writer/glossary-writer.js +121 -0
  170. package/dist/esm/modules/word/writer/header-footer-writer.js +105 -20
  171. package/dist/esm/modules/word/writer/math-writer.js +7 -2
  172. package/dist/esm/utils/font-metrics.js +43 -0
  173. package/dist/esm/utils/theme-colors.js +4 -1
  174. package/dist/iife/excelts.iife.js +496 -59
  175. package/dist/iife/excelts.iife.js.map +1 -1
  176. package/dist/iife/excelts.iife.min.js +39 -39
  177. package/dist/types/modules/excel/cell.d.ts +4 -0
  178. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +8 -1
  179. package/dist/types/modules/excel/types.d.ts +81 -0
  180. package/dist/types/modules/excel/utils/drawing-utils.d.ts +8 -0
  181. package/dist/types/modules/excel/workbook.browser.d.ts +16 -0
  182. package/dist/types/modules/excel/worksheet.d.ts +31 -1
  183. package/dist/types/modules/excel/xlsx/xform/comment/vml-shape-xform.d.ts +7 -0
  184. package/dist/types/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +6 -0
  185. package/dist/types/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.d.ts +1 -0
  186. package/dist/types/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +2 -0
  187. package/dist/types/modules/excel/xlsx/xform/drawing/shape-xform.d.ts +47 -0
  188. package/dist/types/modules/pdf/core/pdf-stream.d.ts +28 -1
  189. package/dist/types/modules/pdf/font/font-manager.d.ts +26 -0
  190. package/dist/types/modules/pdf/render/page-renderer.d.ts +51 -3
  191. package/dist/types/modules/word/advanced/glossary.d.ts +10 -36
  192. package/dist/types/modules/word/advanced/ole-objects.d.ts +28 -0
  193. package/dist/types/modules/word/builder/run-builders.d.ts +7 -1
  194. package/dist/types/modules/word/constants.d.ts +4 -0
  195. package/dist/types/modules/word/convert/docx-to-semantic.d.ts +2 -1
  196. package/dist/types/modules/word/convert/html/html-import.d.ts +32 -1
  197. package/dist/types/modules/word/convert/html/html.d.ts +2 -2
  198. package/dist/types/modules/word/convert/markdown/markdown-import.d.ts +48 -18
  199. package/dist/types/modules/word/convert/markdown/markdown.d.ts +1 -1
  200. package/dist/types/modules/word/html.d.ts +2 -2
  201. package/dist/types/modules/word/index.base.d.ts +3 -3
  202. package/dist/types/modules/word/markdown.d.ts +1 -1
  203. package/dist/types/modules/word/query/compat.d.ts +10 -2
  204. package/dist/types/modules/word/types.d.ts +96 -1
  205. package/dist/types/modules/word/writer/glossary-writer.d.ts +28 -0
  206. package/dist/types/utils/font-metrics.d.ts +8 -0
  207. package/package.json +3 -1
@@ -21,46 +21,60 @@ import { isRun } from "../../core/text-utils.js";
21
21
  /**
22
22
  * Convert a Markdown string into a complete DocxDocument.
23
23
  *
24
+ * Supports the full GFM feature set including inline images (embedded via the
25
+ * `resolveImage` callback) and footnotes (`[^id]` references with `[^id]: …`
26
+ * definitions). Because image resolution and document packaging are inherently
27
+ * asynchronous, this function is async.
28
+ *
24
29
  * @param markdown - The GFM Markdown string.
25
30
  * @param options - Optional conversion settings.
26
- * @returns A DocxDocument ready to be packaged.
31
+ * @returns A Promise resolving to a DocxDocument ready to be packaged.
27
32
  */
28
- export function markdownToDocx(markdown, options) {
29
- const { body, state } = markdownToDocxBodyInternal(markdown, options);
33
+ export async function markdownToDocx(markdown, options) {
34
+ const { body, state } = await markdownToDocxBodyInternal(markdown, options);
30
35
  return {
31
36
  body,
32
37
  styles: defaultMarkdownStyles(),
33
38
  abstractNumberings: state.abstractNumberings,
34
- numberingInstances: state.numberingInstances
39
+ numberingInstances: state.numberingInstances,
40
+ ...(state.footnotes.length > 0 ? { footnotes: state.footnotes } : {}),
41
+ ...(state.images.length > 0 ? { images: state.images } : {})
35
42
  };
36
43
  }
37
44
  /**
38
- * Convert a Markdown string into an array of DOCX body content blocks.
45
+ * Convert a Markdown string into DOCX body content plus the supporting
46
+ * document-level definitions it references.
39
47
  *
40
- * **Caveat — body content is not self-contained.**
41
- * - **Lists** (bullet / numbered / task) reference numbering ids that
42
- * live in document-level `abstractNumberings` + `numberingInstances`,
43
- * which this helper does NOT return.
44
- * - **Block quotes** reference the named `Quote` style.
45
- * - **Code blocks** reference the named code styles.
48
+ * **Caveat — body content is not self-contained.** The returned `body` may
49
+ * reference:
50
+ * - **Numbering** (`abstractNumberings` / `numberingInstances`) — used by
51
+ * bullet / numbered / task lists.
52
+ * - **Footnotes** (`footnotes`) referenced by footnote-reference runs.
53
+ * - **Images** (`images`) referenced by inline image runs.
54
+ * - The named `Quote` / `CodeBlock` styles (for block quotes / code blocks).
46
55
  *
47
- * Splicing markdown that uses any of these constructs into a document that
48
- * lacks the matching numbering / styles yields invalid OOXML. Either keep
49
- * the input flat (paragraphs + headings + inline formatting) before
50
- * splicing, or use the higher-level {@link markdownToDocx} which returns a
51
- * complete `DocxDocument` with the supporting definitions populated.
56
+ * Splice the relevant arrays into your host document alongside the body, or
57
+ * use the higher-level {@link markdownToDocx} which returns a complete
58
+ * `DocxDocument` with everything populated.
52
59
  *
53
60
  * @param markdown - The GFM Markdown string.
54
61
  * @param options - Optional conversion settings.
55
- * @returns Array of BodyContent blocks (no numbering / styles attached).
62
+ * @returns A Promise resolving to the body and its supporting definitions.
56
63
  */
57
- export function markdownToDocxBody(markdown, options) {
58
- return markdownToDocxBodyInternal(markdown, options).body;
64
+ export async function markdownToDocxBody(markdown, options) {
65
+ const { body, state } = await markdownToDocxBodyInternal(markdown, options);
66
+ return {
67
+ body,
68
+ abstractNumberings: state.abstractNumberings,
69
+ numberingInstances: state.numberingInstances,
70
+ footnotes: state.footnotes,
71
+ images: state.images
72
+ };
59
73
  }
60
74
  /**
61
75
  * Internal implementation: converts markdown and returns both body and state.
62
76
  */
63
- function markdownToDocxBodyInternal(markdown, options) {
77
+ async function markdownToDocxBodyInternal(markdown, options) {
64
78
  const state = createState();
65
79
  const opts = {
66
80
  codeFont: "Courier New",
@@ -68,8 +82,14 @@ function markdownToDocxBodyInternal(markdown, options) {
68
82
  ...options
69
83
  };
70
84
  const lines = markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
71
- const blocks = parseMarkdownBlocks(lines, 0, lines.length);
72
- const body = convertBlocks(blocks, opts, state);
85
+ // First pass: extract footnote definitions (`[^id]: …`) so references can
86
+ // resolve regardless of definition order, then parse the remaining blocks.
87
+ const { lines: contentLines, definitions } = extractFootnoteDefinitions(lines);
88
+ state.footnoteDefinitions = definitions;
89
+ const blocks = parseMarkdownBlocks(contentLines, 0, contentLines.length);
90
+ const body = await convertBlocks(blocks, opts, state);
91
+ // Emit footnote definitions that were actually referenced, in reference order.
92
+ await finalizeFootnotes(state, opts);
73
93
  return { body, state };
74
94
  }
75
95
  function createState() {
@@ -78,10 +98,106 @@ function createState() {
78
98
  numberingInstances: [],
79
99
  nextNumId: 1,
80
100
  bulletNumId: undefined,
81
- orderedNumId: undefined
101
+ orderedNumId: undefined,
102
+ footnotes: [],
103
+ images: [],
104
+ footnoteDefinitions: new Map(),
105
+ footnoteIds: new Map(),
106
+ footnoteOrder: [],
107
+ nextFootnoteId: 1,
108
+ nextImageId: 1,
109
+ nextDrawingId: 1
82
110
  };
83
111
  }
84
112
  // =============================================================================
113
+ // Footnote definition extraction (first pass)
114
+ // =============================================================================
115
+ /**
116
+ * Matches a footnote definition line: `[^label]: content`.
117
+ * Continuation lines (indented under a definition) are folded in.
118
+ */
119
+ const FOOTNOTE_DEF_RE = /^\[\^([^\]]+)\]:\s?(.*)$/;
120
+ /**
121
+ * Strip footnote definitions (`[^id]: …`) out of the line stream before block
122
+ * parsing, returning the remaining content lines plus a label→text map. A
123
+ * definition may span continuation lines that are indented by at least one
124
+ * space; those are joined with a single space.
125
+ */
126
+ function extractFootnoteDefinitions(lines) {
127
+ const definitions = new Map();
128
+ const contentLines = [];
129
+ let i = 0;
130
+ while (i < lines.length) {
131
+ // Preserve fenced code blocks verbatim — a `[^id]:`-looking line inside a
132
+ // code fence is code, not a footnote definition.
133
+ const fenceMatch = lines[i].match(/^(`{3,}|~{3,})/);
134
+ if (fenceMatch) {
135
+ const fence = fenceMatch[1];
136
+ const closeRe = new RegExp(`^${fence[0]}{${fence.length},}$`);
137
+ contentLines.push(lines[i]);
138
+ i++;
139
+ while (i < lines.length) {
140
+ contentLines.push(lines[i]);
141
+ const isClose = closeRe.test(lines[i].trim());
142
+ i++;
143
+ if (isClose) {
144
+ break;
145
+ }
146
+ }
147
+ continue;
148
+ }
149
+ const match = lines[i].match(FOOTNOTE_DEF_RE);
150
+ if (match) {
151
+ const label = match[1];
152
+ const parts = [match[2]];
153
+ i++;
154
+ // Fold indented continuation lines into the definition.
155
+ while (i < lines.length && /^\s+\S/.test(lines[i])) {
156
+ parts.push(lines[i].trim());
157
+ i++;
158
+ }
159
+ definitions.set(label, parts.join(" ").trim());
160
+ continue;
161
+ }
162
+ contentLines.push(lines[i]);
163
+ i++;
164
+ }
165
+ return { lines: contentLines, definitions };
166
+ }
167
+ /**
168
+ * After conversion, emit a `FootnoteDef` for every footnote that was actually
169
+ * referenced, in reference order. References to undefined labels still get a
170
+ * footnote (with empty content) so the reference mark remains valid OOXML.
171
+ */
172
+ function finalizeFootnotes(state, opts) {
173
+ return (async () => {
174
+ for (const label of state.footnoteOrder) {
175
+ const id = state.footnoteIds.get(label);
176
+ if (id === undefined) {
177
+ continue;
178
+ }
179
+ const text = state.footnoteDefinitions.get(label) ?? "";
180
+ const inlines = parseInlines(text);
181
+ const para = await convertParagraph(inlines, opts, state, { style: "FootnoteText" });
182
+ state.footnotes.push({ id, content: [para] });
183
+ }
184
+ })();
185
+ }
186
+ /**
187
+ * Resolve (or assign) the numeric footnote id for a markdown label, recording
188
+ * reference order on first use.
189
+ */
190
+ function resolveFootnoteId(label, state) {
191
+ const existing = state.footnoteIds.get(label);
192
+ if (existing !== undefined) {
193
+ return existing;
194
+ }
195
+ const id = state.nextFootnoteId++;
196
+ state.footnoteIds.set(label, id);
197
+ state.footnoteOrder.push(label);
198
+ return id;
199
+ }
200
+ // =============================================================================
85
201
  // Block Parser
86
202
  // =============================================================================
87
203
  function parseMarkdownBlocks(lines, start, end) {
@@ -432,6 +548,15 @@ function parseInlines(text) {
432
548
  continue;
433
549
  }
434
550
  }
551
+ // Footnote reference [^label]
552
+ if (text[i] === "[" && text[i + 1] === "^") {
553
+ const result = parseFootnoteRef(text, i);
554
+ if (result) {
555
+ nodes.push(result.node);
556
+ i = result.end;
557
+ continue;
558
+ }
559
+ }
435
560
  // Link [text](url "title")
436
561
  if (text[i] === "[") {
437
562
  const result = parseLink(text, i);
@@ -537,6 +662,17 @@ function parseInlineCode(text, start) {
537
662
  }
538
663
  return { node: { type: "code", text: code }, end: afterClose };
539
664
  }
665
+ function parseFootnoteRef(text, start) {
666
+ // [^label] — label may not contain ']' or whitespace.
667
+ const match = text.slice(start).match(/^\[\^([^\]\s]+)\]/);
668
+ if (!match) {
669
+ return null;
670
+ }
671
+ return {
672
+ node: { type: "footnoteRef", label: match[1] },
673
+ end: start + match[0].length
674
+ };
675
+ }
540
676
  function parseImage(text, start) {
541
677
  // ![alt](url "title")
542
678
  const altStart = start + 2; // skip "!["
@@ -759,20 +895,20 @@ function parseStrikethrough(text, start) {
759
895
  end: closeIdx + 2
760
896
  };
761
897
  }
762
- function convertBlocks(blocks, opts, state) {
898
+ async function convertBlocks(blocks, opts, state) {
763
899
  const result = [];
764
900
  for (const block of blocks) {
765
- const converted = convertBlock(block, opts, 0, state);
901
+ const converted = await convertBlock(block, opts, 0, state);
766
902
  result.push(...converted);
767
903
  }
768
904
  return result;
769
905
  }
770
- function convertBlock(block, opts, listLevel, state) {
906
+ async function convertBlock(block, opts, listLevel, state) {
771
907
  switch (block.type) {
772
908
  case "heading":
773
- return [convertHeading(block)];
909
+ return [await convertHeading(block, opts, state)];
774
910
  case "paragraph":
775
- return [convertParagraph(block.inlines, opts)];
911
+ return [await convertParagraph(block.inlines, opts, state)];
776
912
  case "blockquote":
777
913
  return convertBlockquote(block, opts, state);
778
914
  case "fencedCode":
@@ -782,7 +918,7 @@ function convertBlock(block, opts, listLevel, state) {
782
918
  case "list":
783
919
  return convertList(block, opts, listLevel, state);
784
920
  case "table":
785
- return [convertTable(block, opts)];
921
+ return [await convertTable(block, opts, state)];
786
922
  case "html":
787
923
  // Pass through as plain text paragraph
788
924
  return [
@@ -792,8 +928,8 @@ function convertBlock(block, opts, listLevel, state) {
792
928
  return [];
793
929
  }
794
930
  }
795
- function convertHeading(block) {
796
- const children = inlinesToRuns(block.inlines, {});
931
+ async function convertHeading(block, opts, state) {
932
+ const children = await inlinesToRuns(block.inlines, opts, state, true);
797
933
  return {
798
934
  type: "paragraph",
799
935
  properties: {
@@ -803,19 +939,19 @@ function convertHeading(block) {
803
939
  children
804
940
  };
805
941
  }
806
- function convertParagraph(inlines, opts, props) {
807
- const children = inlinesToRuns(inlines, opts);
942
+ async function convertParagraph(inlines, opts, state, props) {
943
+ const children = await inlinesToRuns(inlines, opts, state, false);
808
944
  return {
809
945
  type: "paragraph",
810
946
  properties: props,
811
947
  children
812
948
  };
813
949
  }
814
- function convertBlockquote(block, opts, state) {
950
+ async function convertBlockquote(block, opts, state) {
815
951
  // Convert blockquote children with "Quote" style and left indent
816
952
  const result = [];
817
953
  for (const child of block.children) {
818
- const converted = convertBlock(child, opts, 0, state);
954
+ const converted = await convertBlock(child, opts, 0, state);
819
955
  for (const item of converted) {
820
956
  if (item.type === "paragraph") {
821
957
  result.push({
@@ -875,7 +1011,7 @@ function convertThematicBreak() {
875
1011
  children: []
876
1012
  };
877
1013
  }
878
- function convertList(block, opts, parentLevel, state) {
1014
+ async function convertList(block, opts, parentLevel, state) {
879
1015
  // Ordered lists with a non-default `start` need their own numbering
880
1016
  // instance so the override actually takes effect — sharing one numId
881
1017
  // across all lists would force every list to start at the same number.
@@ -888,7 +1024,7 @@ function convertList(block, opts, parentLevel, state) {
888
1024
  for (const child of item.children) {
889
1025
  if (child.type === "list") {
890
1026
  // Nested list — increase level
891
- const nested = convertList(child, opts, parentLevel + 1, state);
1027
+ const nested = await convertList(child, opts, parentLevel + 1, state);
892
1028
  result.push(...nested);
893
1029
  }
894
1030
  else if (child.type === "paragraph") {
@@ -897,7 +1033,7 @@ function convertList(block, opts, parentLevel, state) {
897
1033
  const props = {
898
1034
  numbering: { numId, level: parentLevel }
899
1035
  };
900
- const para = convertParagraph(child.inlines, opts, props);
1036
+ const para = await convertParagraph(child.inlines, opts, state, props);
901
1037
  // Handle task list checkbox prefix
902
1038
  if (item.checked !== undefined) {
903
1039
  const checkbox = item.checked ? "☑ " : "☐ ";
@@ -918,11 +1054,11 @@ function convertList(block, opts, parentLevel, state) {
918
1054
  const props = {
919
1055
  indent: { left: 720 * (parentLevel + 1) }
920
1056
  };
921
- result.push(convertParagraph(child.inlines, opts, props));
1057
+ result.push(await convertParagraph(child.inlines, opts, state, props));
922
1058
  }
923
1059
  }
924
1060
  else {
925
- const converted = convertBlock(child, opts, parentLevel, state);
1061
+ const converted = await convertBlock(child, opts, parentLevel, state);
926
1062
  result.push(...converted);
927
1063
  firstBlock = false;
928
1064
  }
@@ -930,10 +1066,12 @@ function convertList(block, opts, parentLevel, state) {
930
1066
  }
931
1067
  return result;
932
1068
  }
933
- function convertTable(block, opts) {
1069
+ async function convertTable(block, opts, state) {
934
1070
  const colCount = block.headers.length;
935
1071
  // Header row
936
- const headerCells = block.headers.map((cell, ci) => {
1072
+ const headerCells = [];
1073
+ for (let ci = 0; ci < block.headers.length; ci++) {
1074
+ const cell = block.headers[ci];
937
1075
  const cellProps = {
938
1076
  shading: { fill: "F0F0F0" },
939
1077
  verticalAlign: "center"
@@ -941,7 +1079,7 @@ function convertTable(block, opts) {
941
1079
  const paraProps = {
942
1080
  alignment: block.alignments[ci]
943
1081
  };
944
- const para = convertParagraph(cell, opts, paraProps);
1082
+ const para = await convertParagraph(cell, opts, state, paraProps);
945
1083
  // Bold header text
946
1084
  const boldPara = {
947
1085
  ...para,
@@ -952,21 +1090,22 @@ function convertTable(block, opts) {
952
1090
  return child;
953
1091
  })
954
1092
  };
955
- return { properties: cellProps, content: [boldPara] };
956
- });
1093
+ headerCells.push({ properties: cellProps, content: [boldPara] });
1094
+ }
957
1095
  // Data rows
958
- const dataRows = block.rows.map(rowCells => {
1096
+ const dataRows = [];
1097
+ for (const rowCells of block.rows) {
959
1098
  const cells = [];
960
1099
  for (let ci = 0; ci < colCount; ci++) {
961
1100
  const cellInlines = ci < rowCells.length ? rowCells[ci] : [];
962
1101
  const paraProps = {
963
1102
  alignment: block.alignments[ci]
964
1103
  };
965
- const para = convertParagraph(cellInlines, opts, paraProps);
1104
+ const para = await convertParagraph(cellInlines, opts, state, paraProps);
966
1105
  cells.push({ content: [para] });
967
1106
  }
968
- return { cells };
969
- });
1107
+ dataRows.push({ cells });
1108
+ }
970
1109
  const allRows = [
971
1110
  { properties: { tableHeader: true }, cells: headerCells },
972
1111
  ...dataRows
@@ -991,37 +1130,41 @@ function convertTable(block, opts) {
991
1130
  // =============================================================================
992
1131
  // Inline to Run Conversion
993
1132
  // =============================================================================
994
- function inlinesToRuns(inlines, opts) {
1133
+ async function inlinesToRuns(inlines, opts, state, isHeading) {
995
1134
  const result = [];
996
1135
  for (const node of inlines) {
997
- inlineToRuns(node, result, {}, opts);
1136
+ await inlineToRuns(node, result, {}, opts, state, isHeading);
998
1137
  }
999
1138
  return result;
1000
1139
  }
1001
- function inlineToRuns(node, output, inheritedProps, opts) {
1140
+ async function inlineToRuns(node, output, inheritedProps, opts, state, isHeading) {
1002
1141
  switch (node.type) {
1003
1142
  case "text": {
1004
- const textProps = {
1005
- ...inheritedProps,
1006
- ...(opts.defaultFont && !inheritedProps.font ? { font: opts.defaultFont } : {}),
1007
- ...(opts.defaultFontSize && !inheritedProps.size ? { size: opts.defaultFontSize } : {})
1008
- };
1143
+ // Headings derive their font/size from the named Heading style; only
1144
+ // body text picks up the document default font/size.
1145
+ const textProps = isHeading
1146
+ ? inheritedProps
1147
+ : {
1148
+ ...inheritedProps,
1149
+ ...(opts.defaultFont && !inheritedProps.font ? { font: opts.defaultFont } : {}),
1150
+ ...(opts.defaultFontSize && !inheritedProps.size ? { size: opts.defaultFontSize } : {})
1151
+ };
1009
1152
  output.push(makeRun(node.text, textProps));
1010
1153
  break;
1011
1154
  }
1012
1155
  case "bold":
1013
1156
  for (const child of node.children) {
1014
- inlineToRuns(child, output, { ...inheritedProps, bold: true }, opts);
1157
+ await inlineToRuns(child, output, { ...inheritedProps, bold: true }, opts, state, isHeading);
1015
1158
  }
1016
1159
  break;
1017
1160
  case "italic":
1018
1161
  for (const child of node.children) {
1019
- inlineToRuns(child, output, { ...inheritedProps, italic: true }, opts);
1162
+ await inlineToRuns(child, output, { ...inheritedProps, italic: true }, opts, state, isHeading);
1020
1163
  }
1021
1164
  break;
1022
1165
  case "strikethrough":
1023
1166
  for (const child of node.children) {
1024
- inlineToRuns(child, output, { ...inheritedProps, strike: true }, opts);
1167
+ await inlineToRuns(child, output, { ...inheritedProps, strike: true }, opts, state, isHeading);
1025
1168
  }
1026
1169
  break;
1027
1170
  case "code":
@@ -1036,7 +1179,7 @@ function inlineToRuns(node, output, inheritedProps, opts) {
1036
1179
  const linkChildren = [];
1037
1180
  for (const child of node.children) {
1038
1181
  const tempOutput = [];
1039
- inlineToRuns(child, tempOutput, { ...inheritedProps, color: "0563C1", underline: "single" }, opts);
1182
+ await inlineToRuns(child, tempOutput, { ...inheritedProps, color: "0563C1", underline: "single" }, opts, state, isHeading);
1040
1183
  for (const run of tempOutput) {
1041
1184
  if ("content" in run) {
1042
1185
  linkChildren.push(run);
@@ -1053,20 +1196,71 @@ function inlineToRuns(node, output, inheritedProps, opts) {
1053
1196
  output.push(link);
1054
1197
  break;
1055
1198
  }
1056
- case "image":
1057
- // Images require async resolution for sync API we insert a placeholder
1058
- // The resolveImage callback in options would be used in an async variant
1059
- output.push(makeRun(`[Image: ${node.alt || node.url}]`, {
1060
- ...inheritedProps,
1061
- italic: true,
1062
- color: "666666"
1063
- }));
1199
+ case "image": {
1200
+ const run = await resolveImageRun(node, inheritedProps, opts, state);
1201
+ output.push(run);
1064
1202
  break;
1203
+ }
1204
+ case "footnoteRef": {
1205
+ const id = resolveFootnoteId(node.label, state);
1206
+ output.push({
1207
+ properties: { ...inheritedProps, style: "FootnoteReference" },
1208
+ content: [{ type: "footnoteRef", id }]
1209
+ });
1210
+ break;
1211
+ }
1065
1212
  case "lineBreak":
1066
1213
  output.push(makeRun("", undefined, [{ type: "break" }]));
1067
1214
  break;
1068
1215
  }
1069
1216
  }
1217
+ /**
1218
+ * Resolve an inline image. If `resolveImage` returns image data, embed it as a
1219
+ * real `InlineImageContent` run and register the media in `state.images`;
1220
+ * otherwise fall back to an italic `[Image: alt]` placeholder so the output
1221
+ * remains valid.
1222
+ */
1223
+ async function resolveImageRun(node, inheritedProps, opts, state) {
1224
+ const data = opts.resolveImage ? await opts.resolveImage(node.url, node.alt) : undefined;
1225
+ if (!data) {
1226
+ return makeRun(`[Image: ${node.alt || node.url}]`, {
1227
+ ...inheritedProps,
1228
+ italic: true,
1229
+ color: "666666"
1230
+ });
1231
+ }
1232
+ const n = state.nextImageId++;
1233
+ const fileName = `image${n}.${imageExtension(data.mediaType)}`;
1234
+ const rId = `mdimg${n}`;
1235
+ // For SVG, the packager auto-splits the PNG fallback into its own media
1236
+ // part, assigns a second relationship, and back-fills `svgRId` on the inline
1237
+ // image — so we only ever push a single ImageDef and a single rId here.
1238
+ state.images.push({
1239
+ data: data.data,
1240
+ mediaType: data.mediaType,
1241
+ fileName,
1242
+ rId,
1243
+ ...(data.fallbackData ? { fallbackData: data.fallbackData } : {})
1244
+ });
1245
+ const DEFAULT_DIM = 914400; // 1 inch fallback when no size given
1246
+ return {
1247
+ content: [
1248
+ {
1249
+ type: "image",
1250
+ rId,
1251
+ width: (data.width ?? DEFAULT_DIM),
1252
+ height: (data.height ?? DEFAULT_DIM),
1253
+ altText: node.alt || undefined,
1254
+ name: node.alt || fileName,
1255
+ drawingId: state.nextDrawingId++
1256
+ }
1257
+ ]
1258
+ };
1259
+ }
1260
+ /** Map an image media type to its word/media/ file extension. */
1261
+ function imageExtension(mediaType) {
1262
+ return mediaType === "jpeg" ? "jpg" : mediaType;
1263
+ }
1070
1264
  // =============================================================================
1071
1265
  // Numbering Helpers
1072
1266
  // =============================================================================
@@ -1320,6 +1514,22 @@ function defaultMarkdownStyles() {
1320
1514
  name: "Code Block",
1321
1515
  basedOn: "Normal",
1322
1516
  runProperties: { font: "Courier New", size: 20 }
1517
+ },
1518
+ {
1519
+ // Character style applied to the in-text footnote reference mark so it
1520
+ // renders as a superscript number, matching Word's built-in style.
1521
+ type: "character",
1522
+ styleId: "FootnoteReference",
1523
+ name: "footnote reference",
1524
+ runProperties: { vertAlign: "superscript" }
1525
+ },
1526
+ {
1527
+ // Paragraph style for footnote body text (smaller font, like Word).
1528
+ type: "paragraph",
1529
+ styleId: "FootnoteText",
1530
+ name: "footnote text",
1531
+ basedOn: "Normal",
1532
+ runProperties: { size: 20 }
1323
1533
  }
1324
1534
  ];
1325
1535
  }