@beyondwork/docx-react-component 1.0.28 → 1.0.30

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 (92) hide show
  1. package/package.json +26 -37
  2. package/src/api/public-types.ts +531 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/index.ts +201 -79
  5. package/src/core/commands/table-structure-commands.ts +138 -5
  6. package/src/core/state/text-transaction.ts +370 -3
  7. package/src/index.ts +41 -0
  8. package/src/io/docx-session.ts +318 -25
  9. package/src/io/export/serialize-footnotes.ts +41 -46
  10. package/src/io/export/serialize-headers-footers.ts +36 -40
  11. package/src/io/export/serialize-main-document.ts +55 -89
  12. package/src/io/export/serialize-numbering.ts +104 -4
  13. package/src/io/export/serialize-runtime-revisions.ts +196 -2
  14. package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
  15. package/src/io/export/table-properties-xml.ts +318 -0
  16. package/src/io/normalize/normalize-text.ts +34 -3
  17. package/src/io/ooxml/parse-comments.ts +6 -0
  18. package/src/io/ooxml/parse-footnotes.ts +69 -13
  19. package/src/io/ooxml/parse-headers-footers.ts +54 -11
  20. package/src/io/ooxml/parse-main-document.ts +112 -42
  21. package/src/io/ooxml/parse-numbering.ts +341 -26
  22. package/src/io/ooxml/parse-revisions.ts +118 -4
  23. package/src/io/ooxml/parse-styles.ts +176 -0
  24. package/src/io/ooxml/parse-tables.ts +34 -25
  25. package/src/io/ooxml/revision-boundaries.ts +127 -3
  26. package/src/io/ooxml/workflow-payload.ts +544 -0
  27. package/src/model/canonical-document.ts +91 -1
  28. package/src/model/snapshot.ts +112 -1
  29. package/src/preservation/store.ts +73 -3
  30. package/src/review/store/comment-store.ts +19 -1
  31. package/src/review/store/revision-actions.ts +29 -0
  32. package/src/review/store/revision-store.ts +12 -1
  33. package/src/review/store/revision-types.ts +11 -0
  34. package/src/runtime/context-analytics.ts +824 -0
  35. package/src/runtime/document-locations.ts +521 -0
  36. package/src/runtime/document-navigation.ts +14 -1
  37. package/src/runtime/document-outline.ts +440 -0
  38. package/src/runtime/document-runtime.ts +941 -45
  39. package/src/runtime/event-refresh-hints.ts +137 -0
  40. package/src/runtime/numbering-prefix.ts +67 -39
  41. package/src/runtime/page-layout-estimation.ts +100 -7
  42. package/src/runtime/resolved-numbering-geometry.ts +293 -0
  43. package/src/runtime/session-capabilities.ts +2 -2
  44. package/src/runtime/suggestions-snapshot.ts +137 -0
  45. package/src/runtime/surface-projection.ts +223 -27
  46. package/src/runtime/table-style-resolver.ts +409 -0
  47. package/src/runtime/view-state.ts +17 -1
  48. package/src/runtime/workflow-markup.ts +54 -14
  49. package/src/ui/WordReviewEditor.tsx +1269 -87
  50. package/src/ui/editor-command-bag.ts +7 -0
  51. package/src/ui/editor-runtime-boundary.ts +111 -10
  52. package/src/ui/editor-shell-view.tsx +17 -15
  53. package/src/ui/editor-surface-controller.tsx +5 -0
  54. package/src/ui/headless/selection-tool-context.ts +19 -0
  55. package/src/ui/headless/selection-tool-resolver.ts +752 -0
  56. package/src/ui/headless/selection-tool-types.ts +129 -0
  57. package/src/ui/headless/selection-toolbar-model.ts +10 -33
  58. package/src/ui/runtime-shortcut-dispatch.ts +365 -0
  59. package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
  60. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
  61. package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
  62. package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
  63. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
  64. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
  65. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
  66. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
  67. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
  69. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
  70. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
  72. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
  73. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
  76. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
  77. package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
  78. package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
  79. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
  80. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  81. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
  82. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
  83. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
  84. package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
  85. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
  87. package/src/ui-tailwind/theme/editor-theme.css +58 -40
  88. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
  89. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
  90. package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
  91. package/src/validation/compatibility-engine.ts +246 -2
  92. package/src/validation/docx-comment-proof.ts +24 -11
