@beyondwork/docx-react-component 1.0.58 → 1.0.59

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 (134) 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 +978 -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 +2 -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/xml-element-serialize.ts +32 -0
  69. package/src/io/ooxml/xml-parser.ts +183 -0
  70. package/src/legal/bookmarks.ts +1 -1
  71. package/src/legal/cross-references.ts +1 -1
  72. package/src/legal/defined-terms.ts +1 -1
  73. package/src/legal/{_document-root.ts → document-root.ts} +8 -0
  74. package/src/legal/signature-blocks.ts +1 -1
  75. package/src/model/canonical-document.ts +159 -6
  76. package/src/model/chart-types.ts +439 -0
  77. package/src/model/snapshot.ts +3 -1
  78. package/src/review/store/comment-remapping.ts +24 -11
  79. package/src/review/store/revision-actions.ts +482 -2
  80. package/src/review/store/revision-store.ts +15 -0
  81. package/src/review/store/revision-types.ts +76 -0
  82. package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
  83. package/src/runtime/collab/runtime-collab-sync.ts +33 -0
  84. package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
  85. package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
  86. package/src/runtime/document-runtime.ts +476 -34
  87. package/src/runtime/document-search.ts +115 -0
  88. package/src/runtime/edit-ops/index.ts +18 -2
  89. package/src/runtime/footnote-resolver.ts +130 -0
  90. package/src/runtime/layout/layout-engine-instance.ts +31 -4
  91. package/src/runtime/layout/layout-engine-version.ts +37 -1
  92. package/src/runtime/layout/page-graph.ts +14 -1
  93. package/src/runtime/layout/resolved-formatting-state.ts +21 -0
  94. package/src/runtime/numbering-prefix.ts +17 -0
  95. package/src/runtime/query-scopes.ts +5 -8
  96. package/src/runtime/resolved-numbering-geometry.ts +37 -6
  97. package/src/runtime/revision-runtime.ts +27 -1
  98. package/src/runtime/selection/post-edit-validator.ts +60 -6
  99. package/src/runtime/structure-ops/index.ts +20 -4
  100. package/src/runtime/surface-projection.ts +290 -21
  101. package/src/runtime/table-schema.ts +6 -0
  102. package/src/runtime/theme-color-resolver.ts +2 -2
  103. package/src/runtime/units.ts +9 -0
  104. package/src/runtime/workflow-rail-segments.ts +4 -0
  105. package/src/ui/WordReviewEditor.tsx +187 -43
  106. package/src/ui/editor-runtime-boundary.ts +10 -0
  107. package/src/ui/editor-shell-view.tsx +4 -1
  108. package/src/ui/headless/chrome-registry.ts +53 -0
  109. package/src/ui/headless/selection-tool-resolver.ts +11 -1
  110. package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
  111. package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
  112. package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
  113. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
  114. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
  115. package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
  116. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
  117. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
  118. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
  119. package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
  120. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
  121. package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
  122. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
  123. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
  124. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
  125. package/src/ui-tailwind/index.ts +9 -0
  126. package/src/ui-tailwind/page-chrome-model.ts +77 -5
  127. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
  128. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
  129. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
  130. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
  131. package/src/ui-tailwind/theme/tokens.ts +14 -0
  132. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
  133. package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
  134. package/src/validation/diagnostics.ts +1 -0
@@ -509,17 +509,38 @@ function deleteRow(
509
509
  }
510
510
 
511
511
  const deleteIndex = selection.anchorCell.rowIndex;
512
- const grid = buildLogicalGrid(table);
512
+ const nextTable = removeTableRowPure(table, deleteIndex);
513
+ const focusRowIndex = Math.max(0, Math.min(deleteIndex, nextTable.rows.length - 1));
514
+
515
+ return commitTableChange(
516
+ document,
517
+ root,
518
+ selection.tableBlockIndex,
519
+ nextTable,
520
+ fallbackSelection,
521
+ focusRowIndex,
522
+ selection.anchorCell.columnIndex,
523
+ );
524
+ }
513
525
 
