@beyondwork/docx-react-component 1.0.19 → 1.0.21

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 (71) hide show
  1. package/package.json +44 -25
  2. package/src/api/public-types.ts +336 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/formatting-commands.ts +1 -1
  5. package/src/core/commands/index.ts +14 -2
  6. package/src/core/search/search-text.ts +28 -0
  7. package/src/core/state/editor-state.ts +3 -0
  8. package/src/index.ts +21 -0
  9. package/src/io/docx-session.ts +363 -17
  10. package/src/io/export/serialize-comments.ts +104 -34
  11. package/src/io/export/serialize-footnotes.ts +198 -1
  12. package/src/io/export/serialize-headers-footers.ts +203 -10
  13. package/src/io/export/serialize-main-document.ts +83 -3
  14. package/src/io/export/split-review-boundaries.ts +181 -19
  15. package/src/io/normalize/normalize-text.ts +82 -8
  16. package/src/io/ooxml/highlight-colors.ts +39 -0
  17. package/src/io/ooxml/parse-comments.ts +85 -19
  18. package/src/io/ooxml/parse-fields.ts +396 -0
  19. package/src/io/ooxml/parse-footnotes.ts +240 -2
  20. package/src/io/ooxml/parse-headers-footers.ts +431 -7
  21. package/src/io/ooxml/parse-inline-media.ts +15 -1
  22. package/src/io/ooxml/parse-main-document.ts +396 -14
  23. package/src/io/ooxml/parse-revisions.ts +317 -38
  24. package/src/legal/bookmarks.ts +44 -0
  25. package/src/legal/cross-references.ts +59 -1
  26. package/src/model/canonical-document.ts +117 -1
  27. package/src/model/snapshot.ts +85 -1
  28. package/src/review/store/revision-store.ts +6 -0
  29. package/src/review/store/revision-types.ts +1 -0
  30. package/src/runtime/document-navigation.ts +52 -13
  31. package/src/runtime/document-runtime.ts +1521 -75
  32. package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
  33. package/src/runtime/session-capabilities.ts +33 -3
  34. package/src/runtime/surface-projection.ts +86 -25
  35. package/src/runtime/table-schema.ts +2 -2
  36. package/src/runtime/view-state.ts +24 -6
  37. package/src/runtime/workflow-markup.ts +349 -0
  38. package/src/ui/WordReviewEditor.tsx +915 -1314
  39. package/src/ui/editor-command-bag.ts +120 -0
  40. package/src/ui/editor-runtime-boundary.ts +1448 -0
  41. package/src/ui/editor-shell-view.tsx +134 -0
  42. package/src/ui/editor-surface-controller.tsx +55 -0
  43. package/src/ui/headless/revision-decoration-model.ts +4 -4
  44. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  45. package/src/ui/workflow-surface-blocked-rails.ts +94 -0
  46. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  47. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  48. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  49. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  50. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
  51. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  52. package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
  53. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
  54. package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
  55. package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
  56. package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
  57. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
  58. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
  59. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  60. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
  61. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  62. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
  63. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  64. package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
  65. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  66. package/src/ui-tailwind/theme/editor-theme.css +130 -0
  67. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
  68. package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
  69. package/src/validation/compatibility-engine.ts +27 -4
  70. package/src/validation/compatibility-report.ts +1 -0
  71. package/src/validation/docx-comment-proof.ts +220 -0
@@ -18,25 +18,9 @@ export function splitDocumentAtReviewBoundaries(
18
18
  return content;
19
19
  }
20
20
 
21
- let cursor = 0;
22
- const children = content.children.map((block, index) => {
23
- if (index > 0 && content.children[index - 1]?.type === "paragraph" && block.type === "paragraph") {
24
- cursor += 1;
25
- }
26
-
27
- if (block.type !== "paragraph") {
28
- cursor += 1;
29
- return block;
30
- }
31
-
32
- const next = splitParagraph(block, splitPositions, cursor);
33
- cursor = next.cursor;
34
- return next.paragraph;
35
- });
36
-
37
21
  return {
38
22
  type: "doc",
39
- children,
23
+ children: splitBlockNodes(content.children, splitPositions, 0, true).children,
40
24
  };
41
25
  }
42
26
 
@@ -98,7 +82,7 @@ function splitParagraph(
98
82
  }
99
83
 
100
84
  children.push(child);
