@stll/folio-core 0.7.0 → 0.9.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 (193) hide show
  1. package/README.md +39 -0
  2. package/dist/ai-edits/apply.js +18 -4
  3. package/dist/ai-edits/blockRange.d.ts +13 -2
  4. package/dist/ai-edits/blockRange.js +23 -2
  5. package/dist/ai-edits/headless.d.ts +31 -4
  6. package/dist/ai-edits/headless.js +148 -35
  7. package/dist/ai-edits/index.d.ts +6 -4
  8. package/dist/ai-edits/index.js +4 -1
  9. package/dist/ai-edits/scoped-reading.d.ts +9 -0
  10. package/dist/ai-edits/scoped-reading.js +60 -0
  11. package/dist/ai-edits/snapshot.js +53 -7
  12. package/dist/ai-edits/types.d.ts +46 -3
  13. package/dist/compat/eigenpal.d.ts +8 -3
  14. package/dist/compat/eigenpal.js +7 -2
  15. package/dist/controller/fontReadiness.d.ts +29 -0
  16. package/dist/controller/fontReadiness.js +139 -0
  17. package/dist/controller/latestRequestGate.d.ts +13 -0
  18. package/dist/controller/latestRequestGate.js +16 -0
  19. package/dist/controller/layoutPipeline.js +88 -10
  20. package/dist/document-operations.d.ts +10 -3
  21. package/dist/document-operations.js +29 -14
  22. package/dist/docx/blockContentParser.js +53 -10
  23. package/dist/docx/commentParser.js +1 -1
  24. package/dist/docx/compatibility.d.ts +30 -2
  25. package/dist/docx/compatibility.js +118 -39
  26. package/dist/docx/conformance.d.ts +6 -0
  27. package/dist/docx/conformance.js +18 -0
  28. package/dist/docx/encryption/agileDecryption.js +1 -1
  29. package/dist/docx/encryption/encryptionInfo.js +1 -1
  30. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  31. package/dist/docx/ensureParaIds.d.ts +29 -0
  32. package/dist/docx/ensureParaIds.js +423 -0
  33. package/dist/docx/fontTableParser.d.ts +6 -0
  34. package/dist/docx/fontTableParser.js +72 -0
  35. package/dist/docx/groupDrawingParser.js +7 -2
  36. package/dist/docx/headerFooterParser.js +1 -1
  37. package/dist/docx/normalizeBaseDirection.js +1 -1
  38. package/dist/docx/numberingParser.js +2 -0
  39. package/dist/docx/paragraphParser.js +21 -1
  40. package/dist/docx/paragraphTextBoxEnrichment.js +141 -2
  41. package/dist/docx/parser.d.ts +1 -1
  42. package/dist/docx/parser.js +14 -9
  43. package/dist/docx/rezip.js +70 -5
  44. package/dist/docx/runConsolidator.js +4 -0
  45. package/dist/docx/runParser.js +19 -1
  46. package/dist/docx/selectiveSave.js +3 -3
  47. package/dist/docx/selectiveXmlPatch.d.ts +10 -1
  48. package/dist/docx/selectiveXmlPatch.js +1 -1
  49. package/dist/docx/serializer/fontTableSerializer.d.ts +6 -0
  50. package/dist/docx/serializer/fontTableSerializer.js +26 -0
  51. package/dist/docx/serializer/headerFooterSerializer.js +1 -1
  52. package/dist/docx/serializer/paragraphSerializer.js +8 -0
  53. package/dist/docx/serializer/runSerializer.js +14 -2
  54. package/dist/docx/serializer/settingsSerializer.d.ts +6 -0
  55. package/dist/docx/serializer/settingsSerializer.js +16 -0
  56. package/dist/docx/serializer/stylesSerializer.d.ts +6 -0
  57. package/dist/docx/serializer/stylesSerializer.js +56 -0
  58. package/dist/docx/serializer/tableSerializer.js +29 -26
  59. package/dist/docx/serializer/themeSerializer.d.ts +6 -0
  60. package/dist/docx/serializer/themeSerializer.js +36 -0
  61. package/dist/docx/settingsParser.js +46 -0
  62. package/dist/docx/styleParser.js +26 -1
  63. package/dist/docx/tableParser.js +19 -1
  64. package/dist/docx/textBoxParser.js +6 -1
  65. package/dist/docx/vmlImageParser.js +146 -1
  66. package/dist/i18n/messages/catalogs.gen.d.ts +5916 -5916
  67. package/dist/i18n/messages/catalogs.gen.js +5910 -5910
  68. package/dist/index.d.ts +8 -3
  69. package/dist/index.js +7 -2
  70. package/dist/layout-bridge/convert/footnoteLayout.d.ts +3 -0
  71. package/dist/layout-bridge/convert/footnoteLayout.js +3 -1
  72. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +3 -0
  73. package/dist/layout-bridge/convert/headerFooterLayout.js +22 -3
  74. package/dist/layout-bridge/convert/paragraphFrames.d.ts +7 -0
  75. package/dist/layout-bridge/convert/paragraphFrames.js +136 -0
  76. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +14 -2
  77. package/dist/layout-bridge/convert/toFlowBlocks.js +93 -17
  78. package/dist/layout-bridge/engine/hitTest.js +2 -1
  79. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  80. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  81. package/dist/layout-bridge/engine/selectionRects.js +2 -1
  82. package/dist/layout-engine/headerFooterRefs.d.ts +7 -0
  83. package/dist/layout-engine/headerFooterRefs.js +40 -0
  84. package/dist/layout-engine/index.d.ts +3 -2
  85. package/dist/layout-engine/index.js +68 -106
  86. package/dist/layout-engine/measure/cache.js +11 -3
  87. package/dist/layout-engine/measure/font-metrics.worker.js +1 -0
  88. package/dist/layout-engine/measure/index.d.ts +2 -1
  89. package/dist/layout-engine/measure/index.js +2 -1
  90. package/dist/layout-engine/measure/lineBreakProvider.d.ts +33 -0
  91. package/dist/layout-engine/measure/lineBreakProvider.js +252 -0
  92. package/dist/layout-engine/measure/lineBreaks.d.ts +7 -2
  93. package/dist/layout-engine/measure/lineBreaks.js +11 -18
  94. package/dist/layout-engine/measure/listMarkerWidth.d.ts +4 -1
  95. package/dist/layout-engine/measure/listMarkerWidth.js +33 -8
  96. package/dist/layout-engine/measure/measureBlocks.d.ts +4 -2
  97. package/dist/layout-engine/measure/measureBlocks.js +138 -86
  98. package/dist/layout-engine/measure/measureContainer.js +32 -9
  99. package/dist/layout-engine/measure/measureHelpers.js +5 -2
  100. package/dist/layout-engine/measure/measureParagraph.js +346 -74
  101. package/dist/layout-engine/measure/measureTypes.d.ts +2 -1
  102. package/dist/layout-engine/measure/measureWorker.d.ts +2 -2
  103. package/dist/layout-engine/measure/measureWorker.js +8 -19
  104. package/dist/layout-engine/measure/measureWorkerProtocol.d.ts +3 -14
  105. package/dist/layout-engine/measure/tableCellFloating.d.ts +10 -3
  106. package/dist/layout-engine/measure/tableCellFloating.js +37 -29
  107. package/dist/layout-engine/measure/tableCellFlow.d.ts +27 -0
  108. package/dist/layout-engine/measure/tableCellFlow.js +73 -0
  109. package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -0
  110. package/dist/layout-engine/measure/tableCellGrid.js +62 -0
  111. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +18 -0
  112. package/dist/layout-engine/measure/textBoxParagraphLayout.js +32 -0
  113. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +18 -0
  114. package/dist/layout-engine/measure/textMeasurementPolicy.js +24 -0
  115. package/dist/layout-engine/paginator.d.ts +2 -1
  116. package/dist/layout-engine/paginator.js +7 -6
  117. package/dist/layout-engine/paragraphFrame.d.ts +22 -0
  118. package/dist/layout-engine/paragraphFrame.js +17 -0
  119. package/dist/layout-engine/paragraphSequence.d.ts +7 -0
  120. package/dist/layout-engine/paragraphSequence.js +25 -0
  121. package/dist/layout-engine/paragraphSpacing.d.ts +17 -0
  122. package/dist/layout-engine/paragraphSpacing.js +30 -0
  123. package/dist/layout-engine/renderedBreakReconciliation.d.ts +63 -0
  124. package/dist/layout-engine/renderedBreakReconciliation.js +86 -0
  125. package/dist/layout-engine/tableRowBreak.d.ts +5 -2
  126. package/dist/layout-engine/tableRowBreak.js +62 -29
  127. package/dist/layout-engine/types.d.ts +54 -12
  128. package/dist/layout-engine/types.js +15 -2
  129. package/dist/layout-painter/anchoredImagePosition.d.ts +23 -0
  130. package/dist/layout-painter/anchoredImagePosition.js +95 -0
  131. package/dist/layout-painter/borderStroke.d.ts +23 -0
  132. package/dist/layout-painter/borderStroke.js +39 -0
  133. package/dist/layout-painter/renderImage.d.ts +5 -4
  134. package/dist/layout-painter/renderImage.js +18 -4
  135. package/dist/layout-painter/renderPage.d.ts +2 -27
  136. package/dist/layout-painter/renderPage.js +15 -101
  137. package/dist/layout-painter/renderParagraph.js +45 -26
  138. package/dist/layout-painter/renderTable.d.ts +2 -0
  139. package/dist/layout-painter/renderTable.js +257 -47
  140. package/dist/layout-painter/renderTextBox.js +17 -7
  141. package/dist/managers/DocumentLoaderManager.d.ts +1 -1
  142. package/dist/managers/DocumentLoaderManager.js +2 -2
  143. package/dist/paged-layout/sectionBlockWidths.d.ts +3 -0
  144. package/dist/paged-layout/sectionBlockWidths.js +9 -0
  145. package/dist/paged-layout/sectionGeometry.js +1 -1
  146. package/dist/prosemirror/attrs/index.d.ts +4 -2
  147. package/dist/prosemirror/attrs/index.js +32 -2
  148. package/dist/prosemirror/commands/propertyChangeScope.js +1 -1
  149. package/dist/prosemirror/conversion/fromProseDoc.js +35 -18
  150. package/dist/prosemirror/conversion/toProseDoc.js +73 -37
  151. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  152. package/dist/prosemirror/extensions/core/ParagraphExtension.js +8 -2
  153. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +1 -1
  154. package/dist/prosemirror/extensions/features/ListExtension.js +2 -0
  155. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +68 -16
  156. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +1 -0
  157. package/dist/prosemirror/extensions/marks/HighlightExtension.js +1 -1
  158. package/dist/prosemirror/extensions/marks/LanguageExtension.d.ts +7 -0
  159. package/dist/prosemirror/extensions/marks/LanguageExtension.js +30 -0
  160. package/dist/prosemirror/extensions/marks/RunShadingExtension.js +1 -1
  161. package/dist/prosemirror/extensions/marks/TextColorExtension.js +1 -1
  162. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +1 -1
  163. package/dist/prosemirror/extensions/marks/markUtils.js +12 -0
  164. package/dist/prosemirror/extensions/nodes/ImageExtension.js +2 -1
  165. package/dist/prosemirror/extensions/nodes/TableExtension.js +4 -2
  166. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
  167. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  168. package/dist/prosemirror/schema/index.d.ts +2 -2
  169. package/dist/prosemirror/schema/marks.d.ts +6 -1
  170. package/dist/prosemirror/schema/nodes.d.ts +16 -12
  171. package/dist/prosemirror/styles/resolvedStyleAttrs.js +2 -0
  172. package/dist/prosemirror/utils/tabCalculator.d.ts +3 -1
  173. package/dist/prosemirror/utils/tabCalculator.js +12 -10
  174. package/dist/prosemirror/validation.js +4 -1
  175. package/dist/redline.js +13 -3
  176. package/dist/server.d.ts +9 -4
  177. package/dist/server.js +7 -2
  178. package/dist/style-sets/extract.d.ts +35 -0
  179. package/dist/style-sets/extract.js +123 -0
  180. package/dist/style-sets/stellaStyle.d.ts +13 -0
  181. package/dist/style-sets/stellaStyle.js +516 -0
  182. package/dist/style-sets/types.d.ts +31 -0
  183. package/dist/style-sets/types.js +5 -0
  184. package/dist/types/index.d.ts +2 -1
  185. package/dist/utils/createDocument.d.ts +12 -2
  186. package/dist/utils/createDocument.js +55 -36
  187. package/dist/utils/fontResolver.js +6 -6
  188. package/dist/utils/formatToStyle.js +1 -1
  189. package/dist/utils/hexId.d.ts +8 -1
  190. package/dist/utils/hexId.js +11 -3
  191. package/dist/utils/paragraphFormattingMerge.d.ts +14 -3
  192. package/dist/utils/paragraphFormattingMerge.js +14 -4
  193. package/package.json +5 -1
