@beyondwork/docx-react-component 1.0.1 → 1.0.3

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 (172) hide show
  1. package/README.md +44 -104
  2. package/package.json +50 -30
  3. package/src/README.md +85 -0
  4. package/src/api/README.md +22 -0
  5. package/src/api/public-types.ts +525 -0
  6. package/src/compare/diff-engine.ts +530 -0
  7. package/src/compare/export-redlines.ts +162 -0
  8. package/src/compare/snapshot.ts +37 -0
  9. package/src/component-inventory.md +99 -0
  10. package/src/core/README.md +10 -0
  11. package/src/core/commands/README.md +3 -0
  12. package/src/core/commands/formatting-commands.ts +161 -0
  13. package/src/core/commands/image-commands.ts +144 -0
  14. package/src/core/commands/index.ts +1013 -0
  15. package/src/core/commands/list-commands.ts +370 -0
  16. package/src/core/commands/review-commands.ts +108 -0
  17. package/src/core/commands/text-commands.ts +119 -0
  18. package/src/core/schema/README.md +3 -0
  19. package/src/core/schema/text-schema.ts +512 -0
  20. package/src/core/selection/README.md +3 -0
  21. package/src/core/selection/mapping.ts +238 -0
  22. package/src/core/selection/review-anchors.ts +94 -0
  23. package/src/core/state/README.md +3 -0
  24. package/src/core/state/editor-state.ts +580 -0
  25. package/src/core/state/text-transaction.ts +276 -0
  26. package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
  27. package/src/formats/xlsx/io/parse-sheet.ts +289 -0
  28. package/src/formats/xlsx/io/parse-styles.ts +57 -0
  29. package/src/formats/xlsx/io/parse-workbook.ts +75 -0
  30. package/src/formats/xlsx/io/xlsx-session.ts +306 -0
  31. package/src/formats/xlsx/model/cell.ts +189 -0
  32. package/src/formats/xlsx/model/sheet.ts +244 -0
  33. package/src/formats/xlsx/model/styles.ts +118 -0
  34. package/src/formats/xlsx/model/workbook.ts +449 -0
  35. package/src/index.ts +45 -0
  36. package/src/io/README.md +10 -0
  37. package/src/io/docx-session.ts +1763 -0
  38. package/src/io/export/README.md +3 -0
  39. package/src/io/export/export-session.ts +165 -0
  40. package/src/io/export/minimal-docx.ts +115 -0
  41. package/src/io/export/reattach-preserved-parts.ts +54 -0
  42. package/src/io/export/serialize-comments.ts +876 -0
  43. package/src/io/export/serialize-footnotes.ts +217 -0
  44. package/src/io/export/serialize-headers-footers.ts +200 -0
  45. package/src/io/export/serialize-main-document.ts +982 -0
  46. package/src/io/export/serialize-numbering.ts +97 -0
  47. package/src/io/export/serialize-revisions.ts +389 -0
  48. package/src/io/export/serialize-runtime-revisions.ts +265 -0
  49. package/src/io/export/serialize-tables.ts +147 -0
  50. package/src/io/export/split-review-boundaries.ts +194 -0
  51. package/src/io/normalize/README.md +3 -0
  52. package/src/io/normalize/normalize-text.ts +437 -0
  53. package/src/io/ooxml/README.md +3 -0
  54. package/src/io/ooxml/parse-comments.ts +779 -0
  55. package/src/io/ooxml/parse-complex-content.ts +287 -0
  56. package/src/io/ooxml/parse-fields.ts +438 -0
  57. package/src/io/ooxml/parse-footnotes.ts +403 -0
  58. package/src/io/ooxml/parse-headers-footers.ts +483 -0
  59. package/src/io/ooxml/parse-inline-media.ts +431 -0
  60. package/src/io/ooxml/parse-main-document.ts +1846 -0
  61. package/src/io/ooxml/parse-numbering.ts +425 -0
  62. package/src/io/ooxml/parse-revisions.ts +658 -0
  63. package/src/io/ooxml/parse-shapes.ts +271 -0
  64. package/src/io/ooxml/parse-tables.ts +568 -0
  65. package/src/io/ooxml/parse-theme.ts +314 -0
  66. package/src/io/ooxml/part-manifest.ts +136 -0
  67. package/src/io/ooxml/revision-boundaries.ts +351 -0
  68. package/src/io/opc/README.md +3 -0
  69. package/src/io/opc/corrupt-package.ts +166 -0
  70. package/src/io/opc/docx-package.ts +74 -0
  71. package/src/io/opc/package-reader.ts +325 -0
  72. package/src/io/opc/package-writer.ts +273 -0
  73. package/src/legal/bookmarks.ts +196 -0
  74. package/src/legal/cross-references.ts +356 -0
  75. package/src/legal/defined-terms.ts +203 -0
  76. package/src/model/README.md +3 -0
  77. package/src/model/canonical-document.ts +1911 -0
  78. package/src/model/cds-1.0.0.ts +196 -0
  79. package/src/model/snapshot.ts +393 -0
  80. package/src/preservation/README.md +3 -0
  81. package/src/preservation/markup-compatibility.ts +48 -0
  82. package/src/preservation/opaque-fragment-store.ts +89 -0
  83. package/src/preservation/opaque-region.ts +233 -0
  84. package/src/preservation/package-preservation.ts +120 -0
  85. package/src/preservation/preserved-part-manifest.ts +56 -0
  86. package/src/preservation/relationship-retention.ts +57 -0
  87. package/src/preservation/store.ts +185 -0
  88. package/src/review/README.md +16 -0
  89. package/src/review/store/README.md +3 -0
  90. package/src/review/store/comment-anchors.ts +70 -0
  91. package/src/review/store/comment-remapping.ts +154 -0
  92. package/src/review/store/comment-store.ts +331 -0
  93. package/src/review/store/comment-thread.ts +109 -0
  94. package/src/review/store/revision-actions.ts +394 -0
  95. package/src/review/store/revision-store.ts +303 -0
  96. package/src/review/store/revision-types.ts +168 -0
  97. package/src/review/store/runtime-comment-store.ts +43 -0
  98. package/src/runtime/README.md +3 -0
  99. package/src/runtime/ai-action-policy.ts +764 -0
  100. package/src/runtime/document-runtime.ts +967 -0
  101. package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
  102. package/src/runtime/review-runtime.ts +44 -0
  103. package/src/runtime/revision-runtime.ts +107 -0
  104. package/src/runtime/session-capabilities.ts +138 -0
  105. package/src/runtime/surface-projection.ts +570 -0
  106. package/src/runtime/table-commands.ts +87 -0
  107. package/src/runtime/table-schema.ts +140 -0
  108. package/src/runtime/virtualized-rendering.ts +258 -0
  109. package/src/ui/README.md +30 -0
  110. package/src/ui/WordReviewEditor.tsx +1506 -0
  111. package/src/ui/comments/README.md +3 -0
  112. package/src/ui/compatibility/README.md +3 -0
  113. package/src/ui/editor-surface/README.md +3 -0
  114. package/src/ui/headless/comment-decoration-model.ts +124 -0
  115. package/src/ui/headless/revision-decoration-model.ts +128 -0
  116. package/src/ui/headless/selection-helpers.ts +34 -0
  117. package/src/ui/headless/use-editor-keyboard.ts +98 -0
  118. package/src/ui/review/README.md +3 -0
  119. package/src/ui/shared/revision-filters.ts +31 -0
  120. package/src/ui/status/README.md +3 -0
  121. package/src/ui/theme/README.md +3 -0
  122. package/src/ui/toolbar/README.md +3 -0
  123. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
  124. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
  125. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
  126. package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
  127. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
  128. package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
  129. package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
  130. package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
  131. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
  132. package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
  133. package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
  134. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
  135. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
  136. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
  137. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
  138. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
  139. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
  140. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
  141. package/src/ui-tailwind/index.ts +61 -0
  142. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
  143. package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
  144. package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
  145. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
  146. package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
  147. package/src/ui-tailwind/theme/editor-theme.css +190 -0
  148. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
  149. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
  150. package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
  151. package/src/validation/README.md +3 -0
  152. package/src/validation/compatibility-engine.ts +317 -0
  153. package/src/validation/compatibility-report.ts +160 -0
  154. package/src/validation/diagnostics.ts +203 -0
  155. package/src/validation/import-diagnostics.ts +128 -0
  156. package/src/validation/low-priority-word-surfaces.ts +373 -0
  157. package/dist/chunk-32W6IVQE.js +0 -7725
  158. package/dist/chunk-32W6IVQE.js.map +0 -1
  159. package/dist/index.cjs +0 -23722
  160. package/dist/index.cjs.map +0 -1
  161. package/dist/index.d.cts +0 -7
  162. package/dist/index.d.ts +0 -7
  163. package/dist/index.js +0 -16011
  164. package/dist/index.js.map +0 -1
  165. package/dist/public-types-DqCURAz8.d.cts +0 -1152
  166. package/dist/public-types-DqCURAz8.d.ts +0 -1152
  167. package/dist/tailwind.cjs +0 -8295
  168. package/dist/tailwind.cjs.map +0 -1
  169. package/dist/tailwind.d.cts +0 -323
  170. package/dist/tailwind.d.ts +0 -323
  171. package/dist/tailwind.js +0 -553
  172. package/dist/tailwind.js.map +0 -1
