@beyondwork/docx-react-component 1.0.18 → 1.0.20

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 (105) hide show
  1. package/README.md +8 -2
  2. package/package.json +24 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +710 -4
  5. package/src/api/session-state.ts +60 -0
  6. package/src/core/commands/formatting-commands.ts +2 -1
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +19 -3
  9. package/src/core/commands/list-commands.ts +231 -36
  10. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  11. package/src/core/commands/section-layout-commands.ts +680 -0
  12. package/src/core/commands/style-commands.ts +262 -0
  13. package/src/core/search/search-text.ts +357 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +4 -1
  16. package/src/index.ts +51 -0
  17. package/src/io/docx-session.ts +623 -56
  18. package/src/io/export/serialize-comments.ts +104 -34
  19. package/src/io/export/serialize-footnotes.ts +198 -1
  20. package/src/io/export/serialize-headers-footers.ts +203 -10
  21. package/src/io/export/serialize-main-document.ts +285 -8
  22. package/src/io/export/serialize-numbering.ts +28 -7
  23. package/src/io/export/split-review-boundaries.ts +181 -19
  24. package/src/io/normalize/normalize-text.ts +144 -32
  25. package/src/io/ooxml/highlight-colors.ts +39 -0
  26. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  27. package/src/io/ooxml/parse-comments.ts +85 -19
  28. package/src/io/ooxml/parse-fields.ts +396 -0
  29. package/src/io/ooxml/parse-footnotes.ts +452 -22
  30. package/src/io/ooxml/parse-headers-footers.ts +657 -29
  31. package/src/io/ooxml/parse-inline-media.ts +30 -0
  32. package/src/io/ooxml/parse-main-document.ts +807 -20
  33. package/src/io/ooxml/parse-numbering.ts +7 -0
  34. package/src/io/ooxml/parse-revisions.ts +317 -38
  35. package/src/io/ooxml/parse-settings.ts +184 -0
  36. package/src/io/ooxml/parse-shapes.ts +25 -0
  37. package/src/io/ooxml/parse-styles.ts +463 -0
  38. package/src/io/ooxml/parse-theme.ts +32 -0
  39. package/src/legal/bookmarks.ts +44 -0
  40. package/src/legal/cross-references.ts +59 -1
  41. package/src/model/canonical-document.ts +250 -4
  42. package/src/model/cds-1.0.0.ts +13 -0
  43. package/src/model/snapshot.ts +87 -2
  44. package/src/review/store/revision-store.ts +6 -0
  45. package/src/review/store/revision-types.ts +1 -0
  46. package/src/runtime/document-layout.ts +332 -0
  47. package/src/runtime/document-navigation.ts +603 -0
  48. package/src/runtime/document-runtime.ts +1754 -78
  49. package/src/runtime/document-search.ts +145 -0
  50. package/src/runtime/numbering-prefix.ts +47 -26
  51. package/src/runtime/page-layout-estimation.ts +212 -0
  52. package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
  53. package/src/runtime/session-capabilities.ts +35 -3
  54. package/src/runtime/story-context.ts +164 -0
  55. package/src/runtime/story-targeting.ts +162 -0
  56. package/src/runtime/surface-projection.ts +324 -36
  57. package/src/runtime/table-schema.ts +89 -7
  58. package/src/runtime/view-state.ts +477 -0
  59. package/src/runtime/workflow-markup.ts +349 -0
  60. package/src/ui/WordReviewEditor.tsx +2469 -1344
  61. package/src/ui/browser-export.ts +52 -0
  62. package/src/ui/editor-command-bag.ts +120 -0
  63. package/src/ui/editor-runtime-boundary.ts +1422 -0
  64. package/src/ui/editor-shell-view.tsx +134 -0
  65. package/src/ui/editor-surface-controller.tsx +51 -0
  66. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  67. package/src/ui/headless/revision-decoration-model.ts +4 -4
  68. package/src/ui/headless/selection-helpers.ts +20 -0
  69. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  70. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  71. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  72. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  73. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  74. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  75. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  76. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  77. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
  78. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  79. package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
  80. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
  81. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  82. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
  83. package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
  84. package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
  85. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
  86. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  87. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
  88. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  89. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  90. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
  91. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  92. package/src/ui-tailwind/index.ts +2 -1
  93. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  94. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  95. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  96. package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
  97. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  98. package/src/ui-tailwind/theme/editor-theme.css +127 -0
  99. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  100. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
  101. package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
  102. package/src/validation/compatibility-engine.ts +119 -24
  103. package/src/validation/compatibility-report.ts +1 -0
  104. package/src/validation/diagnostics.ts +1 -0
  105. package/src/validation/docx-comment-proof.ts +707 -0
