@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
@@ -3,7 +3,9 @@ import type {
3
3
  FootnoteCollection,
4
4
  FootnoteDefinition,
5
5
  InlineNode,
6
+ ParagraphIndentation,
6
7
  ParagraphNode,
8
+ ParagraphSpacing,
7
9
  TableCellNode,
8
10
  TableNode,
9
11
  TableRowNode,
@@ -149,6 +151,7 @@ function parseNoteElement(
149
151
  type: "opaque_block",
150
152
  fragmentId: `fragment:note-tbl-${noteId}`,
151
153
  warningId: `warning:note-opaque-table`,
154
+ rawXml: serializeElementToXml(child),
152
155
  });
153
156
  }
154
157
  } else {
@@ -156,6 +159,7 @@ function parseNoteElement(
156
159
  type: "opaque_block",
157
160
  fragmentId: `fragment:note-opaque-${noteId}`,
158
161
  warningId: `warning:note-opaque-block`,
162
+ rawXml: serializeElementToXml(child),
159
163
  });
160
164
  }
161
165
  }
@@ -165,7 +169,15 @@ function parseNoteElement(
165
169
 
166
170
  function parseParagraphElement(pElement: XmlElementNode): ParagraphNode {
167
171
  let styleId: string | undefined;
172
+ let alignment: ParagraphNode["alignment"];
173
+ let spacing: ParagraphNode["spacing"];
174
+ let indentation: ParagraphNode["indentation"];
168
175
  const children: InlineNode[] = [];
176
+ let activeComplexField: {
177
+ instruction: string;
178
+ children: InlineNode[];
179
+ mode: "instruction" | "result";
180
+ } | null = null;
169
181
 
170
182
  for (const child of pElement.children) {
171
183
  if (child.type !== "element") {
@@ -177,24 +189,141 @@ function parseParagraphElement(pElement: XmlElementNode): ParagraphNode {
177
189
  if (name === "pPr") {
178
190
  const pStyle = findChildElementOptional(child, "pStyle");
179
191
  styleId = pStyle?.attributes["w:val"] ?? pStyle?.attributes.val;
192
+ const jc = findChildElementOptional(child, "jc");
193
+ const jcVal = jc?.attributes["w:val"] ?? jc?.attributes.val;
194
+ if (jcVal === "left" || jcVal === "center" || jcVal === "right" || jcVal === "both" || jcVal === "distribute") {
195
+ alignment = jcVal;
196
+ }
197
+ spacing = readParagraphSpacing(child);
198
+ indentation = readParagraphIndentation(child);
180
199
  } else if (name === "r") {
181
- children.push(...parseRunElement(child));
200
+ activeComplexField = appendRunNodes(child, children, activeComplexField);
182
201
  } else if (name === "hyperlink") {
202
+ if (activeComplexField && activeComplexField.instruction.trim().length > 0) {
203
+ children.push({
204
+ type: "field",
205
+ fieldType: "complex",
206
+ instruction: activeComplexField.instruction,
207
+ children: activeComplexField.children,
208
+ });
209
+ activeComplexField = null;
210
+ }
183
211
  children.push(parseHyperlinkElement(child));
184
212
  } else if (name === "bookmarkStart" || name === "bookmarkEnd") {
185
- children.push(parseBookmarkElement(child));
213
+ const bookmarkNode = parseBookmarkElement(child);
214
+ if (activeComplexField?.mode === "result") {
215
+ activeComplexField.children.push(bookmarkNode);
216
+ } else {
217
+ if (activeComplexField && activeComplexField.instruction.trim().length > 0) {
218
+ children.push({
219
+ type: "field",
220
+ fieldType: "complex",
221
+ instruction: activeComplexField.instruction,
222
+ children: activeComplexField.children,
223
+ });
224
+ activeComplexField = null;
225
+ }
226
+ children.push(bookmarkNode);
227
+ }
186
228
  } else if (name === "fldSimple") {
229
+ if (activeComplexField && activeComplexField.instruction.trim().length > 0) {
230
+ children.push({
231
+ type: "field",
232
+ fieldType: "complex",
233
+ instruction: activeComplexField.instruction,
234
+ children: activeComplexField.children,
235
+ });
236
+ activeComplexField = null;
237
+ }
187
238
  pushFieldNode(children, child, "simple");
188
239
  }
189
240
  }
190
241
 
242
+ if (activeComplexField && activeComplexField.instruction.trim().length > 0) {
243
+ children.push({
244
+ type: "field",
245
+ fieldType: "complex",
246
+ instruction: activeComplexField.instruction,
247
+ children: activeComplexField.children,
248
+ });
249
+ }
250
+
191
251
  return {
192
252
  type: "paragraph",
193
253
  ...(styleId ? { styleId } : {}),
254
+ ...(alignment ? { alignment } : {}),
255
+ ...(spacing ? { spacing } : {}),
256
+ ...(indentation ? { indentation } : {}),
194
257
  children,
195
258
  };
196
259
  }
197
260
 
261
+ function appendRunNodes(
262
+ rElement: XmlElementNode,
263
+ nodes: InlineNode[],
264
+ activeComplexField: {
265
+ instruction: string;
266
+ children: InlineNode[];
267
+ mode: "instruction" | "result";
268
+ } | null,
269
+ ): {
270
+ instruction: string;
271
+ children: InlineNode[];
272
+ mode: "instruction" | "result";
273
+ } | null {
274
+ const marks: TextMark[] = parseRunProperties(rElement);
275
+
276
+ for (const child of rElement.children) {
277
+ if (child.type !== "element") {
278
+ continue;
279
+ }
280
+
281
+ const name = localName(child.name);
282
+ if (name === "fldChar") {
283
+ const fldType = child.attributes["w:fldCharType"] ?? child.attributes.fldCharType;
284
+ if (fldType === "begin") {
285
+ activeComplexField = { instruction: "", children: [], mode: "instruction" };
286
+ } else if (fldType === "separate" && activeComplexField) {
287
+ activeComplexField.mode = "result";
288
+ } else if (fldType === "end" && activeComplexField) {
289
+ if (activeComplexField.instruction.trim().length > 0) {
290
+ nodes.push({
291
+ type: "field",
292
+ fieldType: "complex",
293
+ instruction: activeComplexField.instruction,
294
+ children: activeComplexField.children,
295
+ });
296
+ }
297
+ activeComplexField = null;
298
+ }
299
+ continue;
300
+ }
301
+
302
+ if (name === "instrText") {
303
+ if (activeComplexField) {
304
+ activeComplexField.instruction += extractTextContent(child);
305
+ } else {
306
+ pushFieldNode(nodes, child, "complex");
307
+ }
308
+ continue;
309
+ }
310
+
311
+ const inlineNode = parseRunChildNode(child, marks);
312
+ if (!inlineNode) {
313
+ continue;
314
+ }
315
+
316
+ if (activeComplexField?.mode === "result") {
317
+ activeComplexField.children.push(inlineNode);
318
+ continue;
319
+ }
320
+
321
+ nodes.push(inlineNode);
322
+ }
323
+
324
+ return activeComplexField;
325
+ }
326
+
198
327
  function parseRunElement(rElement: XmlElementNode): InlineNode[] {
199
328
  const nodes: InlineNode[] = [];
200
329
  const marks: TextMark[] = parseRunProperties(rElement);
@@ -243,6 +372,52 @@ function parseRunElement(rElement: XmlElementNode): InlineNode[] {
243
372
  return nodes;
244
373
  }
245
374
 
375
+ function parseRunChildNode(
376
+ child: XmlElementNode,
377
+ marks: TextMark[],
378
+ ): InlineNode | null {
379
+ const name = localName(child.name);
380
+
381
+ if (name === "t") {
382
+ const text = extractTextContent(child);
383
+ if (text.length > 0) {
384
+ return {
385
+ type: "text",
386
+ text,
387
+ ...(marks.length > 0 ? { marks } : {}),
388
+ };
389
+ }
390
+ return null;
391
+ }
392
+ if (name === "br") {
393
+ return { type: "hard_break" };
394
+ }
395
+ if (name === "tab") {
396
+ return { type: "tab" };
397
+ }
398
+ if (name === "footnoteReference") {
399
+ const noteId =
400
+ child.attributes["w:id"] ?? child.attributes.id ?? "";
401
+ if (noteId) {
402
+ return { type: "footnote_ref", noteId, noteKind: "footnote" };
403
+ }
404
+ return null;
405
+ }
406
+ if (name === "endnoteReference") {
407
+ const noteId =
408
+ child.attributes["w:id"] ?? child.attributes.id ?? "";
409
+ if (noteId) {
410
+ return { type: "footnote_ref", noteId, noteKind: "endnote" };
411
+ }
412
+ return null;
413
+ }
414
+ if (name === "bookmarkStart" || name === "bookmarkEnd") {
415
+ return parseBookmarkElement(child);
416
+ }
417
+
418
+ return null;
419
+ }
420
+
246
421
  function parseHyperlinkElement(element: XmlElementNode): Extract<InlineNode, { type: "hyperlink" }> {
247
422
  const href = element.attributes["w:anchor"]
248
423
  ? `#${element.attributes["w:anchor"]}`
@@ -339,12 +514,75 @@ function parseRunProperties(rElement: XmlElementNode): TextMark[] {
339
514
  case "strike":
340
515
  if (val !== "0" && val !== "false") marks.push({ type: "strikethrough" });
341
516
  break;
517
+ case "dstrike":
518
+ if (val !== "0" && val !== "false") marks.push({ type: "doubleStrikethrough" });
519
+ break;
520
+ case "rFonts": {
521
+ const family =
522
+ child.attributes["w:ascii"] ??
523
+ child.attributes["w:hAnsi"] ??
524
+ child.attributes.ascii ??
525
+ child.attributes.hAnsi;
526
+ if (family) marks.push({ type: "fontFamily", val: family });
527
+ break;
528
+ }
529
+ case "sz": {
530
+ const szVal = child.attributes["w:val"] ?? child.attributes.val;
531
+ if (szVal) {
532
+ const size = Number.parseInt(szVal, 10);
533
+ if (Number.isFinite(size) && size > 0) marks.push({ type: "fontSize", val: size });
534
+ }
535
+ break;
536
+ }
537
+ case "color": {
538
+ const colorVal = child.attributes["w:val"] ?? child.attributes.val;
539
+ if (colorVal && colorVal !== "auto") marks.push({ type: "textColor", color: colorVal });
540
+ break;
541
+ }
542
+ case "smallCaps":
543
+ if (val !== "0" && val !== "false") marks.push({ type: "smallCaps" });
544
+ break;
545
+ case "caps":
546
+ if (val !== "0" && val !== "false") marks.push({ type: "allCaps" });
547
+ break;
342
548
  }
343
549
  }
344
550
 
345
551
  return marks;
346
552
  }
347
553
 
554
+ function readParagraphSpacing(pPr: XmlElementNode): ParagraphSpacing | undefined {
555
+ const spacingNode = findChildElementOptional(pPr, "spacing");
556
+ if (!spacingNode) return undefined;
557
+ const result: ParagraphSpacing = {};
558
+ const before = spacingNode.attributes["w:before"] ?? spacingNode.attributes.before;
559
+ if (before) result.before = Number.parseInt(before, 10);
560
+ const after = spacingNode.attributes["w:after"] ?? spacingNode.attributes.after;
561
+ if (after) result.after = Number.parseInt(after, 10);
562
+ const line = spacingNode.attributes["w:line"] ?? spacingNode.attributes.line;
563
+ if (line) result.line = Number.parseInt(line, 10);
564
+ const lineRule = spacingNode.attributes["w:lineRule"] ?? spacingNode.attributes.lineRule;
565
+ if (lineRule === "auto" || lineRule === "exact" || lineRule === "atLeast") {
566
+ result.lineRule = lineRule;
567
+ }
568
+ return Object.keys(result).length > 0 ? result : undefined;
569
+ }
570
+
571
+ function readParagraphIndentation(pPr: XmlElementNode): ParagraphIndentation | undefined {
572
+ const indNode = findChildElementOptional(pPr, "ind");
573
+ if (!indNode) return undefined;
574
+ const result: ParagraphIndentation = {};
575
+ const left = indNode.attributes["w:left"] ?? indNode.attributes.left;
576
+ if (left) result.left = Number.parseInt(left, 10);
577
+ const right = indNode.attributes["w:right"] ?? indNode.attributes.right;
578
+ if (right) result.right = Number.parseInt(right, 10);
579
+ const firstLine = indNode.attributes["w:firstLine"] ?? indNode.attributes.firstLine;
580
+ if (firstLine) result.firstLine = Number.parseInt(firstLine, 10);
581
+ const hanging = indNode.attributes["w:hanging"] ?? indNode.attributes.hanging;
582
+ if (hanging) result.hanging = Number.parseInt(hanging, 10);
583
+ return Object.keys(result).length > 0 ? result : undefined;
584
+ }
585
+
348
586
  function extractTextContent(tElement: XmlElementNode): string {
349
587
  let text = "";
350
588
  for (const child of tElement.children) {