@beyondwork/docx-react-component 1.0.58 → 1.0.60

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 (135) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -1
  3. package/src/api/awareness-identity-types.ts +4 -2
  4. package/src/api/comment-negotiation-types.ts +4 -1
  5. package/src/api/external-custody-types.ts +16 -0
  6. package/src/api/internal/build-ref-projections.ts +108 -0
  7. package/src/api/package-version.ts +1 -1
  8. package/src/api/participants-types.ts +11 -1
  9. package/src/api/public-types.ts +980 -10
  10. package/src/api/scope-metadata-resolver-types.ts +6 -0
  11. package/src/compare/diff-engine.ts +3 -0
  12. package/src/core/commands/formatting-commands.ts +1 -0
  13. package/src/core/commands/index.ts +225 -16
  14. package/src/core/commands/legacy-form-field-commands.ts +181 -0
  15. package/src/core/commands/table-structure-commands.ts +149 -31
  16. package/src/core/selection/mapping.ts +20 -0
  17. package/src/core/state/editor-state.ts +4 -1
  18. package/src/index.ts +28 -0
  19. package/src/io/docx-session.ts +22 -3
  20. package/src/io/export/export-session.ts +11 -7
  21. package/src/io/export/ooxml-namespaces.ts +47 -0
  22. package/src/io/export/reattach-preserved-parts.ts +4 -16
  23. package/src/io/export/serialize-comments.ts +3 -131
  24. package/src/io/export/serialize-ffdata.ts +89 -0
  25. package/src/io/export/serialize-headers-footers.ts +5 -0
  26. package/src/io/export/serialize-main-document.ts +224 -34
  27. package/src/io/export/serialize-numbering.ts +22 -2
  28. package/src/io/export/serialize-revisions.ts +99 -0
  29. package/src/io/export/serialize-tables.ts +9 -0
  30. package/src/io/export/split-review-boundaries.ts +1 -0
  31. package/src/io/export/table-properties-xml.ts +14 -0
  32. package/src/io/load-scheduler.ts +70 -28
  33. package/src/io/normalize/normalize-text.ts +13 -0
  34. package/src/io/ooxml/_mini-xml.ts +198 -0
  35. package/src/io/ooxml/canonicalize-payload.ts +1 -4
  36. package/src/io/ooxml/chart/chart-style-table.ts +4 -3
  37. package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
  38. package/src/io/ooxml/chart/parse-series.ts +2 -1
  39. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  40. package/src/io/ooxml/chart/types.ts +6 -434
  41. package/src/io/ooxml/comment-presentation-payload.ts +6 -5
  42. package/src/io/ooxml/highlight-colors.ts +8 -5
  43. package/src/io/ooxml/parse-anchor.ts +68 -53
  44. package/src/io/ooxml/parse-comments.ts +14 -142
  45. package/src/io/ooxml/parse-complex-content.ts +3 -106
  46. package/src/io/ooxml/parse-drawing.ts +100 -195
  47. package/src/io/ooxml/parse-ffdata.ts +93 -0
  48. package/src/io/ooxml/parse-fields.ts +7 -146
  49. package/src/io/ooxml/parse-fill.ts +88 -8
  50. package/src/io/ooxml/parse-font-table.ts +5 -105
  51. package/src/io/ooxml/parse-footnotes.ts +28 -152
  52. package/src/io/ooxml/parse-headers-footers.ts +106 -212
  53. package/src/io/ooxml/parse-inline-media.ts +3 -200
  54. package/src/io/ooxml/parse-main-document.ts +180 -217
  55. package/src/io/ooxml/parse-numbering.ts +154 -335
  56. package/src/io/ooxml/parse-object.ts +147 -0
  57. package/src/io/ooxml/parse-ole-relationship.ts +82 -0
  58. package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
  59. package/src/io/ooxml/parse-picture-sdt.ts +85 -0
  60. package/src/io/ooxml/parse-picture.ts +72 -42
  61. package/src/io/ooxml/parse-revisions.ts +285 -51
  62. package/src/io/ooxml/parse-settings.ts +6 -99
  63. package/src/io/ooxml/parse-shapes.ts +25 -140
  64. package/src/io/ooxml/parse-styles.ts +3 -218
  65. package/src/io/ooxml/parse-tables.ts +76 -256
  66. package/src/io/ooxml/parse-theme.ts +1 -4
  67. package/src/io/ooxml/property-grab-bag.ts +5 -47
  68. package/src/io/ooxml/workflow-payload.ts +6 -1
  69. package/src/io/ooxml/xml-element-serialize.ts +32 -0
  70. package/src/io/ooxml/xml-parser.ts +183 -0
  71. package/src/legal/bookmarks.ts +1 -1
  72. package/src/legal/cross-references.ts +1 -1
  73. package/src/legal/defined-terms.ts +1 -1
  74. package/src/legal/{_document-root.ts → document-root.ts} +8 -0
  75. package/src/legal/signature-blocks.ts +1 -1
  76. package/src/model/canonical-document.ts +159 -6
  77. package/src/model/chart-types.ts +439 -0
  78. package/src/model/snapshot.ts +5 -1
  79. package/src/review/store/comment-remapping.ts +24 -11
  80. package/src/review/store/revision-actions.ts +482 -2
  81. package/src/review/store/revision-store.ts +15 -0
  82. package/src/review/store/revision-types.ts +76 -0
  83. package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
  84. package/src/runtime/collab/runtime-collab-sync.ts +33 -0
  85. package/src/runtime/diagnostics/build-diagnostic.ts +153 -0
  86. package/src/runtime/diagnostics/code-metadata-table.ts +230 -0
  87. package/src/runtime/document-runtime.ts +821 -54
  88. package/src/runtime/document-search.ts +115 -0
  89. package/src/runtime/edit-ops/index.ts +18 -2
  90. package/src/runtime/footnote-resolver.ts +130 -0
  91. package/src/runtime/layout/layout-engine-instance.ts +31 -4
  92. package/src/runtime/layout/layout-engine-version.ts +37 -1
  93. package/src/runtime/layout/page-graph.ts +14 -1
  94. package/src/runtime/layout/resolved-formatting-state.ts +21 -0
  95. package/src/runtime/numbering-prefix.ts +17 -0
  96. package/src/runtime/query-scopes.ts +108 -10
  97. package/src/runtime/resolved-numbering-geometry.ts +37 -6
  98. package/src/runtime/revision-runtime.ts +27 -1
  99. package/src/runtime/selection/post-edit-validator.ts +60 -6
  100. package/src/runtime/structure-ops/index.ts +20 -4
  101. package/src/runtime/surface-projection.ts +290 -21
  102. package/src/runtime/table-schema.ts +6 -0
  103. package/src/runtime/theme-color-resolver.ts +2 -2
  104. package/src/runtime/units.ts +9 -0
  105. package/src/runtime/workflow-rail-segments.ts +4 -0
  106. package/src/ui/WordReviewEditor.tsx +187 -43
  107. package/src/ui/editor-runtime-boundary.ts +10 -0
  108. package/src/ui/editor-shell-view.tsx +4 -1
  109. package/src/ui/headless/chrome-registry.ts +53 -0
  110. package/src/ui/headless/selection-tool-resolver.ts +11 -1
  111. package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
  112. package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
  113. package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
  114. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
  115. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
  116. package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
  117. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
  118. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
  119. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
  120. package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
  121. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
  122. package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
  123. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
  124. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
  125. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
  126. package/src/ui-tailwind/index.ts +9 -0
  127. package/src/ui-tailwind/page-chrome-model.ts +77 -5
  128. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
  129. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
  130. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
  131. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
  132. package/src/ui-tailwind/theme/tokens.ts +14 -0
  133. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
  134. package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
  135. package/src/validation/diagnostics.ts +1 -0
