@beyondwork/docx-react-component 1.0.41 → 1.0.42

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 (88) hide show
  1. package/package.json +13 -1
  2. package/src/api/awareness-identity-types.ts +35 -0
  3. package/src/api/comment-negotiation-types.ts +130 -0
  4. package/src/api/comment-presentation-types.ts +106 -0
  5. package/src/api/external-custody-types.ts +74 -0
  6. package/src/api/participants-types.ts +18 -0
  7. package/src/api/public-types.ts +347 -4
  8. package/src/api/scope-metadata-resolver-types.ts +88 -0
  9. package/src/core/commands/formatting-commands.ts +1 -1
  10. package/src/core/commands/index.ts +568 -1
  11. package/src/index.ts +118 -1
  12. package/src/io/export/escape-xml-attribute.ts +26 -0
  13. package/src/io/export/external-send.ts +188 -0
  14. package/src/io/export/serialize-comments.ts +13 -16
  15. package/src/io/export/serialize-footnotes.ts +17 -24
  16. package/src/io/export/serialize-headers-footers.ts +17 -24
  17. package/src/io/export/serialize-main-document.ts +59 -62
  18. package/src/io/export/serialize-numbering.ts +20 -27
  19. package/src/io/export/serialize-runtime-revisions.ts +2 -9
  20. package/src/io/export/serialize-tables.ts +8 -15
  21. package/src/io/export/table-properties-xml.ts +25 -32
  22. package/src/io/import/external-reimport.ts +40 -0
  23. package/src/io/ooxml/bw-xml.ts +244 -0
  24. package/src/io/ooxml/canonicalize-payload.ts +301 -0
  25. package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
  26. package/src/io/ooxml/comment-presentation-payload.ts +311 -0
  27. package/src/io/ooxml/external-custody-payload.ts +102 -0
  28. package/src/io/ooxml/participants-payload.ts +97 -0
  29. package/src/io/ooxml/payload-signature.ts +112 -0
  30. package/src/io/ooxml/workflow-payload-validator.ts +271 -0
  31. package/src/io/ooxml/workflow-payload.ts +146 -7
  32. package/src/runtime/awareness-identity.ts +173 -0
  33. package/src/runtime/collab/event-types.ts +27 -0
  34. package/src/runtime/collab-session-bridge.ts +157 -0
  35. package/src/runtime/collab-session-facet.ts +193 -0
  36. package/src/runtime/collab-session.ts +273 -0
  37. package/src/runtime/comment-negotiation-sync.ts +91 -0
  38. package/src/runtime/comment-negotiation.ts +158 -0
  39. package/src/runtime/comment-presentation.ts +223 -0
  40. package/src/runtime/document-runtime.ts +280 -93
  41. package/src/runtime/external-send-runtime.ts +117 -0
  42. package/src/runtime/layout/docx-font-loader.ts +11 -30
  43. package/src/runtime/layout/inert-layout-facet.ts +2 -0
  44. package/src/runtime/layout/layout-engine-instance.ts +122 -12
  45. package/src/runtime/layout/page-graph.ts +79 -7
  46. package/src/runtime/layout/paginated-layout-engine.ts +230 -34
  47. package/src/runtime/layout/public-facet.ts +185 -13
  48. package/src/runtime/layout/table-row-split.ts +316 -0
  49. package/src/runtime/markdown-sanitizer.ts +132 -0
  50. package/src/runtime/participants.ts +134 -0
  51. package/src/runtime/resign-payload.ts +120 -0
  52. package/src/runtime/tamper-gate.ts +157 -0
  53. package/src/runtime/workflow-markup.ts +9 -0
  54. package/src/runtime/workflow-rail-segments.ts +244 -5
  55. package/src/ui/WordReviewEditor.tsx +587 -0
  56. package/src/ui/editor-runtime-boundary.ts +1 -0
  57. package/src/ui/editor-shell-view.tsx +11 -0
  58. package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
  59. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
  60. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
  61. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
  62. package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
  63. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
  64. package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
  65. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
  66. package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
  67. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
  69. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
  70. package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
  71. package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
  72. package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
  73. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +58 -0
  74. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
  75. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
  76. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
  77. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
  78. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
  79. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
  80. package/src/ui-tailwind/editor-surface/pm-schema.ts +15 -13
  81. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
  82. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -14
  83. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
  84. package/src/ui-tailwind/index.ts +32 -0
  85. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
  87. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  88. package/src/ui-tailwind/tw-review-workspace.tsx +293 -34
