@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
@@ -17,6 +17,12 @@ export interface PMStateResult {
17
17
  positionMap: PositionMap;
18
18
  }
19
19
 
20
+ export interface MediaPreviewDescriptor {
21
+ src: string;
22
+ widthEmu?: number;
23
+ heightEmu?: number;
24
+ }
25
+
20
26
  /**
21
27
  * Create a ProseMirror EditorState from a runtime surface snapshot.
22
28
  *
@@ -27,8 +33,9 @@ export function createPMStateFromSnapshot(
27
33
  surface: EditorSurfaceSnapshot,
28
34
  selection: SelectionSnapshot,
29
35
  plugins: Plugin[],
36
+ mediaPreviews: Record<string, MediaPreviewDescriptor> = {},
30
37
  ): PMStateResult {
31
- const doc = buildPMDoc(surface);
38
+ const doc = buildPMDoc(surface, mediaPreviews);
32
39
  const positionMap = buildPositionMap(surface);
33
40
  const pmSelection = createPMSelectionFromSnapshot(doc, positionMap, selection);
34
41
 
@@ -50,7 +57,8 @@ export function createPMSelectionFromSnapshot(
50
57
  const pmHead = clamp(positionMap.runtimeToPm(selection.head), 1, positionMap.pmDocSize - 1);
51
58
  try {
52
59
  if (selection.activeRange.kind === "node") {
53
- return NodeSelection.create(doc, pmAnchor);
60
+ const pmNodePos = clamp(pmAnchor - 1, 0, positionMap.pmDocSize - 2);
61
+ return NodeSelection.create(doc, pmNodePos);
54
62
  }
55
63
 
56
64
  const forward = selection.head >= selection.anchor;
@@ -98,31 +106,52 @@ function resolveInlineBoundary(
98
106
  return null;
99
107
  }
100
108
 
101
- function buildPMDoc(surface: EditorSurfaceSnapshot): PMNode {
102
- const blocks: PMNode[] = [];
109
+ function buildPMDoc(
110
+ surface: EditorSurfaceSnapshot,
111
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
112
+ ): PMNode {
113
+ const blocks = buildPMBlocks(surface.blocks, mediaPreviews);
114
+
115
+ // Ensure at least one block (PM requires non-empty doc)
116
+ if (blocks.length === 0) {
117
+ blocks.push(editorSchema.nodes.paragraph.create());
118
+ }
119
+
120
+ return editorSchema.nodes.doc.create(null, Fragment.from(blocks));
121
+ }
122
+
123
+ function buildPMBlocks(
124
+ blocks: SurfaceBlockSnapshot[],
125
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
126
+ ): PMNode[] {
127
+ const nodes: PMNode[] = [];
128
+
129
+ for (let index = 0; index < blocks.length; index += 1) {
130
+ const block = blocks[index];
131
+ const previousBlock = index > 0 ? blocks[index - 1] : null;
132
+ const nextBlock = blocks[index + 1];
133
+ const previousParagraph = previousBlock?.kind === "paragraph" ? previousBlock : null;
134
+ const nextParagraph = nextBlock?.kind === "paragraph" ? nextBlock : null;
103
135
 
104
- for (const block of surface.blocks) {
105
136
  if (block.kind === "paragraph") {
106
- blocks.push(buildParagraph(block));
137
+ nodes.push(buildParagraph(block, previousParagraph, nextParagraph, mediaPreviews));
107
138
  } else if (block.kind === "table") {
108
- blocks.push(buildTable(block));
139
+ nodes.push(buildTable(block, mediaPreviews));
109
140
  } else if (block.kind === "sdt_block") {
110
- blocks.push(buildSdtBlock(block));
141
+ nodes.push(buildSdtBlock(block, mediaPreviews));
111
142
  } else {
112
- blocks.push(buildOpaqueBlock(block));
143
+ nodes.push(buildOpaqueBlock(block));
113
144
  }
114
145
  }
115
146
 
116
- // Ensure at least one block (PM requires non-empty doc)
117
- if (blocks.length === 0) {
118
- blocks.push(editorSchema.nodes.paragraph.create());
119
- }
120
-
121
- return editorSchema.nodes.doc.create(null, Fragment.from(blocks));
147
+ return nodes;
122
148
  }
123
149
 
124
150
  function buildParagraph(
125
151
  block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
152
+ previousParagraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null,
153
+ nextParagraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null,
154
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
126
155
  ): PMNode {
127
156
  const content: PMNode[] = [];
128
157
  const tabStops = block.tabStops ?? [];
@@ -148,11 +177,28 @@ function buildParagraph(
148
177
  );
149
178
  tabIndex++;
150
179
  } else {
151
- const nodes = buildInlineContent(segment);
180
+ const nodes = buildInlineContent(segment, mediaPreviews);
152
181
  content.push(...nodes);
153
182
  }
154
183
  }
155
184
 
185
+ const listContinuation = Boolean(
186
+ block.numbering &&
187
+ nextParagraph?.numbering &&
188
+ nextParagraph.numbering.numberingInstanceId === block.numbering.numberingInstanceId &&
189
+ nextParagraph.numbering.level === block.numbering.level,
190
+ );
191
+ const contextualSpacingAfter = Boolean(
192
+ block.contextualSpacing &&
193
+ nextParagraph?.contextualSpacing &&
194
+ (nextParagraph.styleId ?? "__default__") === (block.styleId ?? "__default__"),
195
+ );
196
+ const contextualSpacingBefore = Boolean(
197
+ block.contextualSpacing &&
198
+ previousParagraph?.contextualSpacing &&
199
+ (previousParagraph.styleId ?? "__default__") === (block.styleId ?? "__default__"),
200
+ );
201
+
156
202
  return editorSchema.nodes.paragraph.create(
157
203
  {
158
204
  styleId: block.styleId ?? null,
@@ -169,9 +215,14 @@ function buildParagraph(
169
215
  spacingAfter: block.spacing?.after ?? null,
170
216
  lineSpacing: block.spacing?.line ?? null,
171
217
  lineRule: block.spacing?.lineRule ?? null,
218
+ contextualSpacing: block.contextualSpacing ?? null,
219
+ listContinuation: listContinuation || null,
220
+ contextualSpacingBefore: contextualSpacingBefore || null,
221
+ contextualSpacingAfter: contextualSpacingAfter || null,
172
222
  indentLeft: block.indentation?.left ?? null,
173
223
  indentRight: block.indentation?.right ?? null,
174
224
  indentFirstLine: block.indentation?.firstLine ?? null,
225
+ indentHanging: block.indentation?.hanging ?? null,
175
226
  shadingFill: block.shading?.fill ?? null,
176
227
  borderTop: (block.borders as Record<string, unknown>)?.top ?? null,
177
228
  borderBottom: (block.borders as Record<string, unknown>)?.bottom ?? null,
@@ -185,7 +236,10 @@ function buildParagraph(
185
236
  );
186
237
  }
187
238
 
188
- function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
239
+ function buildInlineContent(
240
+ segment: SurfaceInlineSegment,
241
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
242
+ ): PMNode[] {
189
243
  switch (segment.kind) {
190
244
  case "text": {
191
245
  if (!segment.text) return [];
@@ -253,6 +307,8 @@ function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
253
307
  return [editorSchema.nodes.tab_char.create()];
254
308
 
255
309
  case "image":
310
+ {
311
+ const preview = mediaPreviews[segment.mediaId];
256
312
  return [
257
313
  editorSchema.nodes.image_atom.create({
258
314
  mediaId: segment.mediaId,
@@ -260,8 +316,12 @@ function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
260
316
  state: segment.state,
261
317
  display: segment.display ?? "inline",
262
318
  detail: segment.detail ?? null,
319
+ src: preview?.src ?? null,
320
+ widthEmu: preview?.widthEmu ?? null,
321
+ heightEmu: preview?.heightEmu ?? null,
263
322
  }),
264
323
  ];
324
+ }
265
325
 
266
326
  case "opaque_inline":
267
327
  return [buildOpaqueInlineOrComplexAtom(segment)];
@@ -274,6 +334,17 @@ function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
274
334
  return [text];
275
335
  }
276
336
 
337
+ case "field_ref":
338
+ return [
339
+ editorSchema.nodes.field_ref_atom.create({
340
+ fieldFamily: segment.fieldFamily,
341
+ fieldTarget: segment.fieldTarget ?? null,
342
+ instruction: segment.instruction,
343
+ refreshStatus: segment.refreshStatus,
344
+ label: segment.label,
345
+ }),
346
+ ];
347
+
277
348
  default:
278
349
  return [];
279
350
  }
@@ -281,23 +352,13 @@ function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
281
352
 
282
353
  function buildTable(
283
354
  block: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
355
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
284
356
  ): PMNode {
285
357
  const rows: PMNode[] = [];
286
358
  for (const row of block.rows) {
287
359
  const cells: PMNode[] = [];
288
360
  for (const cell of row.cells) {
289
- const cellContent: PMNode[] = [];
290
- for (const child of cell.content) {
291
- if (child.kind === "paragraph") {
292
- cellContent.push(buildParagraph(child as Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>));
293
- } else if (child.kind === "table") {
294
- cellContent.push(buildTable(child as Extract<SurfaceBlockSnapshot, { kind: "table" }>));
295
- } else if (child.kind === "sdt_block") {
296
- cellContent.push(buildSdtBlock(child as Extract<SurfaceBlockSnapshot, { kind: "sdt_block" }>));
297
- } else if (child.kind === "opaque_block") {
298
- cellContent.push(buildOpaqueBlock(child as Extract<SurfaceBlockSnapshot, { kind: "opaque_block" }>));
299
- }
300
- }
361
+ const cellContent = buildPMBlocks(cell.content, mediaPreviews);
301
362
  // Ensure at least one paragraph in cell (PM requires non-empty)
302
363
  if (cellContent.length === 0) {
303
364
  cellContent.push(editorSchema.nodes.paragraph.create());
@@ -347,19 +408,9 @@ function buildTable(
347
408
 
348
409
  function buildSdtBlock(
349
410
  block: Extract<SurfaceBlockSnapshot, { kind: "sdt_block" }>,
411
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
350
412
  ): PMNode {
351
- const children = block.children.map((child) => {
352
- if (child.kind === "paragraph") {
353
- return buildParagraph(child);
354
- }
355
- if (child.kind === "table") {
356
- return buildTable(child);
357
- }
358
- if (child.kind === "sdt_block") {
359
- return buildSdtBlock(child);
360
- }
361
- return buildOpaqueBlock(child);
362
- });
413
+ const children = buildPMBlocks(block.children, mediaPreviews);
363
414
 
364
415
  if (children.length === 0) {
365
416
  children.push(editorSchema.nodes.paragraph.create());
@@ -392,15 +443,14 @@ function buildOpaqueInlineOrComplexAtom(
392
443
  const label = segment.label;
393
444
  const detail = segment.detail;
394
445
 
395
- if (label === "Chart") {
446
+ if (label === "Embedded chart") {
396
447
  return editorSchema.nodes.chart_atom.create({ detail });
397
448
  }
398
- if (label === "SmartArt") {
449
+ if (label === "SmartArt diagram") {
399
450
  return editorSchema.nodes.smartart_atom.create({ detail });
400
451
  }
401
- if (label === "Shape") {
402
- // Extract text hint from detail if present
403
- const textMatch = /Text: "([^"]+)"/.exec(detail);
452
+ if (label === "Drawing shape" || label === "Text box") {
453
+ const textMatch = /(?:Text content|Content): "([^"]+)"/.exec(detail);
404
454
  const geometryMatch = /Geometry: ([^.]+)\./.exec(detail);
405
455
  return editorSchema.nodes.shape_atom.create({
406
456
  text: textMatch ? textMatch[1] : null,
@@ -417,8 +467,8 @@ function buildOpaqueInlineOrComplexAtom(
417
467
  detail,
418
468
  });
419
469
  }
420
- if (label === "VML shape") {
421
- const textMatch = /Text: "([^"]+)"/.exec(detail);
470
+ if (label === "Legacy VML drawing") {
471
+ const textMatch = /Text content: "([^"]+)"/.exec(detail);
422
472
  const typeMatch = /Type: ([^.]+)\./.exec(detail);
423
473
  return editorSchema.nodes.vml_atom.create({
424
474
  text: textMatch ? textMatch[1] : null,
@@ -0,0 +1,55 @@
1
+ import type {
2
+ EditorStoryTarget,
3
+ EditorSurfaceSnapshot,
4
+ } from "../../api/public-types.ts";
5
+
6
+ const surfaceIdentityMap = new WeakMap<EditorSurfaceSnapshot, number>();
7
+ let nextSurfaceIdentity = 0;
8
+
9
+ function getSurfaceIdentity(surface: EditorSurfaceSnapshot): number {
10
+ const cached = surfaceIdentityMap.get(surface);
11
+ if (cached !== undefined) {
12
+ return cached;
13
+ }
14
+ const identity = nextSurfaceIdentity;
15
+ nextSurfaceIdentity += 1;
16
+ surfaceIdentityMap.set(surface, identity);
17
+ return identity;
18
+ }
19
+
20
+ export function createSurfaceDocumentBuildKey(input: {
21
+ surface: EditorSurfaceSnapshot | null | undefined;
22
+ activeStory: EditorStoryTarget;
23
+ mediaPreviewKey: string;
24
+ }): string {
25
+ return JSON.stringify({
26
+ surfaceIdentity:
27
+ input.surface === undefined || input.surface === null
28
+ ? "loading"
29
+ : getSurfaceIdentity(input.surface),
30
+ activeStory: input.activeStory,
31
+ mediaPreviewKey: input.mediaPreviewKey,
32
+ });
33
+ }
34
+
35
+ export function createSurfaceDecorationKey(input: {
36
+ markupDisplay: string;
37
+ showTrackedChanges: boolean;
38
+ canEdit: boolean;
39
+ activeCommentId?: string;
40
+ activeRevisionId?: string;
41
+ workflowScopeSignature?: string;
42
+ workflowCandidateSignature?: string;
43
+ workflowBlockedSignature?: string;
44
+ }): string {
45
+ return JSON.stringify({
46
+ markupDisplay: input.markupDisplay,
47
+ showTrackedChanges: input.showTrackedChanges,
48
+ canEdit: input.canEdit,
49
+ activeCommentId: input.activeCommentId ?? null,
50
+ activeRevisionId: input.activeRevisionId ?? null,
51
+ workflowScopeSignature: input.workflowScopeSignature ?? null,
52
+ workflowCandidateSignature: input.workflowCandidateSignature ?? null,
53
+ workflowBlockedSignature: input.workflowBlockedSignature ?? null,
54
+ });
55
+ }
@@ -7,13 +7,14 @@ export interface TwOpaqueBlockProps {
7
7
  block: Extract<SurfaceBlockSnapshot, { kind: "opaque_block" }>;
8
8
  selection: SelectionSnapshot;
9
9
  onSelectionChange?: (selection: SelectionSnapshot) => void;
10
+ workflowTargeted?: boolean;
10
11
  }
11
12
 
12
13
  const focusRingClass =
13
14
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
14
15
 
15
16
  export function TwOpaqueBlock(props: TwOpaqueBlockProps) {
16
- const { block, selection } = props;
17
+ const { block, selection, workflowTargeted } = props;
17
18
  const selected = selectionTouchesRange(selection, block.from, block.to);
18
19
 
19
20
  return (
@@ -44,6 +45,11 @@ export function TwOpaqueBlock(props: TwOpaqueBlockProps) {
44
45
  <span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-semibold text-comment bg-warning-soft">
45
46
  preserve-only
46
47
  </span>
48
+ {workflowTargeted && (
49
+ <span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-semibold text-amber-700 bg-amber-100">
50
+ workflow-targeted
51
+ </span>
52
+ )}
47
53
  </div>
48
54
  <p className="text-sm text-secondary">{block.detail}</p>
49
55
  </button>