@beyondwork/docx-react-component 1.0.55 → 1.0.57

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 (107) hide show
  1. package/package.json +43 -32
  2. package/src/api/public-types.ts +157 -0
  3. package/src/compare/diff-engine.ts +3 -0
  4. package/src/core/commands/formatting-commands.ts +1 -0
  5. package/src/core/commands/index.ts +17 -11
  6. package/src/core/selection/mapping.ts +18 -1
  7. package/src/core/selection/review-anchors.ts +29 -18
  8. package/src/io/chart-preview-resolver.ts +175 -41
  9. package/src/io/docx-session.ts +57 -2
  10. package/src/io/export/serialize-main-document.ts +82 -0
  11. package/src/io/export/serialize-styles.ts +61 -3
  12. package/src/io/export/table-properties-xml.ts +19 -4
  13. package/src/io/normalize/normalize-text.ts +33 -0
  14. package/src/io/ooxml/parse-anchor.ts +182 -0
  15. package/src/io/ooxml/parse-drawing.ts +319 -0
  16. package/src/io/ooxml/parse-fields.ts +115 -2
  17. package/src/io/ooxml/parse-fill.ts +215 -0
  18. package/src/io/ooxml/parse-font-table.ts +190 -0
  19. package/src/io/ooxml/parse-footnotes.ts +52 -1
  20. package/src/io/ooxml/parse-main-document.ts +241 -1
  21. package/src/io/ooxml/parse-numbering.ts +96 -0
  22. package/src/io/ooxml/parse-picture.ts +107 -0
  23. package/src/io/ooxml/parse-settings.ts +34 -0
  24. package/src/io/ooxml/parse-shapes.ts +87 -0
  25. package/src/io/ooxml/parse-solid-fill.ts +11 -0
  26. package/src/io/ooxml/parse-styles.ts +74 -1
  27. package/src/io/ooxml/parse-theme.ts +60 -0
  28. package/src/io/paste/html-clipboard.ts +449 -0
  29. package/src/io/paste/word-clipboard.ts +5 -1
  30. package/src/legal/_document-root.ts +26 -0
  31. package/src/legal/bookmarks.ts +4 -3
  32. package/src/legal/cross-references.ts +3 -2
  33. package/src/legal/defined-terms.ts +2 -1
  34. package/src/legal/signature-blocks.ts +2 -1
  35. package/src/model/canonical-document.ts +415 -3
  36. package/src/runtime/chart/chart-model-store.ts +73 -10
  37. package/src/runtime/document-runtime.ts +693 -41
  38. package/src/runtime/edit-ops/index.ts +129 -0
  39. package/src/runtime/event-refresh-hints.ts +7 -0
  40. package/src/runtime/field-resolver.ts +341 -0
  41. package/src/runtime/footnote-resolver.ts +55 -0
  42. package/src/runtime/hyperlink-color-resolver.ts +13 -10
  43. package/src/runtime/object-grab/index.ts +51 -0
  44. package/src/runtime/paragraph-style-resolver.ts +105 -0
  45. package/src/runtime/resolved-numbering-geometry.ts +12 -0
  46. package/src/runtime/selection/cursor-ops.ts +186 -15
  47. package/src/runtime/selection/index.ts +17 -1
  48. package/src/runtime/structure-ops/index.ts +77 -0
  49. package/src/runtime/styles-cascade.ts +33 -0
  50. package/src/runtime/surface-projection.ts +186 -12
  51. package/src/runtime/theme-color-resolver.ts +189 -44
  52. package/src/runtime/units.ts +46 -0
  53. package/src/runtime/view-state.ts +13 -2
  54. package/src/ui/WordReviewEditor.tsx +168 -10
  55. package/src/ui/editor-runtime-boundary.ts +94 -1
  56. package/src/ui/editor-shell-view.tsx +1 -1
  57. package/src/ui/runtime-shortcut-dispatch.ts +17 -3
  58. package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
  59. package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
  60. package/src/ui-tailwind/chart/render/area.tsx +22 -4
  61. package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
  62. package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
  63. package/src/ui-tailwind/chart/render/combo.tsx +37 -4
  64. package/src/ui-tailwind/chart/render/line.tsx +28 -5
  65. package/src/ui-tailwind/chart/render/pie.tsx +36 -16
  66. package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
  67. package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
  68. package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
  69. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
  70. package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
  72. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
  73. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
  76. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
  77. package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
  78. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -0
  79. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
  80. package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
  81. package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
  82. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
  83. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
  85. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
  86. package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
  87. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
  88. package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
  89. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
  90. package/src/ui-tailwind/editor-surface/pm-schema.ts +192 -11
  91. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +28 -3
  92. package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
  93. package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
  94. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
  95. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
  96. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
  97. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
  98. package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
  99. package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
  100. package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
  101. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
  102. package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
  103. package/src/ui-tailwind/theme/editor-theme.css +1 -0
  104. package/src/ui-tailwind/theme/tokens.css +6 -0
  105. package/src/ui-tailwind/theme/tokens.ts +10 -0
  106. package/src/validation/compatibility-engine.ts +2 -0
  107. package/src/validation/docx-comment-proof.ts +12 -3