@@ -3,6 +3,7 @@ import {
3
3
  mapRevisionBoundaries,
4
4
  type RevisionParagraphBoundary,
5
5
  } from "../ooxml/revision-boundaries.ts";
6
+ import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
6
7
 
7
8
  interface XmlReplacement {
8
9
  start: number;
@@ -360,7 +361,7 @@ function serializeRevisionAttributes(revision: RevisionRecord): string {
360
361
 
361
362
  return Object.entries(attributes)
362
363
  .filter(([, value]) => value && value.length > 0)
363
- .map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`)
364
+ .map(([name, value]) => ` ${name}="${escapeXmlAttribute(value)}"`)
364
365
  .join("");
365
366
  }
366
367
 
@@ -453,11 +454,3 @@ function applyReplacements(documentXml: string, replacements: readonly XmlReplac
453
454
 
454
455
  return output;
455
456
  }
456
-
457
- function escapeAttribute(value: string): string {
458
- return value
459
- .replace(/&/g, "&")
460
- .replace(/</g, "&lt;")
461
- .replace(/>/g, "&gt;")
462
- .replace(/"/g, "&quot;");
463
- }
@@ -11,6 +11,7 @@ import type {
11
11
  ParsedTableWidth,
12
12
  } from "../ooxml/parse-tables.ts";
13
13
  import { twip } from "./twip.ts";
14
+ import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
14
15
 
15
16
  export function serializeTable(table: ParsedTable): string {
16
17
  const propertiesXml = table.propertiesXml ?? buildTablePropertiesXml(table);
@@ -60,10 +61,10 @@ function buildTablePropertiesXml(table: ParsedTable): string {
60
61
  children.push(table.bidiVisual ? `<w:bidiVisual/>` : `<w:bidiVisual w:val="0"/>`);
61
62
  }
62
63
  if (table.caption !== undefined) {
63
- children.push(`<w:tblCaption w:val="${escapeAttribute(table.caption)}"/>`);
64
+ children.push(`<w:tblCaption w:val="${escapeXmlAttribute(table.caption)}"/>`);
64
65
  }
65
66
  if (table.description !== undefined) {
66
- children.push(`<w:tblDescription w:val="${escapeAttribute(table.description)}"/>`);
67
+ children.push(`<w:tblDescription w:val="${escapeXmlAttribute(table.description)}"/>`);
67
68
  }
68
69
  if (table.floating) {
69
70
  const floatingXml = serializeTableFloating(table.floating);
@@ -106,7 +107,7 @@ function serializeTableFloating(floating: NonNullable<ParsedTable["floating"]>):
106
107
  function buildRowPropertiesXml(row: ParsedTableRow): string {
107
108
  const children: string[] = [];
108
109
  if (row.cnfStyle) {
109
- children.push(`<w:cnfStyle w:val="${escapeAttribute(row.cnfStyle)}"/>`);
110
+ children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(row.cnfStyle)}"/>`);
110
111
  }