514
- // For every logical column, check if this row is the origin of a vMerge
515
- // chain that extends beyond this row. If so, we must promote the first
516
- // following continue cell to "restart" and copy over the origin's
517
- // content/properties so the chain survives the deletion.
518
- //
519
- // For columns where this row is a mid-chain continue, the chain's owner
520
- // origin stays in place we just need to drop 1 from its effective
521
- // rowSpan (which happens automatically when the row is removed from
522
- // the canonical tree).
526
+ /**
527
+ * Pure model-level row removal for a table node.
528
+ *
529
+ * Removes `table.rows[rowIndex]` while preserving the vMerge chain invariant:
530
+ * if the deleted row is the origin of a chain that extends beyond it, the
531
+ * first following continue cell is promoted to "restart" and inherits the
532
+ * origin's content/properties. For mid-chain continues, nothing needs to be
533
+ * rewritten because the chain's origin stays in place.
534
+ *
535
+ * Used by `deleteRow` above (UI command path) and by Lane 7b's row-insertion
536
+ * reject path in `src/review/store/revision-actions.ts`.
537
+ */
538
+ export function removeTableRowPure(table: TableNode, rowIndex: number): TableNode {
539
+ if (rowIndex < 0 || rowIndex >= table.rows.length) {
540
+ return table;
541
+ }
542
+
543
+ const grid = buildLogicalGrid(table);
523
544
  const promotions: Array<{
524
545
  originColumnIndex: number;
525
546
  originColumnSpan: number;
@@ -528,43 +549,140 @@ function deleteRow(
528
549
  }> = [];
529
550
 
530
551
  for (let column = 0; column < grid.columnCount; ) {
531
- const origin = originAt(grid, deleteIndex, column);
552
+ const origin = originAt(grid, rowIndex, column);
532
553
  if (!origin) {
533
554
  column += 1;
534
555
  continue;
535
556
  }
536
- if (origin.rowIndex === deleteIndex && origin.rowSpan > 1) {
557
+ if (origin.rowIndex === rowIndex && origin.rowSpan > 1) {
537
558
  promotions.push({
538
559
  originColumnIndex: origin.columnIndex,
539
560
  originColumnSpan: origin.columnSpan,
540
- promotedRowIndex: deleteIndex + 1,
561
+ promotedRowIndex: rowIndex + 1,
541
562
  originCell: origin.cell,
542
563
  });
543
564
  }
544
565
  column = origin.columnIndex + origin.columnSpan;
545
566
  }
546
567
 
547
- // Apply promotions to the row immediately after the deleted one.
548
- const nextRows = table.rows.map((row, rowIndex) => {
549
- if (rowIndex !== deleteIndex + 1 || promotions.length === 0) return row;
550
- return applyVMergePromotions(row, promotions);
551
- }).filter((_, rowIndex) => rowIndex !== deleteIndex);
568
+ const nextRows = table.rows
569
+ .map((row, index) => {
570
+ if (index !== rowIndex + 1 || promotions.length === 0) return row;
571
+ return applyVMergePromotions(row, promotions);
572
+ })
573
+ .filter((_, index) => index !== rowIndex);
552
574
 
553
- const focusRowIndex = Math.max(0, Math.min(deleteIndex, nextRows.length - 1));
554
- const nextTable: TableNode = {
555
- ...table,
556
- rows: nextRows,
575
+ return { ...table, rows: nextRows };
576
+ }
577
+
578
+ /**
579
+ * Pure model-level cell removal for a row within a table.
580
+ *
581
+ * Removes `table.rows[rowIndex].cells[cellIndex]` while preserving:
582
+ *
583
+ * 1. **Row logical-column footprint.** The row occupies the same total
584
+ * logical column count as before — if the removed cell had
585
+ * `gridSpan = N`, an adjacent cell absorbs +N via its own `gridSpan`.
586
+ * Preference: the immediately preceding cell absorbs the delta; if
587
+ * the removed cell is the first in the row, the immediately following
588
+ * cell absorbs instead. This keeps the table-level `gridColumns` array
589
+ * unchanged (important — shrinking `gridColumns` would cascade-change
590
+ * every sibling row).
591
+ *
592
+ * 2. **vMerge chain integrity.** If the removed cell is the origin of a
593
+ * vMerge chain that extends past this row, the chain's first
594
+ * following continue cell is promoted to `"restart"` (inheriting the
595
+ * origin's content/properties). Mid-chain continues and tail
596
+ * continues are unaffected — when the origin's content moves, the
597
+ * chain shape stays valid by construction.
598
+ *
599
+ * 3. **Single-cell row invariant.** If the row contains only one cell,
600
+ * removing it is equivalent to removing the row entirely — a
601
+ * different operation with different grid semantics. This helper
602
+ * declines the operation and returns `table` unchanged; callers must
603
+ * use `removeTableRowPure` instead.
604
+ *
605
+ * Used by Lane 7b's cellIns reject path in
606
+ * `src/review/store/revision-actions.ts`. The Lane 7b parser stamps
607
+ * `metadata.tableRevisionCoordinates.cellIndex` at parse time so the
608
+ * reject path never needs a runtime position → cell reverse lookup.
609
+ */
610
+ export function removeCellFromRow(
611
+ table: TableNode,
612
+ rowIndex: number,
613
+ cellIndex: number,
614
+ ): TableNode {
615
+ if (rowIndex < 0 || rowIndex >= table.rows.length) {
616
+ return table;
617
+ }
618
+ const row = table.rows[rowIndex];
619
+ if (!row || cellIndex < 0 || cellIndex >= row.cells.length) {
620
+ return table;
621
+ }
622
+ if (row.cells.length <= 1) {
623
+ // Last cell in row — out of scope. Caller should use removeTableRowPure.
624
+ return table;
625
+ }
626
+
627
+ const removed = row.cells[cellIndex]!;
628
+ const removedSpan = Math.max(1, removed.gridSpan ?? 1);
629
+
630
+ // Compute vMerge promotions for the removed cell's logical column range,
631
+ // mirroring the row-removal logic but scoped to one row.
632
+ const grid = buildLogicalGrid(table);
633
+ const promotions: Array<{
634
+ originColumnIndex: number;
635
+ originColumnSpan: number;
636
+ promotedRowIndex: number;
637
+ originCell: TableCellNode;
638
+ }> = [];
639
+
640
+ // Walk the logical columns the removed cell covers. If it's a vMerge origin
641
+ // spanning rows, promote the next-row continue.
642
+ let cursor = row.gridBefore ?? 0;
643
+ for (let i = 0; i < cellIndex; i += 1) {
644
+ cursor += Math.max(1, row.cells[i]!.gridSpan ?? 1);
645
+ }
646
+ const columnStart = cursor;
647
+ const columnEnd = columnStart + removedSpan;
648
+ for (let column = columnStart; column < columnEnd; ) {
649
+ const origin = originAt(grid, rowIndex, column);
650
+ if (!origin) {
651
+ column += 1;
652
+ continue;
653
+ }
654
+ if (origin.rowIndex === rowIndex && origin.rowSpan > 1) {
655
+ promotions.push({
656
+ originColumnIndex: origin.columnIndex,
657
+ originColumnSpan: origin.columnSpan,
658
+ promotedRowIndex: rowIndex + 1,
659
+ originCell: origin.cell,
660
+ });
661
+ }
662
+ column = origin.columnIndex + origin.columnSpan;
663
+ }
664
+
665
+ const nextRowCells = row.cells.filter((_, index) => index !== cellIndex);
666
+ // Rebalance gridSpan: preceding cell absorbs the delta if it exists,
667
+ // otherwise the new first cell (which was the following cell pre-removal).
668
+ const absorberIndex = cellIndex > 0 ? cellIndex - 1 : 0;
669
+ const absorber = nextRowCells[absorberIndex]!;
670
+ const absorberSpan = Math.max(1, absorber.gridSpan ?? 1);
671
+ nextRowCells[absorberIndex] = {
672
+ ...absorber,
673
+ gridSpan: absorberSpan + removedSpan,
557
674
  };
558
675
 
559
- return commitTableChange(
560
- document,
561
- root,
562
- selection.tableBlockIndex,
563
- nextTable,
564
- fallbackSelection,
565
- focusRowIndex,
566
- selection.anchorCell.columnIndex,
567
- );
676
+ const nextRow: TableRowNode = { ...row, cells: nextRowCells };
677
+ const nextRows = table.rows.map((candidate, index) => {
678
+ if (index === rowIndex) return nextRow;
679
+ if (index === rowIndex + 1 && promotions.length > 0) {
680
+ return applyVMergePromotions(candidate, promotions);
681
+ }
682
+ return candidate;
683
+ });
684
+
685
+ return { ...table, rows: nextRows };
568
686
  }
569
687
 
570
688
  function applyVMergePromotions(
@@ -236,6 +236,26 @@ export function areAnchorsEqual(
236
236
  return false;
237
237
  }
238
238
 
239
+ /**
240
+ * Returns `true` when no step in the mapping can change this anchor's
241
+ * positions. Safe to use as a pre-check before calling `mapAnchor` to avoid
242
+ * allocating a new anchor object in hot per-revision / per-comment remap loops.
243
+ *
244
+ * Conservative: if any step starts at or before the anchor's end position the
245
+ * function returns `false` and the caller must run the full `mapAnchor` path.
246
+ * Detached anchors always return `true` — `mapAnchor` already short-circuits
247
+ * them by reference.
248
+ */
249
+ export function anchorUnaffectedByMapping(
250
+ anchor: EditorAnchorProjection,
251
+ mapping: TransactionMapping,
252
+ ): boolean {
253
+ if (anchor.kind === "detached") return true;
254
+ if (mapping.steps.length === 0) return true;
255
+ const end = anchor.kind === "range" ? anchor.range.to : anchor.at;
256
+ return mapping.steps.every((s) => s.from > end);
257
+ }
258
+
239
259
  export function storyTargetsEqual(
240
260
  left: EditorStoryTarget | undefined,
241
261
  right: EditorStoryTarget | undefined,
@@ -33,7 +33,8 @@ export type EditorWarningCode =
33
33
  | "revision_anchor_detached"
34
34
  | "large_document_degraded"
35
35
  | "font_substitution"
36
- | "image_missing";
36
+ | "image_missing"
37
+ | "review_target_not_found";
37
38
 
38
39
  export interface EditorWarning {
39
40
  warningId: string;
package/src/index.ts CHANGED
@@ -288,6 +288,22 @@ export type {
288
288
  EditorStatePolicyMigration,
289
289
  EditorStatePartLoadFailure,
290
290
  EditorStatePartPersistFailure,
291
+ // Lane 8 Track H (v2.x) — agent-helper widening types. Runtime wiring
292
+ // lands in Phase 2; re-exported from the root as stable type surface so
293
+ // workblocks and wrappers can start consuming the shapes in Phase 1.
294
+ TextProjection,
295
+ TextProjectionLine,
296
+ TextProjectionStoryEntry,
297
+ TextProjectionOptions,
298
+ ChangeAnchor,
299
+ ChangeFilter,
300
+ BatchEditOperation,
301
+ BatchEditOptions,
302
+ BatchEditResult,
303
+ BatchEditEntryResult,
304
+ WorkflowOverlayPatch,
305
+ AiExplanationScopeInput,
306
+ AiExplanationScopeResult,
291
307
  } from "./api/public-types.ts";
292
308
 
293
309
  // L7 Phase 2.5 — prerender cache public API. Platforms / ingest workers
@@ -301,3 +317,15 @@ export type {
301
317
  PrerenderCounters,
302
318
  } from "./runtime/prerender/prerender-document.ts";
303
319
  export type { CacheEnvelope } from "./runtime/prerender/cache-envelope.ts";
320
+
321
+ // design-close-chrome Phase 4 / R9 — selection-tool registry extensibility.
322
+ // Hosts can inject entries into the floating selection-tool precedence
323
+ // chain by passing `customSelectionTools` on `WordReviewEditorProps`.
324
+ // Render-side widening (host entries carrying a custom render function)
325
+ // is deferred to Lane 8 API ergonomics.
326
+ export {
327
+ resolveSelectionToolRegistry,
328
+ SELECTION_TOOL_REGISTRY,
329
+ } from "./ui/headless/chrome-registry.ts";
330
+ export type { SelectionToolRegistryEntry } from "./ui/headless/chrome-registry.ts";
331
+ export type { SelectionToolKind } from "./ui/headless/selection-tool-types.ts";
@@ -32,6 +32,15 @@ import {
32
32
  createDefaultCanonicalDocument,
33
33
  createSelectionSnapshot,
34
34
  } from "../core/state/editor-state.ts";
35
+ // NOTE: docx-session.ts is intentionally an integration orchestrator, not a
36
+ // pure io module. It coordinates io (OOXML parse/export) with runtime
37
+ // (surface projection, cache envelopes, read-only diagnostics) and
38
+ // presentation. The runtime imports below (surface-projection,
39
+ // prerender/cache-envelope, read-only-diagnostics-runtime) are load-bearing
40
+ // and violate the nominal io→runtime boundary on purpose. The proper
41
+ // long-term fix is to relocate this file to src/session/ (see
42
+ // docs/plans/architecture-lane.md §F2). Do NOT add more runtime imports
43
+ // here without first reading that deferral rationale.
35
44
  import { createEditorSurfaceSnapshot } from "../runtime/surface-projection.ts";
36
45
  import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
37
46
  import {
@@ -73,8 +82,6 @@ import {
73
82
  WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
74
83
  WORKFLOW_PAYLOAD_CUSTOM_PROPS_RELATIONSHIP_TYPE,
75
84
  WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE,
76
- WORKFLOW_PAYLOAD_ITEM_PROPS_PART_PATH,
77
- WORKFLOW_PAYLOAD_PART_PATH,
78
85
  WORKFLOW_PAYLOAD_RELATIONSHIP_TYPE,
79
86
  } from "./ooxml/workflow-payload.ts";
80
87
  import {
@@ -457,6 +464,10 @@ export function loadDocxEditorSession(
457
464
  const parsedNumbering = numberingPartPath
458
465
  ? parseNumberingXml(
459
466
  decodeUtf8(sourcePackage.parts.get(numberingPartPath)?.bytes ?? new Uint8Array()),
467
+ {
468
+ relationships: sourcePackage.parts.get(numberingPartPath)?.relationships,
469
+ partPath: numberingPartPath,
470
+ },
460
471
  )
461
472
  : createEmptyNumberingCatalog();
462
473
  const mediaParts = collectInlineMediaParts(sourcePackage);
@@ -1329,6 +1340,10 @@ export async function loadDocxEditorSessionAsync(
1329
1340
  const parsedNumbering = numberingPartPath
1330
1341
  ? parseNumberingXml(
1331
1342
  decodeUtf8(sourcePackage.parts.get(numberingPartPath)?.bytes ?? new Uint8Array()),
1343
+ {
1344
+ relationships: sourcePackage.parts.get(numberingPartPath)?.relationships,
1345
+ partPath: numberingPartPath,
1346
+ },
1332
1347
  )
1333
1348
  : createEmptyNumberingCatalog();
1334
1349
  const mediaParts = collectInlineMediaParts(sourcePackage);
@@ -2017,6 +2032,7 @@ function exportDocxEditorSession(
2017
2032
  documentAttributes: state.sourceDocumentAttributes,
2018
2033
  media: currentDocument.media as MediaCatalog,
2019
2034
  finalSectionProperties: currentDocument.subParts?.finalSectionProperties,
2035
+ namespaceFlavor: options?.exportStrictOoxml ? "strict" : "transitional",
2020
2036
  },
2021
2037
  );
2022
2038
  const revisionDocument = serializeRuntimeRevisionsIntoDocumentXml(
@@ -2163,6 +2179,9 @@ function exportDocxEditorSession(
2163
2179
  state.sourcePackage,
2164
2180
  sessionState.documentId,
2165
2181
  );
2182
+ const internalEditorState = (
2183
+ options as { _editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload } | undefined
2184
+ )?._editorState;
2166
2185
 
2167
2186
  const exportSession = createExportSession(state.sourcePackage, [
2168
2187
  state.sourceDocumentPartPath,
@@ -2418,7 +2437,6 @@ function exportDocxEditorSession(
2418
2437
 
2419
2438
  ensureHostMetadataParts(exportSession, state.sourcePackage, currentDocument);
2420
2439
  // Schema 1.2: pass through editorState payload collected by the runtime channel.
2421
- const internalEditorState = (options as { _editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload } | undefined)?._editorState;
2422
2440
  ensureWorkflowPayloadParts(
2423
2441
  exportSession,
2424
2442
  sessionState,
@@ -2728,6 +2746,7 @@ function ensureImportedNumberingCatalogSupportsContent(
2728
2746
  ...catalog.instances,
2729
2747
  ...syntheticNullCatalog.instances,
2730
2748
  },
2749
+ ...(catalog.numPicBullets !== undefined ? { numPicBullets: catalog.numPicBullets } : {}),
2731
2750
  };
2732
2751
  }
2733
2752
 
@@ -42,7 +42,10 @@ export class ExportSession {
42
42
  return normalized;
43
43
  }),
44
44
  );
45
- this.workingParts = cloneParts(sourcePackage.parts);
45
+ // Clone bytes only for owned paths; non-owned paths get their bytes from
46
+ // reattachPreservedParts (which always clones from sourcePackage) so the
47
+ // up-front copy here is wasted work for the majority of parts.
48
+ this.workingParts = clonePartsForExport(sourcePackage.parts, this.ownedPaths);
46
49
  this.packageRelationships = sourcePackage.manifest.packageRelationships.map(cloneRelationship);
47
50
  }
48
51
 
@@ -137,15 +140,16 @@ export function createExportSession(
137
140
  return new ExportSession(sourcePackage, ownedOutputPaths);
138
141
  }
139
142
 
140
- function cloneParts(parts: Map<string, OpcPackagePart>): Map<string, OpcPackagePart> {
143
+ function clonePartsForExport(
144
+ parts: Map<string, OpcPackagePart>,
145
+ ownedPaths: ReadonlySet<string>,
146
+ ): Map<string, OpcPackagePart> {
141
147
  return new Map(
142
148
  [...parts.entries()].map(([path, part]) => [
143
149
  path,
144
- {
145
- ...part,
146
- relationships: part.relationships.map(cloneRelationship),
147
- bytes: new Uint8Array(part.bytes),
148
- },
150
+ ownedPaths.has(path)
151
+ ? { ...part, relationships: part.relationships.map(cloneRelationship), bytes: new Uint8Array(part.bytes) }
152
+ : { ...part, relationships: part.relationships.map(cloneRelationship) },
149
153
  ]),
150
154
  );
151
155
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Lane 7c Slice 7c.4 — O6 Strict-flavor namespace URI constants.
3
+ *
4
+ * OOXML has two URI flavors (ECMA-376 Annex L):
5
+ * - Transitional: classic "http://schemas.openxmlformats.org/…" URIs
6
+ * - Strict: "http://purl.oclc.org/ooxml/…" URIs (ISO 29500 Strict)
7
+ *
8
+ * `nsUris(flavor)` returns the correct set for the requested flavor.
9
+ * All serializers default to "transitional" to preserve backward compatibility.
10
+ */
11
+
12
+ export const TRANSITIONAL_NS = {
13
+ w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
14
+ r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
15
+ a: "http://schemas.openxmlformats.org/drawingml/2006/main",
16
+ pic: "http://schemas.openxmlformats.org/drawingml/2006/picture",
17
+ wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
18
+ mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
19
+ } as const;
20
+
21
+ export const STRICT_NS = {
22
+ w: "http://purl.oclc.org/ooxml/wordprocessingml/main",
23
+ r: "http://purl.oclc.org/ooxml/officeDocument/relationships",
24
+ a: "http://purl.oclc.org/ooxml/drawingml/main",
25
+ pic: "http://purl.oclc.org/ooxml/drawingml/picture",
26
+ wp: "http://purl.oclc.org/ooxml/drawingml/wordprocessingDrawing",
27
+ mc: "http://purl.oclc.org/ooxml/markup-compatibility",
28
+ } as const;
29
+
30
+ export type NamespaceFlavor = "transitional" | "strict";
31
+
32
+ export type NsUriSet = { readonly w: string; readonly r: string; readonly a: string; readonly pic: string; readonly wp: string; readonly mc: string };
33
+
34
+ export function nsUris(flavor: NamespaceFlavor): NsUriSet {
35
+ return flavor === "strict" ? STRICT_NS : TRANSITIONAL_NS;
36
+ }
37
+
38
+ /** Extension namespaces present in Transitional but omitted in Strict (ECMA-376 Annex B). */
39
+ export const TRANSITIONAL_EXTENSION_NAMESPACES = {
40
+ w14: "http://schemas.microsoft.com/office/word/2010/wordml",
41
+ w15: "http://schemas.microsoft.com/office/word/2012/wordml",
42
+ w16cid: "http://schemas.microsoft.com/office/word/2016/wordml/cid",
43
+ w16se: "http://schemas.microsoft.com/office/word/2015/wordml/symex",
44
+ wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
45
+ wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
46
+ wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
47
+ } as const;
@@ -12,22 +12,10 @@ export function reattachPreservedParts(
12
12
  continue;
13
13
  }
14
14
 
15
- if (!workingParts.has(path)) {
16
- workingParts.set(path, clonePart(sourcePart));
17
- continue;
18
- }
19
-
20
- const currentPart = workingParts.get(path);
21
- if (!currentPart) {
22
- continue;
23
- }
24
-
25
- currentPart.contentType = sourcePart.contentType;
26
- currentPart.relationships = sourcePart.relationships.map(cloneRelationship);
27
- currentPart.relationshipsPartPath = sourcePart.relationshipsPartPath;
28
- currentPart.compression = sourcePart.compression;
29
- currentPart.bytes = new Uint8Array(sourcePart.bytes);
30
- currentPart.crc32 = sourcePart.crc32;
15
+ // Always replace with a fresh clone so round-trip bytes are always the
16
+ // originals, regardless of whether the working entry has a bytes copy or
17
+ // a reference from the copy-on-write constructor path.
18
+ workingParts.set(path, clonePart(sourcePart));
31
19
  }
32
20
 
33
21
  const knownPackageRelationshipIds = new Set(packageRelationships.map((relationship) => relationship.id));
@@ -2,6 +2,8 @@ import type { CommentEntry, CommentThread } from "../../review/store/comment-sto
2
2
  import type { RevisionParagraphBoundary } from "../ooxml/revision-boundaries.ts";
3
3
  import type { ImportedCommentDefinition } from "../ooxml/parse-comments.ts";
4
4
  import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
5
+ import { parseXmlWithOffsets as parseXml } from "../ooxml/xml-parser.ts";
6
+ import { localName } from "../ooxml/xml-attr-helpers.ts";
5
7
 
6
8
  interface XmlElementNode {
7
9
  type: "element";
@@ -600,7 +602,7 @@ function parseOoxmlNumericId(value: string): number | undefined {
600
602
  }
601
603
 
602
604
  export function mapParagraphBoundaries(documentXml: string): ParagraphBoundaryMap[] {
603
- const root = parseXml(documentXml);
605
+ const root = parseXml(documentXml) as XmlElementNode;
604
606
  const documentElement = findChildElement(root, "document");
605
607
  const bodyElement = findChildElement(documentElement, "body");
606
608
  const paragraphs: ParagraphBoundaryMap[] = [];
@@ -878,103 +880,6 @@ function openingTagLength(xml: string, start: number): number {
878
880
  return end - start + 1;
879
881
  }
880
882
 
881
- function parseXml(xml: string): XmlElementNode {
882
- const root: XmlElementNode = {
883
- type: "element",
884
- name: "#document",
885
- attributes: {},
886
- children: [],
887
- start: 0,
888
- end: xml.length,
889
- };
890
- const stack: XmlElementNode[] = [root];
891
- const tokenPattern =
892
- /<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<!DOCTYPE[\s\S]*?>|<!\[CDATA\[[\s\S]*?\]\]>|<[^>]+>|[^<]+/gu;
893
-
894
- for (const match of xml.matchAll(tokenPattern)) {
895
- const token = match[0] ?? "";
896
- const start = match.index ?? 0;
897
- const end = start + token.length;
898
-
899
- if (token.startsWith("<?") || token.startsWith("<!DOCTYPE") || token.startsWith("<!--")) {
900
- continue;
901
- }
902
-
903
- if (token.startsWith("<![CDATA[")) {
904
- const text = token.slice(9, -3);
905
- stack[stack.length - 1]?.children.push({
906
- type: "text",
907
- text,
908
- start,
909
- end,
910
- });
911
- continue;
912
- }
913
-
914
- if (token.startsWith("</")) {
915
- const node = stack.pop();
916
- if (!node) {
917
- throw new Error("Malformed XML: unexpected closing tag.");
918
- }
919
- node.end = end;
920
- continue;
921
- }
922
-
923
- if (token.startsWith("<")) {
924
- const selfClosing = /\/>$/.test(token);
925
- const tagBody = token.slice(1, token.length - (selfClosing ? 2 : 1)).trim();
926
- const { name, attributes } = parseTag(tagBody);
927
- const node: XmlElementNode = {
928
- type: "element",
929
- name,
930
- attributes,
931
- children: [],
932
- start,
933
- end,
934
- };
935
- stack[stack.length - 1]?.children.push(node);
936
- if (!selfClosing) {
937
- stack.push(node);
938
- }
939
- continue;
940
- }
941
-
942
- const text = decodeXmlText(token);
943
- if (text.length > 0) {
944
- stack[stack.length - 1]?.children.push({
945
- type: "text",
946
- text,
947
- start,
948
- end,
949
- });
950
- }
951
- }
952
-
953
- if (stack.length !== 1) {
954
- throw new Error("Malformed XML: unclosed tag.");
955
- }
956
-
957
- return root;
958
- }
959
-
960
- function parseTag(tagBody: string): { name: string; attributes: Record<string, string> } {
961
- const whitespaceIndex = tagBody.search(/\s/u);
962
- const name = whitespaceIndex === -1 ? tagBody : tagBody.slice(0, whitespaceIndex);
963
- const rawAttributes = whitespaceIndex === -1 ? "" : tagBody.slice(whitespaceIndex + 1);
964
- const attributes: Record<string, string> = {};
965
- const pattern = /([A-Za-z_][A-Za-z0-9:._-]*)\s*=\s*("([^"]*)"|'([^']*)')/gu;
966
-
967
- for (const match of rawAttributes.matchAll(pattern)) {
968
- const key = match[1];
969
- const value = match[3] ?? match[4] ?? "";
970
- if (key) {
971
- attributes[key] = decodeXmlText(value);
972
- }
973
- }
974
-
975
- return { name, attributes };
976
- }
977
-
978
883
  function findChildElement(node: XmlElementNode, name: string): XmlElementNode {
979
884
  const match = node.children.find(
980
885
  (child): child is XmlElementNode =>
@@ -988,36 +893,3 @@ function findChildElement(node: XmlElementNode, name: string): XmlElementNode {
988
893
  return match;
989
894
  }
990
895
 
991
- function localName(name: string): string {
992
- const index = name.indexOf(":");
993
- return index === -1 ? name : name.slice(index + 1);
994
- }
995
-
996
- function decodeXmlText(text: string): string {
997
- return text.replace(
998
- /&(?:#x([0-9A-Fa-f]+)|#([0-9]+)|([A-Za-z]+));/gu,
999
- (_, hex, dec, named) => {
1000
- if (hex) {
1001
- return String.fromCodePoint(Number.parseInt(hex, 16));
1002
- }
1003
- if (dec) {
1004
- return String.fromCodePoint(Number.parseInt(dec, 10));
1005
- }
1006
-
1007
- switch (named) {
1008
- case "amp":
1009
- return "&";
1010
- case "lt":
1011
- return "<";
1012
- case "gt":
1013
- return ">";
1014
- case "quot":
1015
- return "\"";
1016
- case "apos":
1017
- return "'";
1018
- default:
1019
- return `&${named};`;
1020
- }
1021
- },
1022
- );
1023
- }