@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
@@ -14,9 +14,10 @@
14
14
  * guard — adding a new body variant without a matching layout function
15
15
  * is a build error, never a silent drop.
16
16
  */
17
- import { measureTextWidth, mapToStandardFont } from "../../../utils/font-metrics.js";
17
+ import { measureTextWidth, mapToStandardFont, styledFontVariant } from "../../../utils/font-metrics.js";
18
18
  import { ommlToMathML } from "../advanced/math-convert.js";
19
19
  import { extractMathText, isHyperlink, isRun } from "../core/text-utils.js";
20
+ import { resolveStyle } from "../query/style-resolve.js";
20
21
  import { EMU_PER_POINT } from "../units.js";
21
22
  import { layoutDocument } from "./layout.js";
22
23
  import { DEFAULT_PAGE_HEIGHT_TWIPS, DEFAULT_PAGE_MARGIN_TWIPS, DEFAULT_PAGE_WIDTH_TWIPS } from "./layout-constants.js";
@@ -30,6 +31,33 @@ import { DEFAULT_PAGE_HEIGHT_TWIPS, DEFAULT_PAGE_MARGIN_TWIPS, DEFAULT_PAGE_WIDT
30
31
  export function layoutDocumentFull(doc, options) {
31
32
  // First pass: get page assignments via the existing lightweight layout
32
33
  const layoutResult = layoutDocument(doc, options);
34
+ // Resolve list markers once over the whole document so ordered-list
35
+ // counters increment correctly across pages. Stored in a module-level
36
+ // context so that every `layoutParagraph` call — including those reached
37
+ // through tables, text boxes, SDTs, footnotes, etc. — can render markers
38
+ // without threading the map through every container function. Layout runs
39
+ // fully synchronously (no `await`), so a single shared slot is safe.
40
+ const listMarkers = computeListMarkers(doc);
41
+ activeListMarkers = listMarkers;
42
+ activeDoc = doc;
43
+ try {
44
+ return layoutDocumentFullInner(doc, options, layoutResult, listMarkers);
45
+ }
46
+ finally {
47
+ activeListMarkers = undefined;
48
+ activeDoc = undefined;
49
+ }
50
+ }
51
+ /** Active list-marker map for the in-flight layout (see layoutDocumentFull). */
52
+ let activeListMarkers;
53
+ /**
54
+ * Active document for the in-flight layout, so `layoutParagraph` can resolve
55
+ * paragraph-style run properties (size/color/font) via `resolveStyle` without
56
+ * threading `doc` through every container function. Layout is synchronous so a
57
+ * single shared slot is safe.
58
+ */
59
+ let activeDoc;
60
+ function layoutDocumentFullInner(doc, options, layoutResult, listMarkers) {
33
61
  // Second pass: compute precise positions for each page. Footnote
34
62
  // ids that don't fit on a given page are carried over to the next
35
63
  // (a later page may still have room thanks to less body content
@@ -38,7 +66,7 @@ export function layoutDocumentFull(doc, options) {
38
66
  const bodyPageCount = layoutResult.pageCount;
39
67
  let pendingFootnoteIds = [];
40
68
  for (let pageNum = 1; pageNum <= bodyPageCount; pageNum++) {
41
- const result = buildPage(doc, pageNum, layoutResult, options, pendingFootnoteIds);
69
+ const result = buildPage(doc, pageNum, layoutResult, options, pendingFootnoteIds, listMarkers);
42
70
  pages.push(result.page);
43
71
  pendingFootnoteIds = result.deferredFootnoteIds;
44
72
  }
@@ -52,7 +80,7 @@ export function layoutDocumentFull(doc, options) {
52
80
  // entries for already-placed body items still point at earlier
53
81
  // pages, so the synthetic page won't pick up extra body
54
82
  // content; only the carried footnote queue renders.
55
- layoutResult, options, pendingFootnoteIds);
83
+ layoutResult, options, pendingFootnoteIds, listMarkers);
56
84
  pages.push(overflowResult.page);
57
85
  }
58
86
  return {
@@ -170,7 +198,7 @@ function availableSlotForLine(ctx, lineY, lineHeight) {
170
198
  function twipsToPt(twips) {
171
199
  return twips / 20;
172
200
  }
173
- function buildPage(doc, pageNumber, layout, options, pendingFootnoteIds) {
201
+ function buildPage(doc, pageNumber, layout, options, pendingFootnoteIds, listMarkers) {
174
202
  const sectionProps = doc.sectionProperties;
175
203
  const geometry = computePageGeometry(sectionProps, options?.pageGeometry);
176
204
  const content = [];
@@ -205,7 +233,7 @@ function buildPage(doc, pageNumber, layout, options, pendingFootnoteIds) {
205
233
  };
206
234
  switch (item.type) {
207
235
  case "paragraph": {
208
- const laid = layoutParagraph(item, cursorY, geometry.contentWidth, options, pageContext, imageMap);
236
+ const laid = layoutParagraph(item, cursorY, geometry.contentWidth, options, pageContext, imageMap, listMarkers);
209
237
  content.push({ ...laid, sourceIndex: i });
210
238
  cursorY = laid.rect.y + laid.rect.height;
211
239
  break;
@@ -698,13 +726,219 @@ function computeSectionBreaks(layout) {
698
726
  }
699
727
  return breaks;
700
728
  }
701
- // =============================================================================
702
- // Internal: Paragraph Layout
703
- // =============================================================================
704
- function layoutParagraph(para, startY, contentWidth, options, pageContext, imageMap) {
729
+ /**
730
+ * Resolve list markers for every numbered / bulleted paragraph in the
731
+ * document, in reading order, so ordered-list counters increment correctly
732
+ * across paragraphs (and reset when a lower level reappears). Returns a map
733
+ * keyed by the paragraph object.
734
+ *
735
+ * Markers are derived from `paragraph.properties.numbering` → the matching
736
+ * `NumberingInstance` → its `AbstractNumbering` level definition. Bullet
737
+ * levels emit their symbol; ordered levels emit a counter formatted per the
738
+ * level's `NumberFormat` (decimal / lower-upper letter / lower-upper roman),
739
+ * falling back to decimal for formats we don't render numerically.
740
+ */
741
+ function computeListMarkers(doc) {
742
+ const markers = new Map();
743
+ const instances = doc.numberingInstances;
744
+ const abstracts = doc.abstractNumberings;
745
+ if (!instances || !abstracts || instances.length === 0 || abstracts.length === 0) {
746
+ return markers;
747
+ }
748
+ const instById = new Map(instances.map(n => [n.numId, n]));
749
+ const absById = new Map(abstracts.map(a => [a.abstractNumId, a]));
750
+ // Per (numId) counters, one slot per level. Counters reset at deeper
751
+ // levels when a shallower level advances.
752
+ const counters = new Map();
753
+ // numIds whose list was interrupted by non-list content since their last
754
+ // item; the next item with that numId restarts its numbering. This makes
755
+ // two visually separate ordered lists (sharing a numId, separated by a
756
+ // plain paragraph) each start at 1 — matching user expectation rather than
757
+ // running a single continuous sequence.
758
+ const interrupted = new Set();
759
+ // numIds seen at least once, so we know which to mark interrupted.
760
+ const seenNumIds = new Set();
761
+ // Flatten paragraphs into document reading order (descending into tables),
762
+ // so list continuity is judged across the whole body, not per-cell.
763
+ const orderedParagraphs = [];
764
+ const walk = (items) => {
765
+ for (const item of items) {
766
+ if (item.type === "paragraph") {
767
+ orderedParagraphs.push(item);
768
+ }
769
+ else if (item.type === "table") {
770
+ for (const row of item.rows) {
771
+ for (const cell of row.cells) {
772
+ walk(cell.content);
773
+ }
774
+ }
775
+ }
776
+ }
777
+ };
778
+ const resolveParagraphMarker = (para) => {
779
+ const numbering = para.properties?.numbering;
780
+ if (!numbering) {
781
+ // Non-list paragraph: any list seen so far is now interrupted, so a
782
+ // later paragraph reusing the same numId restarts its sequence.
783
+ for (const id of seenNumIds) {
784
+ interrupted.add(id);
785
+ }
786
+ return;
787
+ }
788
+ const inst = instById.get(numbering.numId);
789
+ if (!inst) {
790
+ return;
791
+ }
792
+ const abs = absById.get(inst.abstractNumId);
793
+ if (!abs) {
794
+ return;
795
+ }
796
+ const level = numbering.level ?? 0;
797
+ const levelDef = inst.overrides?.find(o => o.level === level)?.levelDef ??
798
+ abs.levels.find(l => l.level === level);
799
+ if (!levelDef) {
800
+ return;
801
+ }
802
+ seenNumIds.add(numbering.numId);
803
+ const indentPt = (level + 1) * 36; // 0.5" per level
804
+ if (levelDef.format === "bullet") {
805
+ // Bullet symbol. Word authors bullets with Symbol/Wingdings private-use
806
+ // code points (e.g. U+F0B7 ·, U+F0A7 ▪) that PDF standard fonts can't
807
+ // render. Normalize the common ones to WinAnsi-renderable equivalents;
808
+ // fall back to a round bullet when empty or unknown.
809
+ const symbol = normalizeBulletGlyph(levelDef.text);
810
+ markers.set(para, { text: `${symbol} `, indentPt });
811
+ // A bullet item does not clear the interruption flag for ordered
812
+ // siblings, but it is itself a list item — keep it out of `interrupted`.
813
+ interrupted.delete(numbering.numId);
814
+ return;
815
+ }
816
+ // Ordered list: advance this level's counter and reset deeper levels.
817
+ let levelCounts = counters.get(numbering.numId);
818
+ if (!levelCounts) {
819
+ levelCounts = [];
820
+ counters.set(numbering.numId, levelCounts);
821
+ }
822
+ // If this numId's run was interrupted by non-list content, restart it.
823
+ if (interrupted.has(numbering.numId)) {
824
+ levelCounts.length = 0;
825
+ interrupted.delete(numbering.numId);
826
+ }
827
+ const startOverride = inst.overrides?.find(o => o.level === level)?.startOverride;
828
+ const start = startOverride ?? levelDef.start ?? 1;
829
+ if (levelCounts[level] === undefined) {
830
+ levelCounts[level] = start;
831
+ }
832
+ else {
833
+ levelCounts[level] += 1;
834
+ }
835
+ // Reset any deeper levels.
836
+ for (let l = level + 1; l < levelCounts.length; l++) {
837
+ levelCounts[l] = undefined;
838
+ }
839
+ const counter = levelCounts[level];
840
+ const numeral = formatListCounter(counter, levelDef.format);
841
+ // Honour the level's `text` template (e.g. "%1.") when present; else
842
+ // fall back to "<n>.".
843
+ const text = levelDef.text ? levelDef.text.replace(/%\d+/g, numeral) : `${numeral}.`;
844
+ markers.set(para, { text: `${text} `, indentPt });
845
+ };
846
+ walk(doc.body);
847
+ for (const para of orderedParagraphs) {
848
+ resolveParagraphMarker(para);
849
+ }
850
+ return markers;
851
+ }
852
+ /** Normalize a Word bullet glyph to a WinAnsi-renderable equivalent. */
853
+ function normalizeBulletGlyph(text) {
854
+ if (!text || text.length === 0) {
855
+ return "\u2022"; // round bullet
856
+ }
857
+ const cp = text.codePointAt(0);
858
+ switch (cp) {
859
+ // Symbol-font private-use code points Word emits for default bullets.
860
+ case 0xf0b7: // Symbol "·" → round bullet
861
+ case 0x00b7: // middle dot
862
+ return "\u2022";
863
+ case 0xf0a7: // Symbol filled small square
864
+ case 0xf0a8:
865
+ return "\u25aa";
866
+ case 0xf0fc: // Wingdings check
867
+ return "\u2713";
868
+ default:
869
+ // Already a renderable glyph (e.g. "o", "-", "•") — keep it.
870
+ return text;
871
+ }
872
+ }
873
+ /** Format an ordered-list counter per its OOXML number format. */
874
+ function formatListCounter(n, format) {
875
+ switch (format) {
876
+ case "lowerLetter":
877
+ return toAlpha(n).toLowerCase();
878
+ case "upperLetter":
879
+ return toAlpha(n).toUpperCase();
880
+ case "lowerRoman":
881
+ return toRoman(n).toLowerCase();
882
+ case "upperRoman":
883
+ return toRoman(n).toUpperCase();
884
+ case "decimalZero":
885
+ return n < 10 ? `0${n}` : String(n);
886
+ default:
887
+ // decimal and any non-numeric/locale formats we don't render.
888
+ return String(n);
889
+ }
890
+ }
891
+ /** 1 → "A", 26 → "Z", 27 → "AA" (spreadsheet-style alpha). */
892
+ function toAlpha(n) {
893
+ let s = "";
894
+ let v = n;
895
+ while (v > 0) {
896
+ const rem = (v - 1) % 26;
897
+ s = String.fromCharCode(65 + rem) + s;
898
+ v = Math.floor((v - 1) / 26);
899
+ }
900
+ return s || "A";
901
+ }
902
+ /** Convert a positive integer to a Roman numeral (uppercase). */
903
+ function toRoman(n) {
904
+ if (n <= 0) {
905
+ return String(n);
906
+ }
907
+ const table = [
908
+ [1000, "M"],
909
+ [900, "CM"],
910
+ [500, "D"],
911
+ [400, "CD"],
912
+ [100, "C"],
913
+ [90, "XC"],
914
+ [50, "L"],
915
+ [40, "XL"],
916
+ [10, "X"],
917
+ [9, "IX"],
918
+ [5, "V"],
919
+ [4, "IV"],
920
+ [1, "I"]
921
+ ];
922
+ let v = n;
923
+ let s = "";
924
+ for (const [val, sym] of table) {
925
+ while (v >= val) {
926
+ s += sym;
927
+ v -= val;
928
+ }
929
+ }
930
+ return s;
931
+ }
932
+ function layoutParagraph(para, startY, contentWidth, options, pageContext, imageMap, listMarkers) {
705
933
  const props = para.properties;
706
934
  const spacing = props?.spacing;
707
- const headingScale = getHeadingFontScale(getHeadingLevel(props));
935
+ // Resolve effective run properties from the paragraph's style chain. When
936
+ // the style supplies a concrete font size we honour it; only when it does
937
+ // not do we fall back to the heuristic heading scale so headings stay
938
+ // distinct in documents lacking a styles table.
939
+ const styleRunProps = activeDoc ? resolveStyle(activeDoc, para).runProperties : undefined;
940
+ const styleHasSize = styleRunProps?.size != null;
941
+ const headingScale = styleHasSize ? 1 : getHeadingFontScale(getHeadingLevel(props));
708
942
  // Space before
709
943
  let spaceBefore = 0;
710
944
  if (spacing?.beforeAutoSpacing) {
@@ -714,7 +948,15 @@ function layoutParagraph(para, startY, contentWidth, options, pageContext, image
714
948
  spaceBefore = twipsToPt(spacing.before);
715
949
  }
716
950
  const indent = props?.indent;
717
- const leftIndentPt = indent?.left ? twipsToPt(indent.left) : 0;
951
+ // Prefer an explicitly threaded map; fall back to the active layout's
952
+ // shared map so list markers also render inside tables, text boxes, SDTs,
953
+ // footnotes, etc. (whose layoutParagraph calls don't thread it through).
954
+ const marker = (listMarkers ?? activeListMarkers)?.get(para);
955
+ // List paragraphs are indented by their numbering level; the marker text
956
+ // is injected as a leading run below. An explicit paragraph indent (rare on
957
+ // list items) still wins when larger.
958
+ const markerIndentPt = marker ? marker.indentPt : 0;
959
+ const leftIndentPt = Math.max(indent?.left ? twipsToPt(indent.left) : 0, markerIndentPt);
718
960
  const firstLineIndentPt = indent?.firstLine ? twipsToPt(indent.firstLine) : 0;
719
961
  const alignment = props?.alignment ?? "left";
720
962
  // Line height
@@ -735,7 +977,20 @@ function layoutParagraph(para, startY, contentWidth, options, pageContext, image
735
977
  }
736
978
  lineHeightPt *= headingScale;
737
979
  // Collect runs
738
- const segments = collectParagraphSegments(para);
980
+ const segments = mergeStyleRunProps(collectParagraphSegments(para), styleRunProps);
981
+ // Inject the list marker (bullet / number) as a leading text run so it
982
+ // renders inline at the start of the first line, inheriting the first
983
+ // text run's formatting (font / size) for visual consistency.
984
+ if (marker) {
985
+ let firstRunProps;
986
+ for (const s of segments) {
987
+ if (!("type" in s) || s.type === undefined) {
988
+ firstRunProps = s.properties;
989
+ break;
990
+ }
991
+ }
992
+ segments.unshift({ text: marker.text, properties: firstRunProps });
993
+ }
739
994
  const fullAvailableWidth = contentWidth - leftIndentPt;
740
995
  // When a page has wrap exclusions (square / tight / through floats)
741
996
  // we wrap line-by-line, asking the page context for the widest free
@@ -784,7 +1039,7 @@ function layoutParagraph(para, startY, contentWidth, options, pageContext, image
784
1039
  }
785
1040
  else {
786
1041
  const fontSize = getRunFontSizePt(seg.properties) * headingScale;
787
- const fontName = mapToStandardFont(resolveRunFontName(seg.properties));
1042
+ const fontName = styledFontVariant(resolveRunFontName(seg.properties), seg.properties?.bold, seg.properties?.italic);
788
1043
  lineWidth += measureTextWidth(seg.text, fontName, fontSize);
789
1044
  }
790
1045
  }
@@ -815,7 +1070,7 @@ function layoutParagraph(para, startY, contentWidth, options, pageContext, image
815
1070
  }
816
1071
  const fontSize = getRunFontSizePt(seg.properties) * headingScale;
817
1072
  const fontName = resolveRunFontName(seg.properties);
818
- const measuredFont = mapToStandardFont(fontName);
1073
+ const measuredFont = styledFontVariant(fontName, seg.properties?.bold, seg.properties?.italic);
819
1074
  const segWidth = measureTextWidth(seg.text, measuredFont, fontSize);
820
1075
  runs.push({
821
1076
  text: seg.text,
@@ -933,7 +1188,8 @@ function layoutTable(table, startY, contentWidth, sourceIndex, options, imageMap
933
1188
  rect: { x: cellX, y: startY + cursorY, width: cellWidth, height: cellHeight },
934
1189
  row: ri,
935
1190
  col: ci,
936
- content: cellContent
1191
+ content: cellContent,
1192
+ borders: resolveCellBorders(table.properties?.borders, cell.properties?.borders, ri === 0, ri === table.rows.length - 1, startCol === 0, endCol >= colWidths.length)
937
1193
  });
938
1194
  gridCol += span;
939
1195
  }
@@ -952,6 +1208,38 @@ function layoutTable(table, startY, contentWidth, sourceIndex, options, imageMap
952
1208
  sourceIndex
953
1209
  };
954
1210
  }
1211
+ /**
1212
+ * Resolve the four visible borders of a table cell into layout-model form
1213
+ * (`{ width: pt, color: hex }`). A cell's own border wins; otherwise the
1214
+ * table-level border applies — outer edges use `top/left/bottom/right`, inner
1215
+ * edges use `insideH/insideV`. OOXML border `size` is in eighths of a point.
1216
+ */
1217
+ function resolveCellBorders(tableBorders, cellBorders, isTopRow, isBottomRow, isLeftCol, isRightCol) {
1218
+ const edge = (cellEdge, outerEdge, innerEdge, isOuter) => {
1219
+ const b = cellEdge ?? (isOuter ? outerEdge : innerEdge);
1220
+ if (!b || b.style === "none" || b.style === "nil") {
1221
+ return undefined;
1222
+ }
1223
+ // `size` is in eighths of a point; default to a hairline (0.5pt) when
1224
+ // a border is declared without an explicit size.
1225
+ const width = b.size != null ? b.size / 8 : 0.5;
1226
+ const color = !b.color || b.color === "auto" ? "000000" : b.color;
1227
+ return { width: Math.max(0.25, width), color };
1228
+ };
1229
+ const top = edge(cellBorders?.top, tableBorders?.top, tableBorders?.insideH, isTopRow);
1230
+ const bottom = edge(cellBorders?.bottom, tableBorders?.bottom, tableBorders?.insideH, isBottomRow);
1231
+ const left = edge(cellBorders?.left, tableBorders?.left, tableBorders?.insideV, isLeftCol);
1232
+ const right = edge(cellBorders?.right, tableBorders?.right, tableBorders?.insideV, isRightCol);
1233
+ if (!top && !bottom && !left && !right) {
1234
+ return undefined;
1235
+ }
1236
+ return {
1237
+ ...(top ? { top } : {}),
1238
+ ...(bottom ? { bottom } : {}),
1239
+ ...(left ? { left } : {}),
1240
+ ...(right ? { right } : {})
1241
+ };
1242
+ }
955
1243
  /**
956
1244
  * Resolve a table's per-column widths in points.
957
1245
  *
@@ -1001,6 +1289,21 @@ function collectParagraphSegments(para) {
1001
1289
  }
1002
1290
  return segments;
1003
1291
  }
1292
+ /**
1293
+ * Overlay resolved paragraph-style run properties under each segment's own
1294
+ * (inline) properties, so style-defined size/color/font apply when a run does
1295
+ * not override them. Inline run properties always win.
1296
+ */
1297
+ function mergeStyleRunProps(segments, styleRunProps) {
1298
+ if (!styleRunProps) {
1299
+ return segments;
1300
+ }
1301
+ return segments.map(seg => {
1302
+ const own = seg.properties;
1303
+ const merged = own ? { ...styleRunProps, ...own } : styleRunProps;
1304
+ return { ...seg, properties: merged };
1305
+ });
1306
+ }
1004
1307
  /**
1005
1308
  * Emit `ParagraphSegment` tokens for a single run, preserving the
1006
1309
  * relative order of text fragments and inline images. Consecutive
@@ -1056,7 +1359,7 @@ function wrapSegmentsToLinesWithExclusions(segments, leftIndentPt, firstLineInde
1056
1359
  continue;
1057
1360
  }
1058
1361
  const fontSize = getRunFontSizePt(seg.properties) * headingScale;
1059
- const fontName = mapToStandardFont(resolveRunFontName(seg.properties));
1362
+ const fontName = styledFontVariant(resolveRunFontName(seg.properties), seg.properties?.bold, seg.properties?.italic);
1060
1363
  // Split on runs of whitespace, keeping the whitespace tokens so
1061
1364
  // wrapping can decide whether to drop trailing space at line end.
1062
1365
  const tokens = seg.text.split(/(\s+)/);
@@ -1211,14 +1514,18 @@ function wrapSegmentsToLines(segments, availableWidth, firstLineIndent, headingS
1211
1514
  }
1212
1515
  const text = segment.text;
1213
1516
  const fontSize = getRunFontSizePt(segment.properties) * headingScale;
1214
- const fontName = mapToStandardFont(resolveRunFontName(segment.properties));
1517
+ const fontName = styledFontVariant(resolveRunFontName(segment.properties), segment.properties?.bold, segment.properties?.italic);
1215
1518
  const segmentWidth = measureTextWidth(text, fontName, fontSize);
1216
- if (currentLineWidth + segmentWidth <= effectiveWidth || currentLine.length === 0) {
1519
+ if (currentLineWidth + segmentWidth <= effectiveWidth) {
1520
+ // Whole segment fits on the current line — fast path.
1217
1521
  currentLine.push(segment);
1218
1522
  currentLineWidth += segmentWidth;
1219
1523
  }
1220
1524
  else {
1221
- // Word-level splitting
1525
+ // Segment does not fit — split it into words and wrap. The inner
1526
+ // loop's `currentLine.length === 0 && bufferedText.length === 0`
1527
+ // guard guarantees at least one word per line (preventing a dead
1528
+ // loop when even a single word is wider than the line).
1222
1529
  const words = text.split(/(\s+)/);
1223
1530
  let bufferedText = "";
1224
1531
  let bufferedWidth = 0;
@@ -10,6 +10,7 @@
10
10
  import { measureTextWidth, mapToStandardFont } from "../../../utils/font-metrics.js";
11
11
  import { xmlEncode, xmlEncodeAttr } from "../../xml/encode.js";
12
12
  import { isHyperlink, isRun } from "../core/text-utils.js";
13
+ import { resolveStyle } from "../query/style-resolve.js";
13
14
  import { EMU_PER_POINT } from "../units.js";
14
15
  import { layoutDocument } from "./layout.js";
15
16
  import { DEFAULT_PAGE_HEIGHT_TWIPS, DEFAULT_PAGE_MARGIN_TWIPS, DEFAULT_PAGE_WIDTH_TWIPS } from "./layout-constants.js";
@@ -189,7 +190,18 @@ function renderParagraph(para, state) {
189
190
  const props = para.properties;
190
191
  const spacing = props?.spacing;
191
192
  const headingLevel = getHeadingLevel(props);
192
- const headingScale = getHeadingFontScale(headingLevel);
193
+ // Resolve the paragraph's effective run properties from the style chain
194
+ // (Heading1 → … → docDefaults). When the style provides a concrete font
195
+ // size we honour it directly; only when no style size is available do we
196
+ // fall back to the heuristic heading scale so headings still look distinct
197
+ // in documents that lack a styles table.
198
+ const styleRunProps = resolveStyle(state.doc, para).runProperties;
199
+ const styleHasSize = styleRunProps.size != null;
200
+ const fallbackScale = getHeadingFontScale(headingLevel);
201
+ const headingScale = styleHasSize ? 1 : fallbackScale;
202
+ // Synthesise bold for headings only in fallback mode (no real style size);
203
+ // otherwise the style's own bold flag governs.
204
+ const synthesizeHeadingBold = headingLevel > 0 && !styleHasSize;
193
205
  // Space before
194
206
  let spaceBefore = 0;
195
207
  if (spacing?.beforeAutoSpacing) {
@@ -231,7 +243,7 @@ function renderParagraph(para, state) {
231
243
  // Apply heading scale to line height
232
244
  lineHeightPt *= headingScale;
233
245
  // Collect runs and render as text spans on lines
234
- const runs = collectParagraphRuns(para);
246
+ const runs = collectParagraphRuns(para, styleRunProps);
235
247
  if (runs.length === 0) {
236
248
  // Empty paragraph — advance by line height
237
249
  state.cursorY += lineHeightPt;
@@ -261,7 +273,7 @@ function renderParagraph(para, state) {
261
273
  for (const segment of line) {
262
274
  const fontSize = getRunFontSizePt(segment.properties) * headingScale;
263
275
  const fontFamily = resolveFontFamily(getRunFontName({ properties: segment.properties, content: [] }), state.fontsMap);
264
- const isBold = segment.properties?.bold || headingLevel > 0;
276
+ const isBold = segment.properties?.bold || synthesizeHeadingBold;
265
277
  const isItalic = segment.properties?.italic;
266
278
  const color = resolveColor(segment.properties?.color);
267
279
  const underline = segment.properties?.underline;
@@ -313,20 +325,32 @@ function renderParagraph(para, state) {
313
325
  state.cursorY += spaceAfter;
314
326
  }
315
327
  /** Collect all text segments from a paragraph's children. */
316
- function collectParagraphRuns(para) {
328
+ function collectParagraphRuns(para, styleRunProps) {
329
+ // Merge the resolved paragraph-style run properties as a fallback under each
330
+ // run's own (inline) properties, so style-defined size/color/font apply when
331
+ // the run does not override them.
332
+ const merge = (own) => {
333
+ if (!styleRunProps) {
334
+ return own;
335
+ }
336
+ if (!own) {
337
+ return styleRunProps;
338
+ }
339
+ return { ...styleRunProps, ...own };
340
+ };
317
341
  const segments = [];
318
342
  for (const child of para.children) {
319
343
  if (isRun(child)) {
320
344
  const text = getRunText(child);
321
345
  if (text.length > 0) {
322
- segments.push({ text, properties: child.properties });
346
+ segments.push({ text, properties: merge(child.properties) });
323
347
  }
324
348
  }
325
349
  else if (isHyperlink(child)) {
326
350
  for (const run of child.children) {
327
351
  const text = getRunText(run);
328
352
  if (text.length > 0) {
329
- segments.push({ text, properties: run.properties });
353
+ segments.push({ text, properties: merge(run.properties) });
330
354
  }
331
355
  }
332
356
  }
@@ -518,8 +542,11 @@ function renderTable(table, state) {
518
542
  let cellCursorY = rowStartY + cellPadding;
519
543
  for (const content of cell.content) {
520
544
  if (content.type === "paragraph") {
521
- // Simplified: render first run of each paragraph
522
- const runs = collectParagraphRuns(content);
545
+ // Simplified: render first run of each paragraph, with the
546
+ // paragraph style's run properties as a fallback so styled cell
547
+ // text (e.g. a heading paragraph) honours its size/colour/font.
548
+ const cellStyleRunProps = resolveStyle(state.doc, content).runProperties;
549
+ const runs = collectParagraphRuns(content, cellStyleRunProps);
523
550
  if (runs.length > 0) {
524
551
  const allText = runs.map(r => r.text).join("");
525
552
  const firstRun = runs[0];
@@ -11,4 +11,4 @@
11
11
  * ```
12
12
  */
13
13
  export { renderToMarkdown, markdownToDocx, markdownToDocxBody } from "./convert/markdown/markdown.js";
14
- export type { MarkdownRenderOptions, MarkdownImportOptions, MarkdownImageData } from "./convert/markdown/markdown.js";
14
+ export type { MarkdownRenderOptions, MarkdownImportOptions, MarkdownImageData, MarkdownBodyResult } from "./convert/markdown/markdown.js";
@@ -9,8 +9,12 @@ export type CompatibilityMode = 11 | 12 | 14 | 15;
9
9
  /**
10
10
  * Get the compatibility mode of a document.
11
11
  *
12
- * Looks at the `compatSetting` named "compatibilityMode" in document settings.
13
- * Returns 15 (Word 2013+) by default if not found.
12
+ * The mode is stored in `settings.compatibilityMode` (the canonical scalar
13
+ * field populated by the reader). For backward compatibility we also honour an
14
+ * explicit `compatibilityMode` entry in `settings.compatSettings` if present,
15
+ * since the writer accepts that advanced-override path.
16
+ *
17
+ * Returns 15 (Word 2013+) by default if nothing is stored.
14
18
  *
15
19
  * @param doc - The document to inspect.
16
20
  * @returns The compatibility mode version number.
@@ -19,6 +23,10 @@ export declare function getCompatibilityMode(doc: DocxDocument): CompatibilityMo
19
23
  /**
20
24
  * Set the compatibility mode of a document (mutates settings in place).
21
25
  *
26
+ * Writes the canonical `settings.compatibilityMode` scalar field and removes
27
+ * any stale `compatibilityMode` override entry from `settings.compatSettings`
28
+ * so the two sources never disagree.
29
+ *
22
30
  * @param doc - The document to modify (mutated in place).
23
31
  * @param mode - The target compatibility mode (11=Word 2003, 12=Word 2007, 14=Word 2010, 15=Word 2013+).
24
32
  */
@@ -10,21 +10,29 @@ import {} from "../core/internal-utils.js";
10
10
  /**
11
11
  * Get the compatibility mode of a document.
12
12
  *
13
- * Looks at the `compatSetting` named "compatibilityMode" in document settings.
14
- * Returns 15 (Word 2013+) by default if not found.
13
+ * The mode is stored in `settings.compatibilityMode` (the canonical scalar
14
+ * field populated by the reader). For backward compatibility we also honour an
15
+ * explicit `compatibilityMode` entry in `settings.compatSettings` if present,
16
+ * since the writer accepts that advanced-override path.
17
+ *
18
+ * Returns 15 (Word 2013+) by default if nothing is stored.
15
19
  *
16
20
  * @param doc - The document to inspect.
17
21
  * @returns The compatibility mode version number.
18
22
  */
19
23
  export function getCompatibilityMode(doc) {
20
- if (!doc.settings?.compatSettings) {
24
+ const settings = doc.settings;
25
+ if (!settings) {
21
26
  return 15;
22
27
  }
23
- const modeSetting = doc.settings.compatSettings.find(s => s.name === "compatibilityMode");
24
- if (!modeSetting?.val) {
28
+ // Prefer an explicit override entry in compatSettings (advanced path) so a
29
+ // hand-authored value wins, then fall back to the canonical scalar field.
30
+ const overrideEntry = settings.compatSettings?.find(s => s.name === "compatibilityMode");
31
+ const raw = overrideEntry?.val ?? settings.compatibilityMode;
32
+ if (raw === undefined) {
25
33
  return 15;
26
34
  }
27
- const n = parseInt(modeSetting.val, 10);
35
+ const n = typeof raw === "number" ? raw : parseInt(raw, 10);
28
36
  if (n === 11 || n === 12 || n === 14 || n === 15) {
29
37
  return n;
30
38
  }
@@ -33,26 +41,26 @@ export function getCompatibilityMode(doc) {
33
41
  /**
34
42
  * Set the compatibility mode of a document (mutates settings in place).
35
43
  *
44
+ * Writes the canonical `settings.compatibilityMode` scalar field and removes
45
+ * any stale `compatibilityMode` override entry from `settings.compatSettings`
46
+ * so the two sources never disagree.
47
+ *
36
48
  * @param doc - The document to modify (mutated in place).
37
49
  * @param mode - The target compatibility mode (11=Word 2003, 12=Word 2007, 14=Word 2010, 15=Word 2013+).
38
50
  */
39
51
  export function setCompatibilityMode(doc, mode) {
40
52
  const settings = doc.settings ? { ...doc.settings } : {};
41
- const compatSettings = settings.compatSettings
42
- ? [...settings.compatSettings]
43
- : [];
44
- const idx = compatSettings.findIndex(s => s.name === "compatibilityMode");
45
- const entry = {
46
- name: "compatibilityMode",
47
- uri: "http://schemas.microsoft.com/office/word",
48
- val: String(mode)
49
- };
50
- if (idx >= 0) {
51
- compatSettings[idx] = entry;
52
- }
53
- else {
54
- compatSettings.push(entry);
53
+ settings.compatibilityMode = mode;
54
+ // Drop any stale override entry so getCompatibilityMode/writer don't read a
55
+ // conflicting value from the array. The scalar field is now authoritative.
56
+ if (settings.compatSettings) {
57
+ const filtered = settings.compatSettings.filter(s => s.name !== "compatibilityMode");
58
+ if (filtered.length > 0) {
59
+ settings.compatSettings = filtered;
60
+ }
61
+ else {
62
+ delete settings.compatSettings;
63
+ }
55
64
  }
56
- settings.compatSettings = compatSettings;
57
65
  doc.settings = settings;
58
66
  }