@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
@@ -0,0 +1,349 @@
1
+ import type {
2
+ CommentSidebarSnapshot,
3
+ EditorAnchorProjection,
4
+ EditorStoryTarget,
5
+ FieldSnapshot,
6
+ ProtectionSnapshot,
7
+ RuntimeRenderSnapshot,
8
+ SurfaceBlockSnapshot,
9
+ SurfaceInlineSegment,
10
+ TrackedChangesSnapshot,
11
+ WorkflowCandidateRange,
12
+ WorkflowCandidateRangeOptions,
13
+ WorkflowFieldMarkup,
14
+ WorkflowHighlightMarkup,
15
+ WorkflowMarkupItem,
16
+ WorkflowMarkupSnapshot,
17
+ WorkflowOpaqueFragmentMarkup,
18
+ WorkflowProtectedRangeMarkup,
19
+ WorkflowRevisionMarkup,
20
+ WorkflowCommentMarkup,
21
+ } from "../api/public-types";
22
+ import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
23
+ import { searchSurfaceBlocks } from "../core/search/search-text.ts";
24
+ import { describeOpaqueFragment } from "../preservation/store.ts";
25
+ import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
26
+
27
+ const BLOCKED_IMPORT_FEATURE_KEYS = new Set(["alt-chunk", "alternate-content", "custom-xml"]);
28
+
29
+ export function collectWorkflowMarkupSnapshot(input: {
30
+ renderSnapshot: RuntimeRenderSnapshot;
31
+ fieldSnapshot: FieldSnapshot;
32
+ protectionSnapshot: ProtectionSnapshot;
33
+ preservation: CanonicalDocumentEnvelope["preservation"];
34
+ }): WorkflowMarkupSnapshot {
35
+ const highlights: WorkflowHighlightMarkup[] = [];
36
+ const fields: WorkflowFieldMarkup[] = [];
37
+ const opaqueFragments: WorkflowOpaqueFragmentMarkup[] = [];
38
+ const surface = input.renderSnapshot.surface;
39
+
40
+ if (surface) {
41
+ collectSurfaceMarkup(
42
+ surface.blocks,
43
+ MAIN_STORY_TARGET,
44
+ input.preservation,
45
+ highlights,
46
+ opaqueFragments,
47
+ );
48
+ for (const story of surface.secondaryStories) {
49
+ collectSurfaceMarkup(
50
+ story.blocks,
51
+ story.target,
52
+ input.preservation,
53
+ highlights,
54
+ opaqueFragments,
55
+ );
56
+ }
57
+
58
+ fields.push(...collectFieldMarkup(surface, input.fieldSnapshot));
59
+ }
60
+ opaqueFragments.push(...collectOpaqueFragmentMarkup(input.preservation, opaqueFragments));
61
+
62
+ const comments = input.renderSnapshot.comments.threads.map((thread): WorkflowCommentMarkup => ({
63
+ markupId: `comment:${thread.commentId}`,
64
+ kind: "comment",
65
+ commentId: thread.commentId,
66
+ anchor: thread.anchor,
67
+ label: thread.anchorLabel,
68
+ excerpt: thread.excerpt,
69
+ status: thread.status,
70
+ warningCount: thread.warningCount,
71
+ entryCount: thread.entryCount,
72
+ createdAt: thread.createdAt,
73
+ createdBy: thread.createdBy,
74
+ }));
75
+
76
+ const revisions = input.renderSnapshot.trackedChanges.revisions.map(
77
+ (revision): WorkflowRevisionMarkup => ({
78
+ markupId: `revision:${revision.revisionId}`,
79
+ kind: "revision",
80
+ revisionId: revision.revisionId,
81
+ revisionKind: revision.kind,
82
+ anchor: revision.anchor,
83
+ label: revision.label,
84
+ excerpt: revision.excerpt,
85
+ status: revision.status,
86
+ actionability: revision.actionability,
87
+ authorId: revision.authorId,
88
+ createdAt: revision.createdAt,
89
+ preserveOnlyReason: revision.preserveOnlyReason,
90
+ }),
91
+ );
92
+
93
+ const protectedRanges = input.protectionSnapshot.ranges
94
+ .filter(
95
+ (range): range is typeof range & { start: number; end: number } =>
96
+ typeof range.start === "number" && typeof range.end === "number",
97
+ )
98
+ .map(
99
+ (range): WorkflowProtectedRangeMarkup => ({
100
+ markupId: `protected-range:${range.rangeId}`,
101
+ kind: "protected_range",
102
+ rangeId: range.rangeId,
103
+ anchor: createRangeAnchor(range.start, range.end),
104
+ storyTarget: MAIN_STORY_TARGET,
105
+ label: `Protected range ${range.rangeId}`,
106
+ excerpt: range.enforcementReason,
107
+ enforced: range.enforced,
108
+ enforcementReason: range.enforcementReason,
109
+ editorGroup: range.editorGroup,
110
+ editor: range.editor,
111
+ }),
112
+ );
113
+
114
+ const items: WorkflowMarkupItem[] = [
115
+ ...highlights,
116
+ ...comments,
117
+ ...revisions,
118
+ ...fields,
119
+ ...protectedRanges,
120
+ ...opaqueFragments,
121
+ ];
122
+
123
+ return {
124
+ totalCount: items.length,
125
+ items,
126
+ highlights,
127
+ comments,
128
+ revisions,
129
+ fields,
130
+ protectedRanges,
131
+ opaqueFragments,
132
+ };
133
+ }
134
+
135
+ export function deriveWorkflowCandidateRangesFromMarkup(
136
+ snapshot: WorkflowMarkupSnapshot,
137
+ options: WorkflowCandidateRangeOptions = {},
138
+ ): WorkflowCandidateRange[] {
139
+ const allowedKinds = options.kinds ? new Set(options.kinds) : null;
140
+ const includeDetached = options.includeDetached === true;
141
+
142
+ return snapshot.items
143
+ .filter((item) => (allowedKinds ? allowedKinds.has(item.kind) : true))
144
+ .filter((item) => includeDetached || item.anchor.kind !== "detached")
145
+ .map(
146
+ (item): WorkflowCandidateRange => ({
147
+ candidateId: item.markupId,
148
+ storyTarget: item.storyTarget,
149
+ anchor: item.anchor,
150
+ label: item.label,
151
+ source: options.source ?? "host",
152
+ }),
153
+ );
154
+ }
155
+
156
+ function collectSurfaceMarkup(
157
+ blocks: readonly SurfaceBlockSnapshot[],
158
+ storyTarget: EditorStoryTarget,
159
+ preservation: CanonicalDocumentEnvelope["preservation"],
160
+ highlights: WorkflowHighlightMarkup[],
161
+ opaqueFragments: WorkflowOpaqueFragmentMarkup[],
162
+ ): void {
163
+ for (const block of blocks) {
164
+ if (block.kind === "paragraph") {
165
+ for (const segment of block.segments) {
166
+ collectSegmentMarkup(segment, storyTarget, preservation, highlights, opaqueFragments);
167
+ }
168
+ continue;
169
+ }
170
+
171
+ if (block.kind === "table") {
172
+ for (const row of block.rows) {
173
+ for (const cell of row.cells) {
174
+ collectSurfaceMarkup(cell.content, storyTarget, preservation, highlights, opaqueFragments);
175
+ }
176
+ }
177
+ continue;
178
+ }
179
+
180
+ if (block.kind === "sdt_block") {
181
+ collectSurfaceMarkup(block.children, storyTarget, preservation, highlights, opaqueFragments);
182
+ continue;
183
+ }
184
+
185
+ const fragment = preservation.opaqueFragments[block.fragmentId];
186
+ const descriptor = fragment ? describeOpaqueFragment(fragment) : null;
187
+ const blockedReasonCode =
188
+ fragment && BLOCKED_IMPORT_FEATURE_KEYS.has(descriptor?.featureKey ?? "")
189
+ ? "workflow_blocked_import"
190
+ : "workflow_preserve_only";
191
+ opaqueFragments.push({
192
+ markupId: `opaque:${block.fragmentId}`,
193
+ kind: "opaque_fragment",
194
+ fragmentId: block.fragmentId,
195
+ warningId: block.warningId,
196
+ anchor: createRangeAnchor(block.from, block.to),
197
+ storyTarget,
198
+ label: block.label,
199
+ excerpt: block.detail,
200
+ detail: block.detail,
201
+ blockedReasonCode,
202
+ });
203
+ }
204
+ }
205
+
206
+ function collectSegmentMarkup(
207
+ segment: SurfaceInlineSegment,
208
+ storyTarget: EditorStoryTarget,
209
+ preservation: CanonicalDocumentEnvelope["preservation"],
210
+ highlights: WorkflowHighlightMarkup[],
211
+ opaqueFragments: WorkflowOpaqueFragmentMarkup[],
212
+ ): void {
213
+ if (segment.kind === "text" && segment.markAttrs?.backgroundColor) {
214
+ highlights.push({
215
+ markupId: `highlight:${storyTargetKey(storyTarget)}:${segment.from}:${segment.to}:${segment.markAttrs.backgroundColor}`,
216
+ kind: "highlight",
217
+ anchor: createRangeAnchor(segment.from, segment.to),
218
+ storyTarget,
219
+ label: `Highlight ${segment.markAttrs.backgroundColor}`,
220
+ excerpt: segment.text,
221
+ color: segment.markAttrs.backgroundColor,
222
+ text: segment.text,
223
+ });
224
+ return;
225
+ }
226
+
227
+ if (segment.kind === "opaque_inline") {
228
+ const fragment = preservation.opaqueFragments[segment.fragmentId];
229
+ const descriptor = fragment ? describeOpaqueFragment(fragment) : null;
230
+ const blockedReasonCode =
231
+ fragment && BLOCKED_IMPORT_FEATURE_KEYS.has(descriptor?.featureKey ?? "")
232
+ ? "workflow_blocked_import"
233
+ : "workflow_preserve_only";
234
+ opaqueFragments.push({
235
+ markupId: `opaque:${segment.fragmentId}`,
236
+ kind: "opaque_fragment",
237
+ fragmentId: segment.fragmentId,
238
+ warningId: segment.warningId,
239
+ anchor: createRangeAnchor(segment.from, segment.to),
240
+ storyTarget,
241
+ label: segment.label,
242
+ excerpt: segment.detail,
243
+ detail: segment.detail,
244
+ blockedReasonCode,
245
+ });
246
+ }
247
+ }
248
+
249
+ function collectFieldMarkup(
250
+ surface: RuntimeRenderSnapshot["surface"],
251
+ fieldSnapshot: FieldSnapshot,
252
+ ): WorkflowFieldMarkup[] {
253
+ if (!surface) {
254
+ return [];
255
+ }
256
+
257
+ const stories = [
258
+ { blocks: surface.blocks, storyTarget: MAIN_STORY_TARGET },
259
+ ...surface.secondaryStories.map((story) => ({
260
+ blocks: story.blocks,
261
+ storyTarget: story.target,
262
+ })),
263
+ ];
264
+
265
+ return fieldSnapshot.fields.flatMap((field) => {
266
+ const displayText = field.displayText.trim();
267
+ if (!displayText) {
268
+ return [];
269
+ }
270
+
271
+ for (const story of stories) {
272
+ const matches = searchSurfaceBlocks(story.blocks, displayText, { limit: 2 });
273
+ if (matches.length === 1) {
274
+ const match = matches[0]!;
275
+ return [
276
+ {
277
+ markupId: `field:${field.index}`,
278
+ kind: "field",
279
+ anchor: createRangeAnchor(match.from, match.to),
280
+ storyTarget: story.storyTarget,
281
+ label: displayText,
282
+ excerpt: field.instruction,
283
+ fieldIndex: field.index,
284
+ fieldFamily: field.fieldFamily,
285
+ fieldTarget: field.fieldTarget,
286
+ refreshStatus: field.refreshStatus,
287
+ displayText: field.displayText,
288
+ } satisfies WorkflowFieldMarkup,
289
+ ];
290
+ }
291
+ }
292
+
293
+ return [];
294
+ });
295
+ }
296
+
297
+ function collectOpaqueFragmentMarkup(
298
+ preservation: CanonicalDocumentEnvelope["preservation"],
299
+ existing: WorkflowOpaqueFragmentMarkup[],
300
+ ): WorkflowOpaqueFragmentMarkup[] {
301
+ const seen = new Set(existing.map((item) => item.fragmentId));
302
+
303
+ return Object.values(preservation.opaqueFragments)
304
+ .filter((fragment) => !seen.has(fragment.fragmentId))
305
+ .map((fragment) => {
306
+ const descriptor = describeOpaqueFragment(fragment);
307
+ const blockedReasonCode = BLOCKED_IMPORT_FEATURE_KEYS.has(descriptor.featureKey)
308
+ ? "workflow_blocked_import"
309
+ : "workflow_preserve_only";
310
+
311
+ return {
312
+ markupId: `opaque:${fragment.fragmentId}`,
313
+ kind: "opaque_fragment",
314
+ fragmentId: fragment.fragmentId,
315
+ warningId: fragment.warningId,
316
+ anchor: createRangeAnchor(fragment.lastKnownRange.from, fragment.lastKnownRange.to),
317
+ storyTarget: MAIN_STORY_TARGET,
318
+ label: descriptor.label,
319
+ excerpt: descriptor.detail,
320
+ detail: descriptor.detail,
321
+ blockedReasonCode,
322
+ } satisfies WorkflowOpaqueFragmentMarkup;
323
+ });
324
+ }
325
+
326
+ function createRangeAnchor(from: number, to: number): EditorAnchorProjection {
327
+ return {
328
+ kind: "range",
329
+ from,
330
+ to,
331
+ assoc: {
332
+ start: -1,
333
+ end: 1,
334
+ },
335
+ };
336
+ }
337
+
338
+ function storyTargetKey(storyTarget: EditorStoryTarget): string {
339
+ switch (storyTarget.kind) {
340
+ case "main":
341
+ return "main";
342
+ case "header":
343
+ case "footer":
344
+ return `${storyTarget.kind}:${storyTarget.relationshipId}:${storyTarget.variant}:${storyTarget.sectionIndex ?? "none"}`;
345
+ case "footnote":
346
+ case "endnote":
347
+ return `${storyTarget.kind}:${storyTarget.noteId}`;
348
+ }
349
+ }