@beyondwork/docx-react-component 1.0.56 → 1.0.58

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 (113) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/api/public-types.ts +330 -0
  4. package/src/compare/diff-engine.ts +3 -0
  5. package/src/core/commands/formatting-commands.ts +1 -0
  6. package/src/core/commands/index.ts +17 -11
  7. package/src/core/selection/mapping.ts +18 -1
  8. package/src/core/selection/review-anchors.ts +29 -18
  9. package/src/io/chart-preview-resolver.ts +175 -41
  10. package/src/io/docx-session.ts +57 -2
  11. package/src/io/export/serialize-main-document.ts +82 -0
  12. package/src/io/export/serialize-styles.ts +61 -3
  13. package/src/io/export/table-properties-xml.ts +19 -4
  14. package/src/io/normalize/normalize-text.ts +33 -0
  15. package/src/io/ooxml/parse-anchor.ts +182 -0
  16. package/src/io/ooxml/parse-drawing.ts +319 -0
  17. package/src/io/ooxml/parse-fields.ts +115 -2
  18. package/src/io/ooxml/parse-fill.ts +215 -0
  19. package/src/io/ooxml/parse-font-table.ts +190 -0
  20. package/src/io/ooxml/parse-footnotes.ts +52 -1
  21. package/src/io/ooxml/parse-main-document.ts +241 -1
  22. package/src/io/ooxml/parse-numbering.ts +96 -0
  23. package/src/io/ooxml/parse-picture.ts +158 -0
  24. package/src/io/ooxml/parse-settings.ts +34 -0
  25. package/src/io/ooxml/parse-shapes.ts +87 -0
  26. package/src/io/ooxml/parse-solid-fill.ts +11 -0
  27. package/src/io/ooxml/parse-styles.ts +74 -1
  28. package/src/io/ooxml/parse-theme.ts +60 -0
  29. package/src/io/paste/html-clipboard.ts +449 -0
  30. package/src/io/paste/word-clipboard.ts +5 -1
  31. package/src/legal/_document-root.ts +26 -0
  32. package/src/legal/bookmarks.ts +4 -3
  33. package/src/legal/cross-references.ts +3 -2
  34. package/src/legal/defined-terms.ts +2 -1
  35. package/src/legal/signature-blocks.ts +2 -1
  36. package/src/model/canonical-document.ts +421 -3
  37. package/src/runtime/chart/chart-model-store.ts +73 -10
  38. package/src/runtime/document-runtime.ts +760 -41
  39. package/src/runtime/document-search.ts +61 -0
  40. package/src/runtime/edit-ops/index.ts +129 -0
  41. package/src/runtime/event-refresh-hints.ts +7 -0
  42. package/src/runtime/field-resolver.ts +341 -0
  43. package/src/runtime/footnote-resolver.ts +55 -0
  44. package/src/runtime/hyperlink-color-resolver.ts +13 -10
  45. package/src/runtime/object-grab/index.ts +51 -0
  46. package/src/runtime/paragraph-style-resolver.ts +105 -0
  47. package/src/runtime/query-scopes.ts +186 -0
  48. package/src/runtime/resolved-numbering-geometry.ts +12 -0
  49. package/src/runtime/scope-resolver.ts +60 -0
  50. package/src/runtime/selection/cursor-ops.ts +186 -15
  51. package/src/runtime/selection/index.ts +17 -1
  52. package/src/runtime/structure-ops/index.ts +77 -0
  53. package/src/runtime/styles-cascade.ts +33 -0
  54. package/src/runtime/surface-projection.ts +192 -12
  55. package/src/runtime/theme-color-resolver.ts +189 -44
  56. package/src/runtime/units.ts +46 -0
  57. package/src/runtime/view-state.ts +13 -2
  58. package/src/ui/WordReviewEditor.tsx +239 -11
  59. package/src/ui/editor-runtime-boundary.ts +97 -1
  60. package/src/ui/editor-shell-view.tsx +1 -1
  61. package/src/ui/runtime-shortcut-dispatch.ts +17 -3
  62. package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
  63. package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
  64. package/src/ui-tailwind/chart/render/area.tsx +22 -4
  65. package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
  66. package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
  67. package/src/ui-tailwind/chart/render/combo.tsx +37 -4
  68. package/src/ui-tailwind/chart/render/line.tsx +28 -5
  69. package/src/ui-tailwind/chart/render/pie.tsx +36 -16
  70. package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
  71. package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
  72. package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
  73. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
  74. package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
  75. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
  76. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
  77. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
  78. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
  79. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
  80. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
  81. package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
  82. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +24 -0
  83. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
  85. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +157 -0
  86. package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
  87. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
  88. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
  89. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
  90. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
  91. package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
  92. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
  93. package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
  94. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
  95. package/src/ui-tailwind/editor-surface/pm-schema.ts +214 -11
  96. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +32 -2
  97. package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
  98. package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
  99. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
  100. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
  101. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
  102. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
  103. package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
  104. package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
  105. package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
  106. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
  107. package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
  108. package/src/ui-tailwind/theme/editor-theme.css +1 -0
  109. package/src/ui-tailwind/theme/tokens.css +6 -0
  110. package/src/ui-tailwind/theme/tokens.ts +10 -0
  111. package/src/ui-tailwind/tw-review-workspace.tsx +23 -0
  112. package/src/validation/compatibility-engine.ts +2 -0
  113. package/src/validation/docx-comment-proof.ts +12 -3