111
112
  if (row.cantSplit !== undefined) {
112
113
  children.push(row.cantSplit ? `<w:cantSplit/>` : `<w:cantSplit w:val="0"/>`);
@@ -131,7 +132,7 @@ function ensureCellProperties(cell: ParsedTableCell): string {
131
132
 
132
133
  const children: string[] = [];
133
134
  if (cell.cnfStyle) {
134
- children.push(`<w:cnfStyle w:val="${escapeAttribute(cell.cnfStyle)}"/>`);
135
+ children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(cell.cnfStyle)}"/>`);
135
136
  }
136
137
  if (cell.width) {
137
138
  children.push(serializeWidth("tcW", cell.width));
@@ -174,15 +175,15 @@ function ensureCellProperties(cell: ParsedTableCell): string {
174
175
  }
175
176
 
176
177
  function serializeWidth(element: string, width: ParsedTableWidth): string {
177
- return `<w:${element} w:w="${twip(width.value)}" w:type="${width.type}"/>`;
178
+ return `<w:${element} w:w="${twip(width.value)}" w:type="${escapeXmlAttribute(width.type)}"/>`;
178
179
  }
179
180
 
180
181
  function serializeBorderSpec(element: string, spec: ParsedBorderSpec): string {
181
182
  const attrs: string[] = [];
182
- if (spec.value) attrs.push(`w:val="${spec.value}"`);
183
+ if (spec.value) attrs.push(`w:val="${escapeXmlAttribute(spec.value)}"`);
183
184
  if (spec.size !== undefined) attrs.push(`w:sz="${twip(spec.size)}"`);
184
185
  if (spec.space !== undefined) attrs.push(`w:space="${twip(spec.space)}"`);
185
- if (spec.color) attrs.push(`w:color="${spec.color}"`);
186
+ if (spec.color) attrs.push(`w:color="${escapeXmlAttribute(spec.color)}"`);
186
187
  const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
187
188
  return `<w:${element}${attrsStr}/>`;
188
189
  }
@@ -240,11 +241,3 @@ function serializeTableCellMargins(margins: ParsedCellMargins): string {
240
241
  if (margins.right !== undefined) parts.push(`<w:right w:w="${twip(margins.right)}" w:type="dxa"/>`);
241
242
  return parts.join("");
242
243
  }
243
-
244
- function escapeAttribute(value: string): string {
245
- return value
246
- .replace(/&/gu, "&amp;")
247
- .replace(/"/gu, "&quot;")
248
- .replace(/</gu, "&lt;")
249
- .replace(/>/gu, "&gt;");
250
- }
@@ -1,4 +1,5 @@
1
1
  import { twip } from "./twip.ts";
2
+ import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
2
3
 
3
4
  interface TableWidthLike {
4
5
  value: number;
@@ -233,31 +234,31 @@ function stripKnownProperties(xml: string, stripSpec: PropertyStripSpec): string
233
234
  function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
234
235
  const children: string[] = [];
235
236
  if (table.styleId) {
236
- children.push(`<w:tblStyle w:val="${escapeAttribute(table.styleId)}"/>`);
237
+ children.push(`<w:tblStyle w:val="${escapeXmlAttribute(table.styleId)}"/>`);
237
238
  }
238
239
  if (table.width) {
239
240
  children.push(serializeWidth("tblW", table.width));
240
241
  }
241
242
  if (table.alignment) {
242
- children.push(`<w:jc w:val="${escapeAttribute(table.alignment)}"/>`);
243
+ children.push(`<w:jc w:val="${escapeXmlAttribute(table.alignment)}"/>`);
243
244
  }
244
245
  if (table.indent) {
245
- children.push(`<w:tblInd w:w="${table.indent.value}" w:type="${escapeAttribute(table.indent.type)}"/>`);
246
+ children.push(`<w:tblInd w:w="${table.indent.value}" w:type="${escapeXmlAttribute(table.indent.type)}"/>`);
246
247
  }
247
248
  if (table.layoutMode) {
248
- children.push(`<w:tblLayout w:type="${escapeAttribute(table.layoutMode)}"/>`);
249
+ children.push(`<w:tblLayout w:type="${escapeXmlAttribute(table.layoutMode)}"/>`);
249
250
  }
250
251
  if (table.cellSpacing) {
251
- children.push(`<w:tblCellSpacing w:w="${table.cellSpacing.value}" w:type="${escapeAttribute(table.cellSpacing.type)}"/>`);
252
+ children.push(`<w:tblCellSpacing w:w="${table.cellSpacing.value}" w:type="${escapeXmlAttribute(table.cellSpacing.type)}"/>`);
252
253
  }
253
254
  if (table.bidiVisual !== undefined) {
254
255
  children.push(table.bidiVisual ? `<w:bidiVisual/>` : `<w:bidiVisual w:val="0"/>`);
255
256
  }
256
257
  if (table.caption !== undefined) {
257
- children.push(`<w:tblCaption w:val="${escapeAttribute(table.caption)}"/>`);
258
+ children.push(`<w:tblCaption w:val="${escapeXmlAttribute(table.caption)}"/>`);
258
259
  }
259
260
  if (table.description !== undefined) {
260
- children.push(`<w:tblDescription w:val="${escapeAttribute(table.description)}"/>`);
261
+ children.push(`<w:tblDescription w:val="${escapeXmlAttribute(table.description)}"/>`);
261
262
  }
262
263
  if (table.floating) {
263
264
  const floatingXml = serializeTableFloating(table.floating);
@@ -286,11 +287,11 @@ function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
286
287
 
287
288
  function serializeTableFloating(floating: TableFloatingPropertiesLike): string {
288
289
  const attrs: string[] = [];
289
- if (floating.horizontalAnchor) attrs.push(`w:horzAnchor="${escapeAttribute(floating.horizontalAnchor)}"`);
290
- if (floating.verticalAnchor) attrs.push(`w:vertAnchor="${escapeAttribute(floating.verticalAnchor)}"`);
291
- if (floating.horizontalAlign) attrs.push(`w:tblpXSpec="${escapeAttribute(floating.horizontalAlign)}"`);
290
+ if (floating.horizontalAnchor) attrs.push(`w:horzAnchor="${escapeXmlAttribute(floating.horizontalAnchor)}"`);
291
+ if (floating.verticalAnchor) attrs.push(`w:vertAnchor="${escapeXmlAttribute(floating.verticalAnchor)}"`);
292
+ if (floating.horizontalAlign) attrs.push(`w:tblpXSpec="${escapeXmlAttribute(floating.horizontalAlign)}"`);
292
293
  if (floating.horizontalOffset !== undefined) attrs.push(`w:tblpX="${floating.horizontalOffset}"`);
293
- if (floating.verticalAlign) attrs.push(`w:tblpYSpec="${escapeAttribute(floating.verticalAlign)}"`);
294
+ if (floating.verticalAlign) attrs.push(`w:tblpYSpec="${escapeXmlAttribute(floating.verticalAlign)}"`);
294
295
  if (floating.verticalOffset !== undefined) attrs.push(`w:tblpY="${floating.verticalOffset}"`);
295
296
  if (floating.leftFromText !== undefined) attrs.push(`w:leftFromText="${floating.leftFromText}"`);
296
297
  if (floating.rightFromText !== undefined) attrs.push(`w:rightFromText="${floating.rightFromText}"`);
@@ -306,7 +307,7 @@ function serializeTableFloating(floating: TableFloatingPropertiesLike): string {
306
307
  function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
307
308
  const children: string[] = [];
308
309
  if (row.cnfStyle) {
309
- children.push(`<w:cnfStyle w:val="${escapeAttribute(row.cnfStyle)}"/>`);
310
+ children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(row.cnfStyle)}"/>`);
310
311
  }
311
312
  if (row.gridBefore !== undefined) {
312
313
  children.push(`<w:gridBefore w:val="${twip(row.gridBefore)}"/>`);
@@ -328,7 +329,7 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
328
329
  children.push(row.cantSplit ? `<w:cantSplit/>` : `<w:cantSplit w:val="0"/>`);
329
330
  }
330
331
  if (row.height !== undefined) {
331
- const hRuleAttr = row.heightRule ? ` w:hRule="${escapeAttribute(row.heightRule)}"` : "";
332
+ const hRuleAttr = row.heightRule ? ` w:hRule="${escapeXmlAttribute(row.heightRule)}"` : "";
332
333
  children.push(`<w:trHeight w:val="${twip(row.height)}"${hRuleAttr}/>`);
333
334
  }
334
335
  // ST_OnOff element (A.3):
@@ -343,7 +344,7 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
343
344
  children.push(`<w:tblHeader w:val="false"/>`);
344
345
  }
345
346
  if (row.horizontalAlignment) {
346
- children.push(`<w:jc w:val="${escapeAttribute(row.horizontalAlignment)}"/>`);
347
+ children.push(`<w:jc w:val="${escapeXmlAttribute(row.horizontalAlignment)}"/>`);
347
348
  }
348
349
  return children.join("");
349
350
  }
@@ -351,7 +352,7 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
351
352
  function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string {
352
353
  const children: string[] = [];
353
354
  if (cell.cnfStyle) {
354
- children.push(`<w:cnfStyle w:val="${escapeAttribute(cell.cnfStyle)}"/>`);
355
+ children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(cell.cnfStyle)}"/>`);
355
356
  }
356
357
  if (cell.width) {
357
358
  children.push(serializeWidth("tcW", cell.width));
@@ -391,10 +392,10 @@ function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string
391
392
  children.push(cell.fitText ? `<w:tcFitText/>` : `<w:tcFitText w:val="0"/>`);
392
393
  }
393
394
  if (cell.textDirection) {
394
- children.push(`<w:textDirection w:val="${escapeAttribute(cell.textDirection)}"/>`);
395
+ children.push(`<w:textDirection w:val="${escapeXmlAttribute(cell.textDirection)}"/>`);
395
396
  }
396
397
  if (cell.verticalAlign) {
397
- children.push(`<w:vAlign w:val="${escapeAttribute(cell.verticalAlign)}"/>`);
398
+ children.push(`<w:vAlign w:val="${escapeXmlAttribute(cell.verticalAlign)}"/>`);
398
399
  }
399
400
  return children.join("");
400
401
  }
@@ -402,7 +403,7 @@ function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string
402
403
  function serializeWidth(elementName: "tblW" | "tcW", width: TableWidthLike): string {
403
404
  // OOXML allows w:w to be percentage (pct) or twentieths-of-a-percent too, but
404
405
  // both are integer-typed in the schema. Always round at the authoring edge.
405
- return `<w:${elementName} w:w="${twip(width.value)}" w:type="${escapeAttribute(width.type)}"/>`;
406
+ return `<w:${elementName} w:w="${twip(width.value)}" w:type="${escapeXmlAttribute(width.type)}"/>`;
406
407
  }
407
408
 
408
409
  function serializeBorders(borders: TableBordersLike): string {
@@ -415,10 +416,10 @@ function serializeBorders(borders: TableBordersLike): string {
415
416
 
416
417
  function serializeBorderSpec(elementName: string, border: BorderSpecLike): string {
417
418
  const attrs: string[] = [];
418
- if (border.value) attrs.push(`w:val="${escapeAttribute(border.value)}"`);
419
+ if (border.value) attrs.push(`w:val="${escapeXmlAttribute(border.value)}"`);
419
420
  if (border.size !== undefined) attrs.push(`w:sz="${twip(border.size)}"`);
420
421
  if (border.space !== undefined) attrs.push(`w:space="${twip(border.space)}"`);
421
- if (border.color) attrs.push(`w:color="${escapeAttribute(border.color)}"`);
422
+ if (border.color) attrs.push(`w:color="${escapeXmlAttribute(border.color)}"`);
422
423
  return attrs.length > 0 ? `<w:${elementName} ${attrs.join(" ")}/>` : "";
423
424
  }
424
425
 
@@ -434,7 +435,7 @@ function serializeTableCellMargins(margins: TableCellMarginsLike): string {
434
435
  function serializeTableLook(tblLook: TableLookLike): string {
435
436
  const attrs: string[] = [];
436
437
  if (tblLook.val) {
437
- attrs.push(`w:val="${escapeAttribute(tblLook.val)}"`);
438
+ attrs.push(`w:val="${escapeXmlAttribute(tblLook.val)}"`);
438
439
  }
439
440
  for (const [key, attr] of [
440
441
  ["firstRow", "w:firstRow"],
@@ -457,16 +458,8 @@ function serializeTableLook(tblLook: TableLookLike): string {
457
458
 
458
459
  function serializeCellShading(shading: CellShadingLike): string {
459
460
  const attrs: string[] = [];
460
- if (shading.val) attrs.push(`w:val="${escapeAttribute(shading.val)}"`);
461
- if (shading.color) attrs.push(`w:color="${escapeAttribute(shading.color)}"`);
462
- if (shading.fill) attrs.push(`w:fill="${escapeAttribute(shading.fill)}"`);
461
+ if (shading.val) attrs.push(`w:val="${escapeXmlAttribute(shading.val)}"`);
462
+ if (shading.color) attrs.push(`w:color="${escapeXmlAttribute(shading.color)}"`);
463
+ if (shading.fill) attrs.push(`w:fill="${escapeXmlAttribute(shading.fill)}"`);
463
464
  return attrs.length > 0 ? `<w:shd ${attrs.join(" ")}/>` : "";
464
465
  }
465
-
466
- function escapeAttribute(value: string): string {
467
- return value
468
- .replace(/&/gu, "&amp;")
469
- .replace(/"/gu, "&quot;")
470
- .replace(/</gu, "&lt;")
471
- .replace(/>/gu, "&gt;");
472
- }
@@ -0,0 +1,40 @@
1
+ import type {
2
+ ExternalCustody,
3
+ ExternalCustodyResolver,
4
+ ExternalCustodyRestoredContent,
5
+ } from "../../api/external-custody-types.ts";
6
+
7
+ export type ReimportResult =
8
+ | { outcome: "skipped" /* no custody attached */ }
9
+ | { outcome: "resolver_missing" /* custody present, no host resolver */ }
10
+ | { outcome: "tampered" /* incoming doc body hash ≠ originContentHash */ }
11
+ | { outcome: "deferred" /* resolver returned undefined; keep custody for retry */ }
12
+ | { outcome: "restored"; content: ExternalCustodyRestoredContent };
13
+
14
+ export interface MaybeRestoreArgs {
15
+ custody: ExternalCustody | undefined;
16
+ resolver: ExternalCustodyResolver | undefined;
17
+ /** sha256:{hex} of canonicalized word/document.xml of the incoming docx. */
18
+ incomingDocxHash: string;
19
+ }
20
+
21
+ /**
22
+ * Re-import pipeline. Classifies the call into a closed outcome set so
23
+ * the caller can decide how to merge the restored content back into
24
+ * the runtime state.
25
+ */
26
+ export async function maybeRestoreFromExternalCustody(
27
+ args: MaybeRestoreArgs,
28
+ ): Promise<ReimportResult> {
29
+ if (!args.custody) return { outcome: "skipped" };
30
+ if (!args.resolver) return { outcome: "resolver_missing" };
31
+ if (args.custody.originContentHash !== args.incomingDocxHash) {
32
+ return { outcome: "tampered" };
33
+ }
34
+ const restored = await args.resolver.restore({
35
+ custodyId: args.custody.custodyId,
36
+ originContentHash: args.custody.originContentHash,
37
+ });
38
+ if (!restored) return { outcome: "deferred" };
39
+ return { outcome: "restored", content: restored };
40
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Minimal XML helpers shared by the `bw:` round-trip builders +
3
+ * parsers. Intentionally not a general-purpose parser — only covers
4
+ * the shapes the bw schema uses: elements with attributes, text +
5
+ * CDATA content, no processing instructions, no mixed namespaces.
6
+ *
7
+ * The canonicalizer in `canonicalize-payload.ts` has its own inline
8
+ * parser because its contract is stricter (attribute sort, sort-key
9
+ * tables). These helpers are for conventional build/parse work where
10
+ * we just need the tree.
11
+ */
12
+
13
+ export interface BwElement {
14
+ kind: "element";
15
+ name: string;
16
+ attributes: Record<string, string>;
17
+ children: BwNode[];
18
+ }
19
+
20
+ export interface BwText {
21
+ kind: "text";
22
+ text: string;
23
+ }
24
+
25
+ export type BwNode = BwElement | BwText;
26
+
27
+ export function parseBwXml(src: string): BwElement {
28
+ let i = 0;
29
+ skipPrologAndWs();
30
+ const root = readElement();
31
+ if (!root) throw new Error("bw-xml: no root element");
32
+ return root;
33
+
34
+ function skipPrologAndWs(): void {
35
+ while (i < src.length) {
36
+ if (src.startsWith("<?", i)) {
37
+ const end = src.indexOf("?>", i);
38
+ if (end < 0) throw new Error("bw-xml: unterminated prolog");
39
+ i = end + 2;
40
+ continue;
41
+ }
42
+ if (src.startsWith("<!--", i)) {
43
+ const end = src.indexOf("-->", i);
44
+ if (end < 0) throw new Error("bw-xml: unterminated comment");
45
+ i = end + 3;
46
+ continue;
47
+ }
48
+ if (/\s/.test(src[i]!)) {
49
+ i += 1;
50
+ continue;
51
+ }
52
+ break;
53
+ }
54
+ }
55
+
56
+ function readElement(): BwElement | null {
57
+ while (i < src.length) {
58
+ if (src.startsWith("<!--", i)) {
59
+ const end = src.indexOf("-->", i);
60
+ if (end < 0) throw new Error("bw-xml: unterminated comment");
61
+ i = end + 3;
62
+ continue;
63
+ }
64
+ if (/\s/.test(src[i]!)) {
65
+ i += 1;
66
+ continue;
67
+ }
68
+ break;
69
+ }
70
+ if (src[i] !== "<") return null;
71
+ if (src.startsWith("</", i)) return null;
72
+ i += 1;
73
+
74
+ const nameStart = i;
75
+ while (i < src.length && !/[\s/>]/.test(src[i]!)) i += 1;
76
+ const name = src.slice(nameStart, i);
77
+
78
+ const attrs: Record<string, string> = {};
79
+ while (i < src.length) {
80
+ while (i < src.length && /\s/.test(src[i]!)) i += 1;
81
+ if (src[i] === "/" || src[i] === ">") break;
82
+ const aStart = i;
83
+ while (i < src.length && src[i] !== "=" && !/\s/.test(src[i]!)) i += 1;
84
+ const aName = src.slice(aStart, i);
85
+ while (i < src.length && /\s/.test(src[i]!)) i += 1;
86
+ if (src[i] !== "=") {
87
+ attrs[aName] = "";
88
+ continue;
89
+ }
90
+ i += 1;
91
+ while (i < src.length && /\s/.test(src[i]!)) i += 1;
92
+ const quote = src[i];
93
+ if (quote !== '"' && quote !== "'") {
94
+ throw new Error(`bw-xml: unquoted attr at ${i}`);
95
+ }
96
+ i += 1;
97
+ const vStart = i;
98
+ while (i < src.length && src[i] !== quote) i += 1;
99
+ attrs[aName] = xmlDecode(src.slice(vStart, i));
100
+ i += 1;
101
+ }
102
+
103
+ if (src[i] === "/") {
104
+ i += 1;
105
+ if (src[i] !== ">") throw new Error("bw-xml: bad self-close");
106
+ i += 1;
107
+ return { kind: "element", name, attributes: attrs, children: [] };
108
+ }
109
+ if (src[i] !== ">") throw new Error("bw-xml: expected >");
110
+ i += 1;
111
+
112
+ const children: BwNode[] = [];
113
+ while (i < src.length) {
114
+ if (src.startsWith("</", i)) {
115
+ i += 2;
116
+ const endStart = i;
117
+ while (i < src.length && src[i] !== ">") i += 1;
118
+ const endName = src.slice(endStart, i).trim();
119
+ if (endName !== name) {
120
+ throw new Error(
121
+ `bw-xml: mismatched close: opened <${name}> got </${endName}>`,
122
+ );
123
+ }
124
+ i += 1;
125
+ return { kind: "element", name, attributes: attrs, children };
126
+ }
127
+ if (src.startsWith("<!--", i)) {
128
+ const end = src.indexOf("-->", i);
129
+ if (end < 0) throw new Error("bw-xml: unterminated comment");
130
+ i = end + 3;
131
+ continue;
132
+ }
133
+ if (src.startsWith("<![CDATA[", i)) {
134
+ const end = src.indexOf("]]>", i);
135
+ if (end < 0) throw new Error("bw-xml: unterminated CDATA");
136
+ children.push({ kind: "text", text: src.slice(i + 9, end) });
137
+ i = end + 3;
138
+ continue;
139
+ }
140
+ if (src[i] === "<") {
141
+ const child = readElement();
142
+ if (child) children.push(child);
143
+ continue;
144
+ }
145
+ const textStart = i;
146
+ while (i < src.length && src[i] !== "<") i += 1;
147
+ const raw = src.slice(textStart, i);
148
+ if (raw.length > 0) {
149
+ children.push({ kind: "text", text: xmlDecode(raw) });
150
+ }
151
+ }
152
+ throw new Error(`bw-xml: unterminated <${name}>`);
153
+ }
154
+ }
155
+
156
+ // ----- build helpers --------------------------------------------------------
157
+
158
+ export function renderElement(
159
+ name: string,
160
+ attrs: Record<string, string | undefined>,
161
+ children: readonly string[] = [],
162
+ ): string {
163
+ const pairs: string[] = [];
164
+ for (const [k, v] of Object.entries(attrs)) {
165
+ if (v === undefined || v === "") continue;
166
+ pairs.push(`${k}="${xmlEncode(v)}"`);
167
+ }
168
+ const head = pairs.length ? `${name} ${pairs.join(" ")}` : name;
169
+ const body = children.filter((c) => c.length > 0).join("");
170
+ if (body.length === 0) return `<${head}/>`;
171
+ return `<${head}>${body}</${name}>`;
172
+ }
173
+
174
+ export function renderText(text: string): string {
175
+ return xmlEncode(text);
176
+ }
177
+
178
+ export function renderCdata(text: string): string {
179
+ const safe = text.replace(/\]\]>/g, "]]]]><![CDATA[>");
180
+ return `<![CDATA[${safe}]]>`;
181
+ }
182
+
183
+ // ----- element traversal helpers -------------------------------------------
184
+
185
+ export function childrenOf(el: BwElement, localName: string): BwElement[] {
186
+ const out: BwElement[] = [];
187
+ for (const child of el.children) {
188
+ if (child.kind === "element" && stripNs(child.name) === localName) {
189
+ out.push(child);
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+
195
+ export function firstChild(
196
+ el: BwElement,
197
+ localName: string,
198
+ ): BwElement | undefined {
199
+ for (const child of el.children) {
200
+ if (child.kind === "element" && stripNs(child.name) === localName) {
201
+ return child;
202
+ }
203
+ }
204
+ return undefined;
205
+ }
206
+
207
+ export function textOf(el: BwElement): string {
208
+ return el.children
209
+ .map((c) => (c.kind === "text" ? c.text : ""))
210
+ .join("");
211
+ }
212
+
213
+ export function stripNs(qname: string): string {
214
+ const colon = qname.indexOf(":");
215
+ return colon < 0 ? qname : qname.slice(colon + 1);
216
+ }
217
+
218
+ export function attrNumber(
219
+ value: string | undefined,
220
+ ): number | undefined {
221
+ if (value === undefined || value === "") return undefined;
222
+ const n = Number(value);
223
+ return Number.isFinite(n) ? n : undefined;
224
+ }
225
+
226
+ // ----- encoding -------------------------------------------------------------
227
+
228
+ export function xmlEncode(text: string): string {
229
+ return text
230
+ .replace(/&/g, "&amp;")
231
+ .replace(/</g, "&lt;")
232
+ .replace(/>/g, "&gt;")
233
+ .replace(/"/g, "&quot;")
234
+ .replace(/'/g, "&apos;");
235
+ }
236
+
237
+ export function xmlDecode(text: string): string {
238
+ return text
239
+ .replace(/&lt;/g, "<")
240
+ .replace(/&gt;/g, ">")
241
+ .replace(/&quot;/g, '"')
242
+ .replace(/&apos;/g, "'")
243
+ .replace(/&amp;/g, "&");
244
+ }