@beyondwork/docx-react-component 1.0.60 → 1.0.62

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 (42) hide show
  1. package/package.json +33 -44
  2. package/src/api/public-types.ts +41 -0
  3. package/src/io/docx-session.ts +167 -8
  4. package/src/io/export/serialize-footnotes.ts +36 -5
  5. package/src/io/export/serialize-headers-footers.ts +7 -0
  6. package/src/io/export/serialize-main-document.ts +25 -18
  7. package/src/io/export/serialize-paragraph-formatting.ts +6 -0
  8. package/src/io/export/serialize-settings.ts +130 -3
  9. package/src/io/normalize/normalize-text.ts +8 -4
  10. package/src/io/ooxml/classify-embedding.ts +193 -0
  11. package/src/io/ooxml/parse-footnotes.ts +11 -0
  12. package/src/io/ooxml/parse-headers-footers.ts +117 -42
  13. package/src/io/ooxml/parse-main-document.ts +20 -8
  14. package/src/io/ooxml/parse-object.ts +23 -0
  15. package/src/io/ooxml/parse-paragraph-formatting.ts +25 -1
  16. package/src/io/ooxml/parse-settings.ts +91 -1
  17. package/src/model/canonical-document.ts +36 -2
  18. package/src/runtime/document-runtime.ts +424 -0
  19. package/src/runtime/footnote-resolver.ts +32 -8
  20. package/src/runtime/layout/layout-engine-version.ts +7 -1
  21. package/src/runtime/layout/measurement-backend-canvas.ts +1 -1
  22. package/src/runtime/layout/measurement-backend-empirical.ts +1 -1
  23. package/src/runtime/layout/paginated-layout-engine.ts +41 -8
  24. package/src/runtime/layout/resolved-formatting-document.ts +11 -9
  25. package/src/runtime/layout/resolved-formatting-state.ts +4 -0
  26. package/src/runtime/numbering-prefix.ts +26 -2
  27. package/src/runtime/surface-projection.ts +75 -14
  28. package/src/runtime/table-schema.ts +26 -0
  29. package/src/ui/WordReviewEditor.tsx +25 -0
  30. package/src/ui/editor-runtime-boundary.ts +1 -0
  31. package/src/ui/editor-shell-view.tsx +8 -0
  32. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +514 -0
  33. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -0
  34. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +55 -6
  35. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  36. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +4 -0
  37. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -1
  38. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +16 -0
  39. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +319 -0
  40. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +248 -0
  41. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +4 -0
  42. package/src/ui-tailwind/tw-review-workspace.tsx +54 -3
@@ -14,6 +14,7 @@ import type {
14
14
  } from "../../model/canonical-document.ts";
15
15
  import type { LegacyFormFieldNode } from "../../model/canonical-document.ts";
16
16
  import { resolveHighlightColor } from "./highlight-colors.ts";
17
+ import type { ParseDrawingOpts } from "./parse-drawing.ts";
17
18
  import { parseFFDataFromFldChar } from "./parse-ffdata.ts";
18
19
  import { classifyFieldInstruction } from "./parse-fields.ts";
19
20
  import { parseXmlWithOffsets as parseXml } from "./xml-parser.ts";
@@ -49,6 +50,7 @@ import {
49
50
  readTableStyleId,
50
51
  readTableWidth,
51
52
  } from "./parse-tables.ts";
53
+ import { parseDrawingFrame } from "./parse-drawing.ts";
52
54
  import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
53
55
 
54
56
  const TAB_ALIGN_VOCAB = new Set<TabStop["align"]>(["left", "center", "right", "decimal", "num", "bar", "clear"]);
@@ -67,6 +69,8 @@ export interface ParsedHeaderFooterDocument {
67
69
  blocks: BlockNode[];
68
70
  }
69
71
 
72
+ export type ParseHeaderFooterOpts = Omit<ParseDrawingOpts, "blockParser">;
73
+
70
74
  // ---- XML node types (inline, no external dep) ----
71
75
 