@@ -0,0 +1,568 @@
1
+ interface XmlElementNode {
2
+ type: "element";
3
+ name: string;
4
+ attributes: Record<string, string>;
5
+ children: XmlNode[];
6
+ start: number;
7
+ end: number;
8
+ }
9
+
10
+ interface XmlTextNode {
11
+ type: "text";
12
+ text: string;
13
+ start: number;
14
+ end: number;
15
+ }
16
+
17
+ type XmlNode = XmlElementNode | XmlTextNode;
18
+
19
+ export interface ParsedBorderSpec {
20
+ value?: string;
21
+ size?: number;
22
+ space?: number;
23
+ color?: string;
24
+ }
25
+
26
+ export interface ParsedTableBorders {
27
+ top?: ParsedBorderSpec;
28
+ left?: ParsedBorderSpec;
29
+ bottom?: ParsedBorderSpec;
30
+ right?: ParsedBorderSpec;
31
+ insideH?: ParsedBorderSpec;
32
+ insideV?: ParsedBorderSpec;
33
+ }
34
+
35
+ export interface ParsedTableCellBorders {
36
+ top?: ParsedBorderSpec;
37
+ left?: ParsedBorderSpec;
38
+ bottom?: ParsedBorderSpec;
39
+ right?: ParsedBorderSpec;
40
+ insideH?: ParsedBorderSpec;
41
+ insideV?: ParsedBorderSpec;
42
+ }
43
+
44
+ export interface ParsedTableWidth {
45
+ value: number;
46
+ type: "dxa" | "auto" | "pct" | "nil";
47
+ }
48
+
49
+ export interface ParsedCellShading {
50
+ fill?: string;
51
+ color?: string;
52
+ val?: string;
53
+ }
54
+
55
+ export interface ParsedCellMargins {
56
+ top?: number;
57
+ left?: number;
58
+ bottom?: number;
59
+ right?: number;
60
+ }
61
+
62
+ export interface ParsedTableDocument {
63
+ tables: ParsedTable[];
64
+ }
65
+
66
+ export interface ParsedTable {
67
+ type: "table";
68
+ propertiesXml?: string;
69
+ gridColumns: number[];
70
+ rows: ParsedTableRow[];
71
+ rawXml: string;
72
+ width?: ParsedTableWidth;
73
+ alignment?: "left" | "center" | "right";
74
+ borders?: ParsedTableBorders;
75
+ cellMargins?: ParsedCellMargins;
76
+ }
77
+
78
+ export interface ParsedTableRow {
79
+ propertiesXml?: string;
80
+ cells: ParsedTableCell[];
81
+ rawXml: string;
82
+ height?: number;
83
+ heightRule?: "auto" | "atLeast" | "exact";
84
+ isHeader?: boolean;
85
+ }
86
+
87
+ export interface ParsedTableCell {
88
+ propertiesXml?: string;
89
+ gridSpan?: number;
90
+ verticalMerge?: "restart" | "continue";
91
+ blocksXml: string[];
92
+ rawXml: string;
93
+ width?: ParsedTableWidth;
94
+ borders?: ParsedTableCellBorders;
95
+ shading?: ParsedCellShading;
96
+ verticalAlign?: "top" | "center" | "bottom";
97
+ }
98
+
99
+ export function parseTablesFromDocumentXml(xml: string): ParsedTableDocument {
100
+ const root = parseXml(xml);
101
+ const documentElement = findChildElement(root, "document");
102
+ const bodyElement = findChildElement(documentElement, "body");
103
+
104
+ return {
105
+ tables: bodyElement.children
106
+ .filter((node): node is XmlElementNode => node.type === "element" && localName(node.name) === "tbl")
107
+ .map((node) => parseTable(node, xml)),
108
+ };
109
+ }
110
+
111
+ function parseTable(node: XmlElementNode, sourceXml: string): ParsedTable {
112
+ const propertiesNode = findFirstChild(node, "tblPr");
113
+ const gridNode = findFirstChild(node, "tblGrid");
114
+ const rows = node.children
115
+ .filter((child): child is XmlElementNode => child.type === "element" && localName(child.name) === "tr")
116
+ .map((rowNode) => parseRow(rowNode, sourceXml));
117
+
118
+ const width = propertiesNode ? readTableWidth(propertiesNode) : undefined;
119
+ const alignment = propertiesNode ? readTableAlignment(propertiesNode) : undefined;
120
+ const borders = propertiesNode ? readTableBorders(propertiesNode) : undefined;
121
+ const cellMargins = propertiesNode ? readTableCellMargins(propertiesNode) : undefined;
122
+
123
+ return {
124
+ type: "table",
125
+ ...(propertiesNode ? { propertiesXml: sourceXml.slice(propertiesNode.start, propertiesNode.end) } : {}),
126
+ gridColumns: gridNode ? readGridColumns(gridNode) : [],
127
+ rows,
128
+ rawXml: sourceXml.slice(node.start, node.end),
129
+ ...(width ? { width } : {}),
130
+ ...(alignment ? { alignment } : {}),
131
+ ...(borders ? { borders } : {}),
132
+ ...(cellMargins ? { cellMargins } : {}),
133
+ };
134
+ }
135
+
136
+ function parseRow(node: XmlElementNode, sourceXml: string): ParsedTableRow {
137
+ const propertiesNode = findFirstChild(node, "trPr");
138
+ const height = propertiesNode ? readRowHeight(propertiesNode) : undefined;
139
+ const heightRule = propertiesNode ? readRowHeightRule(propertiesNode) : undefined;
140
+ const isHeader = propertiesNode ? readRowIsHeader(propertiesNode) : undefined;
141
+
142
+ return {
143
+ ...(propertiesNode ? { propertiesXml: sourceXml.slice(propertiesNode.start, propertiesNode.end) } : {}),
144
+ cells: node.children
145
+ .filter((child): child is XmlElementNode => child.type === "element" && localName(child.name) === "tc")
146
+ .map((cellNode) => parseCell(cellNode, sourceXml)),
147
+ rawXml: sourceXml.slice(node.start, node.end),
148
+ ...(height !== undefined ? { height } : {}),
149
+ ...(heightRule ? { heightRule } : {}),
150
+ ...(isHeader ? { isHeader } : {}),
151
+ };
152
+ }
153
+
154
+ function parseCell(node: XmlElementNode, sourceXml: string): ParsedTableCell {
155
+ const propertiesNode = findFirstChild(node, "tcPr");
156
+ const gridSpanNode = propertiesNode ? findFirstChild(propertiesNode, "gridSpan") : undefined;
157
+ const verticalMergeNode = propertiesNode ? findFirstChild(propertiesNode, "vMerge") : undefined;
158
+ const blocksXml = node.children
159
+ .filter((child): child is XmlElementNode => child.type === "element" && localName(child.name) !== "tcPr")
160
+ .map((child) => sourceXml.slice(child.start, child.end));
161
+
162
+ const cellWidth = propertiesNode ? readCellWidth(propertiesNode) : undefined;
163
+ const borders = propertiesNode ? readCellBorders(propertiesNode) : undefined;
164
+ const shading = propertiesNode ? readCellShading(propertiesNode) : undefined;
165
+ const verticalAlign = propertiesNode ? readCellVerticalAlign(propertiesNode) : undefined;
166
+
167
+ return {
168
+ ...(propertiesNode ? { propertiesXml: sourceXml.slice(propertiesNode.start, propertiesNode.end) } : {}),
169
+ ...(gridSpanNode ? { gridSpan: parsePositiveInteger(gridSpanNode.attributes["w:val"] ?? gridSpanNode.attributes.val) } : {}),
170
+ ...(verticalMergeNode ? { verticalMerge: readVerticalMerge(verticalMergeNode) } : {}),
171
+ blocksXml,
172
+ rawXml: sourceXml.slice(node.start, node.end),
173
+ ...(cellWidth ? { width: cellWidth } : {}),
174
+ ...(borders ? { borders } : {}),
175
+ ...(shading ? { shading } : {}),
176
+ ...(verticalAlign ? { verticalAlign } : {}),
177
+ };
178
+ }
179
+
180
+ function readGridColumns(node: XmlElementNode): number[] {
181
+ return node.children
182
+ .filter((child): child is XmlElementNode => child.type === "element" && localName(child.name) === "gridCol")
183
+ .map((child) => parsePositiveInteger(child.attributes["w:w"] ?? child.attributes.w ?? "0"));
184
+ }
185
+
186
+ function readVerticalMerge(node: XmlElementNode): "restart" | "continue" {
187
+ const value = (node.attributes["w:val"] ?? node.attributes.val ?? "continue").toLowerCase();
188
+ return value === "restart" ? "restart" : "continue";
189
+ }
190
+
191
+ function parsePositiveInteger(value: string | undefined): number {
192
+ const parsed = Number.parseInt(value ?? "0", 10);
193
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
194
+ }
195
+
196
+ function findChildElement(node: XmlElementNode, childLocalName: string): XmlElementNode {
197
+ const child = findFirstChild(node, childLocalName);
198
+ if (!child) {
199
+ throw new Error(`Expected <${childLocalName}> element.`);
200
+ }
201
+
202
+ return child;
203
+ }
204
+
205
+ function findFirstChild(node: XmlElementNode, childLocalName: string): XmlElementNode | undefined {
206
+ return node.children.find(
207
+ (entry): entry is XmlElementNode =>
208
+ entry.type === "element" && localName(entry.name) === childLocalName,
209
+ );
210
+ }
211
+
212
+ function localName(name: string): string {
213
+ const separatorIndex = name.indexOf(":");
214
+ return separatorIndex >= 0 ? name.slice(separatorIndex + 1) : name;
215
+ }
216
+
217
+ function parseXml(xml: string): XmlElementNode {
218
+ const root: XmlElementNode = {
219
+ type: "element",
220
+ name: "__root__",
221
+ attributes: {},
222
+ children: [],
223
+ start: 0,
224
+ end: xml.length,
225
+ };
226
+ const stack: XmlElementNode[] = [root];
227
+ let cursor = 0;
228
+
229
+ while (cursor < xml.length) {
230
+ if (xml.startsWith("<!--", cursor)) {
231
+ const end = xml.indexOf("-->", cursor);
232
+ cursor = end >= 0 ? end + 3 : xml.length;
233
+ continue;
234
+ }
235
+
236
+ if (xml.startsWith("<?", cursor)) {
237
+ const end = xml.indexOf("?>", cursor);
238
+ cursor = end >= 0 ? end + 2 : xml.length;
239
+ continue;
240
+ }
241
+
242
+ if (xml.startsWith("<![CDATA[", cursor)) {
243
+ const end = xml.indexOf("]]>", cursor);
244
+ const textEnd = end >= 0 ? end : xml.length;
245
+ stack[stack.length - 1]?.children.push({
246
+ type: "text",
247
+ text: xml.slice(cursor + 9, textEnd),
248
+ start: cursor,
249
+ end: end >= 0 ? end + 3 : xml.length,
250
+ });
251
+ cursor = end >= 0 ? end + 3 : xml.length;
252
+ continue;
253
+ }
254
+
255
+ if (xml[cursor] !== "<") {
256
+ const nextTag = xml.indexOf("<", cursor);
257
+ const end = nextTag >= 0 ? nextTag : xml.length;
258
+ const text = decodeXmlEntities(xml.slice(cursor, end));
259
+ if (text.length > 0) {
260
+ stack[stack.length - 1]?.children.push({
261
+ type: "text",
262
+ text,
263
+ start: cursor,
264
+ end,
265
+ });
266
+ }
267
+ cursor = end;
268
+ continue;
269
+ }
270
+
271
+ if (xml[cursor + 1] === "/") {
272
+ const end = xml.indexOf(">", cursor);
273
+ if (end < 0) {
274
+ throw new Error("Malformed XML: missing closing >.");
275
+ }
276
+
277
+ const name = xml.slice(cursor + 2, end).trim();
278
+ const current = stack.pop();
279
+ if (!current || localName(current.name) !== localName(name)) {
280
+ throw new Error(`Malformed XML: unexpected closing tag </${name}>.`);
281
+ }
282
+ current.end = end + 1;
283
+ cursor = end + 1;
284
+ continue;
285
+ }
286
+
287
+ const tagEnd = findTagEnd(xml, cursor);
288
+ const tagBody = xml.slice(cursor + 1, tagEnd);
289
+ const selfClosing = /\/\s*$/.test(tagBody);
290
+ const { name, attributes } = parseTag(tagBody.replace(/\/\s*$/, "").trim());
291
+ const element: XmlElementNode = {
292
+ type: "element",
293
+ name,
294
+ attributes,
295
+ children: [],
296
+ start: cursor,
297
+ end: tagEnd + 1,
298
+ };
299
+ stack[stack.length - 1]?.children.push(element);
300
+
301
+ if (!selfClosing) {
302
+ stack.push(element);
303
+ }
304
+
305
+ cursor = tagEnd + 1;
306
+ }
307
+
308
+ if (stack.length !== 1) {
309
+ throw new Error("Malformed XML: unclosed element.");
310
+ }
311
+
312
+ return root;
313
+ }
314
+
315
+ function parseTag(tagBody: string): { name: string; attributes: Record<string, string> } {
316
+ let cursor = 0;
317
+ while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
318
+ cursor += 1;
319
+ }
320
+
321
+ const nameStart = cursor;
322
+ while (cursor < tagBody.length && !/\s/.test(tagBody[cursor] ?? "")) {
323
+ cursor += 1;
324
+ }
325
+ const name = tagBody.slice(nameStart, cursor);
326
+ const attributes: Record<string, string> = {};
327
+
328
+ while (cursor < tagBody.length) {
329
+ while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
330
+ cursor += 1;
331
+ }
332
+ if (cursor >= tagBody.length) {
333
+ break;
334
+ }
335
+
336
+ const keyStart = cursor;
337
+ while (cursor < tagBody.length && !/[\s=]/.test(tagBody[cursor] ?? "")) {
338
+ cursor += 1;
339
+ }
340
+ const key = tagBody.slice(keyStart, cursor);
341
+
342
+ while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
343
+ cursor += 1;
344
+ }
345
+
346
+ if (tagBody[cursor] !== "=") {
347
+ attributes[key] = "";
348
+ continue;
349
+ }
350
+ cursor += 1;
351
+
352
+ while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
353
+ cursor += 1;
354
+ }
355
+
356
+ const quote = tagBody[cursor];
357
+ if (quote !== `"` && quote !== `'`) {
358
+ throw new Error(`Malformed XML attribute ${key}.`);
359
+ }
360
+ cursor += 1;
361
+
362
+ const valueStart = cursor;
363
+ while (cursor < tagBody.length && tagBody[cursor] !== quote) {
364
+ cursor += 1;
365
+ }
366
+ attributes[key] = decodeXmlEntities(tagBody.slice(valueStart, cursor));
367
+ cursor += 1;
368
+ }
369
+
370
+ return { name, attributes };
371
+ }
372
+
373
+ function findTagEnd(xml: string, start: number): number {
374
+ let cursor = start + 1;
375
+ let quote: string | null = null;
376
+
377
+ while (cursor < xml.length) {
378
+ const current = xml[cursor];
379
+ if (quote) {
380
+ if (current === quote) {
381
+ quote = null;
382
+ }
383
+ cursor += 1;
384
+ continue;
385
+ }
386
+
387
+ if (current === `"` || current === `'`) {
388
+ quote = current;
389
+ cursor += 1;
390
+ continue;
391
+ }
392
+
393
+ if (current === ">") {
394
+ return cursor;
395
+ }
396
+
397
+ cursor += 1;
398
+ }
399
+
400
+ throw new Error("Malformed XML: missing >.");
401
+ }
402
+
403
+ function decodeXmlEntities(value: string): string {
404
+ return value.replace(/&(#x[0-9a-fA-F]+|#\d+|amp|lt|gt|quot|apos);/g, (match, entity) => {
405
+ switch (entity) {
406
+ case "amp":
407
+ return "&";
408
+ case "lt":
409
+ return "<";
410
+ case "gt":
411
+ return ">";
412
+ case "quot":
413
+ return `"`;
414
+ case "apos":
415
+ return "'";
416
+ default:
417
+ if (entity.startsWith("#x")) {
418
+ return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
419
+ }
420
+ if (entity.startsWith("#")) {
421
+ return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
422
+ }
423
+ return match;
424
+ }
425
+ });
426
+ }
427
+
428
+ // Cell property readers
429
+
430
+ function readCellWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
431
+ const widthNode = findFirstChild(propertiesNode, "tcW");
432
+ if (!widthNode) return undefined;
433
+ const value = parsePositiveInteger(widthNode.attributes["w:w"] ?? widthNode.attributes.w);
434
+ const rawType = (widthNode.attributes["w:type"] ?? widthNode.attributes.type ?? "dxa").toLowerCase();
435
+ const type: ParsedTableWidth["type"] =
436
+ rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
437
+ return { value, type };
438
+ }
439
+
440
+ function readCellBorders(propertiesNode: XmlElementNode): ParsedTableCellBorders | undefined {
441
+ const bordersNode = findFirstChild(propertiesNode, "tcBorders");
442
+ if (!bordersNode) return undefined;
443
+ const borders: ParsedTableCellBorders = {};
444
+ for (const child of bordersNode.children) {
445
+ if (child.type !== "element") continue;
446
+ const name = localName(child.name);
447
+ if (name !== "top" && name !== "left" && name !== "bottom" && name !== "right" && name !== "insideH" && name !== "insideV") continue;
448
+ const spec = parseBorderSpec(child);
449
+ if (spec) borders[name] = spec;
450
+ }
451
+ return Object.keys(borders).length > 0 ? borders : undefined;
452
+ }
453
+
454
+ function readCellShading(propertiesNode: XmlElementNode): ParsedCellShading | undefined {
455
+ const shdNode = findFirstChild(propertiesNode, "shd");
456
+ if (!shdNode) return undefined;
457
+ const fill = shdNode.attributes["w:fill"] ?? shdNode.attributes.fill;
458
+ const color = shdNode.attributes["w:color"] ?? shdNode.attributes.color;
459
+ const val = shdNode.attributes["w:val"] ?? shdNode.attributes.val;
460
+ if (!fill && !color && !val) return undefined;
461
+ const result: ParsedCellShading = {};
462
+ if (fill) result.fill = fill;
463
+ if (color) result.color = color;
464
+ if (val) result.val = val;
465
+ return result;
466
+ }
467
+
468
+ function readCellVerticalAlign(propertiesNode: XmlElementNode): "top" | "center" | "bottom" | undefined {
469
+ const vAlignNode = findFirstChild(propertiesNode, "vAlign");
470
+ if (!vAlignNode) return undefined;
471
+ const val = vAlignNode.attributes["w:val"] ?? vAlignNode.attributes.val;
472
+ if (val === "center" || val === "top" || val === "bottom") return val;
473
+ return undefined;
474
+ }
475
+
476
+ // Table property readers
477
+
478
+ function readTableWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
479
+ const widthNode = findFirstChild(propertiesNode, "tblW");
480
+ if (!widthNode) return undefined;
481
+ const value = parsePositiveInteger(widthNode.attributes["w:w"] ?? widthNode.attributes.w);
482
+ const rawType = (widthNode.attributes["w:type"] ?? widthNode.attributes.type ?? "dxa").toLowerCase();
483
+ const type: ParsedTableWidth["type"] =
484
+ rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
485
+ return { value, type };
486
+ }
487
+
488
+ function readTableAlignment(propertiesNode: XmlElementNode): "left" | "center" | "right" | undefined {
489
+ const jcNode = findFirstChild(propertiesNode, "jc");
490
+ if (!jcNode) return undefined;
491
+ const val = jcNode.attributes["w:val"] ?? jcNode.attributes.val;
492
+ if (val === "left" || val === "center" || val === "right") return val;
493
+ return undefined;
494
+ }
495
+
496
+ function readTableBorders(propertiesNode: XmlElementNode): ParsedTableBorders | undefined {
497
+ const bordersNode = findFirstChild(propertiesNode, "tblBorders");
498
+ if (!bordersNode) return undefined;
499
+ const borders: ParsedTableBorders = {};
500
+ for (const child of bordersNode.children) {
501
+ if (child.type !== "element") continue;
502
+ const name = localName(child.name);
503
+ if (name !== "top" && name !== "left" && name !== "bottom" && name !== "right" && name !== "insideH" && name !== "insideV") continue;
504
+ const spec = parseBorderSpec(child);
505
+ if (spec) borders[name] = spec;
506
+ }
507
+ return Object.keys(borders).length > 0 ? borders : undefined;
508
+ }
509
+
510
+ function readTableCellMargins(propertiesNode: XmlElementNode): ParsedCellMargins | undefined {
511
+ const marginsNode = findFirstChild(propertiesNode, "tblCellMar");
512
+ if (!marginsNode) return undefined;
513
+ const readSide = (name: string): number => {
514
+ const n = findFirstChild(marginsNode, name);
515
+ return n ? parsePositiveInteger(n.attributes["w:w"] ?? n.attributes.w) : 0;
516
+ };
517
+ const top = readSide("top");
518
+ const bottom = readSide("bottom");
519
+ const left = readSide("start") || readSide("left");
520
+ const right = readSide("end") || readSide("right");
521
+ if (top === 0 && bottom === 0 && left === 0 && right === 0) return undefined;
522
+ return { top, bottom, left, right };
523
+ }
524
+
525
+ // Row property readers
526
+
527
+ function readRowHeight(propertiesNode: XmlElementNode): number | undefined {
528
+ const heightNode = findFirstChild(propertiesNode, "trHeight");
529
+ if (!heightNode) return undefined;
530
+ return parsePositiveInteger(heightNode.attributes["w:val"] ?? heightNode.attributes.val);
531
+ }
532
+
533
+ function readRowHeightRule(propertiesNode: XmlElementNode): "auto" | "atLeast" | "exact" | undefined {
534
+ const heightNode = findFirstChild(propertiesNode, "trHeight");
535
+ if (!heightNode) return undefined;
536
+ const raw = (heightNode.attributes["w:hRule"] ?? heightNode.attributes.hRule ?? "").toLowerCase();
537
+ if (raw === "atleast") return "atLeast";
538
+ if (raw === "exact") return "exact";
539
+ if (raw === "auto") return "auto";
540
+ return undefined;
541
+ }
542
+
543
+ function readRowIsHeader(propertiesNode: XmlElementNode): boolean | undefined {
544
+ const headerNode = findFirstChild(propertiesNode, "tblHeader");
545
+ if (!headerNode) return undefined;
546
+ const val = headerNode.attributes["w:val"] ?? headerNode.attributes.val;
547
+ return val !== "false" && val !== "0";
548
+ }
549
+
550
+ function parseBorderSpec(child: XmlElementNode): ParsedBorderSpec | undefined {
551
+ const value = child.attributes["w:val"] ?? child.attributes.val;
552
+ const sizeRaw = child.attributes["w:sz"] ?? child.attributes.sz;
553
+ const spaceRaw = child.attributes["w:space"] ?? child.attributes.space;
554
+ const color = child.attributes["w:color"] ?? child.attributes.color;
555
+ if (!value && !sizeRaw && !spaceRaw && !color) return undefined;
556
+ const spec: ParsedBorderSpec = {};
557
+ if (value) spec.value = value;
558
+ if (sizeRaw !== undefined) {
559
+ const n = Number.parseInt(sizeRaw, 10);
560
+ if (Number.isFinite(n)) spec.size = n;
561
+ }
562
+ if (spaceRaw !== undefined) {
563
+ const n = Number.parseInt(spaceRaw, 10);
564
+ if (Number.isFinite(n)) spec.space = n;
565
+ }
566
+ if (color) spec.color = color;
567
+ return spec;
568
+ }