@@ -8,12 +8,35 @@
8
8
  */
9
9
 
10
10
  import type {
11
+ CellShading,
11
12
  CharacterStyleDefinition,
12
13
  LatentStyleDefinition,
13
14
  ParagraphStyleDefinition,
14
15
  StylesCatalog,
16
+ TableBorders,
17
+ TableCellBorders,
18
+ TableCellMargins,
19
+ TableLook,
20
+ TableStyleConditionalRegion,
15
21
  TableStyleDefinition,
22
+ TableStyleFormatting,
23
+ TableWidth,
16
24
  } from "../../model/canonical-document.ts";
25
+ import {
26
+ readCellBorders,
27
+ readCellShading,
28
+ readCellVerticalAlign,
29
+ readCellWidth,
30
+ readRowHeight,
31
+ readRowHeightRule,
32
+ readRowIsHeader,
33
+ readTableAlignment,
34
+ readTableBorders,
35
+ readTableCellMargins,
36
+ readTableLook,
37
+ readTableWidth,
38
+ } from "./parse-tables.ts";
39
+ import { toCanonicalNumberingInstanceId } from "./parse-numbering.ts";
17
40
 
18
41
  // ---------------------------------------------------------------------------
19
42
  // Inline XML node types (same pattern as parse-numbering.ts)
@@ -24,11 +47,15 @@ interface XmlElementNode {
24
47
  name: string;
25
48
  attributes: Record<string, string>;
26
49
  children: XmlNode[];
50
+ start: number;
51
+ end: number;
27
52
  }
28
53
 
29
54
  interface XmlTextNode {
30
55
  type: "text";
31
56
  text: string;
57
+ start: number;
58
+ end: number;
32
59
  }
33
60
 
34
61
  type XmlNode = XmlElementNode | XmlTextNode;