@@ -1,5 +1,7 @@
1
1
  import type {
2
+ EditorStoryTarget,
2
3
  EditorSurfaceSnapshot,
4
+ SecondaryStorySurface,
3
5
  SurfaceBlockSnapshot,
4
6
  SurfaceInlineSegment,
5
7
  SurfaceTableCellSnapshot,
@@ -20,6 +22,7 @@ import type {
20
22
  SdtNode,
21
23
  ShapeNode,
22
24
  SmartArtPreviewNode,
25
+ TableCellBorders,
23
26
  TableNode,
24
27
  TextMark,
25
28
  VmlShapeNode,
@@ -29,10 +32,16 @@ import {
29
32
  describeOpaqueFragment,
30
33
  getOpaqueFragment,
31
34
  } from "../preservation/store.ts";
35
+ import { getStoryBlocks } from "./story-targeting.ts";
32
36
  import {
33
37
  createNumberingPrefixResolver,
34
38
  type NumberingPrefixResolver,
35
39
  } from "./numbering-prefix.ts";
40
+ import {
41
+ collectSectionContexts,
42
+ findHeaderFooterDocumentEntry,
43
+ resolveSectionVariants,
44
+ } from "./story-context.ts";
36
45
 
37
46
  interface ParagraphAccumulator {
38
47
  blockId: string;
@@ -42,14 +51,20 @@ interface ParagraphAccumulator {
42
51
  styleId?: string;
43
52
  numbering?: ParagraphNode["numbering"];
44
53
  numberingPrefix?: string;
54
+ numberingSuffix?: "tab" | "space" | "nothing";
55
+ contextualSpacing?: boolean;
45
56
  segments: SurfaceInlineSegment[];
46
57
  }
47
58
 
48
59
  export function createEditorSurfaceSnapshot(
49
60
  document: CanonicalDocumentEnvelope,
50
61
  _selection: SelectionSnapshot,
62
+ activeStory: EditorStoryTarget = { kind: "main" },
51
63
  ): EditorSurfaceSnapshot {
52
- const root = normalizeDocumentRoot(document.content);
64
+ const root = normalizeDocumentRoot({
65
+ type: "doc",
66
+ children: [...getStoryBlocks(document, activeStory)],
67
+ });
53
68
  const blocks: SurfaceBlockSnapshot[] = [];
54
69
  const lockedFragmentIds: string[] = [];
55
70
  const numberingPrefixResolver = createNumberingPrefixResolver(document.numbering);
@@ -79,13 +94,14 @@ export function createEditorSurfaceSnapshot(
79
94
  }
80
95
  }
81
96
 
82
- blocks.push(...createSecondaryStoryPreviewBlocks(document, cursor));
97
+ const secondaryStories = createSecondaryStorySurfaces(document);
83
98
 
84
99
  return {
85
100
  storySize: cursor,
86
101
  plainText: createPlainText(blocks),
87
102
  blocks,
88
103
  lockedFragmentIds,
104
+ secondaryStories,
89
105
  };
90
106
  }
91
107
 
@@ -263,16 +279,27 @@ function createTableBlock(
263
279
  lockedFragmentIds.push(...result.lockedFragmentIds);
264
280
  innerCursor = result.nextCursor;
265
281
  }
282
+ const cellBorders = resolveCellBorderStyles(cell.borders);
266
283
  cells.push({
267
284
  gridSpan: cell.gridSpan ?? 1,
268
285
  verticalMerge: cell.verticalMerge ?? null,
269
286
  colspan: cell.gridSpan ?? 1,
270
287
  rowspan: rowSpans.get(`${rowIndex}:${cellIndex}`) ?? 1,
271
288
  ...(cell.shading?.fill ? { backgroundColor: `#${cell.shading.fill}` } : {}),
289
+ ...(cell.verticalAlign ? { verticalAlign: cell.verticalAlign } : {}),
290
+ ...(cellBorders.borderTop ? { borderTop: cellBorders.borderTop } : {}),
291
+ ...(cellBorders.borderRight ? { borderRight: cellBorders.borderRight } : {}),
292
+ ...(cellBorders.borderBottom ? { borderBottom: cellBorders.borderBottom } : {}),
293
+ ...(cellBorders.borderLeft ? { borderLeft: cellBorders.borderLeft } : {}),
272
294
  content: cellContent,
273
295
  });
274
296
  }
275
- rows.push({ cells });
297
+ rows.push({
298
+ cells,
299
+ ...(row.height !== undefined ? { height: row.height } : {}),
300
+ ...(row.heightRule ? { heightRule: row.heightRule } : {}),
301
+ ...(row.isHeader ? { isHeader: row.isHeader } : {}),
302
+ });
276
303
  }