@@ -15,8 +15,12 @@ import type { CanonicalDocumentEnvelope, SelectionSnapshot } from "../../core/st
15
15
  import {
16
16
  moveCharLeft,
17
17
  moveCharRight,
18
+ moveDown,
19
+ moveLineEnd,
20
+ moveLineStart,
18
21
  moveParagraphEnd,
19
22
  moveParagraphStart,
23
+ moveUp,
20
24
  moveWordLeft,
21
25
  moveWordRight,
22
26
  type CursorMoveOptions,
@@ -39,7 +43,11 @@ export type CursorMoveOp =
39
43
  | "word-left"
40
44
  | "word-right"
41
45
  | "paragraph-start"
42
- | "paragraph-end";
46
+ | "paragraph-end"
47
+ | "up"
48
+ | "down"
49
+ | "line-start"
50
+ | "line-end";
43
51
 
44
52
  export interface SelectionLayer {
45
53
  /**
@@ -83,6 +91,14 @@ export const selectionLayer: SelectionLayer = {
83
91
  return moveParagraphStart(doc, selection, options);
84
92
  case "paragraph-end":
85
93
  return moveParagraphEnd(doc, selection, options);
94
+ case "up":
95
+ return moveUp(doc, selection, options);
96
+ case "down":
97
+ return moveDown(doc, selection, options);
98
+ case "line-start":
99
+ return moveLineStart(doc, selection, options);
100
+ case "line-end":
101
+ return moveLineEnd(doc, selection, options);
86
102
  }
87
103
  },
88
104
  validate(doc, selection, maxOffset) {
@@ -0,0 +1,77 @@
1
+ /**
2
+ * R.3 StructureLayer — named shell module for structural mutations. See
3
+ * `docs/plans/lane-1-editing-foundation.md` §R.3.
4
+ *
5
+ * Mirrors R.2 EditLayer's pattern: a thin named entry point over existing
6
+ * pure functions. The layer's purpose is to be the single site where callers
7
+ * reach for table / list / section / fragment-splice concerns, so R.5.b
8
+ * (post-edit validation hook) and R.5.a (action bracketing) can attach to a
9
+ * single seam instead of a fanout of helper call-sites.
10
+ *
11
+ * Phase 6/Item-2 scope: expose `applyFragmentInsert` (shipped in I2 Tier B
12
+ * Slice 1). Adding list/table/section-op wrappers is deferred because those
13
+ * existing command helpers already have rich type surfaces that don't need a
14
+ * named re-export to be callable — reach for them directly via
15
+ * `src/core/commands/{list,table,section}-commands.ts` until a specific case
16
+ * surfaces requiring the seam. Keeping this interface minimal avoids the
17
+ * "enumeration trap" that larger shell refactors fall into.
18
+ */
19
+
20
+ import type { CanonicalDocumentFragment } from "../../api/public-types.ts";
21
+ import type {
22
+ CanonicalDocumentEnvelope,
23
+ SelectionSnapshot,
24
+ } from "../../core/state/editor-state.ts";
25
+ import type { StructuralMutationResult } from "../../core/commands/structural-helpers.ts";
26
+ import type { TextCommandContext } from "../../core/commands/text-commands.ts";
27
+ import { countLogicalPositions, parseTextStory } from "../../core/schema/text-schema.ts";
28
+ import { validateSelectionAgainstDocument } from "../selection/post-edit-validator.ts";
29
+ import { applyFragmentInsert } from "./fragment-insert.ts";
30
+
31
+ export interface StructureLayer {
32
+ /**
33
+ * Splice a `CanonicalDocumentFragment` at the current selection. Baseline
34
+ * semantic: the caret paragraph is split; fragment blocks are inserted
35
+ * between the two halves; range selections are deleted first. Empty
36
+ * fragments are no-ops.
37
+ */
38
+ applyFragmentInsert(
39
+ doc: CanonicalDocumentEnvelope,
40
+ selection: SelectionSnapshot,
41
+ fragment: CanonicalDocumentFragment,
42
+ context: TextCommandContext,
43
+ ): StructuralMutationResult;
44
+ }
45
+
46
+ /**
47
+ * R.5.b — every StructureLayer return passes through the post-edit selection
48
+ * validator before leaving the layer. Structural mutations can legitimately
49
+ * shrink the document (fragment with empty trailing paragraphs, etc.), so
50
+ * clamping here is a defense-in-depth hook matching the EditLayer's.
51
+ *
52
+ * `StructuralMutationResult` doesn't carry a pre-computed `storyText`, so
53
+ * we compute the post-mutation story size via `countLogicalPositions` over
54
+ * the parsed story. That's one parse per structural op — acceptable; these
55
+ * ops are off the typing hot path.
56
+ */
57
+ function validateResult(result: StructuralMutationResult): StructuralMutationResult {
58
+ if (!result.changed) {
59
+ return result;
60
+ }
61
+ const story = parseTextStory(result.document.content);
62
+ const maxOffset = countLogicalPositions(story.units);
63
+ const validated = validateSelectionAgainstDocument(result.document, result.selection, maxOffset);
64
+ if (validated === result.selection) {
65
+ return result;
66
+ }
67
+ return { ...result, selection: validated };
68
+ }
69
+
70
+ /**
71
+ * Default stateless StructureLayer instance. Safe to share across runtimes.
72
+ */
73
+ export const structureLayer: StructureLayer = {
74
+ applyFragmentInsert(doc, selection, fragment, context) {
75
+ return validateResult(applyFragmentInsert(doc, selection, fragment, context));
76
+ },
77
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Central entry point for OOXML style cascade resolution.
3
+ *
4
+ * Re-exports the paragraph, character, run, numbering-marker, and table style
5
+ * resolvers from their individual modules so downstream consumers (Lane 3a
6
+ * measurement, Lane 1 style picker, agent tooling) can import from a single,
7
+ * stable location.
8
+ *
9
+ * Adding a new resolver? Add it to its feature module (paragraph, table, etc.)
10
+ * and re-export from here. Do not define new cascade logic in this file.
11
+ */
12
+
13
+ export {
14
+ resolveParagraphStyleChain,
15
+ resolveCharacterStyleChain,
16
+ resolveEffectiveParagraphFormatting,
17
+ resolveEffectiveRunFormatting,
18
+ resolveNumberingMarkerRunFormatting,
19
+ resolveTableCellTextFormatting,
20
+ resolveRunFontFamily,
21
+ getNextStyleId,
22
+ type ParagraphResolveInput,
23
+ type RunResolveInput,
24
+ type MarkerResolveInput,
25
+ } from "./paragraph-style-resolver.ts";
26
+
27
+ export {
28
+ resolveTableStyleResolution,
29
+ type ResolvedTableCellStyle,
30
+ type ResolvedTableRowStyle,
31
+ type ResolvedTableLevelProperties,
32
+ type ResolvedTableStyleResolution,
33
+ } from "./table-style-resolver.ts";
@@ -3,7 +3,9 @@ import type {
3
3
  EditorSurfaceSnapshot,
4
4
  SecondaryStorySurface,
5
5
  SurfaceBlockSnapshot,
6
+ SurfaceDrawingAnchor,
6
7
  SurfaceInlineSegment,
8
+ SurfacePictureEffects,
7
9
  SurfaceTableCellSnapshot,
8
10
  SurfaceTableRowSnapshot,
9
11
  SurfaceTextMark,
@@ -30,6 +32,9 @@ import type {
30
32
  TableCellBorders,
31
33
  TableNode,
32
34
  TextMark,
35
+ DrawingFrameNode,
36
+ PictureContent,
37
+ ShapeContent,
33
38
  VmlShapeNode,
34
39
  WordArtNode,
35
40
  } from "../model/canonical-document.ts";
@@ -57,7 +62,7 @@ import {
57
62
  resolveNumberingMarkerRunFormatting,
58
63
  } from "./paragraph-style-resolver.ts";
59
64
  import { resolveHyperlinkRunFormatting } from "./hyperlink-color-resolver.ts";
60
- import { concretizeThemeColors } from "./theme-color-resolver.ts";
65
+ import { concretizeThemeColors, ThemeColorResolver } from "./theme-color-resolver.ts";
61
66
  import type { CanonicalParagraphFormatting, CanonicalRunFormatting } from "../model/canonical-document.ts";
62
67
 
63
68
  interface ParagraphAccumulator {
@@ -93,6 +98,13 @@ export function createEditorSurfaceSnapshot(
93
98
  const blocks: SurfaceBlockSnapshot[] = [];
94
99
  const lockedFragmentIds: string[] = [];
95
100
  const numberingPrefixResolver = createNumberingPrefixResolver(document.numbering);
101
+ // Open a chartModelStore build pass tagged with the document envelope.
102
+ // Every `chart_preview` node populated during the pass records its id
103
+ // in the pass's seen-set; `endBuildPass()` below evicts store entries
104
+ // from previous documents (different owner) and stale entries from
105
+ // earlier builds (same owner, not seen this pass). Previously the
106
+ // store grew unbounded across document loads — documented v1 gap.
107
+ chartModelStore.beginBuildPass(document);
96
108
  let cursor = 0;
97
109
  const counters = {
98
110
  paragraph: 0,
@@ -157,6 +169,12 @@ export function createEditorSurfaceSnapshot(
157
169
 
158
170
  const secondaryStories = createSecondaryStorySurfaces(document, numberingPrefixResolver);
159
171
 
172
+ // Close the chartModelStore build pass. Evicts entries from previous
173
+ // documents + stale entries whose ids no longer appear in the current
174
+ // document. Must happen AFTER secondary stories (they may also contain
175
+ // chart_preview nodes).
176
+ chartModelStore.endBuildPass();
177
+
160
178
  return {
161
179
  storySize: cursor,
162
180
  plainText: createPlainText(blocks),
@@ -712,6 +730,12 @@ function createParagraphBlock(
712
730
  const lockedFragmentIds: string[] = [];
713
731
  let cursor = start;
714
732
  const children = Array.isArray(paragraph.children) ? paragraph.children : [];
733
+ // Build once per paragraph block — ThemeColorResolver is a thin wrapper
734
+ // around CanonicalTheme that applies clrMap remapping. Constructed here so
735
+ // it is not recreated on every text segment (inner hot loop).
736
+ const themeResolver = document.subParts?.canonicalTheme
737
+ ? new ThemeColorResolver(document.subParts.canonicalTheme)
738
+ : undefined;
715
739
 
716
740
  for (const child of children) {
717
741
  const result = appendInlineSegments(
@@ -722,6 +746,7 @@ function createParagraphBlock(
722
746
  promoteSecondaryStoryTextBoxes,
723
747
  undefined,
724
748
  cullBuild,
749
+ themeResolver,
725
750
  );
726
751
  cursor = result.nextCursor;
727
752
  lockedFragmentIds.push(...result.lockedFragmentIds);
@@ -885,6 +910,7 @@ function appendInlineSegments(
885
910
  promoteSecondaryStoryTextBoxes: boolean,
886
911
  hyperlinkHref?: string,
887
912
  cullBuild: boolean = false,
913
+ themeResolver?: ThemeColorResolver,
888
914
  ): { nextCursor: number; lockedFragmentIds: string[] } {
889
915
  switch (node.type) {
890
916
  case "text": {
@@ -919,11 +945,11 @@ function appendInlineSegments(
919
945
  ? resolveHyperlinkRunFormatting(
920
946
  runResolveInput,
921
947
  document.styles,
922
- document.subParts?.resolvedTheme,
948
+ themeResolver,
923
949
  )
924
950
  : concretizeThemeColors(
925
951
  resolveEffectiveRunFormatting(runResolveInput, document.styles),
926
- document.subParts?.resolvedTheme,
952
+ themeResolver,
927
953
  );
928
954
  paragraph.segments.push({
929
955
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
@@ -967,6 +993,7 @@ function appendInlineSegments(
967
993
  promoteSecondaryStoryTextBoxes,
968
994
  node.href,
969
995
  cullBuild,
996
+ themeResolver,
970
997
  );
971
998
  cursor = result.nextCursor;
972
999
  }
@@ -1020,15 +1047,18 @@ function appendInlineSegments(
1020
1047
  let parsedChartId: string | undefined;
1021
1048
  if (node.parsedData) {
1022
1049
  parsedChartId = stableChartId(node.rawXml);
1023
- if (!chartModelStore.has(parsedChartId)) {
1024
- const { widthPx, heightPx } = extractChartDimensions(node.rawXml);
1025
- chartModelStore.set(parsedChartId, {
1026
- model: node.parsedData,
1027
- widthPx,
1028
- heightPx,
1029
- theme: undefined,
1030
- });
1031
- }
1050
+ // Always call `set` (even when the entry exists) so the active
1051
+ // `chartModelStore` build pass records the id in its seen-set —
1052
+ // the pass uses the seen-set to evict stale entries from earlier
1053
+ // builds. The entry object is a tiny reference so the set cost
1054
+ // is negligible.
1055
+ const { widthPx, heightPx } = extractChartDimensions(node.rawXml);
1056
+ chartModelStore.set(parsedChartId, {
1057
+ model: node.parsedData,
1058
+ widthPx,
1059
+ heightPx,
1060
+ theme: undefined,
1061
+ });
1032
1062
  }
1033
1063
  return appendComplexPreviewSegment(paragraph, node, start, "Embedded chart", createChartDetail(node), {
1034
1064
  previewMediaId: node.previewMediaId,
@@ -1064,6 +1094,58 @@ function appendInlineSegments(
1064
1094
  );
1065
1095
  }
1066
1096
  return appendComplexPreviewSegment(paragraph, node, start, "Legacy VML drawing", createVmlDetail(node));
1097
+ case "drawing_frame": {
1098
+ const c = node.content;
1099
+ if (c.type === "picture") {
1100
+ const mediaId = c.mediaId ?? `drawing-frame-${start}`;
1101
+ const state: "editable" | "missing" = c.mediaId ? "editable" : "missing";
1102
+ const anchor = surfaceAnchorFromGeometry(node.anchor);
1103
+ const pictureEffects = surfacePictureEffectsFromContent(c);
1104
+ paragraph.segments.push({
1105
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
1106
+ kind: "image",
1107
+ from: start,
1108
+ to: start + 1,
1109
+ mediaId,
1110
+ state,
1111
+ display: node.anchor.display,
1112
+ detail: `Drawing frame image (${node.anchor.extent.widthEmu}\u00d7${node.anchor.extent.heightEmu} EMU).`,
1113
+ ...(anchor ? { anchor } : {}),
1114
+ ...(pictureEffects ? { pictureEffects } : {}),
1115
+ });
1116
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
1117
+ }
1118
+ if (c.type === "shape") {
1119
+ const label = c.isTextBox ? "Text box" : "Drawing shape";
1120
+ const detail = `DrawingFrame shape (${node.anchor.wrapMode}).`;
1121
+ const anchor = surfaceAnchorFromGeometry(node.anchor);
1122
+ const txbxText = c.isTextBox ? extractTxbxFirstText(c.txbxBlocks) : undefined;
1123
+ const surfaceFill = c.fill;
1124
+ paragraph.segments.push({
1125
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
1126
+ kind: "shape",
1127
+ from: start,
1128
+ to: start + 1,
1129
+ label,
1130
+ detail,
1131
+ ...(anchor ? { anchor } : {}),
1132
+ ...(c.geometry !== undefined ? { geometry: c.geometry } : {}),
1133
+ ...(surfaceFill ? { fill: surfaceFill } : {}),
1134
+ ...(c.line ? { line: c.line } : {}),
1135
+ ...(c.isTextBox ? { isTextBox: true } : {}),
1136
+ ...(txbxText ? { txbxText } : {}),
1137
+ });
1138
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
1139
+ }
1140
+ const rawXml = "rawXml" in c ? c.rawXml : "";
1141
+ const label =
1142
+ c.type === "chart_preview"
1143
+ ? "Embedded chart"
1144
+ : c.type === "smartart_preview"
1145
+ ? "SmartArt diagram"
1146
+ : "Drawing frame";
1147
+ return appendComplexPreviewSegment(paragraph, { rawXml } as DrawingFrameNode["content"] & { rawXml: string }, start, label, `DrawingFrame ${c.type} (${node.anchor.wrapMode}).`);
1148
+ }
1067
1149
  case "symbol":
1068
1150
  paragraph.segments.push({
1069
1151
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
@@ -1126,6 +1208,7 @@ function appendInlineSegments(
1126
1208
  promoteSecondaryStoryTextBoxes,
1127
1209
  refHyperlinkHref ?? hyperlinkHref,
1128
1210
  cullBuild,
1211
+ themeResolver,
1129
1212
  );
1130
1213
  cursor = result.nextCursor;
1131
1214
  lockedIds.push(...result.lockedFragmentIds);
@@ -1181,6 +1264,101 @@ function appendInlineSegments(
1181
1264
  }
1182
1265
  }
1183
1266
 
1267
+ /**
1268
+ * V2c.4 — Map a canonical `AnchorGeometry` onto the surface anchor type.
1269
+ * Returns `undefined` when the anchor carries only the trivial inline
1270
+ * defaults (no positioning / wrap / dist data) so the emitted segment
1271
+ * stays minimal for the common inline-image case.
1272
+ */
1273
+ function surfaceAnchorFromGeometry(
1274
+ anchor: DrawingFrameNode["anchor"],
1275
+ ): SurfaceDrawingAnchor | undefined {
1276
+ // Cheap inline-default short-circuit: inline pictures with no extra
1277
+ // metadata (no positionH/V, no distMargins, no docPr, no behindDoc/etc.)
1278
+ // don't need the anchor bag — N9 float-wrap consumers only care about
1279
+ // floating drawings or anchors with custom positioning.
1280
+ const trivialInline =
1281
+ anchor.display === "inline" &&
1282
+ anchor.wrapMode === "none" &&
1283
+ !anchor.positionH &&
1284
+ !anchor.positionV &&
1285
+ !anchor.distMargins &&
1286
+ anchor.relativeHeight === undefined &&
1287
+ anchor.behindDoc === undefined &&
1288
+ anchor.layoutInCell === undefined &&
1289
+ anchor.allowOverlap === undefined &&
1290
+ anchor.simplePos === undefined &&
1291
+ !anchor.docPr;
1292
+ if (trivialInline) return undefined;
1293
+ return {
1294
+ display: anchor.display,
1295
+ wrapMode: anchor.wrapMode,
1296
+ extent: { ...anchor.extent },
1297
+ ...(anchor.positionH ? { positionH: { ...anchor.positionH } } : {}),
1298
+ ...(anchor.positionV ? { positionV: { ...anchor.positionV } } : {}),
1299
+ ...(anchor.distMargins ? { distMargins: { ...anchor.distMargins } } : {}),
1300
+ ...(anchor.relativeHeight !== undefined ? { relativeHeight: anchor.relativeHeight } : {}),
1301
+ ...(anchor.behindDoc !== undefined ? { behindDoc: anchor.behindDoc } : {}),
1302
+ ...(anchor.layoutInCell !== undefined ? { layoutInCell: anchor.layoutInCell } : {}),
1303
+ ...(anchor.allowOverlap !== undefined ? { allowOverlap: anchor.allowOverlap } : {}),
1304
+ ...(anchor.simplePos !== undefined ? { simplePos: anchor.simplePos } : {}),
1305
+ ...(anchor.docPr ? { docPr: { ...anchor.docPr } } : {}),
1306
+ };
1307
+ }
1308
+
1309
+ /**
1310
+ * V2c.4 — Project picture-effect fields from `PictureContent`. Returns
1311
+ * `undefined` when none of the effect fields are set so consumers can
1312
+ * fast-path image rendering.
1313
+ */
1314
+ function surfacePictureEffectsFromContent(
1315
+ content: PictureContent,
1316
+ ): SurfacePictureEffects | undefined {
1317
+ const has =
1318
+ content.srcRect !== undefined ||
1319
+ content.rotation !== undefined ||
1320
+ content.flipH !== undefined ||
1321
+ content.flipV !== undefined ||
1322
+ content.presetGeom !== undefined ||
1323
+ content.stretch !== undefined ||
1324
+ content.softEdgeRadius !== undefined ||
1325
+ content.outerShadow !== undefined ||
1326
+ content.glow !== undefined;
1327
+ if (!has) return undefined;
1328
+ return {
1329
+ ...(content.srcRect ? { srcRect: { ...content.srcRect } } : {}),
1330
+ ...(content.rotation !== undefined ? { rotation: content.rotation } : {}),
1331
+ ...(content.flipH !== undefined ? { flipH: content.flipH } : {}),
1332
+ ...(content.flipV !== undefined ? { flipV: content.flipV } : {}),
1333
+ ...(content.presetGeom !== undefined ? { presetGeom: content.presetGeom } : {}),
1334
+ ...(content.stretch !== undefined ? { stretch: content.stretch } : {}),
1335
+ ...(content.softEdgeRadius !== undefined ? { softEdgeRadius: content.softEdgeRadius } : {}),
1336
+ ...(content.outerShadow !== undefined ? { outerShadow: { ...content.outerShadow } } : {}),
1337
+ ...(content.glow !== undefined ? { glow: { ...content.glow } } : {}),
1338
+ };
1339
+ }
1340
+
1341
+ /**
1342
+ * V2c.5 — Extract the first paragraph's plain text from a parsed
1343
+ * `txbxBlocks` tree for the `txbxText` segment preview. The recursion
1344
+ * is shallow — we only walk paragraph-shaped blocks at the top level
1345
+ * and read the immediate run content. Returns `undefined` when no text
1346
+ * is present.
1347
+ */
1348
+ function extractTxbxFirstText(
1349
+ blocks: ShapeContent["txbxBlocks"],
1350
+ ): string | undefined {
1351
+ if (!blocks || blocks.length === 0) return undefined;
1352
+ for (const block of blocks) {
1353
+ if (block.type !== "paragraph") continue;
1354
+ const runs = (block as { runs?: ReadonlyArray<{ text?: string }> }).runs;
1355
+ if (!runs) continue;
1356
+ const text = runs.map((r) => r.text ?? "").join("").trim();
1357
+ if (text) return text;
1358
+ }
1359
+ return undefined;
1360
+ }
1361
+
1184
1362
  function appendComplexPreviewSegment(
1185
1363
  paragraph: ParagraphAccumulator,
1186
1364
  node: { rawXml: string },
@@ -1616,6 +1794,8 @@ function summarizePreviewInline(node: InlineNode): string {
1616
1794
  return node.text ? `[WordArt: ${node.text}]` : "[WordArt]";
1617
1795
  case "vml_shape":
1618
1796
  return node.text ? `[VML: ${node.text}]` : "[Legacy VML drawing]";
1797
+ case "drawing_frame":
1798
+ return node.content.type === "picture" ? "[Image]" : "[Drawing]";
1619
1799
  }
1620
1800
  }
1621
1801