@@ -107,6 +134,7 @@ export function parseStylesXml(xml: string): ParseStylesResult {
107
134
  case "paragraph": {
108
135
  const nextStyle = readLinkedStyleId(child, "next");
109
136
  const outlineLevel = readParagraphStyleOutlineLevel(child);
137
+ const numbering = readParagraphStyleNumbering(child);
110
138
  paragraphs[styleId] = {
111
139
  styleId,
112
140
  displayName,
@@ -115,6 +143,7 @@ export function parseStylesXml(xml: string): ParseStylesResult {
115
143
  ...(basedOn ? { basedOn } : {}),
116
144
  ...(nextStyle ? { nextStyle } : {}),
117
145
  ...(outlineLevel !== undefined ? { outlineLevel } : {}),
146
+ ...(numbering ? { numbering } : {}),
118
147
  };
119
148
  break;
120
149
  }
@@ -129,12 +158,16 @@ export function parseStylesXml(xml: string): ParseStylesResult {
129
158
  break;
130
159
  }
131
160
  case "table": {
161
+ const formatting = readTableStyleFormatting(child);
162
+ const conditionalFormatting = readTableConditionalFormatting(child);
132
163
  tables[styleId] = {
133
164
  styleId,
134
165
  displayName,
135
166
  kind: "table",
136
167
  isDefault,
137
168
  ...(basedOn ? { basedOn } : {}),
169
+ ...(formatting ? { formatting } : {}),
170
+ ...(conditionalFormatting ? { conditionalFormatting } : {}),
138
171
  };
139
172
  break;
140
173
  }
@@ -204,6 +237,140 @@ function readParagraphStyleOutlineLevel(
204
237
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
205
238
  }
206
239
 
240
+ function readParagraphStyleNumbering(
241
+ styleNode: XmlElementNode,
242
+ ): ParagraphStyleDefinition["numbering"] | undefined {
243
+ const paragraphProperties = findChildElementOptional(styleNode, "pPr");
244
+ if (!paragraphProperties) {
245
+ return undefined;
246
+ }
247
+
248
+ const numberingProperties = findChildElementOptional(paragraphProperties, "numPr");
249
+ if (!numberingProperties) {
250
+ return undefined;
251
+ }
252
+
253
+ const levelNode = findChildElementOptional(numberingProperties, "ilvl");
254
+ const instanceNode = findChildElementOptional(numberingProperties, "numId");
255
+ const rawInstanceId = instanceNode?.attributes["w:val"] ?? instanceNode?.attributes.val;
256
+ if (!rawInstanceId) {
257
+ return undefined;
258
+ }
259
+ if (rawInstanceId === "0") {
260
+ return undefined;
261
+ }
262
+
263
+ const rawLevel = levelNode?.attributes["w:val"] ?? levelNode?.attributes.val;
264
+ const parsedLevel =
265
+ rawLevel !== undefined ? Number.parseInt(rawLevel, 10) : Number.NaN;
266
+
267
+ return {
268
+ numberingInstanceId: toCanonicalNumberingInstanceId(rawInstanceId),
269
+ ...(Number.isInteger(parsedLevel) && parsedLevel >= 0 ? { level: parsedLevel } : {}),
270
+ };
271
+ }
272
+ function readTableStyleFormatting(styleNode: XmlElementNode): TableStyleFormatting | undefined {
273
+ const tableProperties = findChildElementOptional(styleNode, "tblPr");
274
+ const rowProperties = findChildElementOptional(styleNode, "trPr");
275
+ const cellProperties = findChildElementOptional(styleNode, "tcPr");
276
+ const formatting: TableStyleFormatting = {};
277
+
278
+ if (tableProperties) {
279
+ const table: NonNullable<TableStyleFormatting["table"]> = {};
280
+ const width = readTableWidth(tableProperties);
281
+ const alignment = readTableAlignment(tableProperties);
282
+ const borders = readTableBorders(tableProperties);
283
+ const cellMargins = readTableCellMargins(tableProperties);
284
+ const tblLook = readTableLook(tableProperties);
285
+
286
+ if (width) table.width = width as TableWidth;
287
+ if (alignment) table.alignment = alignment;
288
+ if (borders) table.borders = borders as TableBorders;
289
+ if (cellMargins) table.cellMargins = cellMargins as TableCellMargins;
290
+ if (tblLook) table.tblLook = tblLook as TableLook;
291
+
292
+ if (Object.keys(table).length > 0) {
293
+ formatting.table = table;
294
+ }
295
+ }
296
+
297
+ if (rowProperties) {
298
+ const row: NonNullable<TableStyleFormatting["row"]> = {};
299
+ const height = readRowHeight(rowProperties);
300
+ const heightRule = readRowHeightRule(rowProperties);
301
+ const isHeader = readRowIsHeader(rowProperties);
302
+
303
+ if (height !== undefined) row.height = height;
304
+ if (heightRule) row.heightRule = heightRule;
305
+ if (isHeader !== undefined) row.isHeader = isHeader;
306
+
307
+ if (Object.keys(row).length > 0) {
308
+ formatting.row = row;
309
+ }
310
+ }
311
+
312
+ if (cellProperties) {
313
+ const cell: NonNullable<TableStyleFormatting["cell"]> = {};
314
+ const width = readCellWidth(cellProperties);
315
+ const borders = readCellBorders(cellProperties);
316
+ const shading = readCellShading(cellProperties);
317
+ const verticalAlign = readCellVerticalAlign(cellProperties);
318
+
319
+ if (width) cell.width = width as TableWidth;
320
+ if (borders) cell.borders = borders as TableCellBorders;
321
+ if (shading) cell.shading = shading as CellShading;
322
+ if (verticalAlign) cell.verticalAlign = verticalAlign;
323
+
324
+ if (Object.keys(cell).length > 0) {
325
+ formatting.cell = cell;
326
+ }
327
+ }
328
+
329
+ return Object.keys(formatting).length > 0 ? formatting : undefined;
330
+ }
331
+
332
+ function readTableConditionalFormatting(
333
+ styleNode: XmlElementNode,
334
+ ): Partial<Record<TableStyleConditionalRegion, TableStyleFormatting>> | undefined {
335
+ const conditionalFormatting: Partial<Record<TableStyleConditionalRegion, TableStyleFormatting>> = {};
336
+
337
+ for (const child of styleNode.children) {
338
+ if (child.type !== "element" || localName(child.name) !== "tblStylePr") {
339
+ continue;
340
+ }
341
+
342
+ const region = mapTableConditionalRegion(child.attributes["w:type"] ?? child.attributes.type);
343
+ if (!region) {
344
+ continue;
345
+ }
346
+
347
+ const formatting = readTableStyleFormatting(child);
348
+ if (formatting) {
349
+ conditionalFormatting[region] = formatting;
350
+ }
351
+ }
352
+
353
+ return Object.keys(conditionalFormatting).length > 0 ? conditionalFormatting : undefined;
354
+ }
355
+
356
+ function mapTableConditionalRegion(value: string | undefined): TableStyleConditionalRegion | undefined {
357
+ switch (value) {
358
+ case "firstRow":
359
+ case "lastRow":
360
+ case "band1Horz":
361
+ case "band2Horz":
362
+ case "band1Vert":
363
+ case "band2Vert":
364
+ return value;
365
+ case "firstCol":
366
+ return "firstColumn";
367
+ case "lastCol":
368
+ return "lastColumn";
369
+ default:
370
+ return undefined;
371
+ }
372
+ }
373
+
207
374
  function readLatentStyles(
208
375
  latentNode: XmlElementNode,
209
376
  out: Record<string, LatentStyleDefinition>,
@@ -263,6 +430,8 @@ function parseXml(xml: string): XmlElementNode {
263
430
  name: "__root__",
264
431
  attributes: {},
265
432
  children: [],
433
+ start: 0,
434
+ end: xml.length,
266
435
  };
267
436
  const stack: XmlElementNode[] = [root];
268
437
  let cursor = 0;
@@ -286,6 +455,8 @@ function parseXml(xml: string): XmlElementNode {
286
455
  stack[stack.length - 1]?.children.push({
287
456
  type: "text",
288
457
  text: xml.slice(cursor + 9, textEnd),
458
+ start: cursor,
459
+ end: end >= 0 ? end + 3 : xml.length,
289
460
  });
290
461
  cursor = end >= 0 ? end + 3 : xml.length;
291
462
  continue;
@@ -299,6 +470,8 @@ function parseXml(xml: string): XmlElementNode {
299
470
  stack[stack.length - 1]?.children.push({
300
471
  type: "text",
301
472
  text,
473
+ start: cursor,
474
+ end,
302
475
  });
303
476
  }
304
477
  cursor = end;
@@ -316,6 +489,7 @@ function parseXml(xml: string): XmlElementNode {
316
489
  if (!current || localName(current.name) !== localName(name)) {
317
490
  throw new Error(`Malformed XML: unexpected closing tag </${name}>.`);
318
491
  }
492
+ current.end = end + 1;
319
493
 
320
494
  cursor = end + 1;
321
495
  continue;
@@ -330,6 +504,8 @@ function parseXml(xml: string): XmlElementNode {
330
504
  name,
331
505
  attributes,
332
506
  children: [],
507
+ start: cursor,
508
+ end: tagEnd + 1,
333
509
  };
334
510
  stack[stack.length - 1]?.children.push(element);
335
511
 
@@ -1,4 +1,4 @@
1
- interface XmlElementNode {
1
+ export interface XmlElementNode {
2
2
  type: "element";
3
3
  name: string;
4
4
  attributes: Record<string, string>;
@@ -7,14 +7,14 @@ interface XmlElementNode {
7
7
  end: number;
8
8
  }
9
9
 
10
- interface XmlTextNode {
10
+ export interface XmlTextNode {
11
11
  type: "text";
12
12
  text: string;
13
13
  start: number;
14
14
  end: number;
15
15
  }
16
16
 
17
- type XmlNode = XmlElementNode | XmlTextNode;
17
+ export type XmlNode = XmlElementNode | XmlTextNode;
18
18
 
19
19
  export interface ParsedBorderSpec {
20
20
  value?: string;
@@ -163,7 +163,7 @@ function parseRow(node: XmlElementNode, sourceXml: string): ParsedTableRow {
163
163
  rawXml: sourceXml.slice(node.start, node.end),
164
164
  ...(height !== undefined ? { height } : {}),
165
165
  ...(heightRule ? { heightRule } : {}),
166
- ...(isHeader ? { isHeader } : {}),
166
+ ...(isHeader !== undefined ? { isHeader } : {}),
167
167
  };
168
168
  }
169
169
 
@@ -193,7 +193,7 @@ function parseCell(node: XmlElementNode, sourceXml: string): ParsedTableCell {
193
193
  };
194
194
  }
195
195
 
196
- function readGridColumns(node: XmlElementNode): number[] {
196
+ export function readGridColumns(node: XmlElementNode): number[] {
197
197
  return node.children
198
198
  .filter((child): child is XmlElementNode => child.type === "element" && localName(child.name) === "gridCol")
199
199
  .map((child) => parsePositiveInteger(child.attributes["w:w"] ?? child.attributes.w ?? "0"));
@@ -443,7 +443,7 @@ function decodeXmlEntities(value: string): string {
443
443
 
444
444
  // Cell property readers
445
445
 
446
- function readCellWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
446
+ export function readCellWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
447
447
  const widthNode = findFirstChild(propertiesNode, "tcW");
448
448
  if (!widthNode) return undefined;
449
449
  const value = parsePositiveInteger(widthNode.attributes["w:w"] ?? widthNode.attributes.w);
@@ -453,7 +453,7 @@ function readCellWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undef
453
453
  return { value, type };
454
454
  }
455
455
 
456
- function readCellBorders(propertiesNode: XmlElementNode): ParsedTableCellBorders | undefined {
456
+ export function readCellBorders(propertiesNode: XmlElementNode): ParsedTableCellBorders | undefined {
457
457
  const bordersNode = findFirstChild(propertiesNode, "tcBorders");
458
458
  if (!bordersNode) return undefined;
459
459
  const borders: ParsedTableCellBorders = {};
@@ -467,7 +467,7 @@ function readCellBorders(propertiesNode: XmlElementNode): ParsedTableCellBorders
467
467
  return Object.keys(borders).length > 0 ? borders : undefined;
468
468
  }
469
469
 
470
- function readCellShading(propertiesNode: XmlElementNode): ParsedCellShading | undefined {
470
+ export function readCellShading(propertiesNode: XmlElementNode): ParsedCellShading | undefined {
471
471
  const shdNode = findFirstChild(propertiesNode, "shd");
472
472
  if (!shdNode) return undefined;
473
473
  const fill = shdNode.attributes["w:fill"] ?? shdNode.attributes.fill;
@@ -481,7 +481,7 @@ function readCellShading(propertiesNode: XmlElementNode): ParsedCellShading | un
481
481
  return result;
482
482
  }
483
483
 
484
- function readCellVerticalAlign(propertiesNode: XmlElementNode): "top" | "center" | "bottom" | undefined {
484
+ export function readCellVerticalAlign(propertiesNode: XmlElementNode): "top" | "center" | "bottom" | undefined {
485
485
  const vAlignNode = findFirstChild(propertiesNode, "vAlign");
486
486
  if (!vAlignNode) return undefined;
487
487
  const val = vAlignNode.attributes["w:val"] ?? vAlignNode.attributes.val;
@@ -491,7 +491,7 @@ function readCellVerticalAlign(propertiesNode: XmlElementNode): "top" | "center"
491
491
 
492
492
  // Table property readers
493
493
 
494
- function readTableWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
494
+ export function readTableWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
495
495
  const widthNode = findFirstChild(propertiesNode, "tblW");
496
496
  if (!widthNode) return undefined;
497
497
  const value = parsePositiveInteger(widthNode.attributes["w:w"] ?? widthNode.attributes.w);
@@ -501,7 +501,7 @@ function readTableWidth(propertiesNode: XmlElementNode): ParsedTableWidth | unde
501
501
  return { value, type };
502
502
  }
503
503
 
504
- function readTableAlignment(propertiesNode: XmlElementNode): "left" | "center" | "right" | undefined {
504
+ export function readTableAlignment(propertiesNode: XmlElementNode): "left" | "center" | "right" | undefined {
505
505
  const jcNode = findFirstChild(propertiesNode, "jc");
506
506
  if (!jcNode) return undefined;
507
507
  const val = jcNode.attributes["w:val"] ?? jcNode.attributes.val;
@@ -509,13 +509,13 @@ function readTableAlignment(propertiesNode: XmlElementNode): "left" | "center" |
509
509
  return undefined;
510
510
  }
511
511
 
512
- function readTableStyleId(propertiesNode: XmlElementNode): string | undefined {
512
+ export function readTableStyleId(propertiesNode: XmlElementNode): string | undefined {
513
513
  const styleNode = findFirstChild(propertiesNode, "tblStyle");
514
514
  if (!styleNode) return undefined;
515
515
  return styleNode.attributes["w:val"] ?? styleNode.attributes.val;
516
516
  }
517
517
 
518
- function readTableLook(propertiesNode: XmlElementNode): ParsedTableLook | undefined {
518
+ export function readTableLook(propertiesNode: XmlElementNode): ParsedTableLook | undefined {
519
519
  const tblLookNode = findFirstChild(propertiesNode, "tblLook");
520
520
  if (!tblLookNode) return undefined;
521
521
 
@@ -543,7 +543,7 @@ function readTableLook(propertiesNode: XmlElementNode): ParsedTableLook | undefi
543
543
  return Object.keys(tableLook).length > 0 ? tableLook : undefined;
544
544
  }
545
545
 
546
- function readTableBorders(propertiesNode: XmlElementNode): ParsedTableBorders | undefined {
546
+ export function readTableBorders(propertiesNode: XmlElementNode): ParsedTableBorders | undefined {
547
547
  const bordersNode = findFirstChild(propertiesNode, "tblBorders");
548
548
  if (!bordersNode) return undefined;
549
549
  const borders: ParsedTableBorders = {};
@@ -557,30 +557,39 @@ function readTableBorders(propertiesNode: XmlElementNode): ParsedTableBorders |
557
557
  return Object.keys(borders).length > 0 ? borders : undefined;
558
558
  }
559
559
 
560
- function readTableCellMargins(propertiesNode: XmlElementNode): ParsedCellMargins | undefined {
560
+ export function readTableCellMargins(propertiesNode: XmlElementNode): ParsedCellMargins | undefined {
561
561
  const marginsNode = findFirstChild(propertiesNode, "tblCellMar");
562
562
  if (!marginsNode) return undefined;
563
- const readSide = (name: string): number => {
564
- const n = findFirstChild(marginsNode, name);
565
- return n ? parsePositiveInteger(n.attributes["w:w"] ?? n.attributes.w) : 0;
563
+ const readSide = (name: string): number | undefined => {
564
+ const node = findFirstChild(marginsNode, name);
565
+ if (!node) return undefined;
566
+ return parsePositiveInteger(node.attributes["w:w"] ?? node.attributes.w);
566
567
  };
567
568
  const top = readSide("top");
568
569
  const bottom = readSide("bottom");
569
- const left = readSide("start") || readSide("left");
570
- const right = readSide("end") || readSide("right");
571
- if (top === 0 && bottom === 0 && left === 0 && right === 0) return undefined;
572
- return { top, bottom, left, right };
570
+ const left = readSide("start") ?? readSide("left");
571
+ const right = readSide("end") ?? readSide("right");
572
+ if (top === undefined && bottom === undefined && left === undefined && right === undefined) {
573
+ return undefined;
574
+ }
575
+
576
+ return {
577
+ ...(top !== undefined ? { top } : {}),
578
+ ...(bottom !== undefined ? { bottom } : {}),
579
+ ...(left !== undefined ? { left } : {}),
580
+ ...(right !== undefined ? { right } : {}),
581
+ };
573
582
  }
574
583
 
575
584
  // Row property readers
576
585
 
577
- function readRowHeight(propertiesNode: XmlElementNode): number | undefined {
586
+ export function readRowHeight(propertiesNode: XmlElementNode): number | undefined {
578
587
  const heightNode = findFirstChild(propertiesNode, "trHeight");
579
588
  if (!heightNode) return undefined;
580
589
  return parsePositiveInteger(heightNode.attributes["w:val"] ?? heightNode.attributes.val);
581
590
  }
582
591
 
583
- function readRowHeightRule(propertiesNode: XmlElementNode): "auto" | "atLeast" | "exact" | undefined {
592
+ export function readRowHeightRule(propertiesNode: XmlElementNode): "auto" | "atLeast" | "exact" | undefined {
584
593
  const heightNode = findFirstChild(propertiesNode, "trHeight");
585
594
  if (!heightNode) return undefined;
586
595
  const raw = (heightNode.attributes["w:hRule"] ?? heightNode.attributes.hRule ?? "").toLowerCase();
@@ -590,7 +599,7 @@ function readRowHeightRule(propertiesNode: XmlElementNode): "auto" | "atLeast" |
590
599
  return undefined;
591
600
  }
592
601
 
593
- function readRowIsHeader(propertiesNode: XmlElementNode): boolean | undefined {
602
+ export function readRowIsHeader(propertiesNode: XmlElementNode): boolean | undefined {
594
603
  const headerNode = findFirstChild(propertiesNode, "tblHeader");
595
604
  if (!headerNode) return undefined;
596
605
  const val = headerNode.attributes["w:val"] ?? headerNode.attributes.val;
@@ -50,8 +50,21 @@ export function mapRevisionBoundaries(
50
50
  }
51
51
 
52
52
  if (localName(child.name) !== "p") {
53
- cursor += 1;
54
- previousWasParagraph = false;
53
+ if (localName(child.name) === "tbl") {
54
+ const nested = collectNestedParagraphBoundaries(
55
+ child.children,
56
+ paragraphIndex,
57
+ cursor,
58
+ previousWasParagraph,
59
+ );
60
+ paragraphIndex = nested.paragraphIndex;
61
+ cursor = nested.cursor;
62
+ previousWasParagraph = nested.previousWasParagraph;
63
+ paragraphs.push(...nested.paragraphs);
64
+ } else {
65
+ cursor += 1;
66
+ previousWasParagraph = false;
67
+ }
55
68
  continue;
56
69
  }
57
70
 
@@ -105,6 +118,117 @@ export function mapRevisionBoundaries(
105
118
  return paragraphs;
106
119
  }
107
120
 
121
+ function collectNestedParagraphBoundaries(
122
+ nodes: XmlNode[],
123
+ paragraphIndex: number,
124
+ cursor: number,
125
+ previousWasParagraph: boolean,
126
+ ): {
127
+ paragraphs: RevisionParagraphBoundary[];
128
+ paragraphIndex: number;
129
+ cursor: number;
130
+ previousWasParagraph: boolean;
131
+ } {
132
+ const paragraphs: RevisionParagraphBoundary[] = [];
133
+ let nextParagraphIndex = paragraphIndex;
134
+ let nextCursor = cursor;
135
+ let nextPreviousWasParagraph = previousWasParagraph;
136
+
137
+ for (const node of nodes) {
138
+ if (node.type !== "element") {
139
+ continue;
140
+ }
141
+ const type = localName(node.name);
142
+
143
+ if (type === "p") {
144
+ nextParagraphIndex += 1;
145
+ const boundaries = new Map<number, number>();
146
+ boundaries.set(nextCursor, node.openingTagEnd);
147
+
148
+ const paragraphProperties = findChildElement(node, "pPr");
149
+ const paragraphRunProperties = paragraphProperties
150
+ ? findChildElement(paragraphProperties, "rPr")
151
+ : undefined;
152
+
153
+ walkStoryNodesForBoundaries(
154
+ node.children,
155
+ boundaries,
156
+ () => nextCursor,
157
+ (next) => {
158
+ nextCursor = next;
159
+ },
160
+ );
161
+
162
+ boundaries.set(nextCursor, node.closingTagStart);
163
+ paragraphs.push({
164
+ paragraphIndex: nextParagraphIndex,
165
+ start: Math.min(...boundaries.keys()),
166
+ end: Math.max(...boundaries.keys()),
167
+ boundaries,
168
+ paragraphStart: node.start,
169
+ paragraphStartTagEnd: node.openingTagEnd,
170
+ paragraphEndTagStart: node.closingTagStart,
171
+ paragraphEnd: node.end,
172
+ ...(paragraphProperties
173
+ ? {
174
+ paragraphPropertiesStart: paragraphProperties.start,
175
+ paragraphPropertiesEnd: paragraphProperties.end,
176
+ }
177
+ : {}),
178
+ ...(paragraphRunProperties
179
+ ? {
180
+ paragraphRunPropertiesStart: paragraphRunProperties.start,
181
+ paragraphRunPropertiesEnd: paragraphRunProperties.end,
182
+ }
183
+ : {}),
184
+ });
185
+ nextPreviousWasParagraph = false;
186
+ continue;
187
+ }
188
+
189
+ if (
190
+ type === "tbl" ||
191
+ type === "tr" ||
192
+ type === "tc" ||
193
+ type === "sdtContent" ||
194
+ type === "customXml" ||
195
+ type === "smartTag"
196
+ ) {
197
+ const nested = collectNestedParagraphBoundaries(
198
+ node.children,
199
+ nextParagraphIndex,
200
+ nextCursor,
201
+ nextPreviousWasParagraph,
202
+ );
203
+ nextParagraphIndex = nested.paragraphIndex;
204
+ nextCursor = nested.cursor;
205
+ nextPreviousWasParagraph = nested.previousWasParagraph;
206
+ paragraphs.push(...nested.paragraphs);
207
+ continue;
208
+ }
209
+
210
+ if (
211
+ type === "tblPr" ||
212
+ type === "tblGrid" ||
213
+ type === "gridCol" ||
214
+ type === "trPr" ||
215
+ type === "tcPr"
216
+ ) {
217
+ continue;
218
+ }
219
+
220
+ nextCursor += 1;
221
+ nextPreviousWasParagraph = false;
222
+ }
223
+
224
+ return {
225
+ paragraphs,
226
+ paragraphIndex: nextParagraphIndex,
227
+ cursor: nextCursor,
228
+ previousWasParagraph: nextPreviousWasParagraph,
229
+ };
230
+ }
231
+
108
232
  function walkStoryNodesForBoundaries(
109
233
  nodes: XmlNode[],
110
234
  boundaries: Map<number, number>,
@@ -163,7 +287,7 @@ function walkStoryNodeForBoundaries(
163
287
  if (!boundaries.has(getCursor())) {
164
288
  boundaries.set(getCursor(), node.start);
165
289
  }
166
- setCursor(getCursor() + text.length);
290
+ setCursor(getCursor() + Array.from(text).length);
167
291
  boundaries.set(getCursor(), node.end);
168
292
  return;
169
293
  }