@@ -74,7 +74,8 @@ interface PendingResolution {
74
74
  readonly heightEmu: number;
75
75
  }
76
76
 
77
- type Pointer = Array<number>;
77
+ type PointerStep = number | { rowIndex: number; cellIndex: number };
78
+ type Pointer = PointerStep[];
78
79
 
79
80
  export async function resolveChartPreviewsForDocument(
80
81
  doc: CanonicalDocument,
@@ -189,30 +190,92 @@ export function scheduleChartPreviewResolution(
189
190
  function collectUnresolvedChartPreviews(doc: CanonicalDocument, pkg: OpcPackage): PendingResolution[] {
190
191
  const out: PendingResolution[] = [];
191
192
  const documentRels = collectDocumentPartRelationships(pkg);
193
+ collectUnresolvedChartPreviewsFromBlocks(
194
+ doc.content.children,
195
+ [],
196
+ documentRels,
197
+ pkg,
198
+ out,
199
+ );
200
+ return out;
201
+ }
192
202
 
193
- const paragraphs = doc.content.children;
194
- for (let i = 0; i < paragraphs.length; i++) {
195
- const block = paragraphs[i]!;
196
- if (block.type !== "paragraph") continue;
197
- const paragraph = block as ParagraphNode;
198
- for (let j = 0; j < paragraph.children.length; j++) {
199
- const child = paragraph.children[j];
200
- if (!child || child.type !== "chart_preview") continue;
201
- const chartNode = child as ChartPreviewNode;
202
- if (chartNode.previewMediaId) continue;
203
- const resolved = resolveChartPart(chartNode, documentRels, pkg);
204
- if (!resolved) continue;
205
- out.push({
206
- pointer: [i, j],
207
- node: chartNode,
208
- chartPartPath: resolved.chartPartPath,
209
- chartXml: resolved.chartXml,
210
- widthEmu: resolved.widthEmu,
211
- heightEmu: resolved.heightEmu,
212
- });
203
+ function collectUnresolvedChartPreviewsFromBlocks(
204
+ blocks: readonly BlockNode[],
205
+ pointerPrefix: Pointer,
206
+ documentRels: Map<string, string>,
207
+ pkg: OpcPackage,
208
+ out: PendingResolution[],
209
+ ): void {
210
+ for (let blockIndex = 0; blockIndex < blocks.length; blockIndex += 1) {
211
+ const block = blocks[blockIndex];
212
+ if (!block) continue;
213
+ switch (block.type) {
214
+ case "paragraph":
215
+ collectUnresolvedChartPreviewsFromParagraph(
216
+ block,
217
+ [...pointerPrefix, blockIndex],
218
+ documentRels,
219
+ pkg,
220
+ out,
221
+ );
222
+ break;
223
+ case "table":
224
+ for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex += 1) {
225
+ const row = block.rows[rowIndex];
226
+ if (!row) continue;
227
+ for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex += 1) {
228
+ const cell = row.cells[cellIndex];
229
+ if (!cell) continue;
230
+ collectUnresolvedChartPreviewsFromBlocks(
231
+ cell.children,
232
+ [...pointerPrefix, blockIndex, { rowIndex, cellIndex }],
233
+ documentRels,
234
+ pkg,
235
+ out,
236
+ );
237
+ }
238
+ }
239
+ break;
240
+ case "sdt":
241
+ case "custom_xml":
242
+ collectUnresolvedChartPreviewsFromBlocks(
243
+ block.children,
244
+ [...pointerPrefix, blockIndex],
245
+ documentRels,
246
+ pkg,
247
+ out,
248
+ );
249
+ break;
250
+ default:
251
+ break;
213
252
  }
214
253
  }
215
- return out;
254
+ }
255
+
256
+ function collectUnresolvedChartPreviewsFromParagraph(
257
+ paragraph: ParagraphNode,
258
+ pointerPrefix: Pointer,
259
+ documentRels: Map<string, string>,
260
+ pkg: OpcPackage,
261
+ out: PendingResolution[],
262
+ ): void {
263
+ for (let childIndex = 0; childIndex < paragraph.children.length; childIndex += 1) {
264
+ const child = paragraph.children[childIndex];
265
+ if (!child || child.type !== "chart_preview") continue;
266
+ const chartNode = child as ChartPreviewNode;
267
+ if (chartNode.previewMediaId) continue;
268
+ const resolved = resolveChartPart(chartNode, documentRels, pkg);
269
+ if (!resolved) continue;
270
+ out.push({
271
+ pointer: [...pointerPrefix, childIndex],
272
+ node: chartNode,
273
+ chartPartPath: resolved.chartPartPath,
274
+ chartXml: resolved.chartXml,
275
+ widthEmu: resolved.widthEmu,
276
+ heightEmu: resolved.heightEmu,
277
+ });
278
+ }
216
279
  }
217
280
 
218
281
  function collectDocumentPartRelationships(pkg: OpcPackage): Map<string, string> {
@@ -323,7 +386,7 @@ function applyResolutions(
323
386
  resolutions: Array<{ entry: PendingResolution; bytes: Uint8Array }>,
324
387
  ): CanonicalDocument {
325
388
  const newMediaItems: Record<string, MediaItem> = { ...doc.media.items };
326
- const updates = new Map<string, { previewMediaId: string }>();
389
+ let newBlocks = doc.content.children;
327
390
 
328
391
  let seq = 0;
329
392
  for (const { entry, bytes } of resolutions) {
@@ -340,31 +403,102 @@ function applyResolutions(
340
403
  widthEmu: entry.widthEmu,
341
404
  heightEmu: entry.heightEmu,
342
405
  };
343
- const pointerKey = entry.pointer.join(",");
344
- updates.set(pointerKey, { previewMediaId: mediaId });
406
+ newBlocks = updateChartPreviewAtPointer(newBlocks, entry.pointer, mediaId);
345
407
  }
346
408
 
347
- if (updates.size === 0) return doc;
348
-
349
- // Clone the content tree along pointer paths only — everything else
350
- // keeps object identity so downstream React memoization stays stable.
351
- const newParagraphs: BlockNode[] = doc.content.children.slice();
352
- for (const [pointerKey, update] of updates) {
353
- const [pi, ci] = pointerKey.split(",").map((s) => parseInt(s, 10)) as [number, number];
354
- const paragraph = newParagraphs[pi];
355
- if (!paragraph || paragraph.type !== "paragraph") continue;
356
- const newChildren: InlineNode[] = (paragraph as ParagraphNode).children.slice();
357
- const existing = newChildren[ci];
358
- if (!existing || existing.type !== "chart_preview") continue;
359
- newChildren[ci] = { ...(existing as ChartPreviewNode), previewMediaId: update.previewMediaId };
360
- newParagraphs[pi] = { ...(paragraph as ParagraphNode), children: newChildren };
361
- }
409
+ if (newBlocks === doc.content.children) return doc;
362
410
 
363
- const newContent: DocumentRootNode = { ...doc.content, children: newParagraphs };
411
+ const newContent: DocumentRootNode = { ...doc.content, children: newBlocks };
364
412
  const newMedia = { items: newMediaItems };
365
413
  return { ...doc, content: newContent, media: newMedia };
366
414
  }
367
415
 
416
+ function updateChartPreviewAtPointer(
417
+ blocks: readonly BlockNode[],
418
+ pointer: Pointer,
419
+ previewMediaId: string,
420
+ ): BlockNode[] {
421
+ if (pointer.length < 2) return blocks as BlockNode[];
422
+ const [head, ...rest] = pointer;
423
+ if (typeof head !== "number") return blocks as BlockNode[];
424
+ const targetBlock = blocks[head];
425
+ if (!targetBlock) return blocks as BlockNode[];
426
+
427
+ switch (targetBlock.type) {
428
+ case "paragraph": {
429
+ if (rest.length !== 1 || typeof rest[0] !== "number") {
430
+ return blocks as BlockNode[];
431
+ }
432
+ const inlineIndex = rest[0];
433
+ const child = targetBlock.children[inlineIndex];
434
+ if (!child || child.type !== "chart_preview") return blocks as BlockNode[];
435
+ const nextChildren: InlineNode[] = targetBlock.children.slice();
436
+ nextChildren[inlineIndex] = {
437
+ ...(child as ChartPreviewNode),
438
+ previewMediaId,
439
+ };
440
+ const nextBlocks = blocks.slice();
441
+ nextBlocks[head] = {
442
+ ...targetBlock,
443
+ children: nextChildren,
444
+ };
445
+ return nextBlocks;
446
+ }
447
+ case "table": {
448
+ const [cellPointer, ...nested] = rest;
449
+ if (
450
+ !cellPointer ||
451
+ typeof cellPointer === "number" ||
452
+ nested.length === 0
453
+ ) {
454
+ return blocks as BlockNode[];
455
+ }
456
+ const row = targetBlock.rows[cellPointer.rowIndex];
457
+ const cell = row?.cells[cellPointer.cellIndex];
458
+ if (!row || !cell) return blocks as BlockNode[];
459
+ const nextChildren = updateChartPreviewAtPointer(
460
+ cell.children,
461
+ nested,
462
+ previewMediaId,
463
+ );
464
+ if (nextChildren === cell.children) return blocks as BlockNode[];
465
+ const nextCells = row.cells.slice();
466
+ nextCells[cellPointer.cellIndex] = {
467
+ ...cell,
468
+ children: nextChildren,
469
+ };
470
+ const nextRows = targetBlock.rows.slice();
471
+ nextRows[cellPointer.rowIndex] = {
472
+ ...row,
473
+ cells: nextCells,
474
+ };
475
+ const nextBlocks = blocks.slice();
476
+ nextBlocks[head] = {
477
+ ...targetBlock,
478
+ rows: nextRows,
479
+ };
480
+ return nextBlocks;
481
+ }
482
+ case "sdt":
483
+ case "custom_xml": {
484
+ const nextChildren = updateChartPreviewAtPointer(
485
+ targetBlock.children,
486
+ rest,
487
+ previewMediaId,
488
+ );
489
+ if (nextChildren === targetBlock.children) return blocks as BlockNode[];
490
+ const nextBlocks = blocks.slice();
491
+ nextBlocks[head] = {
492
+ ...targetBlock,
493
+ children: nextChildren,
494
+ };
495
+ return nextBlocks;
496
+ }
497
+ default:
498
+ return blocks as BlockNode[];
499
+ }
500
+ }
501
+
368
502
  /**
369
503
  * Content-type sniff from the first bytes of the rendered preview.
370
504
  * PNG magic is 0x89 0x50 0x4E 0x47; everything else is assumed to be
@@ -148,10 +148,10 @@ import {
148
148
  parseFooterXml,
149
149
  } from "./ooxml/parse-headers-footers.ts";
150
150
  import { parseFootnotesXml, parseEndnotesXml } from "./ooxml/parse-footnotes.ts";
151
- import { parseThemeXml } from "./ooxml/parse-theme.ts";
152
- import { resolveTheme } from "./ooxml/parse-theme.ts";
151
+ import { materializeCanonicalTheme, parseThemeXml, resolveTheme } from "./ooxml/parse-theme.ts";
153
152
  import { parseSettingsXml } from "./ooxml/parse-settings.ts";
154
153
  import { parseStylesXml, type ParseStylesResult } from "./ooxml/parse-styles.ts";
154
+ import { parseFontTable } from "./ooxml/parse-font-table.ts";
155
155
  import {
156
156
  serializeHeaderXml,
157
157
  serializeHeaderXmlWithRevisions,
@@ -224,6 +224,9 @@ const SETTINGS_RELATIONSHIP_TYPE =
224
224
  const STYLES_RELATIONSHIP_TYPE =
225
225
  "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
226
226
  const STYLES_PART_PATH = "/word/styles.xml";
227
+ const FONT_TABLE_RELATIONSHIP_TYPE =
228
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable";
229
+ const FONT_TABLE_PART_PATH = "/word/fontTable.xml";
227
230
  const FOOTNOTES_PART_PATH = "/word/footnotes.xml";
228
231
  const ENDNOTES_PART_PATH = "/word/endnotes.xml";
229
232
  const SETTINGS_PART_PATH = "/word/settings.xml";
@@ -717,6 +720,13 @@ export function loadDocxEditorSession(
717
720
  decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array()),
718
721
  )
719
722
  : undefined;
723
+ const canonicalTheme =
724
+ parsedTheme !== undefined
725
+ ? materializeCanonicalTheme(
726
+ parsedTheme,
727
+ parsedSettings?.clrSchemeMapping ?? {},
728
+ )
729
+ : undefined;
720
730
  const settingsXmlForProtection =
721
731
  settingsPartPath && sourcePackage.parts.has(settingsPartPath)
722
732
  ? decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array())
@@ -739,6 +749,21 @@ export function loadDocxEditorSession(
739
749
  )
740
750
  : parseStylesXml("");
741
751
 
752
+ // ---- Parse fontTable.xml for canonical font catalog ----
753
+ const fontTablePartPath = resolveDocumentRelatedPartPath(
754
+ sourcePackage,
755
+ mainDocumentPath,
756
+ documentPart.relationships,
757
+ FONT_TABLE_RELATIONSHIP_TYPE,
758
+ FONT_TABLE_PART_PATH,
759
+ );
760
+ const parsedFontTable =
761
+ fontTablePartPath && sourcePackage.parts.has(fontTablePartPath)
762
+ ? parseFontTable(
763
+ decodeUtf8(sourcePackage.parts.get(fontTablePartPath)?.bytes ?? new Uint8Array()),
764
+ )
765
+ : undefined;
766
+
742
767
  const subParts: SubPartsCatalog | undefined =
743
768
  parsedHeaders.length > 0 ||
744
769
  parsedFooters.length > 0 ||
@@ -746,6 +771,7 @@ export function loadDocxEditorSession(
746
771
  parsedTheme !== undefined ||
747
772
  normalizedDocument.finalSectionProperties !== undefined ||
748
773
  resolvedTheme !== undefined ||
774
+ canonicalTheme !== undefined ||
749
775
  parsedSettings !== undefined
750
776
  ? {
751
777
  headers: parsedHeaders,
@@ -756,6 +782,7 @@ export function loadDocxEditorSession(
756
782
  ? { finalSectionProperties: normalizedDocument.finalSectionProperties }
757
783
  : {}),
758
784
  ...(resolvedTheme !== undefined ? { resolvedTheme } : {}),
785
+ ...(canonicalTheme !== undefined ? { canonicalTheme } : {}),
759
786
  ...(parsedSettings !== undefined ? { settings: parsedSettings } : {}),
760
787
  }
761
788
  : undefined;
@@ -775,6 +802,7 @@ export function loadDocxEditorSession(
775
802
  content: normalizedDocument.content,
776
803
  subParts,
777
804
  parsedStyles,
805
+ fontTable: parsedFontTable,
778
806
  preservation: {
779
807
  ...normalizedDocument.preservation,
780
808
  packageParts: {
@@ -1627,6 +1655,13 @@ export async function loadDocxEditorSessionAsync(
1627
1655
  decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array()),
1628
1656
  )
1629
1657
  : undefined;
1658
+ const canonicalTheme =
1659
+ parsedTheme !== undefined
1660
+ ? materializeCanonicalTheme(
1661
+ parsedTheme,
1662
+ parsedSettings?.clrSchemeMapping ?? {},
1663
+ )
1664
+ : undefined;
1630
1665
  const settingsXmlForProtection =
1631
1666
  settingsPartPath && sourcePackage.parts.has(settingsPartPath)
1632
1667
  ? decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array())
@@ -1650,6 +1685,21 @@ export async function loadDocxEditorSessionAsync(
1650
1685
  : parseStylesXml("");
1651
1686
  await scheduler.yield();
1652
1687
 
1688
+ // ---- Parse fontTable.xml for canonical font catalog ----
1689
+ const fontTablePartPath = resolveDocumentRelatedPartPath(
1690
+ sourcePackage,
1691
+ mainDocumentPath,
1692
+ documentPart.relationships,
1693
+ FONT_TABLE_RELATIONSHIP_TYPE,
1694
+ FONT_TABLE_PART_PATH,
1695
+ );
1696
+ const parsedFontTable =
1697
+ fontTablePartPath && sourcePackage.parts.has(fontTablePartPath)
1698
+ ? parseFontTable(
1699
+ decodeUtf8(sourcePackage.parts.get(fontTablePartPath)?.bytes ?? new Uint8Array()),
1700
+ )
1701
+ : undefined;
1702
+
1653
1703
  const subParts: SubPartsCatalog | undefined =
1654
1704
  parsedHeaders.length > 0 ||
1655
1705
  parsedFooters.length > 0 ||
@@ -1657,6 +1707,7 @@ export async function loadDocxEditorSessionAsync(
1657
1707
  parsedTheme !== undefined ||
1658
1708
  normalizedDocument.finalSectionProperties !== undefined ||
1659
1709
  resolvedTheme !== undefined ||
1710
+ canonicalTheme !== undefined ||
1660
1711
  parsedSettings !== undefined
1661
1712
  ? {
1662
1713
  headers: parsedHeaders,
@@ -1667,6 +1718,7 @@ export async function loadDocxEditorSessionAsync(
1667
1718
  ? { finalSectionProperties: normalizedDocument.finalSectionProperties }
1668
1719
  : {}),
1669
1720
  ...(resolvedTheme !== undefined ? { resolvedTheme } : {}),
1721
+ ...(canonicalTheme !== undefined ? { canonicalTheme } : {}),
1670
1722
  ...(parsedSettings !== undefined ? { settings: parsedSettings } : {}),
1671
1723
  }
1672
1724
  : undefined;
@@ -1686,6 +1738,7 @@ export async function loadDocxEditorSessionAsync(
1686
1738
  content: normalizedDocument.content,
1687
1739
  subParts,
1688
1740
  parsedStyles,
1741
+ fontTable: parsedFontTable,
1689
1742
  preservation: {
1690
1743
  ...normalizedDocument.preservation,
1691
1744
  packageParts: {
@@ -2401,6 +2454,7 @@ function createImportedCanonicalDocument(input: {
2401
2454
  content: CanonicalDocumentEnvelope["content"];
2402
2455
  subParts?: SubPartsCatalog;
2403
2456
  parsedStyles?: ParseStylesResult;
2457
+ fontTable?: CanonicalDocumentEnvelope["fontTable"];
2404
2458
  preservation: CanonicalDocumentEnvelope["preservation"];
2405
2459
  diagnostics: CanonicalDocumentEnvelope["diagnostics"];
2406
2460
  review: CanonicalDocumentEnvelope["review"];
@@ -2431,6 +2485,7 @@ function createImportedCanonicalDocument(input: {
2431
2485
  preservation: input.preservation,
2432
2486
  diagnostics: input.diagnostics,
2433
2487
  ...(input.subParts !== undefined ? { subParts: input.subParts } : {}),
2488
+ ...(input.fontTable !== undefined ? { fontTable: input.fontTable } : {}),
2434
2489
  };
2435
2490
  }
2436
2491
 
@@ -3,6 +3,7 @@ import type {
3
3
  BorderSpec,
4
4
  CustomXmlNode,
5
5
  DocumentRootNode,
6
+ FootnoteProperties,
6
7
  InlineNode,
7
8
  MediaCatalog,
8
9
  ParagraphNode,
@@ -530,6 +531,8 @@ function serializeTableInlineNode(
530
531
  case "wordart":
531
532
  case "vml_shape":
532
533
  return wrapInlineRawXml(node.rawXml);
534
+ case "drawing_frame":
535
+ return serializeDrawingFrameNode(node);
533
536
  case "hyperlink": {
534
537
  const hyperlinkOpen = node.href.startsWith("#")
535
538
  ? `<w:hyperlink w:anchor="${escapeXmlAttribute(node.href.slice(1))}">`
@@ -962,6 +965,21 @@ function serializeInlineNode(
962
965
  boundaries,
963
966
  };
964
967
  }
968
+ case "drawing_frame": {
969
+ // CO4 F4.1 — emit preserved rawXml from content (every DrawingFrame
970
+ // variant retains the original w:drawing slice on its content or is
971
+ // a picture that also keeps the slice on its parent). Matches the
972
+ // same "rawXml preservation" contract as shape/chart/smartart above.
973
+ const xml = serializeDrawingFrameNode(node);
974
+ const boundaries = new Map<number, number>();
975
+ boundaries.set(cursor, xmlOffset);
976
+ boundaries.set(cursor + 1, xmlOffset + xml.length);
977
+ return {
978
+ xml,
979
+ cursor: cursor + 1,
980
+ boundaries,
981
+ };
982
+ }
965
983
  case "hyperlink": {
966
984
  const hyperlinkOpen = node.href.startsWith("#")
967
985
  ? `<w:hyperlink w:anchor="${escapeXmlAttribute(node.href.slice(1))}">`
@@ -1125,6 +1143,42 @@ function serializeInlineNode(
1125
1143
  }
1126
1144
  }
1127
1145
 
1146
+ /**
1147
+ * CO4 F4.1 — serialize a DrawingFrameNode by emitting the preserved rawXml.
1148
+ *
1149
+ * Every DrawingFrame variant carries a lossless original-XML reference:
1150
+ * - shape, chart_preview, smartart_preview, opaque → `content.rawXml`
1151
+ * - picture → the parse layer stores the containing drawing XML on the outer
1152
+ * DrawingFrame's context; for the MVP we reconstruct from the anchor +
1153
+ * blipRef if needed. In practice picture content also originates from a
1154
+ * larger drawingXml slice — the parent `w:drawing` substring — which we
1155
+ * don't retain explicitly yet. When `content.rawXml` is absent we emit a
1156
+ * minimal wp:inline/wp:anchor envelope preserving extent + relationship id.
1157
+ *
1158
+ * For the round-trip v1 contract: if any `rawXml` is present, round-trip is
1159
+ * byte-stable modulo whitespace. If the picture-content path has no rawXml,
1160
+ * a reconstructed minimal drawing is emitted.
1161
+ */
1162
+ function serializeDrawingFrameNode(
1163
+ node: Extract<InlineNode, { type: "drawing_frame" }>,
1164
+ ): string {
1165
+ const content = node.content;
1166
+ if ("rawXml" in content && typeof content.rawXml === "string" && content.rawXml.length > 0) {
1167
+ return wrapInlineRawXml(content.rawXml);
1168
+ }
1169
+ // Picture content with no rawXml — reconstruct a minimal inline image so the
1170
+ // round-trip doesn't silently drop the image. Use the blipRef + anchor extent.
1171
+ if (content.type === "picture") {
1172
+ const { widthEmu, heightEmu } = node.anchor.extent;
1173
+ const embed = escapeXmlAttribute(content.blipRef);
1174
+ const envelope = node.anchor.display === "floating" ? "wp:anchor" : "wp:inline";
1175
+ return `<w:r><w:drawing><${envelope} xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${widthEmu}" cy="${heightEmu}"/><wp:docPr id="1" name=""/><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="0" name=""/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${embed}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${widthEmu}" cy="${heightEmu}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></${envelope}></w:drawing></w:r>`;
1176
+ }
1177
+ // Shape/chart/smartart/opaque without rawXml — nothing to emit. Should not
1178
+ // happen in practice since the parser always sets rawXml.
1179
+ return "";
1180
+ }
1181
+
1128
1182
  function serializeImageNode(
1129
1183
  node: Extract<InlineNode, { type: "image" }>,
1130
1184
  state: SerializationState,
@@ -1482,6 +1536,15 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
1482
1536
  }
1483
1537
  }
1484
1538
 
1539
+ // Per-section footnote + endnote configuration. ECMA-376 §17.6.18 places
1540
+ // these after the header/footer references and before <w:type>.
1541
+ if (props.footnotePr) {
1542
+ children.push(serializeFootnoteLikeProperties("w:footnotePr", props.footnotePr));
1543
+ }
1544
+ if (props.endnotePr) {
1545
+ children.push(serializeFootnoteLikeProperties("w:endnotePr", props.endnotePr));
1546
+ }
1547
+
1485
1548
  // Section type
1486
1549
  if (props.sectionType) {
1487
1550
  children.push(`<w:type w:val="${escapeXmlAttribute(props.sectionType)}"/>`);
@@ -1623,6 +1686,25 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
1623
1686
  return `<w:sectPr>${children.join("")}</w:sectPr>`;
1624
1687
  }
1625
1688
 
1689
+ /**
1690
+ * Emit `<w:footnotePr>` or `<w:endnotePr>` from the typed
1691
+ * `FootnoteProperties` / `EndnoteProperties` shape. Child order follows
1692
+ * ECMA-376 §17.11.11–.18: pos → numFmt → numStart → numRestart.
1693
+ * Each child is emitted only when its typed field is present.
1694
+ */
1695
+ function serializeFootnoteLikeProperties(
1696
+ elementName: "w:footnotePr" | "w:endnotePr",
1697
+ props: FootnoteProperties,
1698
+ ): string {
1699
+ const parts: string[] = [];
1700
+ if (props.pos) parts.push(`<w:pos w:val="${escapeXmlAttribute(props.pos)}"/>`);
1701
+ if (props.numFmt) parts.push(`<w:numFmt w:val="${escapeXmlAttribute(props.numFmt)}"/>`);
1702
+ if (props.numStart !== undefined) parts.push(`<w:numStart w:val="${twip(props.numStart)}"/>`);
1703
+ if (props.numRestart) parts.push(`<w:numRestart w:val="${escapeXmlAttribute(props.numRestart)}"/>`);
1704
+ if (parts.length === 0) return `<${elementName}/>`;
1705
+ return `<${elementName}>${parts.join("")}</${elementName}>`;
1706
+ }
1707
+
1626
1708
  function wrapInlineRawXml(rawXml: string): string {
1627
1709
  const trimmed = rawXml.trimStart();
1628
1710
  return trimmed.startsWith("<w:r") ? rawXml : `<w:r>${rawXml}</w:r>`;
@@ -67,16 +67,18 @@ function buildParagraphStyleXml(
67
67
  ): string {
68
68
  const defaultAttr = style.isDefault ? ` w:default="1"` : "";
69
69
  const nameEl = `<w:name w:val="${escXml(style.displayName)}"/>`;
70
+ const aliasesEl = buildAliasesXml(style.aliases);
70
71
  const basedOnEl = style.basedOn
71
72
  ? `<w:basedOn w:val="${escXml(style.basedOn)}"/>`
72
73
  : "";
73
74
  const nextEl = style.nextStyle
74
75
  ? `<w:next w:val="${escXml(style.nextStyle)}"/>`
75
76
  : "";
76
- // ECMA-376 §17.7 emit order: name → basedOn → next → link → ...
77
+ // ECMA-376 §17.7 emit order: name → aliases → basedOn → next → link → autoRedefine → ...
77
78
  const linkEl = style.linkedStyleId
78
79
  ? `<w:link w:val="${escXml(style.linkedStyleId)}"/>`
79
80
  : "";
81
+ const autoRedefineEl = buildOnOffEl("autoRedefine", style.autoRedefine);
80
82
 
81
83
  // Build pPr: may contain numPr (from numbering) and any canonical formatting.
82
84
  // We reconstruct the pPr children in canonical order:
@@ -97,15 +99,37 @@ function buildParagraphStyleXml(
97
99
  return (
98
100
  `<w:style w:type="paragraph" w:styleId="${escXml(style.styleId)}"${defaultAttr}>` +
99
101
  nameEl +
102
+ aliasesEl +
100
103
  basedOnEl +
101
104
  nextEl +
102
105
  linkEl +
106
+ autoRedefineEl +
103
107
  pPrBodyXml +
104
108
  rPrXml +
105
109
  `</w:style>`
106
110
  );
107
111
  }
108
112
 
113
+ /**
114
+ * Emit an ST_OnOff element following A.3 discipline: undefined → "",
115
+ * true → `<w:tag/>`, false → `<w:tag w:val="false"/>`.
116
+ */
117
+ function buildOnOffEl(tag: string, value: boolean | undefined): string {
118
+ if (value === undefined) return "";
119
+ return value ? `<w:${tag}/>` : `<w:${tag} w:val="false"/>`;
120
+ }
121
+
122
+ /**
123
+ * Emit `<w:aliases w:val="A,B"/>` — ECMA-376 §17.7.4.2. Returns empty when
124
+ * `aliases` is undefined or the array is empty. Commas are preserved verbatim;
125
+ * callers are responsible for not embedding commas inside alias names (Word
126
+ * does not escape them either).
127
+ */
128
+ function buildAliasesXml(aliases: string[] | undefined): string {
129
+ if (!aliases || aliases.length === 0) return "";
130
+ return `<w:aliases w:val="${escXml(aliases.join(","))}"/>`;
131
+ }
132
+
109
133
  function buildStyleNumPrXml(
110
134
  numbering: NonNullable<StylesCatalog["paragraphs"][string]["numbering"]>,
111
135
  ): string {
@@ -172,15 +196,44 @@ function buildParagraphPropertiesXmlWithNumPr(
172
196
  return body.length > 0 ? `<w:pPr>${body}</w:pPr>` : "";
173
197
  }
174
198
 
199
+ function buildNumberingStyleXml(
200
+ style: NonNullable<StylesCatalog["numberingStyles"]>[string],
201
+ ): string {
202
+ const defaultAttr = style.isDefault ? ` w:default="1"` : "";
203
+ const nameEl = `<w:name w:val="${escXml(style.displayName)}"/>`;
204
+ const aliasesEl = buildAliasesXml(style.aliases);
205
+ const basedOnEl = style.basedOn
206
+ ? `<w:basedOn w:val="${escXml(style.basedOn)}"/>`
207
+ : "";
208
+ // Emit `<w:pPr><w:numPr><w:numId/></w:numPr></w:pPr>` when an instance ref
209
+ // was captured. Strip canonical "num:" prefix per serialize-numbering pattern.
210
+ let numPrXml = "";
211
+ if (style.numberingInstanceId) {
212
+ const rawId = style.numberingInstanceId.startsWith("num:")
213
+ ? style.numberingInstanceId.slice(4)
214
+ : style.numberingInstanceId;
215
+ numPrXml = `<w:pPr><w:numPr><w:numId w:val="${escXml(rawId)}"/></w:numPr></w:pPr>`;
216
+ }
217
+ return (
218
+ `<w:style w:type="numbering" w:styleId="${escXml(style.styleId)}"${defaultAttr}>` +
219
+ nameEl +
220
+ aliasesEl +
221
+ basedOnEl +
222
+ numPrXml +
223
+ `</w:style>`
224
+ );
225
+ }
226
+
175
227
  function buildCharacterStyleXml(
176
228
  style: StylesCatalog["characters"][string],
177
229
  ): string {
178
230
  const defaultAttr = style.isDefault ? ` w:default="1"` : "";
179
231
  const nameEl = `<w:name w:val="${escXml(style.displayName)}"/>`;
232
+ const aliasesEl = buildAliasesXml(style.aliases);
180
233
  const basedOnEl = style.basedOn
181
234
  ? `<w:basedOn w:val="${escXml(style.basedOn)}"/>`
182
235
  : "";
183
- // ECMA-376 §17.7 emit order: name → basedOn → link → ... → rPr
236
+ // ECMA-376 §17.7 emit order: name → aliases → basedOn → link → ... → rPr
184
237
  const linkEl = style.linkedStyleId
185
238
  ? `<w:link w:val="${escXml(style.linkedStyleId)}"/>`
186
239
  : "";
@@ -189,6 +242,7 @@ function buildCharacterStyleXml(
189
242
  return (
190
243
  `<w:style w:type="character" w:styleId="${escXml(style.styleId)}"${defaultAttr}>` +
191
244
  nameEl +
245
+ aliasesEl +
192
246
  basedOnEl +
193
247
  linkEl +
194
248
  rPrXml +
@@ -213,7 +267,11 @@ export function serializeStylesXml(catalog: StylesCatalog): string {
213
267
  .map((style) => buildCharacterStyleXml(style))
214
268
  .join("");
215
269
 
216
- const body = docDefaultsXml + paragraphStyles + characterStyles;
270
+ const numberingStyles = Object.values(catalog.numberingStyles ?? {})
271
+ .map((style) => buildNumberingStyleXml(style))
272
+ .join("");
273
+
274
+ const body = docDefaultsXml + paragraphStyles + characterStyles + numberingStyles;
217
275
 
218
276
  return [
219
277
  `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,