101
- nextCursor += 1;
85
+ nextCursor += measureInlineNodeForReviewBoundaries(child);
102
86
  }
103
87
 
104
88
  return {
@@ -110,6 +94,147 @@ function splitParagraph(
110
94
  };
111
95
  }
112
96
 
97
+ function splitBlockNodes(
98
+ blocks: readonly DocumentRootNode["children"][number][],
99
+ splitPositions: ReadonlySet<number>,
100
+ cursor: number,
101
+ useSurfaceParagraphSeparators: boolean,
102
+ ): {
103
+ children: DocumentRootNode["children"];
104
+ cursor: number;
105
+ } {
106
+ const children: DocumentRootNode["children"] = [];
107
+ let nextCursor = cursor;
108
+ for (const [index, block] of blocks.entries()) {
109
+ if (block.type === "paragraph") {
110
+ if (useSurfaceParagraphSeparators && index > 0) {
111
+ nextCursor += 1;
112
+ }
113
+ const next = splitParagraph(block, splitPositions, nextCursor);
114
+ children.push(next.paragraph);
115
+ nextCursor = next.cursor;
116
+ continue;
117
+ }
118
+
119
+ if (block.type === "table") {
120
+ const next = splitTableAtReviewBoundaries(block, splitPositions, nextCursor);
121
+ children.push(next.table);
122
+ nextCursor = next.cursor;
123
+ continue;
124
+ }
125
+
126
+ if (block.type === "sdt") {
127
+ const next = splitBlockNodes(block.children, splitPositions, nextCursor, false);
128
+ children.push({
129
+ ...block,
130
+ children: next.children,
131
+ });
132
+ nextCursor = next.cursor;
133
+ continue;
134
+ }
135
+
136
+ if (block.type === "custom_xml") {
137
+ children.push(block);
138
+ nextCursor += 1;
139
+ continue;
140
+ }
141
+
142
+ children.push(block);
143
+ nextCursor += 1;
144
+ }
145
+
146
+ return {
147
+ children,
148
+ cursor: nextCursor,
149
+ };
150
+ }
151
+
152
+ function splitTableAtReviewBoundaries(
153
+ table: Extract<DocumentRootNode["children"][number], { type: "table" }>,
154
+ splitPositions: ReadonlySet<number>,
155
+ cursor: number,
156
+ ): {
157
+ table: Extract<DocumentRootNode["children"][number], { type: "table" }>;
158
+ cursor: number;
159
+ } {
160
+ let nextCursor = cursor;
161
+ const rows = table.rows.map((row) => ({
162
+ ...row,
163
+ cells: row.cells.map((cell) => {
164
+ const next = splitBlockNodes(cell.children, splitPositions, nextCursor, false);
165
+ nextCursor = next.cursor;
166
+ return {
167
+ ...cell,
168
+ children: next.children,
169
+ };
170
+ }),
171
+ }));
172
+
173
+ return {
174
+ table: {
175
+ ...table,
176
+ rows,
177
+ },
178
+ cursor: nextCursor,
179
+ };
180
+ }
181
+
182
+ function advanceCursorThroughTable(
183
+ table: Extract<DocumentRootNode["children"][number], { type: "table" }>,
184
+ cursor: number,
185
+ ): number {
186
+ let nextCursor = cursor;
187
+ for (const row of table.rows) {
188
+ for (const cell of row.cells) {
189
+ nextCursor = measureBlockNodesForReviewBoundaries(cell.children, nextCursor, false);
190
+ }
191
+ }
192
+ return nextCursor;
193
+ }
194
+
195
+ function measureBlockNodesForReviewBoundaries(
196
+ blocks: readonly DocumentRootNode["children"][number][],
197
+ cursor: number,
198
+ useSurfaceParagraphSeparators: boolean,
199
+ ): number {
200
+ let nextCursor = cursor;
201
+ for (const [index, block] of blocks.entries()) {
202
+ if (block.type === "paragraph") {
203
+ if (useSurfaceParagraphSeparators && index > 0) {
204
+ nextCursor += 1;
205
+ }
206
+ nextCursor += block.children.reduce(
207
+ (size, child) => size + measureInlineNodeForReviewBoundaries(child),
208
+ 0,
209
+ );
210
+ continue;
211
+ }
212
+
213
+ if (block.type === "table") {
214
+ nextCursor = advanceCursorThroughTable(block, nextCursor);
215
+ continue;
216
+ }
217
+
218
+ if (block.type === "sdt") {
219
+ nextCursor = measureBlockNodesForReviewBoundaries(
220
+ block.children,
221
+ nextCursor,
222
+ false,
223
+ );
224
+ continue;
225
+ }
226
+
227
+ if (block.type === "custom_xml") {
228
+ nextCursor += 1;
229
+ continue;
230
+ }
231
+
232
+ nextCursor += 1;
233
+ }
234
+
235
+ return nextCursor;
236
+ }
237
+
113
238
  function splitHyperlinkNode(
114
239
  node: HyperlinkNode,
115
240
  splitPositions: ReadonlySet<number>,
@@ -132,7 +257,7 @@ function splitHyperlinkNode(
132
257
  }
133
258
 
134
259
  groups[groups.length - 1]?.push(child);
135
- nextCursor += 1;
260
+ nextCursor += measureInlineNodeForReviewBoundaries(child);
136
261
  if (splitPositions.has(nextCursor)) {
137
262
  groups.push([]);
138
263
  }
@@ -192,3 +317,40 @@ function splitTextNode(
192
317
  cursor: cursor + codepoints.length,
193
318
  };
194
319
  }