@@ -0,0 +1,423 @@
1
+ import { isXmlNameBoundary } from "./selectiveXmlPatch.js";
2
+ import { deterministicHexId } from "../utils/hexId.js";
3
+ import { TaggedError } from "better-result";
4
+ import JSZip from "jszip";
5
+ //#region src/docx/ensureParaIds.ts
6
+ /**
7
+ * Headless `w14:paraId` normalization for a `.docx` buffer.
8
+ *
9
+ * Word 2010+ stamps every paragraph with a `w14:paraId`, but Google Docs
10
+ * exports, LibreOffice, python-docx, and docx4j generally do not. Paragraphs
11
+ * without one are invisible to everything that anchors on paraId (block ids,
12
+ * comment threads, AI-edit targeting) and fall back to positional `seq-NNNN`
13
+ * ids that renumber after structural edits. Hosts call {@link ensureParaIds}
14
+ * once at ingest so every stored version has full id coverage before any
15
+ * snapshot or external anchor is created.
16
+ *
17
+ * IDs remain stable in folio and identity-preserving DOCX round-trips.
18
+ * Microsoft Word can establish a new `w14:docId` and replace all paragraph
19
+ * IDs on its first save of a non-Word-produced package; callers that ingest
20
+ * such an externally edited version must normalize it again and cannot assume
21
+ * the pre-save IDs still match.
22
+ *
23
+ * The pass patches part XML in place (string splices, no model round-trip),
24
+ * so documents carrying features folio's parser does not model come back
25
+ * with those features byte-identical. Contract:
26
+ *
27
+ * - Parts covered: `word/document.xml`, `word/header*.xml`, `word/footer*.xml`,
28
+ * `word/footnotes.xml`, `word/endnotes.xml`. Paragraphs nested in table
29
+ * cells and in `mc:Choice` text boxes are plain `<w:p>` elements inside
30
+ * those parts and are covered by the same scan. The comments part mints its
31
+ * own deterministic paraIds at save time (see `commentSerializer`) and is
32
+ * left untouched; its ids still count toward uniqueness.
33
+ * - Paragraphs inside `mc:Fallback` are never modified: Word regenerates the
34
+ * fallback branch (duplicating the `mc:Choice` ids) on save, so stamping or
35
+ * deduplicating there would churn on every Word round-trip.
36
+ * - Existing ids are preserved. Only three cases get a fresh id: a missing
37
+ * `paraId`, the reserved all-zero value (Word reads `00000000` as "no id"),
38
+ * and a duplicate of an id already seen earlier in the scan (first
39
+ * occurrence keeps it — the same rule `ParaIdAllocatorExtension` applies in
40
+ * the editor). `w14:textId` is written alongside a newly minted `paraId`
41
+ * (same value) and never touched otherwise; it is a text-revision marker,
42
+ * not identity.
43
+ * - Fresh ids are deterministic (`deterministicHexId` over document content,
44
+ * part path, and paragraph ordinal), so the pass is a pure function of the
45
+ * input bytes: retrying an ingest produces identical output.
46
+ * - Each patched part's root element gets `xmlns:w14` / `xmlns:mc`
47
+ * declarations and a `mc:Ignorable` listing `w14` when missing — non-Word
48
+ * producers declare neither, and absent `mc:Ignorable` handling is what
49
+ * makes pre-2010 consumers choke on the new attributes.
50
+ * - Idempotent: a document that already has full coverage is returned as the
51
+ * original bytes, untouched (`alreadyComplete: true`).
52
+ * - Digitally signed packages are returned untouched when already complete.
53
+ * When normalization would rewrite the package, it fails unless the caller
54
+ * explicitly allows signature invalidation after warning the user.
55
+ */
56
+ /** A malformed or unsupported package prevented paragraph-ID normalization. */
57
+ var EnsureParaIdsError = class extends TaggedError("EnsureParaIdsError")() {};
58
+ const W14_NAMESPACE_URI = "http://schemas.microsoft.com/office/word/2010/wordml";
59
+ const MC_NAMESPACE_URI = "http://schemas.openxmlformats.org/markup-compatibility/2006";
60
+ const MC_IGNORABLE_W14 = "w14";
61
+ const DIGITAL_SIGNATURE_PART_PREFIX = "_xmlsignatures/";
62
+ /** Word reads an all-zero `w14:paraId` as "no id assigned". */
63
+ const RESERVED_ZERO_ID_PATTERN = /^0*$/u;
64
+ const DOCUMENT_PART = "word/document.xml";
65
+ const TARGET_PART_PATTERNS = [
66
+ /^word\/document\.xml$/u,
67
+ /^word\/header\d*\.xml$/u,
68
+ /^word\/footer\d*\.xml$/u,
69
+ /^word\/footnotes\.xml$/u,
70
+ /^word\/endnotes\.xml$/u
71
+ ];
72
+ const PARAGRAPH_OPEN = "<w:p";
73
+ const FALLBACK_OPEN = "<mc:Fallback";
74
+ const FALLBACK_CLOSE = "</mc:Fallback>";
75
+ const OPAQUE_XML_REGIONS = [
76
+ {
77
+ open: "<!--",
78
+ close: "-->"
79
+ },
80
+ {
81
+ open: "<![CDATA[",
82
+ close: "]]>"
83
+ },
84
+ {
85
+ open: "<?",
86
+ close: "?>"
87
+ }
88
+ ];
89
+ /**
90
+ * The parser accepts the `w:` fallback prefix for paraId, and comment parts
91
+ * link replies via `w15:paraId` — all three count toward uniqueness.
92
+ */
93
+ const ANY_PARA_ID_PATTERN = /\bw(?:14|15)?:paraId=(?<quote>["'])(?<id>[\s\S]*?)\k<quote>/gu;
94
+ const OPEN_TAG_PARA_ID_PATTERN = /\sw(?:14)?:paraId=(?<quote>["'])(?<id>[\s\S]*?)\k<quote>/u;
95
+ const OPEN_TAG_TEXT_ID_PATTERN = /\sw(?:14)?:textId=(?<quote>["'])(?<id>[\s\S]*?)\k<quote>/u;
96
+ const XMLNS_W14_PATTERN = /\sxmlns:w14=(?<quote>["'])(?<value>[\s\S]*?)\k<quote>/u;
97
+ const XMLNS_MC_PATTERN = /\sxmlns:mc=(?<quote>["'])(?<value>[\s\S]*?)\k<quote>/u;
98
+ const MC_IGNORABLE_PATTERN = /\smc:Ignorable=(?<quote>["'])(?<value>[\s\S]*?)\k<quote>/u;
99
+ const applySplices = (xml, edits) => {
100
+ const ordered = [...edits].sort((a, b) => b.start - a.start);
101
+ let result = xml;
102
+ for (const { start, end, text } of ordered) result = result.slice(0, start) + text + result.slice(end);
103
+ return result;
104
+ };
105
+ const createEnsureParaIdsError = (message, cause) => new EnsureParaIdsError({
106
+ message,
107
+ ...cause === void 0 ? {} : { cause }
108
+ });
109
+ const skipOpaqueXmlRegion = (xml, start, partPath) => {
110
+ for (const { open, close } of OPAQUE_XML_REGIONS) {
111
+ if (!xml.startsWith(open, start)) continue;
112
+ const closeStart = xml.indexOf(close, start + open.length);
113
+ if (closeStart === -1) throw createEnsureParaIdsError(`Unterminated ${open} region in ${partPath}`);
114
+ return closeStart + close.length;
115
+ }
116
+ return null;
117
+ };
118
+ const attributeValueRange = (match, absoluteOffset, group) => {
119
+ const quote = match.groups["quote"];
120
+ const value = match.groups[group];
121
+ const start = absoluteOffset + match.index + match[0].indexOf(quote) + 1;
122
+ return {
123
+ start,
124
+ end: start + value.length
125
+ };
126
+ };
127
+ const collectExistingParaIds = (xml, into) => {
128
+ for (const match of xml.matchAll(ANY_PARA_ID_PATTERN)) {
129
+ const id = match.groups["id"];
130
+ if (id.length > 0) into.add(id.toUpperCase());
131
+ }
132
+ };
133
+ const collectFallbackParaIds = (xml, partPath, into) => {
134
+ let fallbackDepth = 0;
135
+ let pos = 0;
136
+ while (pos < xml.length) {
137
+ const tagStart = xml.indexOf("<", pos);
138
+ if (tagStart === -1) return;
139
+ const opaqueEnd = skipOpaqueXmlRegion(xml, tagStart, partPath);
140
+ if (opaqueEnd !== null) {
141
+ pos = opaqueEnd;
142
+ continue;
143
+ }
144
+ if (xml.startsWith(FALLBACK_OPEN, tagStart) && isXmlNameBoundary(xml[tagStart + 12])) {
145
+ const tagEnd = xml.indexOf(">", tagStart);
146
+ if (tagEnd === -1) throw createEnsureParaIdsError(`Unterminated <mc:Fallback> tag in ${partPath}`);
147
+ if (xml[tagEnd - 1] !== "/") fallbackDepth += 1;
148
+ pos = tagEnd + 1;
149
+ continue;
150
+ }
151
+ if (xml.startsWith(FALLBACK_CLOSE, tagStart)) {
152
+ fallbackDepth = Math.max(0, fallbackDepth - 1);
153
+ pos = tagStart + 14;
154
+ continue;
155
+ }
156
+ if (fallbackDepth === 0 || !xml.startsWith(PARAGRAPH_OPEN, tagStart) || !isXmlNameBoundary(xml[tagStart + 4])) {
157
+ pos = tagStart + 1;
158
+ continue;
159
+ }
160
+ const tagEnd = xml.indexOf(">", tagStart);
161
+ if (tagEnd === -1) throw createEnsureParaIdsError(`Unterminated <w:p> tag in ${partPath}`);
162
+ const id = OPEN_TAG_PARA_ID_PATTERN.exec(xml.slice(tagStart, tagEnd + 1))?.groups?.["id"];
163
+ if (id) into.add(id.toUpperCase());
164
+ pos = tagEnd + 1;
165
+ }
166
+ };
167
+ /**
168
+ * Deterministic fresh id for the paragraph at `ordinal` in `partPath`,
169
+ * salted past collisions with any id anywhere in the document.
170
+ */
171
+ const mintParaId = (context, partPath, ordinal) => {
172
+ const seed = `${context.docKey}:${partPath}:${ordinal}`;
173
+ let id = deterministicHexId(seed);
174
+ for (let salt = 1; context.taken.has(id); salt += 1) id = deterministicHexId(`${seed}:${salt}`);
175
+ context.taken.add(id);
176
+ return id;
177
+ };
178
+ /**
179
+ * One forward scan over a part: every `<w:p>` open tag outside `mc:Fallback`
180
+ * either keeps its paraId (valid, first occurrence) or gets a splice edit
181
+ * minting / replacing one. `seen` spans parts so the first-occurrence rule is
182
+ * document-wide across the scan order.
183
+ */
184
+ const scanPart = (xml, partPath, context, seen) => {
185
+ const edits = [];
186
+ let assigned = 0;
187
+ let deduplicated = 0;
188
+ let ordinal = 0;
189
+ let fallbackDepth = 0;
190
+ let pos = 0;
191
+ while (pos < xml.length) {
192
+ const tagStart = xml.indexOf("<", pos);
193
+ if (tagStart === -1) break;
194
+ const opaqueEnd = skipOpaqueXmlRegion(xml, tagStart, partPath);
195
+ if (opaqueEnd !== null) {
196
+ pos = opaqueEnd;
197
+ continue;
198
+ }
199
+ if (xml.startsWith(FALLBACK_OPEN, tagStart) && isXmlNameBoundary(xml[tagStart + 12])) {
200
+ const tagEnd = xml.indexOf(">", tagStart);
201
+ if (tagEnd === -1) throw createEnsureParaIdsError(`Unterminated <mc:Fallback> tag in ${partPath}`);
202
+ if (xml[tagEnd - 1] !== "/") fallbackDepth += 1;
203
+ pos = tagEnd + 1;
204
+ continue;
205
+ }
206
+ if (xml.startsWith(FALLBACK_CLOSE, tagStart)) {
207
+ fallbackDepth = Math.max(0, fallbackDepth - 1);
208
+ pos = tagStart + 14;
209
+ continue;
210
+ }
211
+ if (!xml.startsWith(PARAGRAPH_OPEN, tagStart) || !isXmlNameBoundary(xml[tagStart + 4])) {
212
+ pos = tagStart + 1;
213
+ continue;
214
+ }
215
+ const tagEnd = xml.indexOf(">", tagStart);
216
+ if (tagEnd === -1) throw createEnsureParaIdsError(`Unterminated <w:p> tag in ${partPath}`);
217
+ pos = tagEnd + 1;
218
+ if (fallbackDepth > 0) continue;
219
+ ordinal += 1;
220
+ const openTag = xml.slice(tagStart, tagEnd + 1);
221
+ const paraIdMatch = OPEN_TAG_PARA_ID_PATTERN.exec(openTag);
222
+ if (!paraIdMatch) {
223
+ const id = mintParaId(context, partPath, ordinal);
224
+ const insertAt = tagStart + 4;
225
+ const textIdMatch = OPEN_TAG_TEXT_ID_PATTERN.exec(openTag);
226
+ if (textIdMatch) {
227
+ edits.push({
228
+ start: insertAt,
229
+ end: insertAt,
230
+ text: ` w14:paraId="${id}"`
231
+ });
232
+ const range = attributeValueRange(textIdMatch, tagStart, "id");
233
+ edits.push({
234
+ ...range,
235
+ text: id
236
+ });
237
+ } else edits.push({
238
+ start: insertAt,
239
+ end: insertAt,
240
+ text: ` w14:paraId="${id}" w14:textId="${id}"`
241
+ });
242
+ assigned += 1;
243
+ seen.add(id);
244
+ continue;
245
+ }
246
+ const value = paraIdMatch.groups["id"].toUpperCase();
247
+ const unassigned = RESERVED_ZERO_ID_PATTERN.test(value);
248
+ if (!unassigned && !seen.has(value)) {
249
+ seen.add(value);
250
+ continue;
251
+ }
252
+ const id = mintParaId(context, partPath, ordinal);
253
+ edits.push({
254
+ ...attributeValueRange(paraIdMatch, tagStart, "id"),
255
+ text: id
256
+ });
257
+ const textIdMatch = OPEN_TAG_TEXT_ID_PATTERN.exec(openTag);
258
+ if (textIdMatch) edits.push({
259
+ ...attributeValueRange(textIdMatch, tagStart, "id"),
260
+ text: id
261
+ });
262
+ else {
263
+ const insertAt = tagStart + 4;
264
+ edits.push({
265
+ start: insertAt,
266
+ end: insertAt,
267
+ text: ` w14:textId="${id}"`
268
+ });
269
+ }
270
+ if (unassigned) assigned += 1;
271
+ else deduplicated += 1;
272
+ seen.add(id);
273
+ }
274
+ return {
275
+ edits,
276
+ assigned,
277
+ deduplicated
278
+ };
279
+ };
280
+ /**
281
+ * Splice edits ensuring the part's root element declares `xmlns:w14` /
282
+ * `xmlns:mc` and lists `w14` in `mc:Ignorable`, so consumers that predate the
283
+ * 2010 extensions skip the new attributes instead of rejecting the part.
284
+ */
285
+ const ensureRootNamespaces = (xml, partPath) => {
286
+ let pos = 0;
287
+ let rootStart = -1;
288
+ while (pos < xml.length) {
289
+ const lt = xml.indexOf("<", pos);
290
+ if (lt === -1) break;
291
+ const opaqueEnd = skipOpaqueXmlRegion(xml, lt, partPath);
292
+ if (opaqueEnd !== null) {
293
+ pos = opaqueEnd;
294
+ continue;
295
+ }
296
+ if (xml[lt + 1] === "!") {
297
+ const skipTo = xml.indexOf(">", lt);
298
+ if (skipTo === -1) throw createEnsureParaIdsError(`Unterminated prolog in ${partPath}`);
299
+ pos = skipTo + 1;
300
+ continue;
301
+ }
302
+ rootStart = lt;
303
+ break;
304
+ }
305
+ if (rootStart === -1) throw createEnsureParaIdsError(`No root element in ${partPath}`);
306
+ const rootEnd = xml.indexOf(">", rootStart);
307
+ if (rootEnd === -1) throw createEnsureParaIdsError(`Unterminated root element in ${partPath}`);
308
+ const rootTag = xml.slice(rootStart, rootEnd + 1);
309
+ const edits = [];
310
+ const declarations = [];
311
+ const mcNamespace = XMLNS_MC_PATTERN.exec(rootTag);
312
+ if (mcNamespace) {
313
+ if (mcNamespace.groups["value"] !== MC_NAMESPACE_URI) throw createEnsureParaIdsError(`xmlns:mc has an unexpected namespace URI in ${partPath}`);
314
+ } else declarations.push(` xmlns:mc="${MC_NAMESPACE_URI}"`);
315
+ const w14Namespace = XMLNS_W14_PATTERN.exec(rootTag);
316
+ if (w14Namespace) {
317
+ if (w14Namespace.groups["value"] !== W14_NAMESPACE_URI) throw createEnsureParaIdsError(`xmlns:w14 has an unexpected namespace URI in ${partPath}`);
318
+ } else declarations.push(` xmlns:w14="${W14_NAMESPACE_URI}"`);
319
+ const ignorable = MC_IGNORABLE_PATTERN.exec(rootTag);
320
+ if (ignorable) {
321
+ const tokens = ignorable.groups["value"].split(/\s+/u).filter((token) => token.length > 0);
322
+ if (!tokens.includes(MC_IGNORABLE_W14)) {
323
+ const { end } = attributeValueRange(ignorable, rootStart, "value");
324
+ const text = tokens.length === 0 ? MC_IGNORABLE_W14 : ` ${MC_IGNORABLE_W14}`;
325
+ edits.push({
326
+ start: end,
327
+ end,
328
+ text
329
+ });
330
+ }
331
+ } else declarations.push(` mc:Ignorable="${MC_IGNORABLE_W14}"`);
332
+ if (declarations.length > 0) {
333
+ let nameEnd = rootStart + 1;
334
+ while (nameEnd < xml.length && !isXmlNameBoundary(xml[nameEnd])) nameEnd += 1;
335
+ edits.push({
336
+ start: nameEnd,
337
+ end: nameEnd,
338
+ text: declarations.join("")
339
+ });
340
+ }
341
+ return edits;
342
+ };
343
+ /** OPC part names are case-insensitive; compare lowercased. */
344
+ const isTargetPart = (path) => TARGET_PART_PATTERNS.some((pattern) => pattern.test(path.toLowerCase()));
345
+ const toUint8Array = (docx) => docx instanceof Uint8Array ? docx : new Uint8Array(docx);
346
+ const hasDigitalSignatureParts = (zip) => Object.values(zip.files).some(({ dir, name }) => !dir && name.toLowerCase().startsWith(DIGITAL_SIGNATURE_PART_PREFIX));
347
+ /**
348
+ * Backfill `w14:paraId` on every paragraph of a `.docx` buffer. See the
349
+ * module doc for the exact contract. Throws {@link EnsureParaIdsError} when
350
+ * the buffer is not a WordprocessingML package or a part is malformed.
351
+ */
352
+ const ensureParaIdsInternal = async (docx, options) => {
353
+ const zip = await JSZip.loadAsync(docx);
354
+ const xmlPartNames = [];
355
+ zip.forEach((relativePath, entry) => {
356
+ const lower = relativePath.toLowerCase();
357
+ if (!entry.dir && lower.startsWith("word/") && lower.endsWith(".xml")) xmlPartNames.push(relativePath);
358
+ });
359
+ const documentPartName = xmlPartNames.find((name) => name.toLowerCase() === DOCUMENT_PART);
360
+ if (documentPartName === void 0) throw createEnsureParaIdsError("word/document.xml not found: not a WordprocessingML package");
361
+ const partTexts = /* @__PURE__ */ new Map();
362
+ for (const name of xmlPartNames) {
363
+ const entry = zip.file(name);
364
+ if (entry) partTexts.set(name, await entry.async("text"));
365
+ }
366
+ const taken = /* @__PURE__ */ new Set();
367
+ for (const text of partTexts.values()) collectExistingParaIds(text, taken);
368
+ const context = {
369
+ taken,
370
+ docKey: deterministicHexId(partTexts.get(documentPartName))
371
+ };
372
+ const targetParts = [documentPartName, ...xmlPartNames.filter((name) => name !== documentPartName && isTargetPart(name)).sort((a, b) => a.localeCompare(b))];
373
+ const seen = /* @__PURE__ */ new Set();
374
+ for (const [partPath, xml] of partTexts) if (isTargetPart(partPath)) collectFallbackParaIds(xml, partPath, seen);
375
+ else collectExistingParaIds(xml, seen);
376
+ const updates = /* @__PURE__ */ new Map();
377
+ let assigned = 0;
378
+ let deduplicated = 0;
379
+ for (const partPath of targetParts) {
380
+ const xml = partTexts.get(partPath);
381
+ const scan = scanPart(xml, partPath, context, seen);
382
+ if (scan.edits.length === 0) continue;
383
+ assigned += scan.assigned;
384
+ deduplicated += scan.deduplicated;
385
+ updates.set(partPath, applySplices(xml, [...scan.edits, ...ensureRootNamespaces(xml, partPath)]));
386
+ }
387
+ if (updates.size === 0) return {
388
+ docx: toUint8Array(docx),
389
+ assigned: 0,
390
+ deduplicated: 0,
391
+ alreadyComplete: true
392
+ };
393
+ if (hasDigitalSignatureParts(zip) && options.allowSignedPackageMutation !== true) throw createEnsureParaIdsError("Refusing to normalize a digitally signed package because rewriting OOXML invalidates its signatures. Warn the user and pass allowSignedPackageMutation only if invalidation is acceptable.");
394
+ for (const [partPath, content] of updates) zip.file(partPath, content, {
395
+ compression: "DEFLATE",
396
+ compressionOptions: { level: 6 }
397
+ });
398
+ return {
399
+ docx: await zip.generateAsync({
400
+ type: "uint8array",
401
+ compression: "DEFLATE",
402
+ compressionOptions: { level: 6 }
403
+ }),
404
+ assigned,
405
+ deduplicated,
406
+ alreadyComplete: false
407
+ };
408
+ };
409
+ /**
410
+ * Backfill `w14:paraId` on every paragraph of a `.docx` buffer. All package,
411
+ * XML, and compression failures are surfaced as {@link EnsureParaIdsError}.
412
+ */
413
+ const ensureParaIds = async (docx, options = {}) => {
414
+ try {
415
+ return await ensureParaIdsInternal(docx, options);
416
+ } catch (error) {
417
+ if (error instanceof EnsureParaIdsError) throw error;
418
+ const message = error instanceof Error ? error.message : String(error);
419
+ throw createEnsureParaIdsError(`Failed to normalize paragraph IDs: ${message}`, error);
420
+ }
421
+ };
422
+ //#endregion
423
+ export { EnsureParaIdsError, ensureParaIds };
@@ -0,0 +1,6 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+
3
+ //#region src/docx/fontTableParser.d.ts
4
+ declare const parseFontTable: (xml: string | null | undefined) => document_d_exports.FontTable | undefined;
5
+ //#endregion
6
+ export { parseFontTable };
@@ -0,0 +1,72 @@
1
+ import { findChild, findChildren, getAttribute, parseXmlDocument } from "./xmlParser.js";
2
+ //#region src/docx/fontTableParser.ts
3
+ const FONT_FAMILIES = [
4
+ "decorative",
5
+ "modern",
6
+ "roman",
7
+ "script",
8
+ "swiss",
9
+ "auto"
10
+ ];
11
+ const FONT_PITCHES = [
12
+ "default",
13
+ "fixed",
14
+ "variable"
15
+ ];
16
+ const parseFontTable = (xml) => {
17
+ if (!xml) return;
18
+ const root = parseXmlDocument(xml);
19
+ if (!root) return;
20
+ const fonts = [];
21
+ for (const element of findChildren(root, "w", "font")) {
22
+ const font = parseFont(element);
23
+ if (font) fonts.push(font);
24
+ }
25
+ return fonts.length > 0 ? { fonts } : void 0;
26
+ };
27
+ const parseFont = (element) => {
28
+ const name = getAttribute(element, "w", "name");
29
+ if (!name) return;
30
+ const font = { name };
31
+ assignValue(font, "altName", childValue(element, "altName"));
32
+ assignValue(font, "panose1", childValue(element, "panose1"));
33
+ assignValue(font, "charset", childValue(element, "charset"));
34
+ const family = childValue(element, "family");
35
+ if (family && isFontFamily(family)) font.family = family;
36
+ const pitch = childValue(element, "pitch");
37
+ if (pitch && isFontPitch(pitch)) font.pitch = pitch;
38
+ const sig = findChild(element, "w", "sig");
39
+ if (sig) {
40
+ const parsed = {};
41
+ assignSignatureValue(parsed, "usb0", getAttribute(sig, "w", "usb0") ?? void 0);
42
+ assignSignatureValue(parsed, "usb1", getAttribute(sig, "w", "usb1") ?? void 0);
43
+ assignSignatureValue(parsed, "usb2", getAttribute(sig, "w", "usb2") ?? void 0);
44
+ assignSignatureValue(parsed, "usb3", getAttribute(sig, "w", "usb3") ?? void 0);
45
+ assignSignatureValue(parsed, "csb0", getAttribute(sig, "w", "csb0") ?? void 0);
46
+ assignSignatureValue(parsed, "csb1", getAttribute(sig, "w", "csb1") ?? void 0);
47
+ if (Object.values(parsed).some((value) => value !== void 0)) font.sig = parsed;
48
+ }
49
+ assignValue(font, "embedRegular", relationshipId(element, "embedRegular"));
50
+ assignValue(font, "embedBold", relationshipId(element, "embedBold"));
51
+ assignValue(font, "embedItalic", relationshipId(element, "embedItalic"));
52
+ assignValue(font, "embedBoldItalic", relationshipId(element, "embedBoldItalic"));
53
+ return font;
54
+ };
55
+ const childValue = (element, name) => {
56
+ const child = findChild(element, "w", name);
57
+ return child ? getAttribute(child, "w", "val") ?? void 0 : void 0;
58
+ };
59
+ const relationshipId = (element, name) => {
60
+ const child = findChild(element, "w", name);
61
+ return child ? getAttribute(child, "r", "id") ?? void 0 : void 0;
62
+ };
63
+ const assignValue = (font, key, value) => {
64
+ if (value !== void 0) font[key] = value;
65
+ };
66
+ const assignSignatureValue = (signature, key, value) => {
67
+ if (value !== void 0) signature[key] = value;
68
+ };
69
+ const isFontFamily = (value) => FONT_FAMILIES.some((family) => family === value);
70
+ const isFontPitch = (value) => FONT_PITCHES.some((pitch) => pitch === value);
71
+ //#endregion
72
+ export { parseFontTable };
@@ -37,7 +37,10 @@ const colorFrom = (parent, fallback) => {
37
37
  const value = getAttribute(parent, "w", "val");
38
38
  if (value && HEX_COLOR.test(value)) return value.toUpperCase();
39
39
  }
40
- const value = getAttribute(findChildByLocalName(findChildByLocalName(parent, "solidFill"), "srgbClr"), null, "val");
40
+ if (findChildByLocalName(parent, "noFill")) return "none";
41
+ const srgb = findChildByLocalName(findChildByLocalName(parent, "solidFill"), "srgbClr");
42
+ const referencedSrgb = findChildByLocalName(parent, "srgbClr");
43
+ const value = getAttribute(srgb ?? referencedSrgb, null, "val");
41
44
  if (value && HEX_COLOR.test(value)) return value.toUpperCase();
42
45
  return fallback;
43
46
  };
@@ -80,9 +83,11 @@ const renderGeometry = (wsp) => {
80
83
  const { x, y, width, height } = childTransform(wsp);
81
84
  if (width <= 0 || height <= 0) return "";
82
85
  const spPr = findChildByLocalName(wsp, "spPr");
86
+ const style = findChildByLocalName(wsp, "style");
87
+ const styleStroke = colorFrom(findChildByLocalName(style, "lnRef"), "none");
83
88
  const fill = colorFrom(spPr, "none");
84
89
  const line = findChildByLocalName(spPr, "ln");
85
- const stroke = colorFrom(line, "none");
90
+ const stroke = colorFrom(line, styleStroke);
86
91
  const strokeWidth = line ? parseNumericAttribute(line, null, "w") ?? DEFAULT_LINE_WIDTH_EMU : 0;
87
92
  const paths = findAllDeep(findChildByLocalName(spPr, "custGeom"), "a", "path");
88
93
  if (paths.length === 0) return `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${fill === "none" ? "none" : `#${fill}`}" stroke="${stroke === "none" ? "none" : `#${stroke}`}" stroke-width="${strokeWidth}"/>`;
@@ -1,8 +1,8 @@
1
1
  import { collectXmlnsDeclarations, parseXml } from "./xmlParser.js";
2
2
  import { parseWatermark } from "./watermarkParser.js";
3
3
  import { parseFooterReference, parseFooterReferences, parseHeaderReference, parseHeaderReferences } from "./headerFooterRefParser.js";
4
- import { assignHeaderFooterVerbatimXml } from "./headerFooterVerbatim.js";
5
4
  import { parseBlockContent } from "./blockContentParser.js";
5
+ import { assignHeaderFooterVerbatimXml } from "./headerFooterVerbatim.js";
6
6
  //#region src/docx/headerFooterParser.ts
7
7
  /**
8
8
  * Parse a header XML file (word/header*.xml)
@@ -1,5 +1,5 @@
1
- import { detectBaseDirection } from "../utils/baseDirection.js";
2
1
  import { createStyleResolver } from "../prosemirror/styles/styleResolver.js";
2
+ import { detectBaseDirection } from "../utils/baseDirection.js";
3
3
  //#region src/docx/normalizeBaseDirection.ts
4
4
  const runText = (run) => {
5
5
  let text = "";
@@ -603,7 +603,9 @@ function computeListRendering(numPr, numbering) {
603
603
  const markerFont = level.rPr?.fontFamily?.ascii || level.rPr?.fontFamily?.hAnsi;
604
604
  if (markerFont) rendering.markerFontFamily = markerFont;
605
605
  if (level.rPr?.fontSize) rendering.markerFontSize = level.rPr.fontSize / 2;
606
+ if (level.rPr?.bold !== void 0) rendering.markerBold = level.rPr.bold;
606
607
  if (level.rPr?.allCaps) rendering.markerAllCaps = true;
608
+ if (level.lvlJc) rendering.markerAlignment = level.lvlJc;
607
609
  if (level.suffix) rendering.markerSuffix = level.suffix;
608
610
  if (instance?.abstractNumId !== void 0) rendering.abstractNumId = instance.abstractNumId;
609
611
  if (overrideForLevel?.startOverride !== void 0) rendering.startOverride = overrideForLevel.startOverride;
@@ -110,10 +110,18 @@ function parseTabStops(tabs) {
110
110
  function parseFrameProperties(framePr) {
111
111
  if (!framePr) return;
112
112
  const frame = {};
113
+ const dropCap = getAttribute(framePr, "w", "dropCap");
114
+ if (dropCap === "none" || dropCap === "drop" || dropCap === "margin") frame.dropCap = dropCap;
115
+ const lines = parseNumericAttribute(framePr, "w", "lines");
116
+ if (lines !== void 0) frame.lines = lines;
113
117
  const w = parseNumericAttribute(framePr, "w", "w");
114
118
  if (w !== void 0) frame.width = w;
115
119
  const h = parseNumericAttribute(framePr, "w", "h");
116
120
  if (h !== void 0) frame.height = h;
121
+ const hSpace = parseNumericAttribute(framePr, "w", "hSpace");
122
+ if (hSpace !== void 0) frame.hSpace = hSpace;
123
+ const vSpace = parseNumericAttribute(framePr, "w", "vSpace");
124
+ if (vSpace !== void 0) frame.vSpace = vSpace;
117
125
  const hAnchor = getAttribute(framePr, "w", "hAnchor");
118
126
  if (hAnchor === "text" || hAnchor === "margin" || hAnchor === "page") frame.hAnchor = hAnchor;
119
127
  const vAnchor = getAttribute(framePr, "w", "vAnchor");
@@ -156,12 +164,18 @@ function collectFirstParagraphPropertyChildren(pPr) {
156
164
  case "keepNext":
157
165
  children.keepNext ??= child;
158
166
  break;
167
+ case "kinsoku":
168
+ children.kinsoku ??= child;
169
+ break;
159
170
  case "numPr":
160
171
  children.numPr ??= child;
161
172
  break;
162
173
  case "outlineLvl":
163
174
  children.outlineLvl ??= child;
164
175
  break;
176
+ case "overflowPunct":
177
+ children.overflowPunct ??= child;
178
+ break;
165
179
  case "pageBreakBefore":
166
180
  children.pageBreakBefore ??= child;
167
181
  break;
@@ -218,6 +232,10 @@ function parseParagraphProperties(pPr, theme, styles) {
218
232
  if (!pPr) return;
219
233
  const formatting = {};
220
234
  const propertyChildren = collectFirstParagraphPropertyChildren(pPr);
235
+ const kinsoku = propertyChildren.kinsoku;
236
+ if (kinsoku) formatting.kinsoku = parseBooleanElement(kinsoku);
237
+ const overflowPunct = propertyChildren.overflowPunct;
238
+ if (overflowPunct) formatting.overflowPunctuation = parseBooleanElement(overflowPunct);
221
239
  const jc = propertyChildren.jc;
222
240
  if (jc) {
223
241
  const val = narrowEnum(getAttribute(jc, "w", "val"), ParagraphAlignmentSchema);
@@ -517,7 +535,7 @@ function isTrackedChangeWrapperChild(content) {
517
535
  return content.type === "run" || content.type === "hyperlink";
518
536
  }
519
537
  function isInlineSdtContent(content) {
520
- return content.type === "run" || content.type === "hyperlink" || content.type === "simpleField" || content.type === "complexField" || content.type === "inlineSdt" || content.type === "mathEquation";
538
+ return content.type === "run" || content.type === "hyperlink" || content.type === "simpleField" || content.type === "complexField" || content.type === "inlineSdt" || content.type === "insertion" || content.type === "deletion" || content.type === "mathEquation";
521
539
  }
522
540
  function pushTrackedChangeWrapper({ contents, type, info, content, preserveEmpty = false }) {
523
541
  if (content.length === 0 && !preserveEmpty) return;
@@ -973,7 +991,9 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
973
991
  const markerFont = level.rPr?.fontFamily?.ascii || level.rPr?.fontFamily?.hAnsi;
974
992
  if (markerFont) listRendering.markerFontFamily = markerFont;
975
993
  if (level.rPr?.fontSize) listRendering.markerFontSize = level.rPr.fontSize / 2;
994
+ if (level.rPr?.bold !== void 0) listRendering.markerBold = level.rPr.bold;
976
995
  if (level.rPr?.allCaps) listRendering.markerAllCaps = true;
996
+ if (level.lvlJc) listRendering.markerAlignment = level.lvlJc;
977
997
  if (level.suffix) listRendering.markerSuffix = level.suffix;
978
998
  let implicitChildLevelAdvances = 0;
979
999
  const foldedMarkerSuffix = [];