277
304
 
278
305
  return {
@@ -283,6 +310,8 @@ function createTableBlock(
283
310
  to: innerCursor,
284
311
  styleId: table.styleId,
285
312
  gridColumns: table.gridColumns,
313
+ ...(table.alignment ? { alignment: table.alignment } : {}),
314
+ ...(table.tblLook ? { tblLook: table.tblLook } : {}),
286
315
  rows,
287
316
  },
288
317
  lockedFragmentIds,
@@ -339,6 +368,23 @@ function computeTableRowSpans(table: TableNode): Map<string, number> {
339
368
  return rowSpans;
340
369
  }
341
370
 
371
+ function resolveCellBorderStyles(
372
+ borders: TableCellBorders | undefined,
373
+ ): { borderTop?: string; borderRight?: string; borderBottom?: string; borderLeft?: string } {
374
+ if (!borders) return {};
375
+ const result: { borderTop?: string; borderRight?: string; borderBottom?: string; borderLeft?: string } = {};
376
+ const sides = [["top", "borderTop"], ["right", "borderRight"], ["bottom", "borderBottom"], ["left", "borderLeft"]] as const;
377
+ for (const [side, key] of sides) {
378
+ const spec = borders[side];
379
+ if (!spec || spec.value === "none" || spec.value === "nil") continue;
380
+ const width = spec.size ? `${Math.max(1, Math.round(spec.size / 8))}px` : "1px";
381
+ const style = spec.value === "double" ? "double" : spec.value === "dashed" || spec.value === "dashSmallGap" ? "dashed" : spec.value === "dotted" ? "dotted" : "solid";
382
+ const color = spec.color && spec.color !== "auto" ? `#${spec.color}` : "currentColor";
383
+ result[key] = `${width} ${style} ${color}`;
384
+ }
385
+ return result;
386
+ }
387
+
342
388
  function createSdtBlock(
343
389
  sdtIndex: number,
344
390
  block: SdtNode,
@@ -381,6 +427,11 @@ function createSdtBlock(
381
427
  ...(block.properties.alias ? { alias: block.properties.alias } : {}),
382
428
  ...(block.properties.tag ? { tag: block.properties.tag } : {}),
383
429
  ...(block.properties.lock ? { lock: block.properties.lock } : {}),
430
+ ...(block.properties.checkbox ? { checkboxChecked: block.properties.checkbox.checked } : {}),
431
+ ...(block.properties.datePicker?.fullDate ? { dateValue: block.properties.datePicker.fullDate } : {}),
432
+ ...(block.properties.dropdownList ? { dropdownItems: block.properties.dropdownList } : {}),
433
+ ...(block.properties.comboBox ? { comboBoxItems: block.properties.comboBox } : {}),
434
+ ...(block.properties.showingPlcHdr ? { showingPlcHdr: true } : {}),
384
435
  children,
385
436
  },
386
437
  lockedFragmentIds,
@@ -407,13 +458,21 @@ function createParagraphBlock(
407
458
  ...(paragraph.styleId ? { styleId: paragraph.styleId } : {}),
408
459
  ...(paragraph.numbering ? { numbering: paragraph.numbering } : {}),
409
460
  ...(paragraph.numbering
410
- ? {
411
- numberingPrefix:
412
- numberingPrefixResolver.resolve(paragraph.numbering) ?? undefined,
413
- }
461
+ ? (() => {
462
+ const detailed = numberingPrefixResolver.resolveDetailed(paragraph.numbering);
463
+ return detailed
464
+ ? {
465
+ numberingPrefix: detailed.text,
466
+ ...(detailed.suffix ? { numberingSuffix: detailed.suffix } : {}),
467
+ }
468
+ : {};
469
+ })()
414
470
  : {}),
415
471
  ...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
416
472
  ...(paragraph.spacing ? { spacing: paragraph.spacing } : {}),
473
+ ...(paragraph.contextualSpacing !== undefined
474
+ ? { contextualSpacing: paragraph.contextualSpacing }
475
+ : {}),
417
476
  ...(paragraph.indentation ? { indentation: paragraph.indentation } : {}),
418
477
  ...(paragraph.borders ? { borders: paragraph.borders } : {}),
419
478
  ...(paragraph.shading ? { shading: paragraph.shading } : {}),
@@ -425,6 +484,7 @@ function createParagraphBlock(
425
484
  ...(paragraph.pageBreakBefore ? { pageBreakBefore: true } : {}),
426
485
  ...(paragraph.outlineLevel !== undefined ? { outlineLevel: paragraph.outlineLevel } : {}),
427
486
  ...(paragraph.bidi ? { bidi: true } : {}),
487
+ ...(paragraph.suppressLineNumbers ? { suppressLineNumbers: true } : {}),
428
488
  segments: [],
429
489
  };
430
490
  const lockedFragmentIds: string[] = [];
@@ -530,20 +590,22 @@ function appendInlineSegments(
530
590
  preview?.detail ??
531
591
  descriptor?.detail ??
532
592
  "Locked whole-unit to keep unsupported inline OOXML intact through export.",
593
+ ...(preview?.presentation ? { presentation: preview.presentation } : {}),
533
594
  state: "locked-preserve-only",
534
595
  });
535
596
  return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
536
597
  }
537
598
  case "chart_preview":
538
- return appendComplexPreviewSegment(paragraph, node, start, "Chart", createChartDetail(node));
599
+ return appendComplexPreviewSegment(paragraph, node, start, "Embedded chart", createChartDetail(node));
539
600
  case "smartart_preview":
540
- return appendComplexPreviewSegment(paragraph, node, start, "SmartArt", createSmartArtDetail(node));
601
+ return appendComplexPreviewSegment(paragraph, node, start, "SmartArt diagram", createSmartArtDetail(node));
541
602
  case "shape":
542
- return appendComplexPreviewSegment(paragraph, node, start, "Shape", createShapeDetail(node));
603
+ return appendComplexPreviewSegment(paragraph, node, start,
604
+ node.isTextBox ? "Text box" : "Drawing shape", createShapeDetail(node));
543
605
  case "wordart":
544
606
  return appendComplexPreviewSegment(paragraph, node, start, "WordArt", createWordArtDetail(node));
545
607
  case "vml_shape":
546
- return appendComplexPreviewSegment(paragraph, node, start, "VML shape", createVmlDetail(node));
608
+ return appendComplexPreviewSegment(paragraph, node, start, "Legacy VML drawing", createVmlDetail(node));
547
609
  case "symbol":
548
610
  paragraph.segments.push({
549
611
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
@@ -569,14 +631,20 @@ function appendInlineSegments(
569
631
  case "footnote_ref":
570
632
  paragraph.segments.push({
571
633
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
572
- kind: "text",
634
+ kind: "note_ref",
573
635
  from: start,
574
636
  to: start + 1,
575
- text: node.noteId ?? "*",
576
- marks: ["superscript" as SurfaceTextMark],
637
+ noteKind: node.noteKind ?? "footnote",
638
+ noteId: node.noteId ?? "",
639
+ label: node.noteId ?? "*",
577
640
  });
578
641
  return { nextCursor: start + 1, lockedFragmentIds: [] };
579
642
  case "field": {
643
+ const isSupportedField =
644
+ node.fieldFamily === "REF" ||
645
+ node.fieldFamily === "PAGEREF" ||
646
+ node.fieldFamily === "NOTEREF" ||
647
+ node.fieldFamily === "TOC";
580
648
  if (node.children && node.children.length > 0) {
581
649
  let cursor = start;
582
650
  const lockedIds: string[] = [];
@@ -587,6 +655,25 @@ function appendInlineSegments(
587
655
  }
588
656
  return { nextCursor: cursor, lockedFragmentIds: lockedIds };
589
657
  }
658
+ if (isSupportedField) {
659
+ // Supported field with no resolved content — show as field chip
660
+ const fieldLabel =
661
+ node.fieldFamily === "TOC"
662
+ ? "Table of Contents"
663
+ : `${node.fieldFamily ?? "Field"}: ${node.fieldTarget ?? node.instruction.trim()}`;
664
+ paragraph.segments.push({
665
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
666
+ kind: "field_ref",
667
+ from: start,
668
+ to: start + 1,
669
+ fieldFamily: node.fieldFamily!,
670
+ fieldTarget: node.fieldTarget,
671
+ instruction: node.instruction,
672
+ refreshStatus: node.refreshStatus ?? "stale",
673
+ label: fieldLabel,
674
+ } as SurfaceInlineSegment);
675
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
676
+ }
590
677
  paragraph.segments.push({
591
678
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
592
679
  kind: "opaque_inline",
@@ -595,7 +682,7 @@ function appendInlineSegments(
595
682
  fragmentId: "",
596
683
  warningId: "",
597
684
  label: "Field",
598
- detail: "Field code preserved for export.",
685
+ detail: `Preserve-only field: ${node.instruction.trim()}`,
599
686
  state: "locked-preserve-only",
600
687
  });
601
688
  return { nextCursor: start + 1, lockedFragmentIds: [] };
@@ -631,38 +718,54 @@ function appendComplexPreviewSegment(
631
718
  }
632
719
 
633
720
  function createChartDetail(node: ChartPreviewNode): string {
634
- return node.previewMediaId
635
- ? `Chart read-only preview. Fallback image ${node.previewMediaId}. Original XML preserved for export.`
636
- : "Chart read-only preview. Original XML preserved for export.";
721
+ const parts = ["Embedded chart."];
722
+ if (node.previewMediaId) {
723
+ parts.push(`Preview available via fallback image (${node.previewMediaId}).`);
724
+ } else {
725
+ parts.push("No fallback preview image found; chart data preserved in package.");
726
+ }
727
+ parts.push("Edit in Word to modify chart data. Original DrawingML preserved for lossless export.");
728
+ return parts.join(" ");
637
729
  }
638
730
 
639
731
  function createSmartArtDetail(node: SmartArtPreviewNode): string {
640
- return node.previewMediaId
641
- ? `SmartArt diagram read-only preview. Fallback image ${node.previewMediaId}. Original XML preserved for export.`
642
- : "SmartArt diagram read-only preview. Original XML preserved for export.";
732
+ const parts = ["SmartArt diagram."];
733
+ if (node.previewMediaId) {
734
+ parts.push(`Preview available via fallback image (${node.previewMediaId}).`);
735
+ } else {
736
+ parts.push("No fallback preview image found; diagram structure preserved in package.");
737
+ }
738
+ parts.push("Edit in Word to modify diagram layout and content. Original DrawingML preserved for lossless export.");
739
+ return parts.join(" ");
643
740
  }
644
741
 
645
742
  function createShapeDetail(node: ShapeNode): string {
646
- const parts = ["Shape read-only preview."];
743
+ if (node.isTextBox) {
744
+ const parts = ["Text box."];
745
+ if (node.text) parts.push(`Content: "${node.text}".`);
746
+ parts.push("Text content is visible; formatting and geometry preserved for export.");
747
+ return parts.join(" ");
748
+ }
749
+ const parts = ["Drawing shape."];
647
750
  if (node.geometry) parts.push(`Geometry: ${node.geometry}.`);
648
- if (node.text) parts.push(`Text: "${node.text}".`);
649
- parts.push("Original XML preserved for export.");
751
+ if (node.text) parts.push(`Text content: "${node.text}".`);
752
+ parts.push("Visual geometry is preview-only. Original DrawingML preserved for export.");
650
753
  return parts.join(" ");
651
754
  }
652
755
 
653
756
  function createWordArtDetail(node: WordArtNode): string {
654
- const parts = ["WordArt read-only preview."];
757
+ const parts = ["WordArt decorative text."];
655
758
  if (node.text) parts.push(`Text: "${node.text}".`);
656
759
  if (node.geometry) parts.push(`Effect: ${node.geometry}.`);
657
- parts.push("Original XML preserved for export.");
760
+ parts.push("Text effect is preview-only. Edit in Word for full formatting. Original DrawingML preserved for export.");
658
761
  return parts.join(" ");
659
762
  }
660
763
 
661
764
  function createVmlDetail(node: VmlShapeNode): string {
662
- const parts = ["VML shape read-only preview."];
765
+ const parts = ["Legacy VML drawing."];
663
766
  if (node.shapeType) parts.push(`Type: ${node.shapeType}.`);
664
- if (node.text) parts.push(`Text: "${node.text}".`);
665
- parts.push("Legacy VML; original XML preserved for export.");
767
+ if (node.text) parts.push(`Text content: "${node.text}".`);
768
+ parts.push("VML content is preview-only. Edit in Word for full control. Original VML XML preserved for export.");
666
769
  return parts.join(" ");
667
770
  }
668
771
 
@@ -717,6 +820,131 @@ function createPlainText(
717
820
  return text.join("");
718
821
  }
719
822
 
823
+ function createSecondaryStorySurfaces(
824
+ document: CanonicalDocumentEnvelope,
825
+ ): SecondaryStorySurface[] {
826
+ const surfaces: SecondaryStorySurface[] = [];
827
+ const subParts = document.subParts;
828
+ if (!subParts) {
829
+ return surfaces;
830
+ }
831
+
832
+ const numberingPrefixResolver = createNumberingPrefixResolver(document.numbering);
833
+
834
+ for (const section of collectSectionContexts(document)) {
835
+ const headerVariants = resolveSectionVariants(
836
+ "header",
837
+ section.index,
838
+ section.properties?.headerReferences,
839
+ subParts.headers ?? [],
840
+ );
841
+ for (const headerVariant of headerVariants) {
842
+ const target: EditorStoryTarget = {
843
+ kind: "header",
844
+ relationshipId: headerVariant.relationshipId,
845
+ variant: headerVariant.variant,
846
+ sectionIndex: section.index,
847
+ };
848
+ const header = findHeaderFooterDocumentEntry(document, target);
849
+ if (!header) {
850
+ continue;
851
+ }
852
+ surfaces.push(
853
+ createStorySurface(
854
+ target,
855
+ `Header · ${headerVariant.variant}`,
856
+ header.blocks,
857
+ document,
858
+ numberingPrefixResolver,
859
+ ),
860
+ );
861
+ }
862
+
863
+ const footerVariants = resolveSectionVariants(
864
+ "footer",
865
+ section.index,
866
+ section.properties?.footerReferences,
867
+ subParts.footers ?? [],
868
+ );
869
+ for (const footerVariant of footerVariants) {
870
+ const target: EditorStoryTarget = {
871
+ kind: "footer",
872
+ relationshipId: footerVariant.relationshipId,
873
+ variant: footerVariant.variant,
874
+ sectionIndex: section.index,
875
+ };
876
+ const footer = findHeaderFooterDocumentEntry(document, target);
877
+ if (!footer) {
878
+ continue;
879
+ }
880
+ surfaces.push(
881
+ createStorySurface(
882
+ target,
883
+ `Footer · ${footerVariant.variant}`,
884
+ footer.blocks,
885
+ document,
886
+ numberingPrefixResolver,
887
+ ),
888
+ );
889
+ }
890
+ }
891
+
892
+ const footnotes = Object.values(subParts.footnoteCollection?.footnotes ?? {}).sort(compareNoteIds);
893
+ for (const note of footnotes) {
894
+ const target: EditorStoryTarget = { kind: "footnote", noteId: note.noteId };
895
+ surfaces.push(createStorySurface(target, `Footnote ${note.noteId}`, note.blocks, document, numberingPrefixResolver));
896
+ }
897
+
898
+ const endnotes = Object.values(subParts.footnoteCollection?.endnotes ?? {}).sort(compareNoteIds);
899
+ for (const note of endnotes) {
900
+ const target: EditorStoryTarget = { kind: "endnote", noteId: note.noteId };
901
+ surfaces.push(createStorySurface(target, `Endnote ${note.noteId}`, note.blocks, document, numberingPrefixResolver));
902
+ }
903
+
904
+ return surfaces;
905
+ }
906
+
907
+ function createStorySurface(
908
+ target: EditorStoryTarget,
909
+ label: string,
910
+ blocks: readonly BlockNode[],
911
+ document: CanonicalDocumentEnvelope,
912
+ numberingPrefixResolver: NumberingPrefixResolver,
913
+ ): SecondaryStorySurface {
914
+ const surfaceBlocks: SurfaceBlockSnapshot[] = [];
915
+ let cursor = 0;
916
+ const counters = {
917
+ paragraph: 0,
918
+ table: 0,
919
+ opaque: 0,
920
+ sdt: 0,
921
+ customXml: 0,
922
+ altChunk: 0,
923
+ };
924
+
925
+ for (let index = 0; index < blocks.length; index += 1) {
926
+ const surfaceBlock = createSurfaceBlock(
927
+ blocks[index],
928
+ document,
929
+ cursor,
930
+ counters,
931
+ numberingPrefixResolver,
932
+ );
933
+ surfaceBlocks.push(surfaceBlock.block);
934
+ cursor = surfaceBlock.nextCursor;
935
+ if (index < blocks.length - 1 && blocks[index + 1]?.type === "paragraph") {
936
+ cursor += 1;
937
+ }
938
+ }
939
+
940
+ return {
941
+ target,
942
+ label,
943
+ storySize: cursor,
944
+ blocks: surfaceBlocks,
945
+ };
946
+ }
947
+
720
948
  function createSecondaryStoryPreviewBlocks(
721
949
  document: CanonicalDocumentEnvelope,
722
950
  cursor: number,
@@ -856,15 +1084,16 @@ function summarizePreviewInline(node: InlineNode): string {
856
1084
  case "column_break":
857
1085
  return "[Column break]";
858
1086
  case "chart_preview":
859
- return "[Chart]";
1087
+ return "[Embedded chart]";
860
1088
  case "smartart_preview":
861
- return "[SmartArt]";
1089
+ return "[SmartArt diagram]";
862
1090
  case "shape":
863
- return node.text ? `[Shape: ${node.text}]` : "[Shape]";
1091
+ if (node.isTextBox && node.text) return `[Text box: ${node.text}]`;
1092
+ return node.text ? `[Shape: ${node.text}]` : "[Drawing shape]";
864
1093
  case "wordart":
865
1094
  return node.text ? `[WordArt: ${node.text}]` : "[WordArt]";
866
1095
  case "vml_shape":
867
- return node.text ? `[VML: ${node.text}]` : "[VML shape]";
1096
+ return node.text ? `[VML: ${node.text}]` : "[Legacy VML drawing]";
868
1097
  }
869
1098
  }
870
1099
 
@@ -887,7 +1116,51 @@ function toSurfaceTabStop(
887
1116
 
888
1117
  function describePreservedInlinePreview(
889
1118
  payloadReference: string,
890
- ): { label: string; detail: string } | null {
1119
+ ): {
1120
+ label: string;
1121
+ detail: string;
1122
+ presentation?: "inline-chip" | "quiet-marker";
1123
+ } | null {
1124
+ if (/\b(?:w:)?proofErr\b/u.test(payloadReference)) {
1125
+ const proofType = /\bw:type="([^"]+)"/u.exec(payloadReference)?.[1];
1126
+ return {
1127
+ label: "Proofing marker",
1128
+ detail:
1129
+ proofType && proofType.trim().length > 0
1130
+ ? `Word proofing marker (${proofType}) preserved for export safety.`
1131
+ : "Word proofing marker preserved for export safety.",
1132
+ presentation: "quiet-marker",
1133
+ };
1134
+ }
1135
+
1136
+ if (/\b(?:w:)?lastRenderedPageBreak\b/u.test(payloadReference)) {
1137
+ return {
1138
+ label: "Rendered page break",
1139
+ detail: "Word rendered page-break marker preserved for export safety.",
1140
+ presentation: "quiet-marker",
1141
+ };
1142
+ }
1143
+
1144
+ if (/\b(?:w:)?permStart\b/u.test(payloadReference)) {
1145
+ const editorGroup = /\bw:edGrp="([^"]+)"/u.exec(payloadReference)?.[1];
1146
+ return {
1147
+ label: "Protected range start",
1148
+ detail:
1149
+ editorGroup && editorGroup.trim().length > 0
1150
+ ? `Protected range start for ${editorGroup} preserved for export safety.`
1151
+ : "Protected range start preserved for export safety.",
1152
+ presentation: "quiet-marker",
1153
+ };
1154
+ }
1155
+
1156
+ if (/\b(?:w:)?permEnd\b/u.test(payloadReference)) {
1157
+ return {
1158
+ label: "Protected range end",
1159
+ detail: "Protected range end preserved for export safety.",
1160
+ presentation: "quiet-marker",
1161
+ };
1162
+ }
1163
+
891
1164
  if (/\b(?:w:)?bookmarkStart\b/u.test(payloadReference)) {
892
1165
  const name = /\bw:name="([^"]+)"/u.exec(payloadReference)?.[1];
893
1166
  return {
@@ -912,8 +1185,15 @@ function describePreservedInlinePreview(
912
1185
  .join("")
913
1186
  .trim();
914
1187
  const instruction = (simpleInstruction ?? complexInstruction ?? "").trim();
1188
+ const family = /^([A-Z]+)/i.exec(instruction)?.[1]?.toUpperCase();
1189
+ const label =
1190
+ family === "TOC"
1191
+ ? "Table of Contents field"
1192
+ : family
1193
+ ? `${family} field`
1194
+ : "Field";
915
1195
  return {
916
- label: "Field",
1196
+ label,
917
1197
  detail:
918
1198
  instruction.length > 0
919
1199
  ? `Read-only field preserved for export safety. Instruction: ${instruction}.`
@@ -955,6 +1235,8 @@ function cloneMarks(marks: TextMark[]): {
955
1235
  fontSize?: number;
956
1236
  textColor?: string;
957
1237
  } = {};
1238
+ let shadingColor: string | undefined;
1239
+ let highlightColor: string | undefined;
958
1240
  for (const mark of marks) {
959
1241
  switch (mark.type) {
960
1242
  case "bold":
@@ -973,7 +1255,10 @@ function cloneMarks(marks: TextMark[]): {
973
1255
  else if (mark.val < 0) supported.push("subscript");
974
1256
  break;
975
1257
  case "backgroundColor":
976
- attrs.backgroundColor = mark.color;
1258
+ shadingColor = mark.color;
1259
+ break;
1260
+ case "highlight":
1261
+ highlightColor = mark.color;
977
1262
  break;
978
1263
  case "charSpacing":
979
1264
  attrs.charSpacing = mark.val;
@@ -1004,6 +1289,9 @@ function cloneMarks(marks: TextMark[]): {
1004
1289
  break;
1005
1290
  }
1006
1291
  }
1292
+ if (highlightColor || shadingColor) {
1293
+ attrs.backgroundColor = highlightColor ?? shadingColor;
1294
+ }
1007
1295
  const hasAttrs = Object.keys(attrs).length > 0;
1008
1296
  return hasAttrs ? { marks: supported, markAttrs: attrs } : { marks: supported };
1009
1297
  }