320
+
321
+ function measureInlineNodeForReviewBoundaries(node: InlineNode): number {
322
+ switch (node.type) {
323
+ case "text":
324
+ return Array.from(node.text).length;
325
+ case "bookmark_start":
326
+ case "bookmark_end":
327
+ return 0;
328
+ case "hyperlink":
329
+ return node.children.reduce(
330
+ (size, child) => size + measureInlineNodeForReviewBoundaries(child),
331
+ 0,
332
+ );
333
+ case "field": {
334
+ const childWidth = node.children.reduce(
335
+ (size, child) => size + measureInlineNodeForReviewBoundaries(child),
336
+ 0,
337
+ );
338
+ return childWidth > 0 ? childWidth : 1;
339
+ }
340
+ case "tab":
341
+ case "hard_break":
342
+ case "column_break":
343
+ case "footnote_ref":
344
+ case "image":
345
+ case "opaque_inline":
346
+ case "chart_preview":
347
+ case "smartart_preview":
348
+ case "shape":
349
+ case "wordart":
350
+ case "vml_shape":
351
+ case "symbol":
352
+ return 1;
353
+ default:
354
+ return 1;
355
+ }
356
+ }
@@ -4,6 +4,7 @@ import type {
4
4
  CustomXmlNode,
5
5
  DiagnosticStore,
6
6
  DocumentRootNode,
7
+ FieldRegistry,
7
8
  InlineNode,
8
9
  MediaCatalog,
9
10
  OpaqueBlockNode,
@@ -34,6 +35,7 @@ import type {
34
35
  ParsedTableCellNode,
35
36
  ParsedTableRowNode,
36
37
  } from "../ooxml/parse-main-document.ts";
38
+ import { classifyFieldInstruction, buildFieldRegistry } from "../ooxml/parse-fields.ts";
37
39
 
38
40
  export interface NormalizedTextDocument {
39
41
  content: DocumentRootNode;
@@ -41,6 +43,8 @@ export interface NormalizedTextDocument {
41
43
  preservation: PreservationStore;
42
44
  diagnostics: DiagnosticStore;
43
45
  finalSectionProperties?: ParsedMainDocument["finalSectionProperties"];
46
+ /** Package-backed field registry built during normalization. */
47
+ fieldRegistry?: FieldRegistry;
44
48
  }
45
49
 
46
50
  interface NormalizationState {
@@ -56,6 +60,7 @@ interface NormalizationState {
56
60
  export function normalizeParsedTextDocument(
57
61
  document: ParsedMainDocument,
58
62
  packagePartName = "/word/document.xml",
63
+ options?: { styles?: import("../../model/canonical-document.ts").StylesCatalog },
59
64
  ): NormalizedTextDocument {
60
65
  const state: NormalizationState = {
61
66
  nextFragmentIndex: 1,
@@ -89,17 +94,24 @@ export function normalizeParsedTextDocument(
89
94
  }
90
95
  }
91
96
 
97
+ const content: DocumentRootNode = { type: "doc", children };
98
+
99
+ // Build the field registry from normalized content.
100
+ // When styles are available, the registry includes full TOC heading resolution.
101
+ // Without styles, it still catalogs all field instances for the supported/preserve-only partition.
102
+ const styles = options?.styles ?? { paragraphs: {}, characters: {}, tables: {} };
103
+ const fieldRegistry = buildFieldRegistry({ content, styles });
104
+ const hasFields = fieldRegistry.supported.length > 0 || fieldRegistry.preserveOnly.length > 0;
105
+
92
106
  return {
93
- content: {
94
- type: "doc",
95
- children,
96
- },
107
+ content,
97
108
  media: state.media,
98
109
  preservation: state.preservation,
99
110
  diagnostics: state.diagnostics,
100
111
  ...(document.finalSectionProperties !== undefined
101
112
  ? { finalSectionProperties: document.finalSectionProperties }
102
113
  : {}),
114
+ ...(hasFields ? { fieldRegistry } : {}),
103
115
  };
104
116
  }
105
117
 
@@ -158,6 +170,9 @@ function normalizeParagraph(
158
170
  ...(paragraph.numbering ? { numbering: paragraph.numbering } : {}),
159
171
  ...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
160
172
  ...(paragraph.spacing ? { spacing: paragraph.spacing } : {}),
173
+ ...(paragraph.contextualSpacing !== undefined
174
+ ? { contextualSpacing: paragraph.contextualSpacing }
175
+ : {}),
161
176
  ...(paragraph.indentation ? { indentation: paragraph.indentation } : {}),
162
177
  ...(paragraph.tabStops && paragraph.tabStops.length > 0 ? { tabStops: paragraph.tabStops } : {}),
163
178
  ...(paragraph.keepNext ? { keepNext: paragraph.keepNext } : {}),
@@ -397,15 +412,33 @@ function normalizeInlineChildren(
397
412
  bookmarkId: node.bookmarkId,
398
413
  });
399
414
  break;
400
- case "field":
415
+ case "footnote_ref":
416
+ normalized.push({
417
+ type: "footnote_ref",
418
+ noteId: node.noteId,
419
+ noteKind: node.noteKind,
420
+ });
421
+ state.cursor += 1;
422
+ break;
423
+ case "field": {
424
+ const classification = classifyFieldInstruction(node.instruction);
425
+ const cursorBeforeField = state.cursor;
426
+ const fieldChildren = node.children
427
+ ? normalizeInlineChildren(node.children, state, packagePartName)
428
+ : normalizeFieldContentXml(node.contentXml ?? "");
429
+ state.cursor = cursorBeforeField;
401
430
  normalized.push({
402
431
  type: "field",
403
432
  fieldType: node.fieldType,
404
433
  instruction: node.instruction,
405
- children: [],
434
+ children: fieldChildren,
435
+ fieldFamily: classification.family,
436
+ ...(classification.target ? { fieldTarget: classification.target } : {}),
437
+ refreshStatus: classification.supported ? "stale" : "preserve-only",
406
438
  });
407
- state.cursor += 1;
439
+ state.cursor += fieldChildren.length > 0 ? fieldChildren.length : 1;
408
440
  break;
441
+ }
409
442
  }
410
443
  }
411
444
 
@@ -416,7 +449,8 @@ function normalizeImageNode(
416
449
  node: ParsedImageNode,
417
450
  state: NormalizationState,
418
451
  ): InlineNode {
419
- if (!state.media.items[node.mediaId]) {
452
+ const existingMediaItem = state.media.items[node.mediaId];
453
+ if (!existingMediaItem) {
420
454
  const packagePartName =
421
455
  typeof node.packagePartName === "string" && node.packagePartName.length > 0
422
456
  ? node.packagePartName
@@ -432,6 +466,17 @@ function normalizeImageNode(
432
466
  packagePartName,
433
467
  ...(node.relationshipId ? { relationshipId: node.relationshipId } : {}),
434
468
  ...(node.altText ? { altText: node.altText } : {}),
469
+ ...(node.widthEmu !== undefined ? { widthEmu: node.widthEmu } : {}),
470
+ ...(node.heightEmu !== undefined ? { heightEmu: node.heightEmu } : {}),
471
+ };
472
+ } else if (
473
+ node.widthEmu !== undefined ||
474
+ node.heightEmu !== undefined
475
+ ) {
476
+ state.media.items[node.mediaId] = {
477
+ ...existingMediaItem,
478
+ ...(node.widthEmu !== undefined ? { widthEmu: node.widthEmu } : {}),
479
+ ...(node.heightEmu !== undefined ? { heightEmu: node.heightEmu } : {}),
435
480
  };
436
481
  }
437
482
 
@@ -559,3 +604,32 @@ function recordOpaqueFragment(
559
604
  warningId,
560
605
  };
561
606
  }
607
+
608
+ /**
609
+ * Extract text content from field contentXml to populate the field's children
610
+ * array. This enables cross-reference and TOC content to be visible in the
611
+ * canonical model and surface projection.
612
+ */
613
+ function normalizeFieldContentXml(contentXml: string | undefined): InlineNode[] {
614
+ if (!contentXml || contentXml.trim().length === 0) {
615
+ return [];
616
+ }
617
+
618
+ // Extract text from <w:t> elements within the content runs
619
+ const textPattern = /<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g;
620
+ const children: InlineNode[] = [];
621
+
622
+ for (const match of contentXml.matchAll(textPattern)) {
623
+ const text = match[1]
624
+ .replace(/&amp;/g, "&")
625
+ .replace(/&lt;/g, "<")
626
+ .replace(/&gt;/g, ">")
627
+ .replace(/&quot;/g, '"')
628
+ .replace(/&apos;/g, "'");
629
+ if (text.length > 0) {
630
+ children.push({ type: "text", text });
631
+ }
632
+ }
633
+
634
+ return children;
635
+ }
@@ -0,0 +1,39 @@
1
+ export const HIGHLIGHT_COLOR_MAP = {
2
+ black: "000000",
3
+ blue: "0000FF",
4
+ cyan: "00FFFF",
5
+ darkBlue: "000080",
6
+ darkCyan: "008080",
7
+ darkGray: "808080",
8
+ darkGreen: "008000",
9
+ darkMagenta: "800080",
10
+ darkRed: "8B0000",
11
+ darkYellow: "808000",
12
+ green: "00FF00",
13
+ lightGray: "C0C0C0",
14
+ magenta: "FF00FF",
15
+ red: "FF0000",
16
+ white: "FFFFFF",
17
+ yellow: "FFFF00",
18
+ } as const;
19
+
20
+ export type HighlightColorName = keyof typeof HIGHLIGHT_COLOR_MAP;
21
+
22
+ export function resolveHighlightColor(
23
+ value: string | null | undefined,
24
+ ): { color: string; val: HighlightColorName } | undefined {
25
+ if (!value || value === "none") {
26
+ return undefined;
27
+ }
28
+
29
+ const normalizedValue = value as HighlightColorName;
30
+ const color = HIGHLIGHT_COLOR_MAP[normalizedValue];
31
+ if (!color) {
32
+ return undefined;
33
+ }
34
+
35
+ return {
36
+ color,
37
+ val: normalizedValue,
38
+ };
39
+ }
@@ -65,6 +65,8 @@ export interface CommentImportDiagnostic {
65
65
  | "preserve_only_revision_overlap";
66
66
  message: string;
67
67
  featureClass: "preserve-only";
68
+ detachedReason?: "incomplete-markers" | "multi-paragraph" | "opaque-region" | "revision-overlap";
69
+ actionabilityNote?: string;
68
70
  }
69
71
 
70
72
  export interface ParsedCommentsResult {
@@ -143,8 +145,10 @@ export function parseCommentsFromOoxml(
143
145
  diagnostics.push({
144
146
  commentId: rootCommentId,
145
147
  code: "missing_anchor_reference",
146
- message: "Comment anchor markers are incomplete and remain preserve-only.",
148
+ message: "Comment anchor markers are incomplete (missing start, end, or reference). Thread is visible but detached and not actionable.",
147
149
  featureClass: "preserve-only",
150
+ detachedReason: "incomplete-markers",
151
+ actionabilityNote: "Re-anchoring requires the host to supply a valid range. The comment body and thread are preserved for display.",
148
152
  });
149
153
  threads.push(
150
154
  createImportedCommentThread({
@@ -171,8 +175,10 @@ export function parseCommentsFromOoxml(
171
175
  commentId: rootCommentId,
172
176
  code: "multi_paragraph_anchor_preserve_only",
173
177
  message:
174
- "Comment anchor spans multiple paragraphs and remains preserve-only for Wave 5.",
178
+ "Comment anchor spans multiple paragraphs. Thread is visible but detached; cross-paragraph anchoring is not yet supported for live editing.",
175
179
  featureClass: "preserve-only",
180
+ detachedReason: "multi-paragraph",
181
+ actionabilityNote: "The comment thread and body are preserved. Operators see the thread in the sidebar but cannot navigate to an inline highlight.",
176
182
  });
177
183
  threads.push(
178
184
  createImportedCommentThread({
@@ -470,32 +476,92 @@ function parseCommentAnchors(documentXml: string): Map<string, CommentAnchorBoun
470
476
  const documentElement = findChildElement(root, "document");
471
477
  const bodyElement = findChildElement(documentElement, "body");
472
478
  const anchors = new Map<string, CommentAnchorBounds>();
473
- let cursor = 0;
474
- let paragraphIndex = -1;
475
- let previousWasParagraph = false;
479
+ walkCommentAnchorBlocks(bodyElement.children, anchors, 0, -1, true);
476
480
 
477
- for (const child of bodyElement.children) {
478
- if (child.type !== "element") {
481
+ return anchors;
482
+ }
483
+
484
+ function walkCommentAnchorBlocks(
485
+ nodes: readonly XmlNode[],
486
+ anchors: Map<string, CommentAnchorBounds>,
487
+ cursor: number,
488
+ paragraphIndex: number,
489
+ useSurfaceParagraphSeparators: boolean,
490
+ ): {
491
+ cursor: number;
492
+ paragraphIndex: number;
493
+ } {
494
+ let nextCursor = cursor;
495
+ let nextParagraphIndex = paragraphIndex;
496
+ let elementIndex = -1;
497
+
498
+ for (const node of nodes) {
499
+ if (node.type !== "element") {
479
500
  continue;
480
501
  }
502
+ elementIndex += 1;
481
503
 
482
- if (localName(child.name) !== "p") {
483
- cursor += 1;
484
- previousWasParagraph = false;
504
+ const name = localName(node.name);
505
+ if (name === "p") {
506
+ if (useSurfaceParagraphSeparators && elementIndex > 0) {
507
+ nextCursor += 1;
508
+ }
509
+ nextParagraphIndex += 1;
510
+ walkParagraph(node, nextParagraphIndex, anchors, () => nextCursor, (next) => {
511
+ nextCursor = next;
512
+ });
485
513
  continue;
486
514
  }
487
515
 
488
- if (previousWasParagraph) {
489
- cursor += 1;
516
+ if (name === "tbl") {
517
+ for (const child of node.children) {
518
+ if (child.type !== "element" || localName(child.name) !== "tr") {
519
+ continue;
520
+ }
521
+ for (const rowChild of child.children) {
522
+ if (rowChild.type !== "element" || localName(rowChild.name) !== "tc") {
523
+ continue;
524
+ }
525
+ const result = walkCommentAnchorBlocks(
526
+ rowChild.children,
527
+ anchors,
528
+ nextCursor,
529
+ nextParagraphIndex,
530
+ false,
531
+ );
532
+ nextCursor = result.cursor;
533
+ nextParagraphIndex = result.paragraphIndex;
534
+ }
535
+ }
536
+ continue;
490
537
  }
491
- paragraphIndex += 1;
492
- walkParagraph(child, paragraphIndex, anchors, () => cursor, (next) => {
493
- cursor = next;
494
- });
495
- previousWasParagraph = true;
538
+
539
+ if (name === "sdt") {
540
+ const sdtContent = findChildElement(node, "sdtContent");
541
+ const result = walkCommentAnchorBlocks(
542
+ sdtContent.children,
543
+ anchors,
544
+ nextCursor,
545
+ nextParagraphIndex,
546
+ false,
547
+ );
548
+ nextCursor = result.cursor;
549
+ nextParagraphIndex = result.paragraphIndex;
550
+ continue;
551
+ }
552
+
553
+ if (name === "customXml") {
554
+ nextCursor += 1;
555
+ continue;
556
+ }
557
+
558
+ nextCursor += 0;
496
559
  }
497
560
 
498
- return anchors;
561
+ return {
562
+ cursor: nextCursor,
563
+ paragraphIndex: nextParagraphIndex,
564
+ };
499
565
  }
500
566
 
501
567
  function walkParagraph(
@@ -554,7 +620,7 @@ function walkInlineNode(
554
620
  .filter((child): child is XmlTextNode => child.type === "text")
555
621
  .map((child) => child.text)
556
622
  .join("");
557
- setCursor(getCursor() + text.length);
623
+ setCursor(getCursor() + Array.from(text).length);
558
624
  return;
559
625
  }
560
626
  case "tab":