@@ -16,6 +16,9 @@ export interface XmlTextNode {
16
16
 
17
17
  export type XmlNode = XmlElementNode | XmlTextNode;
18
18
 
19
+ import { parseXmlWithOffsets } from "./xml-parser.ts";
20
+ import { localName, readStringAttr } from "./xml-attr-helpers.ts";
21
+
19
22
  export interface ParsedBorderSpec {
20
23
  value?: string;
21
24
  size?: number;
@@ -50,6 +53,13 @@ export interface ParsedCellShading {
50
53
  fill?: string;
51
54
  color?: string;
52
55
  val?: string;
56
+ /** SOW gap G3 — theme-fill references kept verbatim for runtime resolve + round-trip. */
57
+ themeFill?: string;
58
+ themeFillTint?: string;
59
+ themeFillShade?: string;
60
+ themeColor?: string;
61
+ themeColorTint?: string;
62
+ themeColorShade?: string;
53
63
  }
54
64
 
55
65
  export interface ParsedCellMargins {
@@ -143,7 +153,7 @@ export interface ParsedTableCell {
143
153
  }
144
154
 
145
155
  export function parseTablesFromDocumentXml(xml: string): ParsedTableDocument {
146
- const root = parseXml(xml);
156
+ const root = parseXmlWithOffsets(xml) as XmlElementNode;
147
157
  const documentElement = findChildElement(root, "document");
148
158
  const bodyElement = findChildElement(documentElement, "body");
149
159
 
@@ -241,7 +251,7 @@ function parseCell(node: XmlElementNode, sourceXml: string): ParsedTableCell {
241
251
 
242
252
  return {
243
253
  ...(propertiesNode ? { propertiesXml: sourceXml.slice(propertiesNode.start, propertiesNode.end) } : {}),
244
- ...(gridSpanNode ? { gridSpan: parsePositiveInteger(gridSpanNode.attributes["w:val"] ?? gridSpanNode.attributes.val) } : {}),
254
+ ...(gridSpanNode ? { gridSpan: parsePositiveInteger(readStringAttr(gridSpanNode, "w:val")) } : {}),
245
255
  ...(verticalMergeNode ? { verticalMerge: readVerticalMerge(verticalMergeNode) } : {}),
246
256
  blocksXml,
247
257
  rawXml: sourceXml.slice(node.start, node.end),
@@ -260,11 +270,11 @@ function parseCell(node: XmlElementNode, sourceXml: string): ParsedTableCell {
260
270
  export function readGridColumns(node: XmlElementNode): number[] {
261
271
  return node.children
262
272
  .filter((child): child is XmlElementNode => child.type === "element" && localName(child.name) === "gridCol")
263
- .map((child) => parsePositiveInteger(child.attributes["w:w"] ?? child.attributes.w ?? "0"));
273
+ .map((child) => parsePositiveInteger(readStringAttr(child, "w:w") ?? "0"));
264
274
  }
265
275
 
266
276
  function readVerticalMerge(node: XmlElementNode): "restart" | "continue" {
267
- const value = (node.attributes["w:val"] ?? node.attributes.val ?? "continue").toLowerCase();
277
+ const value = (readStringAttr(node, "w:val") ?? "continue").toLowerCase();
268
278
  return value === "restart" ? "restart" : "continue";
269
279
  }
270
280
 
@@ -289,229 +299,14 @@ function findFirstChild(node: XmlElementNode, childLocalName: string): XmlElemen
289
299
  );
290
300
  }
291
301
 
292
- function localName(name: string): string {
293
- const separatorIndex = name.indexOf(":");
294
- return separatorIndex >= 0 ? name.slice(separatorIndex + 1) : name;
295
- }
296
-
297
- function parseXml(xml: string): XmlElementNode {
298
- const root: XmlElementNode = {
299
- type: "element",
300
- name: "__root__",
301
- attributes: {},
302
- children: [],
303
- start: 0,
304
- end: xml.length,
305
- };
306
- const stack: XmlElementNode[] = [root];
307
- let cursor = 0;
308
-
309
- while (cursor < xml.length) {
310
- if (xml.startsWith("<!--", cursor)) {
311
- const end = xml.indexOf("-->", cursor);
312
- cursor = end >= 0 ? end + 3 : xml.length;
313
- continue;
314
- }
315
-
316
- if (xml.startsWith("<?", cursor)) {
317
- const end = xml.indexOf("?>", cursor);
318
- cursor = end >= 0 ? end + 2 : xml.length;
319
- continue;
320
- }
321
-
322
- if (xml.startsWith("<![CDATA[", cursor)) {
323
- const end = xml.indexOf("]]>", cursor);
324
- const textEnd = end >= 0 ? end : xml.length;
325
- stack[stack.length - 1]?.children.push({
326
- type: "text",
327
- text: xml.slice(cursor + 9, textEnd),
328
- start: cursor,
329
- end: end >= 0 ? end + 3 : xml.length,
330
- });
331
- cursor = end >= 0 ? end + 3 : xml.length;
332
- continue;
333
- }
334
-
335
- if (xml[cursor] !== "<") {
336
- const nextTag = xml.indexOf("<", cursor);
337
- const end = nextTag >= 0 ? nextTag : xml.length;
338
- const text = decodeXmlEntities(xml.slice(cursor, end));
339
- if (text.length > 0) {
340
- stack[stack.length - 1]?.children.push({
341
- type: "text",
342
- text,
343
- start: cursor,
344
- end,
345
- });
346
- }
347
- cursor = end;
348
- continue;
349
- }
350
-
351
- if (xml[cursor + 1] === "/") {
352
- const end = xml.indexOf(">", cursor);
353
- if (end < 0) {
354
- throw new Error("Malformed XML: missing closing >.");
355
- }
356
-
357
- const name = xml.slice(cursor + 2, end).trim();
358
- const current = stack.pop();
359
- if (!current || localName(current.name) !== localName(name)) {
360
- throw new Error(`Malformed XML: unexpected closing tag </${name}>.`);
361
- }
362
- current.end = end + 1;
363
- cursor = end + 1;
364
- continue;
365
- }
366
-
367
- const tagEnd = findTagEnd(xml, cursor);
368
- const tagBody = xml.slice(cursor + 1, tagEnd);
369
- const selfClosing = /\/\s*$/.test(tagBody);
370
- const { name, attributes } = parseTag(tagBody.replace(/\/\s*$/, "").trim());
371
- const element: XmlElementNode = {
372
- type: "element",
373
- name,
374
- attributes,
375
- children: [],
376
- start: cursor,
377
- end: tagEnd + 1,
378
- };
379
- stack[stack.length - 1]?.children.push(element);
380
-
381
- if (!selfClosing) {
382
- stack.push(element);
383
- }
384
-
385
- cursor = tagEnd + 1;
386
- }
387
-
388
- if (stack.length !== 1) {
389
- throw new Error("Malformed XML: unclosed element.");
390
- }
391
-
392
- return root;
393
- }
394
-
395
- function parseTag(tagBody: string): { name: string; attributes: Record<string, string> } {
396
- let cursor = 0;
397
- while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
398
- cursor += 1;
399
- }
400
-
401
- const nameStart = cursor;
402
- while (cursor < tagBody.length && !/\s/.test(tagBody[cursor] ?? "")) {
403
- cursor += 1;
404
- }
405
- const name = tagBody.slice(nameStart, cursor);
406
- const attributes: Record<string, string> = {};
407
-
408
- while (cursor < tagBody.length) {
409
- while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
410
- cursor += 1;
411
- }
412
- if (cursor >= tagBody.length) {
413
- break;
414
- }
415
-
416
- const keyStart = cursor;
417
- while (cursor < tagBody.length && !/[\s=]/.test(tagBody[cursor] ?? "")) {
418
- cursor += 1;
419
- }
420
- const key = tagBody.slice(keyStart, cursor);
421
-
422
- while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
423
- cursor += 1;
424
- }
425
-
426
- if (tagBody[cursor] !== "=") {
427
- attributes[key] = "";
428
- continue;
429
- }
430
- cursor += 1;
431
-
432
- while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
433
- cursor += 1;
434
- }
435
-
436
- const quote = tagBody[cursor];
437
- if (quote !== `"` && quote !== `'`) {
438
- throw new Error(`Malformed XML attribute ${key}.`);
439
- }
440
- cursor += 1;
441
-
442
- const valueStart = cursor;
443
- while (cursor < tagBody.length && tagBody[cursor] !== quote) {
444
- cursor += 1;
445
- }
446
- attributes[key] = decodeXmlEntities(tagBody.slice(valueStart, cursor));
447
- cursor += 1;
448
- }
449
-
450
- return { name, attributes };
451
- }
452
-
453
- function findTagEnd(xml: string, start: number): number {
454
- let cursor = start + 1;
455
- let quote: string | null = null;
456
-
457
- while (cursor < xml.length) {
458
- const current = xml[cursor];
459
- if (quote) {
460
- if (current === quote) {
461
- quote = null;
462
- }
463
- cursor += 1;
464
- continue;
465
- }
466
-
467
- if (current === `"` || current === `'`) {
468
- quote = current;
469
- cursor += 1;
470
- continue;
471
- }
472
-
473
- if (current === ">") {
474
- return cursor;
475
- }
476
-
477
- cursor += 1;
478
- }
479
-
480
- throw new Error("Malformed XML: missing >.");
481
- }
482
-
483
- function decodeXmlEntities(value: string): string {
484
- return value.replace(/&(#x[0-9a-fA-F]+|#\d+|amp|lt|gt|quot|apos);/g, (match, entity) => {
485
- switch (entity) {
486
- case "amp":
487
- return "&";
488
- case "lt":
489
- return "<";
490
- case "gt":
491
- return ">";
492
- case "quot":
493
- return `"`;
494
- case "apos":
495
- return "'";
496
- default:
497
- if (entity.startsWith("#x")) {
498
- return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
499
- }
500
- if (entity.startsWith("#")) {
501
- return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
502
- }
503
- return match;
504
- }
505
- });
506
- }
507
302
 
508
303
  // Cell property readers
509
304
 
510
305
  export function readCellWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
511
306
  const widthNode = findFirstChild(propertiesNode, "tcW");
512
307
  if (!widthNode) return undefined;
513
- const value = parsePositiveInteger(widthNode.attributes["w:w"] ?? widthNode.attributes.w);
514
- const rawType = (widthNode.attributes["w:type"] ?? widthNode.attributes.type ?? "dxa").toLowerCase();
308
+ const value = parsePositiveInteger(readStringAttr(widthNode, "w:w"));
309
+ const rawType = (readStringAttr(widthNode, "w:type") ?? "dxa").toLowerCase();
515
310
  const type: ParsedTableWidth["type"] =
516
311
  rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
517
312
  return { value, type };
@@ -534,21 +329,46 @@ export function readCellBorders(propertiesNode: XmlElementNode): ParsedTableCell
534
329
  export function readCellShading(propertiesNode: XmlElementNode): ParsedCellShading | undefined {
535
330
  const shdNode = findFirstChild(propertiesNode, "shd");
536
331
  if (!shdNode) return undefined;
537
- const fill = shdNode.attributes["w:fill"] ?? shdNode.attributes.fill;
538
- const color = shdNode.attributes["w:color"] ?? shdNode.attributes.color;
539
- const val = shdNode.attributes["w:val"] ?? shdNode.attributes.val;
540
- if (!fill && !color && !val) return undefined;
332
+ const fill = readStringAttr(shdNode, "w:fill");
333
+ const color = readStringAttr(shdNode, "w:color");
334
+ const val = readStringAttr(shdNode, "w:val");
335
+ // SOW gap G3 theme-shading refs. The CCEP SOW uses
336
+ // `<w:shd w:themeFill="accent5" w:themeFillTint="33"/>` on the Appendix-1
337
+ // mid-table sub-rows; when `w:fill` is absent or "auto", the runtime
338
+ // resolves the paint via ThemeColorResolver.resolveWordThemeColor so the
339
+ // cell picks up the theme cascade rather than rendering unshaded.
340
+ const themeFill = readStringAttr(shdNode, "w:themeFill");
341
+ const themeFillTint = readStringAttr(shdNode, "w:themeFillTint");
342
+ const themeFillShade = readStringAttr(shdNode, "w:themeFillShade");
343
+ const themeColor = readStringAttr(shdNode, "w:themeColor");
344
+ const themeColorTint = readStringAttr(shdNode, "w:themeColorTint");
345
+ const themeColorShade = readStringAttr(shdNode, "w:themeColorShade");
346
+ if (
347
+ !fill &&
348
+ !color &&
349
+ !val &&
350
+ !themeFill &&
351
+ !themeColor
352
+ ) {
353
+ return undefined;
354
+ }
541
355
  const result: ParsedCellShading = {};
542
356
  if (fill) result.fill = fill;
543
357
  if (color) result.color = color;
544
358
  if (val) result.val = val;
359
+ if (themeFill) result.themeFill = themeFill;
360
+ if (themeFillTint) result.themeFillTint = themeFillTint;
361
+ if (themeFillShade) result.themeFillShade = themeFillShade;
362
+ if (themeColor) result.themeColor = themeColor;
363
+ if (themeColorTint) result.themeColorTint = themeColorTint;
364
+ if (themeColorShade) result.themeColorShade = themeColorShade;
545
365
  return result;
546
366
  }
547
367
 
548
368
  export function readCellVerticalAlign(propertiesNode: XmlElementNode): "top" | "center" | "bottom" | undefined {
549
369
  const vAlignNode = findFirstChild(propertiesNode, "vAlign");
550
370
  if (!vAlignNode) return undefined;
551
- const val = vAlignNode.attributes["w:val"] ?? vAlignNode.attributes.val;
371
+ const val = readStringAttr(vAlignNode, "w:val");
552
372
  if (val === "center" || val === "top" || val === "bottom") return val;
553
373
  return undefined;
554
374
  }
@@ -558,8 +378,8 @@ export function readCellVerticalAlign(propertiesNode: XmlElementNode): "top" | "
558
378
  export function readTableWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
559
379
  const widthNode = findFirstChild(propertiesNode, "tblW");
560
380
  if (!widthNode) return undefined;
561
- const value = parsePositiveInteger(widthNode.attributes["w:w"] ?? widthNode.attributes.w);
562
- const rawType = (widthNode.attributes["w:type"] ?? widthNode.attributes.type ?? "dxa").toLowerCase();
381
+ const value = parsePositiveInteger(readStringAttr(widthNode, "w:w"));
382
+ const rawType = (readStringAttr(widthNode, "w:type") ?? "dxa").toLowerCase();
563
383
  const type: ParsedTableWidth["type"] =
564
384
  rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
565
385
  return { value, type };
@@ -568,7 +388,7 @@ export function readTableWidth(propertiesNode: XmlElementNode): ParsedTableWidth
568
388
  export function readTableAlignment(propertiesNode: XmlElementNode): "left" | "center" | "right" | undefined {
569
389
  const jcNode = findFirstChild(propertiesNode, "jc");
570
390
  if (!jcNode) return undefined;
571
- const val = jcNode.attributes["w:val"] ?? jcNode.attributes.val;
391
+ const val = readStringAttr(jcNode, "w:val");
572
392
  if (val === "left" || val === "center" || val === "right") return val;
573
393
  return undefined;
574
394
  }
@@ -576,7 +396,7 @@ export function readTableAlignment(propertiesNode: XmlElementNode): "left" | "ce
576
396
  export function readTableStyleId(propertiesNode: XmlElementNode): string | undefined {
577
397
  const styleNode = findFirstChild(propertiesNode, "tblStyle");
578
398
  if (!styleNode) return undefined;
579
- return styleNode.attributes["w:val"] ?? styleNode.attributes.val;
399
+ return readStringAttr(styleNode, "w:val");
580
400
  }
581
401
 
582
402
  export function readTableLook(propertiesNode: XmlElementNode): ParsedTableLook | undefined {
@@ -584,7 +404,7 @@ export function readTableLook(propertiesNode: XmlElementNode): ParsedTableLook |
584
404
  if (!tblLookNode) return undefined;
585
405
 
586
406
  const tableLook: ParsedTableLook = {};
587
- const val = tblLookNode.attributes["w:val"] ?? tblLookNode.attributes.val;
407
+ const val = readStringAttr(tblLookNode, "w:val");
588
408
  if (val) {
589
409
  tableLook.val = val;
590
410
  }
@@ -627,7 +447,7 @@ export function readTableCellMargins(propertiesNode: XmlElementNode): ParsedCell
627
447
  const readSide = (name: string): number | undefined => {
628
448
  const node = findFirstChild(marginsNode, name);
629
449
  if (!node) return undefined;
630
- return parsePositiveInteger(node.attributes["w:w"] ?? node.attributes.w);
450
+ return parsePositiveInteger(readStringAttr(node, "w:w"));
631
451
  };
632
452
  const top = readSide("top");
633
453
  const bottom = readSide("bottom");
@@ -650,13 +470,13 @@ export function readTableCellMargins(propertiesNode: XmlElementNode): ParsedCell
650
470
  export function readRowHeight(propertiesNode: XmlElementNode): number | undefined {
651
471
  const heightNode = findFirstChild(propertiesNode, "trHeight");
652
472
  if (!heightNode) return undefined;
653
- return parsePositiveInteger(heightNode.attributes["w:val"] ?? heightNode.attributes.val);
473
+ return parsePositiveInteger(readStringAttr(heightNode, "w:val"));
654
474
  }
655
475
 
656
476
  export function readRowHeightRule(propertiesNode: XmlElementNode): "auto" | "atLeast" | "exact" | undefined {
657
477
  const heightNode = findFirstChild(propertiesNode, "trHeight");
658
478
  if (!heightNode) return undefined;
659
- const raw = (heightNode.attributes["w:hRule"] ?? heightNode.attributes.hRule ?? "").toLowerCase();
479
+ const raw = (readStringAttr(heightNode, "w:hRule") ?? "").toLowerCase();
660
480
  if (raw === "atleast") return "atLeast";
661
481
  if (raw === "exact") return "exact";
662
482
  if (raw === "auto") return "auto";
@@ -666,17 +486,17 @@ export function readRowHeightRule(propertiesNode: XmlElementNode): "auto" | "atL
666
486
  export function readRowIsHeader(propertiesNode: XmlElementNode): boolean | undefined {
667
487
  const headerNode = findFirstChild(propertiesNode, "tblHeader");
668
488
  if (!headerNode) return undefined;
669
- const val = headerNode.attributes["w:val"] ?? headerNode.attributes.val;
489
+ const val = readStringAttr(headerNode, "w:val");
670
490
  return val !== "false" && val !== "0";
671
491
  }
672
492
 
673
493
  export function readTableIndent(propertiesNode: XmlElementNode): ParsedTableIndent | undefined {
674
494
  const indentNode = findFirstChild(propertiesNode, "tblInd");
675
495
  if (!indentNode) return undefined;
676
- const valueRaw = indentNode.attributes["w:w"] ?? indentNode.attributes.w;
496
+ const valueRaw = readStringAttr(indentNode, "w:w");
677
497
  const value = valueRaw !== undefined ? Number.parseInt(valueRaw, 10) : 0;
678
498
  if (!Number.isFinite(value)) return undefined;
679
- const rawType = (indentNode.attributes["w:type"] ?? indentNode.attributes.type ?? "dxa").toLowerCase();
499
+ const rawType = (readStringAttr(indentNode, "w:type") ?? "dxa").toLowerCase();
680
500
  const type: ParsedTableIndent["type"] =
681
501
  rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
682
502
  return { value, type };
@@ -685,7 +505,7 @@ export function readTableIndent(propertiesNode: XmlElementNode): ParsedTableInde
685
505
  export function readTableLayoutMode(propertiesNode: XmlElementNode): "fixed" | "autofit" | undefined {
686
506
  const layoutNode = findFirstChild(propertiesNode, "tblLayout");
687
507
  if (!layoutNode) return undefined;
688
- const raw = (layoutNode.attributes["w:type"] ?? layoutNode.attributes.type ?? "").toLowerCase();
508
+ const raw = (readStringAttr(layoutNode, "w:type") ?? "").toLowerCase();
689
509
  if (raw === "fixed") return "fixed";
690
510
  if (raw === "autofit") return "autofit";
691
511
  return undefined;
@@ -694,10 +514,10 @@ export function readTableLayoutMode(propertiesNode: XmlElementNode): "fixed" | "
694
514
  export function readTableCellSpacing(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
695
515
  const spacingNode = findFirstChild(propertiesNode, "tblCellSpacing");
696
516
  if (!spacingNode) return undefined;
697
- const valueRaw = spacingNode.attributes["w:w"] ?? spacingNode.attributes.w;
517
+ const valueRaw = readStringAttr(spacingNode, "w:w");
698
518
  const value = valueRaw !== undefined ? Number.parseInt(valueRaw, 10) : 0;
699
519
  if (!Number.isFinite(value)) return undefined;
700
- const rawType = (spacingNode.attributes["w:type"] ?? spacingNode.attributes.type ?? "dxa").toLowerCase();
520
+ const rawType = (readStringAttr(spacingNode, "w:type") ?? "dxa").toLowerCase();
701
521
  const type: ParsedTableWidth["type"] =
702
522
  rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
703
523
  return { value, type };
@@ -706,19 +526,19 @@ export function readTableCellSpacing(propertiesNode: XmlElementNode): ParsedTabl
706
526
  export function readTableCaption(propertiesNode: XmlElementNode): string | undefined {
707
527
  const captionNode = findFirstChild(propertiesNode, "tblCaption");
708
528
  if (!captionNode) return undefined;
709
- return captionNode.attributes["w:val"] ?? captionNode.attributes.val;
529
+ return readStringAttr(captionNode, "w:val");
710
530
  }
711
531
 
712
532
  export function readTableDescription(propertiesNode: XmlElementNode): string | undefined {
713
533
  const descriptionNode = findFirstChild(propertiesNode, "tblDescription");
714
534
  if (!descriptionNode) return undefined;
715
- return descriptionNode.attributes["w:val"] ?? descriptionNode.attributes.val;
535
+ return readStringAttr(descriptionNode, "w:val");
716
536
  }
717
537
 
718
538
  export function readTableBidiVisual(propertiesNode: XmlElementNode): boolean | undefined {
719
539
  const bidiNode = findFirstChild(propertiesNode, "bidiVisual");
720
540
  if (!bidiNode) return undefined;
721
- const val = bidiNode.attributes["w:val"] ?? bidiNode.attributes.val;
541
+ const val = readStringAttr(bidiNode, "w:val");
722
542
  return val !== "false" && val !== "0" && val !== "off";
723
543
  }
724
544
 
@@ -773,7 +593,7 @@ export function readTableFloating(propertiesNode: XmlElementNode): ParsedTableFl
773
593
  }
774
594
 
775
595
  if (overlapNode) {
776
- const val = (overlapNode.attributes["w:val"] ?? overlapNode.attributes.val ?? "overlap").toLowerCase();
596
+ const val = (readStringAttr(overlapNode, "w:val") ?? "overlap").toLowerCase();
777
597
  floating.overlap = val === "overlap";
778
598
  }
779
599
 
@@ -783,14 +603,14 @@ export function readTableFloating(propertiesNode: XmlElementNode): ParsedTableFl
783
603
  export function readRowCantSplit(propertiesNode: XmlElementNode): boolean | undefined {
784
604
  const cantSplitNode = findFirstChild(propertiesNode, "cantSplit");
785
605
  if (!cantSplitNode) return undefined;
786
- const val = cantSplitNode.attributes["w:val"] ?? cantSplitNode.attributes.val;
606
+ const val = readStringAttr(cantSplitNode, "w:val");
787
607
  return val !== "false" && val !== "0" && val !== "off";
788
608
  }
789
609
 
790
610
  export function readRowHorizontalAlignment(propertiesNode: XmlElementNode): "left" | "center" | "right" | undefined {
791
611
  const jcNode = findFirstChild(propertiesNode, "jc");
792
612
  if (!jcNode) return undefined;
793
- const val = jcNode.attributes["w:val"] ?? jcNode.attributes.val;
613
+ const val = readStringAttr(jcNode, "w:val");
794
614
  if (val === "left" || val === "center" || val === "right") return val;
795
615
  return undefined;
796
616
  }
@@ -798,13 +618,13 @@ export function readRowHorizontalAlignment(propertiesNode: XmlElementNode): "lef
798
618
  export function readRowCnfStyle(propertiesNode: XmlElementNode): string | undefined {
799
619
  const cnfNode = findFirstChild(propertiesNode, "cnfStyle");
800
620
  if (!cnfNode) return undefined;
801
- return cnfNode.attributes["w:val"] ?? cnfNode.attributes.val;
621
+ return readStringAttr(cnfNode, "w:val");
802
622
  }
803
623
 
804
624
  export function readCellTextDirection(propertiesNode: XmlElementNode): "lrTb" | "tbRl" | "btLr" | undefined {
805
625
  const dirNode = findFirstChild(propertiesNode, "textDirection");
806
626
  if (!dirNode) return undefined;
807
- const val = dirNode.attributes["w:val"] ?? dirNode.attributes.val;
627
+ const val = readStringAttr(dirNode, "w:val");
808
628
  if (val === "lrTb" || val === "tbRl" || val === "btLr") return val;
809
629
  return undefined;
810
630
  }
@@ -812,14 +632,14 @@ export function readCellTextDirection(propertiesNode: XmlElementNode): "lrTb" |
812
632
  export function readCellNoWrap(propertiesNode: XmlElementNode): boolean | undefined {
813
633
  const noWrapNode = findFirstChild(propertiesNode, "noWrap");
814
634
  if (!noWrapNode) return undefined;
815
- const val = noWrapNode.attributes["w:val"] ?? noWrapNode.attributes.val;
635
+ const val = readStringAttr(noWrapNode, "w:val");
816
636
  return val !== "false" && val !== "0" && val !== "off";
817
637
  }
818
638
 
819
639
  export function readCellFitText(propertiesNode: XmlElementNode): boolean | undefined {
820
640
  const fitNode = findFirstChild(propertiesNode, "tcFitText");
821
641
  if (!fitNode) return undefined;
822
- const val = fitNode.attributes["w:val"] ?? fitNode.attributes.val;
642
+ const val = readStringAttr(fitNode, "w:val");
823
643
  return val !== "false" && val !== "0" && val !== "off";
824
644
  }
825
645
 
@@ -829,7 +649,7 @@ export function readCellMargins(propertiesNode: XmlElementNode): ParsedCellMargi
829
649
  const readSide = (name: string): number | undefined => {
830
650
  const sideNode = findFirstChild(marginsNode, name);
831
651
  if (!sideNode) return undefined;
832
- const raw = sideNode.attributes["w:w"] ?? sideNode.attributes.w;
652
+ const raw = readStringAttr(sideNode, "w:w");
833
653
  if (raw === undefined) return undefined;
834
654
  const parsed = Number.parseInt(raw, 10);
835
655
  return Number.isFinite(parsed) ? parsed : undefined;
@@ -852,14 +672,14 @@ export function readCellMargins(propertiesNode: XmlElementNode): ParsedCellMargi
852
672
  export function readCellCnfStyle(propertiesNode: XmlElementNode): string | undefined {
853
673
  const cnfNode = findFirstChild(propertiesNode, "cnfStyle");
854
674
  if (!cnfNode) return undefined;
855
- return cnfNode.attributes["w:val"] ?? cnfNode.attributes.val;
675
+ return readStringAttr(cnfNode, "w:val");
856
676
  }
857
677
 
858
678
  function parseBorderSpec(child: XmlElementNode): ParsedBorderSpec | undefined {
859
- const value = child.attributes["w:val"] ?? child.attributes.val;
860
- const sizeRaw = child.attributes["w:sz"] ?? child.attributes.sz;
861
- const spaceRaw = child.attributes["w:space"] ?? child.attributes.space;
862
- const color = child.attributes["w:color"] ?? child.attributes.color;
679
+ const value = readStringAttr(child, "w:val");
680
+ const sizeRaw = readStringAttr(child, "w:sz");
681
+ const spaceRaw = readStringAttr(child, "w:space");
682
+ const color = readStringAttr(child, "w:color");
863
683
  if (!value && !sizeRaw && !spaceRaw && !color) return undefined;
864
684
  const spec: ParsedBorderSpec = {};
865
685
  if (value) spec.value = value;
@@ -8,6 +8,7 @@ import type {
8
8
  } from "../../model/canonical-document.ts";
9
9
  import type { XmlElementNode } from "./xml-element.ts";
10
10
  import { parseXml } from "./xml-parser.ts";
11
+ import { localName } from "./xml-attr-helpers.ts";
11
12
 
12
13
  // ---- Well-known DrawingML color slot names ----
13
14
 
@@ -274,8 +275,4 @@ function findChildElementOptional(
274
275
  );
275
276
  }
276
277
 
277
- function localName(name: string): string {
278
- const idx = name.indexOf(":");
279
- return idx >= 0 ? name.slice(idx + 1) : name;
280
- }
281
278
 
@@ -23,6 +23,9 @@
23
23
  * `modelledChildNames`.
24
24
  */
25
25
 
26
+ import type { SerializableXmlElement } from "./xml-element-serialize.ts";
27
+ import { serializeXmlElementToString } from "./xml-element-serialize.ts";
28
+
26
29
  /**
27
30
  * Input node shape accepted by `capturePropertyGrabBag`. Intentionally
28
31
  * minimal so every caller can adapt their own scanner output — per-file
@@ -56,14 +59,10 @@ export interface GrabBagSourceChild {
56
59
  * the correct trade-off — unmodelled children's semantic content
57
60
  * survives, which closes the silent-drop gap.
58
61
  */
59
- export function buildGrabBagSourceChildFromParsed(node: {
60
- name: string;
61
- attributes: Record<string, string>;
62
- children: Array<{ type: "element"; name: string; attributes: Record<string, string>; children: unknown[] } | { type: "text"; text: string }>;
63
- }): GrabBagSourceChild {
62
+ export function buildGrabBagSourceChildFromParsed(node: SerializableXmlElement): GrabBagSourceChild {
64
63
  return {
65
64
  localName: localNameOf(node.name),
66
- rawXml: serializeElementToString(node),
65
+ rawXml: serializeXmlElementToString(node),
67
66
  };
68
67
  }
69
68
 
@@ -72,47 +71,6 @@ function localNameOf(qualified: string): string {
72
71
  return colon < 0 ? qualified : qualified.slice(colon + 1);
73
72
  }
74
73
 
75
- function escapeAttr(value: string): string {
76
- return value
77
- .replace(/&/gu, "&amp;")
78
- .replace(/</gu, "&lt;")
79
- .replace(/>/gu, "&gt;")
80
- .replace(/"/gu, "&quot;");
81
- }
82
-
83
- function escapeText(value: string): string {
84
- return value
85
- .replace(/&/gu, "&amp;")
86
- .replace(/</gu, "&lt;")
87
- .replace(/>/gu, "&gt;");
88
- }
89
-
90
- function serializeElementToString(node: {
91
- name: string;
92
- attributes: Record<string, string>;
93
- children: Array<{ type: "element"; name: string; attributes: Record<string, string>; children: unknown[] } | { type: "text"; text: string }>;
94
- }): string {
95
- const attrs = Object.entries(node.attributes)
96
- .map(([name, value]) => ` ${name}="${escapeAttr(value)}"`)
97
- .join("");
98
- if (node.children.length === 0) {
99
- return `<${node.name}${attrs}/>`;
100
- }
101
- const body = node.children
102
- .map((child) => {
103
- if (child.type === "text") return escapeText(child.text);
104
- return serializeElementToString(
105
- child as {
106
- name: string;
107
- attributes: Record<string, string>;
108
- children: Array<{ type: "element"; name: string; attributes: Record<string, string>; children: unknown[] } | { type: "text"; text: string }>;
109
- },
110
- );
111
- })
112
- .join("");
113
- return `<${node.name}${attrs}>${body}</${node.name}>`;
114
- }
115
-
116
74
  /**
117
75
  * Descriptor a per-container parser supplies to the helper to declare
118
76
  * which child element names it dispatches into its modelled fields.
@@ -13,6 +13,7 @@ import type {
13
13
  WorkflowWorkItem,
14
14
  } from "../../api/public-types.ts";
15
15
  import type { EditorStateNamespace, EditorStateLocation } from "../../api/editor-state-types.ts";
16
+ import { sha256Hex } from "../source-package-provenance.ts";
16
17
  import {
17
18
  validateWorkflowPayloadEnvelope,
18
19
  type ValidatorIssue,
@@ -42,6 +43,8 @@ export interface EditorStatePayload {
42
43
  unknownNamespaces?: Array<{ name: string; rawXml: string }>;
43
44
  }
44
45
 
46
+ const workflowAnchorHashEncoder = new TextEncoder();
47
+
45
48
  // ---------------------------------------------------------------------------
46
49
  // Schema 1.1 parser helpers (fail-closed per spec §8.2)
47
50
  // ---------------------------------------------------------------------------
@@ -747,7 +750,9 @@ function createAnchorBindingHash(
747
750
  from: number,
748
751
  to: number,
749
752
  ): string {
750
- return `${serializeStoryTargetKey(storyTarget)}:${from}:${to}`;
753
+ return sha256Hex(
754
+ workflowAnchorHashEncoder.encode(`${serializeStoryTargetKey(storyTarget)}:${from}:${to}`),
755
+ );
751
756
  }
752
757
 
753
758
  function getPreservedExtensionsXml(sourcePackage: OpcPackage, payloadPartPath: string): string {