72
76
  interface XmlElementNode {
@@ -121,15 +125,21 @@ export function parseHeaderFooterReferences(
121
125
  /**
122
126
  * Parse a headerN.xml part (<w:hdr> root) into block nodes.
123
127
  */
124
- export function parseHeaderXml(xml: string): ParsedHeaderFooterDocument {
125
- return parseHdrFtrXml(xml, "hdr");
128
+ export function parseHeaderXml(
129
+ xml: string,
130
+ opts: ParseHeaderFooterOpts = { relationships: [] },
131
+ ): ParsedHeaderFooterDocument {
132
+ return parseHdrFtrXml(xml, "hdr", opts);
126
133
  }
127
134
 
128
135
  /**
129
136
  * Parse a footerN.xml part (<w:ftr> root) into block nodes.
130
137
  */
131
- export function parseFooterXml(xml: string): ParsedHeaderFooterDocument {
132
- return parseHdrFtrXml(xml, "ftr");
138
+ export function parseFooterXml(
139
+ xml: string,
140
+ opts: ParseHeaderFooterOpts = { relationships: [] },
141
+ ): ParsedHeaderFooterDocument {
142
+ return parseHdrFtrXml(xml, "ftr", opts);
133
143
  }
134
144
 
135
145
  // ---- Internal helpers ----
@@ -213,6 +223,7 @@ function toHeaderFooterVariant(raw: string): HeaderFooterVariant {
213
223
  function parseHdrFtrXml(
214
224
  xml: string,
215
225
  rootLocalName: "hdr" | "ftr",
226
+ opts: ParseHeaderFooterOpts = { relationships: [] },
216
227
  ): ParsedHeaderFooterDocument {
217
228
  currentSourceXml = xml;
218
229
  let root: XmlElementNode;
@@ -239,12 +250,12 @@ function parseHdrFtrXml(
239
250
  const name = localName(child.name);
240
251
 
241
252
  if (name === "p") {
242
- blocks.push(parseParagraphElement(child, xml));
253
+ blocks.push(parseParagraphElement(child, xml, opts));
243
254
  } else if (name === "tbl") {
244
255
  // Simple tables (no revisions, fields, or nested tables) are promoted
245
256
  // to supported-roundtrip; structurally risky tables stay opaque.
246
257
  if (isSimpleSecondaryStoryTable(child)) {
247
- blocks.push(parseSimpleTableElement(child, xml));
258
+ blocks.push(parseSimpleTableElement(child, xml, opts));
248
259
  } else {
249
260
  blocks.push({
250
261
  type: "opaque_block",
@@ -267,7 +278,11 @@ function parseHdrFtrXml(
267
278
  return { blocks };
268
279
  }
269
280
 
270
- function parseParagraphElement(pElement: XmlElementNode, sourceXml: string): ParagraphNode {
281
+ function parseParagraphElement(
282
+ pElement: XmlElementNode,
283
+ sourceXml: string,
284
+ opts: ParseHeaderFooterOpts,
285
+ ): ParagraphNode {
271
286
  let styleId: string | undefined;
272
287
  let alignment: ParagraphNode["alignment"];
273
288
  let spacing: ParagraphNode["spacing"];
@@ -295,9 +310,9 @@ function parseParagraphElement(pElement: XmlElementNode, sourceXml: string): Par
295
310
  indentation = readParagraphIndentation(child);
296
311
  tabStops = readParagraphTabStops(child);
297
312
  } else if (name === "r") {
298
- activeComplexField = appendRunNodes(child, children, activeComplexField, sourceXml);
313
+ activeComplexField = appendRunNodes(child, children, activeComplexField, sourceXml, opts);
299
314
  } else if (name === "hyperlink") {
300
- children.push(parseHyperlinkElement(child));
315
+ children.push(parseHyperlinkElement(child, opts));
301
316
  } else if (name === "bookmarkStart" || name === "bookmarkEnd") {
302
317
  children.push(parseBookmarkElement(child));
303
318
  } else if (name === "fldSimple") {
@@ -350,6 +365,7 @@ function appendRunNodes(
350
365
  nodes: InlineNode[],
351
366
  activeComplexField: ActiveComplexField | null,
352
367
  sourceXml: string,
368
+ opts: ParseHeaderFooterOpts,
353
369
  ): ActiveComplexField | null {
354
370
  const marks: TextMark[] = parseRunProperties(rElement);
355
371
 
@@ -398,7 +414,7 @@ function appendRunNodes(
398
414
  continue;
399
415
  }
400
416
 
401
- const inlineNode = parseRunChildNode(child, marks);
417
+ const inlineNode = parseRunChildNode(child, marks, opts);
402
418
  if (!inlineNode) {
403
419
  continue;
404
420
  }
@@ -420,7 +436,10 @@ function appendRunNodes(
420
436
  return activeComplexField;
421
437
  }
422
438
 
423
- function parseRunElement(rElement: XmlElementNode): InlineNode[] {
439
+ function parseRunElement(
440
+ rElement: XmlElementNode,
441
+ opts: ParseHeaderFooterOpts,
442
+ ): InlineNode[] {
424
443
  const nodes: InlineNode[] = [];
425
444
  const marks: TextMark[] = parseRunProperties(rElement);
426
445
 
@@ -472,9 +491,9 @@ function parseRunElement(rElement: XmlElementNode): InlineNode[] {
472
491
  pushFieldNode(nodes, child, "complex");
473
492
  } else if (name === "drawing") {
474
493
  const drawingXml = currentSourceXml.slice(child.start, child.end);
475
- const shapeResult = parseShapeXml(drawingXml);
476
- if (shapeResult) {
477
- nodes.push(shapeResult);
494
+ const drawingResult = parseDrawingInlineNode(drawingXml, opts);
495
+ if (drawingResult) {
496
+ nodes.push(drawingResult);
478
497
  }
479
498
  } else if (name === "pict") {
480
499
  const pictXml = currentSourceXml.slice(child.start, child.end);
@@ -483,17 +502,19 @@ function parseRunElement(rElement: XmlElementNode): InlineNode[] {
483
502
  nodes.push(vmlResult);
484
503
  }
485
504
  } else if (name === "AlternateContent") {
505
+ const alternateXml = currentSourceXml.slice(child.start, child.end);
486
506
  const drawingNode = findFirstDescendant(child, "drawing");
487
- if (drawingNode) {
488
- const drawingXml = currentSourceXml.slice(drawingNode.start, drawingNode.end);
489
- const shapeResult = parseShapeXml(drawingXml);
490
- if (shapeResult) {
491
- nodes.push({
492
- ...shapeResult,
493
- rawXml: currentSourceXml.slice(child.start, child.end),
494
- });
495
- continue;
496
- }
507
+ const legacyDrawingXml = drawingNode
508
+ ? currentSourceXml.slice(drawingNode.start, drawingNode.end)
509
+ : undefined;
510
+ const alternateDrawingResult = parseDrawingInlineNode(
511
+ alternateXml,
512
+ opts,
513
+ legacyDrawingXml,
514
+ );
515
+ if (alternateDrawingResult) {
516
+ nodes.push(alternateDrawingResult);
517
+ continue;
497
518
  }
498
519
  const pictNode = findFirstDescendant(child, "pict");
499
520
  if (pictNode) {
@@ -515,6 +536,7 @@ function parseRunElement(rElement: XmlElementNode): InlineNode[] {
515
536
  function parseRunChildNode(
516
537
  child: XmlElementNode,
517
538
  marks: TextMark[],
539
+ opts: ParseHeaderFooterOpts,
518
540
  ): InlineNode | null {
519
541
  const name = localName(child.name);
520
542
 
@@ -566,23 +588,21 @@ function parseRunChildNode(
566
588
  }
567
589
  if (name === "drawing") {
568
590
  const drawingXml = currentSourceXml.slice(child.start, child.end);
569
- return parseShapeXml(drawingXml);
591
+ return parseDrawingInlineNode(drawingXml, opts);
570
592
  }
571
593
  if (name === "pict") {
572
594
  const pictXml = currentSourceXml.slice(child.start, child.end);
573
595
  return parseVmlXml(pictXml);
574
596
  }
575
597
  if (name === "AlternateContent") {
598
+ const alternateXml = currentSourceXml.slice(child.start, child.end);
576
599
  const drawingNode = findFirstDescendant(child, "drawing");
577
- if (drawingNode) {
578
- const drawingXml = currentSourceXml.slice(drawingNode.start, drawingNode.end);
579
- const shapeResult = parseShapeXml(drawingXml);
580
- if (shapeResult) {
581
- return {
582
- ...shapeResult,
583
- rawXml: currentSourceXml.slice(child.start, child.end),
584
- };
585
- }
600
+ const drawingXml = drawingNode
601
+ ? currentSourceXml.slice(drawingNode.start, drawingNode.end)
602
+ : undefined;
603
+ const drawingResult = parseDrawingInlineNode(alternateXml, opts, drawingXml);
604
+ if (drawingResult) {
605
+ return drawingResult;
586
606
  }
587
607
  const pictNode = findFirstDescendant(child, "pict");
588
608
  if (pictNode) {
@@ -600,7 +620,10 @@ function parseRunChildNode(
600
620
  return null;
601
621
  }
602
622
 
603
- function parseHyperlinkElement(element: XmlElementNode): Extract<InlineNode, { type: "hyperlink" }> {
623
+ function parseHyperlinkElement(
624
+ element: XmlElementNode,
625
+ opts: ParseHeaderFooterOpts,
626
+ ): Extract<InlineNode, { type: "hyperlink" }> {
604
627
  const href = element.attributes["w:anchor"]
605
628
  ? `#${element.attributes["w:anchor"]}`
606
629
  : element.attributes["r:id"] ?? "relationship:unknown";
@@ -608,7 +631,7 @@ function parseHyperlinkElement(element: XmlElementNode): Extract<InlineNode, { t
608
631
 
609
632
  for (const child of element.children) {
610
633
  if (child.type === "element" && localName(child.name) === "r") {
611
- for (const runChild of parseRunElement(child)) {
634
+ for (const runChild of parseRunElement(child, opts)) {
612
635
  if (runChild.type === "text" || runChild.type === "hard_break" || runChild.type === "tab") {
613
636
  children.push(runChild);
614
637
  }
@@ -623,6 +646,46 @@ function parseHyperlinkElement(element: XmlElementNode): Extract<InlineNode, { t
623
646
  };
624
647
  }
625
648
 
649
+ function parseDrawingInlineNode(
650
+ rawXml: string,
651
+ opts: ParseHeaderFooterOpts,
652
+ legacyDrawingXml?: string,
653
+ ): InlineNode | null {
654
+ try {
655
+ const frame = parseDrawingFrame(rawXml, {
656
+ ...opts,
657
+ relationships: opts.relationships ?? [],
658
+ });
659
+ if (
660
+ frame &&
661
+ !(
662
+ frame.content.type === "shape" &&
663
+ frame.content.isTextBox
664
+ )
665
+ ) {
666
+ return frame;
667
+ }
668
+ if (frame?.content.type !== "shape" || !frame.content.isTextBox) {
669
+ return frame;
670
+ }
671
+ } catch {
672
+ // Fall through to the legacy shape path.
673
+ }
674
+
675
+ const shapeXml = legacyDrawingXml ?? rawXml;
676
+ const legacyShape = parseShapeXml(shapeXml);
677
+ if (!legacyShape) {
678
+ return null;
679
+ }
680
+ if (legacyDrawingXml) {
681
+ return {
682
+ ...legacyShape,
683
+ rawXml,
684
+ };
685
+ }
686
+ return legacyShape;
687
+ }
688
+
626
689
  function parseBookmarkElement(
627
690
  element: XmlElementNode,
628
691
  ): Extract<InlineNode, { type: "bookmark_start" | "bookmark_end" }> {
@@ -958,7 +1021,11 @@ function isSafeSecondaryStoryFieldFamily(family: string): boolean {
958
1021
  );
959
1022
  }
960
1023
 
961
- function parseSimpleTableElement(tblElement: XmlElementNode, sourceXml: string): TableNode {
1024
+ function parseSimpleTableElement(
1025
+ tblElement: XmlElementNode,
1026
+ sourceXml: string,
1027
+ opts: ParseHeaderFooterOpts,
1028
+ ): TableNode {
962
1029
  let gridColumns: number[] = [];
963
1030
  const rows: TableRowNode[] = [];
964
1031
  let propertiesXml: string | undefined;
@@ -998,7 +1065,7 @@ function parseSimpleTableElement(tblElement: XmlElementNode, sourceXml: string):
998
1065
  } else if (name === "tblGrid") {
999
1066
  gridColumns = readGridColumns(child);
1000
1067
  } else if (name === "tr") {
1001
- rows.push(parseSimpleTableRow(child, sourceXml));
1068
+ rows.push(parseSimpleTableRow(child, sourceXml, opts));
1002
1069
  }
1003
1070
  }
1004
1071
 
@@ -1027,7 +1094,11 @@ function readGridColumns(tblGrid: XmlElementNode): number[] {
1027
1094
  return readSharedGridColumns(tblGrid);
1028
1095
  }
1029
1096
 
1030
- function parseSimpleTableRow(trElement: XmlElementNode, sourceXml: string): TableRowNode {
1097
+ function parseSimpleTableRow(
1098
+ trElement: XmlElementNode,
1099
+ sourceXml: string,
1100
+ opts: ParseHeaderFooterOpts,
1101
+ ): TableRowNode {
1031
1102
  const cells: TableCellNode[] = [];
1032
1103
  let propertiesXml: string | undefined;
1033
1104
  let height: TableRowNode["height"];
@@ -1050,7 +1121,7 @@ function parseSimpleTableRow(trElement: XmlElementNode, sourceXml: string): Tabl
1050
1121
  horizontalAlignment = readRowHorizontalAlignment(child);
1051
1122
  cnfStyle = readRowCnfStyle(child);
1052
1123
  } else if (name === "tc") {
1053
- cells.push(parseSimpleTableCell(child, sourceXml));
1124
+ cells.push(parseSimpleTableCell(child, sourceXml, opts));
1054
1125
  }
1055
1126
  }
1056
1127
 
@@ -1067,7 +1138,11 @@ function parseSimpleTableRow(trElement: XmlElementNode, sourceXml: string): Tabl
1067
1138
  };
1068
1139
  }
1069
1140
 
1070
- function parseSimpleTableCell(tcElement: XmlElementNode, sourceXml: string): TableCellNode {
1141
+ function parseSimpleTableCell(
1142
+ tcElement: XmlElementNode,
1143
+ sourceXml: string,
1144
+ opts: ParseHeaderFooterOpts,
1145
+ ): TableCellNode {
1071
1146
  const children: BlockNode[] = [];
1072
1147
  let propertiesXml: string | undefined;
1073
1148
  let gridSpan: number | undefined;
@@ -1107,7 +1182,7 @@ function parseSimpleTableCell(tcElement: XmlElementNode, sourceXml: string): Tab
1107
1182
  margins = readCellMargins(child);
1108
1183
  cnfStyle = readCellCnfStyle(child);
1109
1184
  } else if (name === "p") {
1110
- children.push(parseParagraphElement(child, sourceXml));
1185
+ children.push(parseParagraphElement(child, sourceXml, opts));
1111
1186
  }
1112
1187
  }
1113
1188
 
@@ -1150,15 +1150,15 @@ function parseBodyChild(
1150
1150
  ...(contextualSpacing !== undefined ? { contextualSpacing } : {}),
1151
1151
  ...(indentation ? { indentation } : {}),
1152
1152
  ...(tabStops && tabStops.length > 0 ? { tabStops } : {}),
1153
- ...(keepNext ? { keepNext } : {}),
1154
- ...(keepLines ? { keepLines } : {}),
1153
+ ...(keepNext !== undefined ? { keepNext } : {}),
1154
+ ...(keepLines !== undefined ? { keepLines } : {}),
1155
1155
  ...(outlineLevel !== undefined ? { outlineLevel } : {}),
1156
- ...(pageBreakBefore ? { pageBreakBefore } : {}),
1157
- ...(widowControl ? { widowControl } : {}),
1156
+ ...(pageBreakBefore !== undefined ? { pageBreakBefore } : {}),
1157
+ ...(widowControl !== undefined ? { widowControl } : {}),
1158
1158
  ...(borders ? { borders } : {}),
1159
1159
  ...(shading ? { shading } : {}),
1160
- ...(bidi ? { bidi } : {}),
1161
- ...(suppressLineNumbers ? { suppressLineNumbers } : {}),
1160
+ ...(bidi !== undefined ? { bidi } : {}),
1161
+ ...(suppressLineNumbers !== undefined ? { suppressLineNumbers } : {}),
1162
1162
  ...(cnfStyle ? { cnfStyle } : {}),
1163
1163
  ...(wordExtensionIds ? { wordExtensionIds } : {}),
1164
1164
  ...(sectionProperties ? { sectionProperties } : {}),
@@ -1848,7 +1848,7 @@ function tableRequiresOpaquePreservation(rawXml: string): boolean {
1848
1848
  // nested tables, floating images, VML preview atoms, and bounded field
1849
1849
  // families already owned by the current field slice. Risky table-local
1850
1850
  // semantics still fail closed to preserve-only.
1851
- if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag|tblCellSpacing)\b/.test(rawXml)) {
1851
+ if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag)\b/.test(rawXml)) {
1852
1852
  return true;
1853
1853
  }
1854
1854
 
@@ -2103,7 +2103,7 @@ function readOnOffParagraphProperty(node: XmlElementNode, name: string): boolean
2103
2103
  );
2104
2104
  if (!propNode) return undefined;
2105
2105
  const val = (propNode.attributes["w:val"] ?? propNode.attributes.val ?? "true").toLowerCase();
2106
- return val !== "false" && val !== "0" && val !== "off" ? true : undefined;
2106
+ return val !== "false" && val !== "0" && val !== "off";
2107
2107
  }
2108
2108
 
2109
2109
  function readOptionalOnOffParagraphProperty(
@@ -2172,9 +2172,21 @@ function readParagraphShading(node: XmlElementNode): ParagraphShading | undefine
2172
2172
  const fill = shadingNode.attributes["w:fill"] ?? shadingNode.attributes.fill;
2173
2173
  const color = shadingNode.attributes["w:color"] ?? shadingNode.attributes.color;
2174
2174
  const val = shadingNode.attributes["w:val"] ?? shadingNode.attributes.val;
2175
+ const themeFill = shadingNode.attributes["w:themeFill"] ?? shadingNode.attributes.themeFill;
2176
+ const themeFillTint = shadingNode.attributes["w:themeFillTint"] ?? shadingNode.attributes.themeFillTint;
2177
+ const themeFillShade = shadingNode.attributes["w:themeFillShade"] ?? shadingNode.attributes.themeFillShade;
2178
+ const themeColor = shadingNode.attributes["w:themeColor"] ?? shadingNode.attributes.themeColor;
2179
+ const themeColorTint = shadingNode.attributes["w:themeColorTint"] ?? shadingNode.attributes.themeColorTint;
2180
+ const themeColorShade = shadingNode.attributes["w:themeColorShade"] ?? shadingNode.attributes.themeColorShade;
2175
2181
  if (fill) shading.fill = fill;
2176
2182
  if (color) shading.color = color;
2177
2183
  if (val) shading.val = val;
2184
+ if (themeFill) shading.themeFill = themeFill;
2185
+ if (themeFillTint) shading.themeFillTint = themeFillTint;
2186
+ if (themeFillShade) shading.themeFillShade = themeFillShade;
2187
+ if (themeColor) shading.themeColor = themeColor;
2188
+ if (themeColorTint) shading.themeColorTint = themeColorTint;
2189
+ if (themeColorShade) shading.themeColorShade = themeColorShade;
2178
2190
  return Object.keys(shading).length > 0 ? shading : undefined;
2179
2191
  }
2180
2192
 
@@ -22,6 +22,7 @@ import type { OleEmbedNode } from "../../model/canonical-document.ts";
22
22
  import type { OpcRelationship } from "./part-manifest.ts";
23
23
  import type { XmlElementNode } from "./xml-element.ts";
24
24
  import { resolveOleRelationship } from "./parse-ole-relationship.ts";
25
+ import { classifyEmbedding } from "./classify-embedding.ts";
25
26
 
26
27
  /**
27
28
  * Parse a `<w:object>` element into an `OleEmbedNode` if it contains an
@@ -64,6 +65,28 @@ export function parseObject(
64
65
  return undefined;
65
66
  }
66
67
 
68
+ // hotfix/ole-digestibility-guard — classify the embedding before
69
+ // constructing a canonical node. When the classifier returns
70
+ // "store-only" (nested Word docs, PDF OLE, Excel/PowerPoint
71
+ // embeddings, unknown ProgIDs), return undefined so the caller's
72
+ // existing opaque-fragment fallback preserves both <w:object> XML
73
+ // and its r:id verbatim. Binary preservation is unaffected —
74
+ // collectPreservedPackageParts indexes embedding parts by path, not
75
+ // by canonical-tree reference.
76
+ //
77
+ // TODO(refactor/01 Step 6-7): replace this skip-construction with
78
+ // extraction + offload via hostAdapter.storeEmbeddedDocument?. See
79
+ // docs/architecture/01-package-session.md §P8 + docs/plans/refactor/
80
+ // 01-package-session.md Steps 6-7.
81
+ const kind = classifyEmbedding({
82
+ progId,
83
+ relationshipType: resolved.relationshipType,
84
+ targetPath: resolved.target,
85
+ });
86
+ if (kind !== "digestible") {
87
+ return undefined;
88
+ }
89
+
67
90
  const metadata: OleEmbedNode["metadata"] = {};
68
91
  if (resolved.originalFilename) {
69
92
  metadata.originalFilename = resolved.originalFilename;
@@ -132,11 +132,35 @@ function readShading(node: XmlElementNode): ParagraphShading | undefined {
132
132
  const val = node.attributes["w:val"] ?? node.attributes.val;
133
133
  const fill = node.attributes["w:fill"] ?? node.attributes.fill;
134
134
  const color = node.attributes["w:color"] ?? node.attributes.color;
135
- if (!val && !fill && !color) return undefined;
135
+ const themeFill = node.attributes["w:themeFill"] ?? node.attributes.themeFill;
136
+ const themeFillTint = node.attributes["w:themeFillTint"] ?? node.attributes.themeFillTint;
137
+ const themeFillShade = node.attributes["w:themeFillShade"] ?? node.attributes.themeFillShade;
138
+ const themeColor = node.attributes["w:themeColor"] ?? node.attributes.themeColor;
139
+ const themeColorTint = node.attributes["w:themeColorTint"] ?? node.attributes.themeColorTint;
140
+ const themeColorShade = node.attributes["w:themeColorShade"] ?? node.attributes.themeColorShade;
141
+ if (
142
+ !val &&
143
+ !fill &&
144
+ !color &&
145
+ !themeFill &&
146
+ !themeFillTint &&
147
+ !themeFillShade &&
148
+ !themeColor &&
149
+ !themeColorTint &&
150
+ !themeColorShade
151
+ ) {
152
+ return undefined;
153
+ }
136
154
  return {
137
155
  ...(val ? { val } : {}),
138
156
  ...(fill ? { fill } : {}),
139
157
  ...(color ? { color } : {}),
158
+ ...(themeFill ? { themeFill } : {}),
159
+ ...(themeFillTint ? { themeFillTint } : {}),
160
+ ...(themeFillShade ? { themeFillShade } : {}),
161
+ ...(themeColor ? { themeColor } : {}),
162
+ ...(themeColorTint ? { themeColorTint } : {}),
163
+ ...(themeColorShade ? { themeColorShade } : {}),
140
164
  };
141
165
  }
142
166
 
@@ -3,6 +3,7 @@ import type {
3
3
  ClrSchemeMapping,
4
4
  ClrSchemeMappingSlot,
5
5
  DocumentSettings,
6
+ FootnoteProperties,
6
7
  ThemeColorSlot,
7
8
  } from "../../model/canonical-document.ts";
8
9
  import { parseXml } from "./xml-parser.ts";
@@ -51,6 +52,13 @@ export function parseSettingsXml(xml: string): DocumentSettings {
51
52
  const compatPartition = compat ? partitionCompat(compat) : undefined;
52
53
  const rootCompatFlags = readRootCompatFlags(settingsElement);
53
54
  const themeFontLangElement = findChildElementOptional(settingsElement, "themeFontLang");
55
+ const defaultTabStop = readDefaultTabStop(settingsElement);
56
+ const footnotePr = readFootnoteLikeProperties(
57
+ findChildElementOptional(settingsElement, "footnotePr"),
58
+ );
59
+ const endnotePr = readFootnoteLikeProperties(
60
+ findChildElementOptional(settingsElement, "endnotePr"),
61
+ );
54
62
  const clrSchemeMapping = parseClrSchemeMapping(settingsElement);
55
63
  const unmodelled = readUnmodelledSettingsChildren(settingsElement);
56
64
 
@@ -71,6 +79,9 @@ export function parseSettingsXml(xml: string): DocumentSettings {
71
79
  ...(themeFontLangElement
72
80
  ? { themeFontLang: { ...themeFontLangElement.attributes } }
73
81
  : {}),
82
+ ...(defaultTabStop !== undefined ? { defaultTabStop } : {}),
83
+ ...(footnotePr ? { footnotePr } : {}),
84
+ ...(endnotePr ? { endnotePr } : {}),
74
85
  ...(clrSchemeMapping !== undefined ? { clrSchemeMapping } : {}),
75
86
  ...(unmodelled.length > 0 ? { unmodelledSettingsChildren: unmodelled } : {}),
76
87
  };
@@ -86,6 +97,9 @@ const MODELLED_SETTINGS_CHILD_NAMES = new Set<string>([
86
97
  "zoom",
87
98
  "compat",
88
99
  "themeFontLang",
100
+ "defaultTabStop",
101
+ "footnotePr",
102
+ "endnotePr",
89
103
  "clrSchemeMapping",
90
104
  ]);
91
105
 
@@ -130,6 +144,83 @@ function readRootCompatFlags(
130
144
  return flags;
131
145
  }
132
146
 
147
+ function readDefaultTabStop(
148
+ settingsElement: XmlElementNode,
149
+ ): number | undefined {
150
+ const element = findChildElementOptional(settingsElement, "defaultTabStop");
151
+ if (!element) return undefined;
152
+ const rawValue = element.attributes["w:val"] ?? element.attributes.val;
153
+ if (!rawValue) return undefined;
154
+ const parsed = Number.parseInt(rawValue, 10);
155
+ return Number.isFinite(parsed) ? parsed : undefined;
156
+ }
157
+
158
+ function readFootnoteLikeProperties(
159
+ element: XmlElementNode | undefined,
160
+ ): FootnoteProperties | undefined {
161
+ if (!element) return undefined;
162
+
163
+ const result: FootnoteProperties = {};
164
+ for (const child of element.children) {
165
+ if (child.type !== "element") continue;
166
+ const name = localName(child.name);
167
+ const val = child.attributes["w:val"] ?? child.attributes.val;
168
+
169
+ if (name === "pos") {
170
+ if (
171
+ val === "pageBottom" ||
172
+ val === "beneathText" ||
173
+ val === "sectEnd" ||
174
+ val === "docEnd"
175
+ ) {
176
+ result.pos = val;
177
+ }
178
+ continue;
179
+ }
180
+
181
+ if (name === "numFmt") {
182
+ if (
183
+ val === "decimal" ||
184
+ val === "upperRoman" ||
185
+ val === "lowerRoman" ||
186
+ val === "upperLetter" ||
187
+ val === "lowerLetter" ||
188
+ val === "ordinal" ||
189
+ val === "cardinalText" ||
190
+ val === "ordinalText" ||
191
+ val === "hex" ||
192
+ val === "chicago" ||
193
+ val === "bullet" ||
194
+ val === "ideographDigital" ||
195
+ val === "japaneseCounting" ||
196
+ val === "arabicAbjad" ||
197
+ val === "arabicAlpha" ||
198
+ val === "none"
199
+ ) {
200
+ result.numFmt = val;
201
+ }
202
+ continue;
203
+ }
204
+
205
+ if (name === "numStart") {
206
+ const parsed = Number.parseInt(val ?? "", 10);
207
+ if (Number.isFinite(parsed)) {
208
+ result.numStart = Math.max(1, parsed);
209
+ }
210
+ continue;
211
+ }
212
+
213
+ if (
214
+ name === "numRestart" &&
215
+ (val === "continuous" || val === "eachSect" || val === "eachPage")
216
+ ) {
217
+ result.numRestart = val;
218
+ }
219
+ }
220
+
221
+ return Object.keys(result).length > 0 ? result : undefined;
222
+ }
223
+
133
224
  interface CompatPartition {
134
225
  compatSettings: CompatSetting[];
135
226
  compatFlags: Record<string, boolean>;
@@ -218,4 +309,3 @@ function readZoomLevel(
218
309
 
219
310
  return { zoomLevel: parsed };
220
311
  }
221
-
@@ -346,14 +346,20 @@ export interface FootnoteDefinition {
346
346
  * footnote/endnote entries with `w:type="separator"` and
347
347
  * `w:type="continuationSeparator"`.
348
348
  *
349
- * Content is stored as raw inner XML (the run children of the <w:p>) so
350
- * Lane 3a page-chrome can render the horizontal rule without re-parsing.
349
+ * The canonical model keeps both the legacy run-only payload and the full
350
+ * first-paragraph XML. The paragraph form closes round-trip fidelity for
351
+ * imported separator paragraph properties, while the run form remains for
352
+ * back-compat with the current note-separator runtime helpers.
351
353
  */
352
354
  export interface FootnoteSeparators {
353
355
  /** Raw XML of the <w:r> children inside the separator paragraph. */
354
356
  separatorContent?: string;
357
+ /** Full XML of the first separator paragraph. */
358
+ separatorParagraphXml?: string;
355
359
  /** Raw XML of the <w:r> children inside the continuationSeparator paragraph. */
356
360
  continuationSeparatorContent?: string;
361
+ /** Full XML of the first continuation-separator paragraph. */
362
+ continuationSeparatorParagraphXml?: string;
357
363
  }
358
364
 
359
365
  export interface FootnoteCollection {
@@ -399,6 +405,22 @@ export interface CompatSetting {
399
405
  export interface DocumentSettings {
400
406
  evenAndOddHeaders?: boolean;
401
407
  zoomLevel?: "pageWidth" | "onePage" | number;
408
+ /**
409
+ * Document-wide default tab stop interval from `<w:defaultTabStop w:val>`.
410
+ * Value is stored in twips and feeds layout measurement whenever a paragraph
411
+ * does not declare an explicit next tab stop.
412
+ */
413
+ defaultTabStop?: number;
414
+ /**
415
+ * Settings-level default footnote configuration from `<w:footnotePr>`.
416
+ * Section-level `SectionProperties.footnotePr` overrides this when present.
417
+ */
418
+ footnotePr?: FootnoteProperties;
419
+ /**
420
+ * Settings-level default endnote configuration from `<w:endnotePr>`.
421
+ * Section-level `SectionProperties.endnotePr` overrides this when present.
422
+ */
423
+ endnotePr?: EndnoteProperties;
402
424
  /**
403
425
  * Ordered list of <w:compatSetting> entries inside <w:compat>. Insertion
404
426
  * order is preserved for serializer diff stability.
@@ -606,6 +628,18 @@ export interface ParagraphShading {
606
628
  fill?: string;
607
629
  color?: string;
608
630
  val?: string;
631
+ /**
632
+ * Theme shading references (§17.3.5). When `themeFill` is set and `fill`
633
+ * is absent or `"auto"`, render-time shading resolves through the theme
634
+ * color resolver. The raw theme attrs remain on the canonical model so
635
+ * export can round-trip the original `<w:shd>` byte-for-byte.
636
+ */
637
+ themeFill?: string;
638
+ themeFillTint?: string;
639
+ themeFillShade?: string;
640
+ themeColor?: string;
641
+ themeColorTint?: string;
642
+ themeColorShade?: string;
609
643
  }
610
644
 
611
645
  /** Body of an OOXML `<w:rPr>` (run properties). All fields optional; absence = "not specified at this level". */