@beyondwork/docx-react-component 1.0.102 → 1.0.104

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 (57) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +63 -1
  3. package/src/api/v3/_runtime-handle.ts +2 -0
  4. package/src/api/v3/ai/outline.ts +2 -7
  5. package/src/api/v3/runtime/geometry.ts +79 -0
  6. package/src/core/commands/formatting-commands.ts +8 -7
  7. package/src/core/commands/paragraph-layout-commands.ts +11 -10
  8. package/src/core/commands/section-layout-commands.ts +7 -6
  9. package/src/core/commands/style-commands.ts +3 -2
  10. package/src/io/normalize/normalize-text.ts +6 -5
  11. package/src/io/ooxml/parse-anchor.ts +15 -15
  12. package/src/io/ooxml/parse-drawing.ts +103 -5
  13. package/src/io/ooxml/parse-fields.ts +43 -21
  14. package/src/io/ooxml/parse-font-table.ts +2 -1
  15. package/src/io/ooxml/parse-footnotes.ts +3 -2
  16. package/src/io/ooxml/parse-headers-footers.ts +7 -6
  17. package/src/io/ooxml/parse-main-document.ts +41 -40
  18. package/src/io/ooxml/parse-numbering.ts +3 -2
  19. package/src/io/ooxml/parse-object.ts +6 -6
  20. package/src/io/ooxml/parse-paragraph-formatting.ts +12 -11
  21. package/src/io/ooxml/parse-picture.ts +16 -16
  22. package/src/io/ooxml/parse-run-formatting.ts +11 -10
  23. package/src/io/ooxml/parse-settings.ts +2 -1
  24. package/src/io/ooxml/parse-shapes.ts +148 -17
  25. package/src/io/ooxml/parse-styles.ts +16 -16
  26. package/src/io/ooxml/parse-theme.ts +5 -4
  27. package/src/model/canonical-document.ts +869 -836
  28. package/src/model/canonical-layout-inputs.ts +979 -0
  29. package/src/model/layout/index.ts +6 -0
  30. package/src/model/layout/page-graph-types.ts +61 -0
  31. package/src/model/layout/runtime-page-graph-types.ts +10 -0
  32. package/src/runtime/collab/runtime-collab-sync.ts +3 -3
  33. package/src/runtime/debug/build-debug-inspector-snapshot.ts +17 -4
  34. package/src/runtime/document-runtime.ts +30 -14
  35. package/src/runtime/event-refresh-hints.ts +3 -0
  36. package/src/runtime/formatting/document-lookup.ts +3 -2
  37. package/src/runtime/formatting/formatting-context.ts +176 -34
  38. package/src/runtime/formatting/index.ts +20 -0
  39. package/src/runtime/formatting/layout-inputs.ts +320 -0
  40. package/src/runtime/formatting/numbering/geometry.ts +13 -12
  41. package/src/runtime/formatting/style-cascade.ts +2 -1
  42. package/src/runtime/formatting/table-style-resolver.ts +8 -7
  43. package/src/runtime/geometry/caret-geometry.ts +82 -10
  44. package/src/runtime/geometry/geometry-facet.ts +36 -0
  45. package/src/runtime/geometry/geometry-index.ts +891 -0
  46. package/src/runtime/geometry/geometry-types.ts +221 -1
  47. package/src/runtime/geometry/index.ts +26 -0
  48. package/src/runtime/geometry/inert-geometry-facet.ts +3 -0
  49. package/src/runtime/geometry/replacement-envelope.ts +41 -2
  50. package/src/runtime/layout/layout-engine-version.ts +16 -1
  51. package/src/runtime/layout/page-graph.ts +191 -1
  52. package/src/runtime/prerender/graph-canonicalize.ts +30 -0
  53. package/src/runtime/surface-projection.ts +74 -39
  54. package/src/runtime/workflow/coordinator.ts +57 -11
  55. package/src/session/import/normalize.ts +2 -1
  56. package/src/session/import/source-package-evidence.ts +612 -1
  57. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +3 -0
@@ -18,7 +18,7 @@
18
18
  * responsibility post-v2.0; this module emits only data.
19
19
  */
20
20
 
21
- import type { OleEmbedNode } from "../../model/canonical-document.ts";
21
+ import type { OleEmbedNode, Mutable } 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";
@@ -93,13 +93,13 @@ export function parseObject(
93
93
 
94
94
  const metadata: OleEmbedNode["metadata"] = {};
95
95
  if (resolved.originalFilename) {
96
- metadata.originalFilename = resolved.originalFilename;
96
+ (metadata as Mutable<typeof metadata>).originalFilename = resolved.originalFilename;
97
97
  }
98
98
  if (classId) {
99
- metadata.classId = classId;
99
+ (metadata as Mutable<typeof metadata>).classId = classId;
100
100
  }
101
101
  if (shapeId) {
102
- metadata.shapeId = shapeId;
102
+ (metadata as Mutable<typeof metadata>).shapeId = shapeId;
103
103
  }
104
104
 
105
105
  const id =
@@ -107,7 +107,7 @@ export function parseObject(
107
107
  ? `ole-embed-${resolved.relationshipId}-${occurrenceIndex}`
108
108
  : `ole-embed-${resolved.relationshipId}`;
109
109
 
110
- const result: OleEmbedNode = {
110
+ const result: Mutable<OleEmbedNode> = {
111
111
  type: "ole_embed",
112
112
  id,
113
113
  embedType: "oleObject",
@@ -116,7 +116,7 @@ export function parseObject(
116
116
  rawXml,
117
117
  };
118
118
  if (progId) {
119
- result.progId = progId;
119
+ (result as Mutable<typeof result>).progId = progId;
120
120
  }
121
121
  return result;
122
122
  }
@@ -14,6 +14,7 @@ import type {
14
14
  ParagraphShading,
15
15
  ParagraphSpacing,
16
16
  TabStop,
17
+ Mutable,
17
18
  } from "../../model/canonical-document.ts";
18
19
  import { readRunProperties } from "./parse-run-formatting.ts";
19
20
  import { findChildOptional, localName, readIntAttr, readIntVal, readOnOff } from "./xml-attr-helpers.ts";
@@ -87,31 +88,31 @@ const PPR_GRAB_BAG_DESCRIPTOR: PropertyGrabBagDescriptor = {
87
88
  };
88
89
 
89
90
  function readSpacing(node: XmlElementNode): ParagraphSpacing | undefined {
90
- const out: ParagraphSpacing = {};
91
+ const out: Mutable<ParagraphSpacing> = {};
91
92
  const before = readIntAttr(node, "w:before");
92
93
  const after = readIntAttr(node, "w:after");
93
94
  const line = readIntAttr(node, "w:line");
94
95
  const rule = node.attributes["w:lineRule"] ?? node.attributes.lineRule;
95
- if (before !== undefined) out.before = before;
96
- if (after !== undefined) out.after = after;
96
+ if (before !== undefined) (out as Mutable<typeof out>).before = before;
97
+ if (after !== undefined) (out as Mutable<typeof out>).after = after;
97
98
  if (line !== undefined) out.line = line;
98
99
  if (rule) {
99
100
  const n = rule.toLowerCase();
100
- if (n === "auto" || n === "exact") out.lineRule = n;
101
+ if (n === "auto" || n === "exact") (out as Mutable<typeof out>).lineRule = n;
101
102
  else if (n === "atleast") out.lineRule = "atLeast";
102
103
  }
103
104
  return Object.keys(out).length > 0 ? out : undefined;
104
105
  }
105
106
 
106
107
  function readIndent(node: XmlElementNode): ParagraphIndentation | undefined {
107
- const out: ParagraphIndentation = {};
108
+ const out: Mutable<ParagraphIndentation> = {};
108
109
  const left = readIntAttr(node, "w:left") ?? readIntAttr(node, "w:start");
109
110
  const right = readIntAttr(node, "w:right") ?? readIntAttr(node, "w:end");
110
111
  const firstLine = readIntAttr(node, "w:firstLine");
111
112
  const hanging = readIntAttr(node, "w:hanging");
112
- if (left !== undefined) out.left = left;
113
- if (right !== undefined) out.right = right;
114
- if (firstLine !== undefined) out.firstLine = firstLine;
113
+ if (left !== undefined) (out as Mutable<typeof out>).left = left;
114
+ if (right !== undefined) (out as Mutable<typeof out>).right = right;
115
+ if (firstLine !== undefined) (out as Mutable<typeof out>).firstLine = firstLine;
115
116
  if (hanging !== undefined) out.hanging = hanging;
116
117
  return Object.keys(out).length > 0 ? out : undefined;
117
118
  }
@@ -138,7 +139,7 @@ function readTabStops(node: XmlElementNode): TabStop[] | undefined {
138
139
  }
139
140
 
140
141
  function readBorders(node: XmlElementNode): ParagraphBorders | undefined {
141
- const out: ParagraphBorders = {};
142
+ const out: Mutable<ParagraphBorders> = {};
142
143
  const sides = ["top", "bottom", "left", "right", "between", "bar"] as const;
143
144
  for (const side of sides) {
144
145
  const child = findChildOptional(node, side);
@@ -205,7 +206,7 @@ function readShading(node: XmlElementNode): ParagraphShading | undefined {
205
206
  * text frames, drop-caps); extension attrs are rare in that corpus.
206
207
  */
207
208
  export function readFrameProperties(node: XmlElementNode): FrameProperties | undefined {
208
- const out: FrameProperties = {};
209
+ const out: Mutable<FrameProperties> = {};
209
210
  const width = readIntAttr(node, "w:w");
210
211
  if (width !== undefined) out.widthTwips = width;
211
212
  const height = readIntAttr(node, "w:h");
@@ -268,7 +269,7 @@ export function readParagraphProperties(
268
269
  ): CanonicalParagraphFormatting | undefined {
269
270
  if (!node) return undefined;
270
271
 
271
- const out: CanonicalParagraphFormatting = {};
272
+ const out: Mutable<CanonicalParagraphFormatting> = {};
272
273
 
273
274
  const spacingNode = findChildOptional(node, "spacing");
274
275
  if (spacingNode) {
@@ -1,4 +1,4 @@
1
- import type { PictureContent } from "../../model/canonical-document.ts";
1
+ import type { PictureContent, Mutable } from "../../model/canonical-document.ts";
2
2
  import {
3
3
  type XmlElementNode,
4
4
  findFirstChild,
@@ -100,21 +100,21 @@ export function parsePicture(graphicDataEl: XmlElementNode): PictureContent | nu
100
100
  const glowEl = effectLst ? findFirstChild(effectLst, "glow") : undefined;
101
101
  const glow = glowEl ? parseGlow(glowEl) : undefined;
102
102
 
103
- const result: PictureContent = { type: "picture", blipRef };
104
- if (isLinked) result.isLinked = true;
103
+ const result: Mutable<PictureContent> = { type: "picture", blipRef };
104
+ if (isLinked) (result as Mutable<typeof result>).isLinked = true;
105
105
  if (lum && (lum.bright !== undefined || lum.contrast !== undefined)) {
106
- result.lum = lum;
106
+ (result as Mutable<typeof result>).lum = lum;
107
107
  }
108
- if (srcRect) result.srcRect = srcRect;
109
- if (stretch !== undefined) result.stretch = stretch;
108
+ if (srcRect) (result as Mutable<typeof result>).srcRect = srcRect;
109
+ if (stretch !== undefined) (result as Mutable<typeof result>).stretch = stretch;
110
110
  if (tile !== undefined) result.tile = tile;
111
- if (rotation !== undefined) result.rotation = rotation;
112
- if (flipH !== undefined) result.flipH = flipH;
113
- if (flipV !== undefined) result.flipV = flipV;
114
- if (presetGeom) result.presetGeom = presetGeom;
115
- if (softEdgeRadius !== undefined) result.softEdgeRadius = softEdgeRadius;
116
- if (outerShadow) result.outerShadow = outerShadow;
117
- if (glow) result.glow = glow;
111
+ if (rotation !== undefined) (result as Mutable<typeof result>).rotation = rotation;
112
+ if (flipH !== undefined) (result as Mutable<typeof result>).flipH = flipH;
113
+ if (flipV !== undefined) (result as Mutable<typeof result>).flipV = flipV;
114
+ if (presetGeom) (result as Mutable<typeof result>).presetGeom = presetGeom;
115
+ if (softEdgeRadius !== undefined) (result as Mutable<typeof result>).softEdgeRadius = softEdgeRadius;
116
+ if (outerShadow) (result as Mutable<typeof result>).outerShadow = outerShadow;
117
+ if (glow) (result as Mutable<typeof result>).glow = glow;
118
118
  return result;
119
119
  }
120
120
 
@@ -172,13 +172,13 @@ function readTileAttrs(
172
172
  const v = tileEl.attributes[key];
173
173
  if (v !== undefined) {
174
174
  const n = parseInt(v, 10);
175
- if (Number.isFinite(n)) result[key] = n;
175
+ if (Number.isFinite(n)) (result as Mutable<typeof result>)[key] = n;
176
176
  }
177
177
  }
178
178
  const flip = tileEl.attributes.flip;
179
- if (flip === "x" || flip === "y" || flip === "xy") result.flip = flip;
179
+ if (flip === "x" || flip === "y" || flip === "xy") (result as Mutable<typeof result>).flip = flip;
180
180
  const algn = tileEl.attributes.algn;
181
- if (algn) result.algn = algn;
181
+ if (algn) (result as Mutable<typeof result>).algn = algn;
182
182
  return Object.keys(result).length > 0 ? result : {};
183
183
  }
184
184
 
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  CanonicalRunFormatting,
3
3
  ThemeFontSlot,
4
+ Mutable,
4
5
  } from "../../model/canonical-document.ts";
5
6
  import { findChildOptional, readIntVal, readOnOff, readStringAttr } from "./xml-attr-helpers.ts";
6
7
  import type { XmlElementNode } from "./xml-element.ts";
@@ -58,7 +59,7 @@ export function readRunProperties(
58
59
  ): CanonicalRunFormatting | undefined {
59
60
  if (!node) return undefined;
60
61
 
61
- const rPr: CanonicalRunFormatting = {};
62
+ const rPr: Mutable<CanonicalRunFormatting> = {};
62
63
 
63
64
  const bold = readOnOff(findChildOptional(node, "b"));
64
65
  if (bold !== undefined) rPr.bold = bold;
@@ -121,9 +122,9 @@ export function readRunProperties(
121
122
  const hAnsi = rFonts.attributes["w:hAnsi"] ?? rFonts.attributes.hAnsi;
122
123
  const eastAsia = rFonts.attributes["w:eastAsia"] ?? rFonts.attributes.eastAsia;
123
124
  const cs = rFonts.attributes["w:cs"] ?? rFonts.attributes.cs;
124
- if (ascii) rPr.fontFamilyAscii = ascii;
125
- if (hAnsi) rPr.fontFamilyHAnsi = hAnsi;
126
- if (eastAsia) rPr.fontFamilyEastAsia = eastAsia;
125
+ if (ascii) (rPr as Mutable<typeof rPr>).fontFamilyAscii = ascii;
126
+ if (hAnsi) (rPr as Mutable<typeof rPr>).fontFamilyHAnsi = hAnsi;
127
+ if (eastAsia) (rPr as Mutable<typeof rPr>).fontFamilyEastAsia = eastAsia;
127
128
  if (cs) rPr.fontFamilyCs = cs;
128
129
  const primary = ascii ?? hAnsi ?? eastAsia ?? cs;
129
130
  if (primary) rPr.fontFamily = primary;
@@ -150,9 +151,9 @@ export function readRunProperties(
150
151
  rFonts.attributes["w:csTheme"] ??
151
152
  rFonts.attributes.csTheme,
152
153
  );
153
- if (asciiTheme) rPr.asciiTheme = asciiTheme;
154
- if (hAnsiTheme) rPr.hAnsiTheme = hAnsiTheme;
155
- if (eastAsiaTheme) rPr.eastAsiaTheme = eastAsiaTheme;
154
+ if (asciiTheme) (rPr as Mutable<typeof rPr>).asciiTheme = asciiTheme;
155
+ if (hAnsiTheme) (rPr as Mutable<typeof rPr>).hAnsiTheme = hAnsiTheme;
156
+ if (eastAsiaTheme) (rPr as Mutable<typeof rPr>).eastAsiaTheme = eastAsiaTheme;
156
157
  if (csTheme) rPr.csTheme = csTheme;
157
158
  }
158
159
 
@@ -171,9 +172,9 @@ export function readRunProperties(
171
172
  color.attributes["w:themeTint"] ?? color.attributes["themeTint"];
172
173
  const shade =
173
174
  color.attributes["w:themeShade"] ?? color.attributes["themeShade"];
174
- if (val) rPr.colorHex = val;
175
- if (theme) rPr.colorThemeSlot = theme;
176
- if (tint) rPr.colorThemeTint = tint;
175
+ if (val) (rPr as Mutable<typeof rPr>).colorHex = val;
176
+ if (theme) (rPr as Mutable<typeof rPr>).colorThemeSlot = theme;
177
+ if (tint) (rPr as Mutable<typeof rPr>).colorThemeTint = tint;
177
178
  if (shade) rPr.colorThemeShade = shade;
178
179
  }
179
180
 
@@ -5,6 +5,7 @@ import type {
5
5
  DocumentSettings,
6
6
  FootnoteProperties,
7
7
  ThemeColorSlot,
8
+ Mutable,
8
9
  } from "../../model/canonical-document.ts";
9
10
  import { parseXml } from "./xml-parser.ts";
10
11
  import { localName } from "./xml-attr-helpers.ts";
@@ -160,7 +161,7 @@ function readFootnoteLikeProperties(
160
161
  ): FootnoteProperties | undefined {
161
162
  if (!element) return undefined;
162
163
 
163
- const result: FootnoteProperties = {};
164
+ const result: Mutable<FootnoteProperties> = {};
164
165
  for (const child of element.children) {
165
166
  if (child.type !== "element") continue;
166
167
  const name = localName(child.name);
@@ -14,6 +14,8 @@ import type {
14
14
  BlockNode,
15
15
  ShapeContent,
16
16
  TextBoxBodyProperties,
17
+ Mutable,
18
+ PreserveOnlyObjectSizing,
17
19
  } from "../../model/canonical-document.ts";
18
20
  import { parseFill } from "./parse-fill.ts";
19
21
  import {
@@ -46,11 +48,12 @@ export interface ParsedWpsShape {
46
48
  * shape-textbox paragraphs). Same shape + semantics as
47
49
  * `ShapeContent.txbxBlocks` on the drawing-frame path.
48
50
  */
49
- txbxBlocks?: ReadonlyArray<BlockNode>;
51
+ txbxBlocks?: BlockNode[];
50
52
  /** DrawML geometry preset, e.g. "rect", "roundRect". */
51
53
  geometry?: string;
52
54
  /** Original drawing XML for lossless round-trip export. */
53
55
  rawXml: string;
56
+ preserveOnlyObject?: PreserveOnlyObjectSizing;
54
57
  }
55
58
 
56
59
  export interface ParsedWordArt {
@@ -61,6 +64,7 @@ export interface ParsedWordArt {
61
64
  geometry?: string;
62
65
  /** Original drawing XML for lossless round-trip export. */
63
66
  rawXml: string;
67
+ preserveOnlyObject?: PreserveOnlyObjectSizing;
64
68
  }
65
69
 
66
70
  export interface ParsedVmlShape {
@@ -71,6 +75,7 @@ export interface ParsedVmlShape {
71
75
  text?: string;
72
76
  /** Original w:pict XML for lossless round-trip export. */
73
77
  rawXml: string;
78
+ preserveOnlyObject?: PreserveOnlyObjectSizing;
74
79
  }
75
80
 
76
81
  export type ParsedShape = ParsedWpsShape | ParsedWordArt | ParsedVmlShape;
@@ -112,6 +117,11 @@ export function parseShapeXml(
112
117
  text: text ?? "",
113
118
  ...(prst ? { geometry: prst } : {}),
114
119
  rawXml: drawingXml,
120
+ preserveOnlyObject: createDrawingPreserveOnlyObject(
121
+ drawingXml,
122
+ "wordart",
123
+ collectRelationshipIds(root),
124
+ ),
115
125
  };
116
126
  }
117
127
 
@@ -129,7 +139,7 @@ export function parseShapeXml(
129
139
  // content (CCEP "Copyright CCEP STRICTLY CONFIDENTIAL" footer band)
130
140
  // is reachable only via the `.text` summary string — L03 cascade +
131
141
  // L11 render can't walk runs/marks.
132
- let txbxBlocks: ReadonlyArray<BlockNode> | undefined;
142
+ let txbxBlocks: BlockNode[] | undefined;
133
143
  if (txbxContentXml && blockParser) {
134
144
  try {
135
145
  // The `blockParser` callback is supplied by parse-main-document.ts
@@ -142,7 +152,7 @@ export function parseShapeXml(
142
152
  // canonical; a structural `as unknown as BlockNode[]` preserves
143
153
  // type safety at every consumer site (L03 cascade, L11 render,
144
154
  // validator walk).
145
- txbxBlocks = blockParser(txbxContentXml) as unknown as ReadonlyArray<BlockNode>;
155
+ txbxBlocks = blockParser(txbxContentXml) as unknown as BlockNode[];
146
156
  } catch {
147
157
  txbxBlocks = undefined;
148
158
  }
@@ -157,6 +167,11 @@ export function parseShapeXml(
157
167
  ...(txbxBlocks && txbxBlocks.length > 0 ? { txbxBlocks } : {}),
158
168
  ...(prst ? { geometry: prst } : {}),
159
169
  rawXml: drawingXml,
170
+ preserveOnlyObject: createDrawingPreserveOnlyObject(
171
+ drawingXml,
172
+ "shape",
173
+ collectRelationshipIds(root),
174
+ ),
160
175
  };
161
176
  }
162
177
 
@@ -199,6 +214,11 @@ export function parseVmlXml(pictXml: string): ParsedVmlShape | null {
199
214
  ...(shapeType ? { shapeType } : {}),
200
215
  ...(text ? { text } : {}),
201
216
  rawXml: pictXml,
217
+ preserveOnlyObject: createVmlPreserveOnlyObject(
218
+ pictXml,
219
+ vmlNode,
220
+ collectRelationshipIds(root),
221
+ ),
202
222
  };
203
223
  }
204
224
 
@@ -271,7 +291,7 @@ export function parseShapeContent(
271
291
  const txbxContentXml = txbxContent ? extractRawXml(txbxContent) : undefined;
272
292
  const text = txbxContent ? extractAllText(txbxContent).trim() || undefined : undefined;
273
293
 
274
- let txbxBlocks: ReadonlyArray<BlockNode> | undefined;
294
+ let txbxBlocks: BlockNode[] | undefined;
275
295
  if (txbxContentXml && blockParser) {
276
296
  try {
277
297
  // See `TxbxBlockParser` doc above: runtime output is canonical
@@ -279,7 +299,7 @@ export function parseShapeContent(
279
299
  // footer fixture 2026-04-24). Cast at the assembly seam so
280
300
  // downstream consumers (L03, L11, validator) get canonical types
281
301
  // without local `as unknown` ceremony.
282
- txbxBlocks = blockParser(txbxContentXml) as unknown as ReadonlyArray<BlockNode>;
302
+ txbxBlocks = blockParser(txbxContentXml) as unknown as BlockNode[];
283
303
  } catch {
284
304
  // Preserve-only fallback: keep txbxContentXml for serialization; leave
285
305
  // txbxBlocks undefined so consumers know recursion did not succeed.
@@ -294,15 +314,15 @@ export function parseShapeContent(
294
314
  // roundRect text boxes + callout/ellipse shapes as non-text.
295
315
  const isTextBox = Boolean(txbxContent);
296
316
 
297
- const result: ShapeContent = { type: "shape", rawXml: drawingRawXml };
298
- if (geometry) result.geometry = geometry;
317
+ const result: Mutable<ShapeContent> = { type: "shape", rawXml: drawingRawXml };
318
+ if (geometry) (result as Mutable<typeof result>).geometry = geometry;
299
319
  if (text) result.text = text;
300
- if (fill) result.fill = fill;
301
- if (line) result.line = line;
302
- if (isTextBox) result.isTextBox = true;
303
- if (textBoxBody) result.textBoxBody = textBoxBody;
304
- if (txbxContentXml) result.txbxContentXml = txbxContentXml;
305
- if (txbxBlocks && txbxBlocks.length > 0) result.txbxBlocks = txbxBlocks;
320
+ if (fill) (result as Mutable<typeof result>).fill = fill;
321
+ if (line) (result as Mutable<typeof result>).line = line;
322
+ if (isTextBox) (result as Mutable<typeof result>).isTextBox = true;
323
+ if (textBoxBody) (result as Mutable<typeof result>).textBoxBody = textBoxBody;
324
+ if (txbxContentXml) (result as Mutable<typeof result>).txbxContentXml = txbxContentXml;
325
+ if (txbxBlocks && txbxBlocks.length > 0) (result as Mutable<typeof result>).txbxBlocks = txbxBlocks;
306
326
  return result;
307
327
  }
308
328
 
@@ -310,7 +330,7 @@ function readTextBoxBody(wsp: XmlElementNode): TextBoxBodyProperties | undefined
310
330
  const bodyPr = findFirstChild(wsp, "bodyPr");
311
331
  if (!bodyPr) return undefined;
312
332
 
313
- const result: TextBoxBodyProperties = {};
333
+ const result: Mutable<TextBoxBodyProperties> = {};
314
334
  const anchor = bodyPr.attributes.anchor;
315
335
  if (anchor === "t" || anchor === "ctr" || anchor === "b") {
316
336
  result.anchor = anchor;
@@ -319,9 +339,9 @@ function readTextBoxBody(wsp: XmlElementNode): TextBoxBodyProperties | undefined
319
339
  const top = readIntAttr(bodyPr, "tIns");
320
340
  const right = readIntAttr(bodyPr, "rIns");
321
341
  const bottom = readIntAttr(bodyPr, "bIns");
322
- if (left !== undefined) result.insetLeftEmu = left;
323
- if (top !== undefined) result.insetTopEmu = top;
324
- if (right !== undefined) result.insetRightEmu = right;
342
+ if (left !== undefined) (result as Mutable<typeof result>).insetLeftEmu = left;
343
+ if (top !== undefined) (result as Mutable<typeof result>).insetTopEmu = top;
344
+ if (right !== undefined) (result as Mutable<typeof result>).insetRightEmu = right;
325
345
  if (bottom !== undefined) result.insetBottomEmu = bottom;
326
346
 
327
347
  return Object.keys(result).length > 0 ? result : undefined;
@@ -360,3 +380,114 @@ function readIntAttr(node: XmlElementNode, attr: string): number | undefined {
360
380
  const parsed = parseInt(raw, 10);
361
381
  return Number.isFinite(parsed) ? parsed : undefined;
362
382
  }
383
+
384
+ function createDrawingPreserveOnlyObject(
385
+ rawXml: string,
386
+ fallbackHint: PreserveOnlyObjectSizing["fallbackHint"],
387
+ relationshipIds: string[],
388
+ ): PreserveOnlyObjectSizing {
389
+ const extentEmu = readDrawingExtentEmu(rawXml);
390
+ return {
391
+ sourceId: `drawing:${hashString(rawXml)}`,
392
+ display: rawXml.includes("<wp:anchor") ? "floating" : "inline",
393
+ ...(extentEmu ? { extentEmu } : {}),
394
+ fallbackHint,
395
+ ...(relationshipIds.length > 0 ? { relationshipIds } : {}),
396
+ };
397
+ }
398
+
399
+ function createVmlPreserveOnlyObject(
400
+ rawXml: string,
401
+ node: XmlElementNode,
402
+ relationshipIds: string[],
403
+ ): PreserveOnlyObjectSizing {
404
+ const extentEmu = readVmlExtentEmu(node.attributes.style);
405
+ return {
406
+ sourceId: `vml:${node.attributes.id ?? hashString(rawXml)}`,
407
+ display: "unknown",
408
+ ...(extentEmu ? { extentEmu } : {}),
409
+ fallbackHint: "vml-shape",
410
+ ...(relationshipIds.length > 0 ? { relationshipIds } : {}),
411
+ };
412
+ }
413
+
414
+ function readDrawingExtentEmu(
415
+ rawXml: string,
416
+ ): { widthEmu: number; heightEmu: number } | undefined {
417
+ const extentMatch = /<wp:extent\b[^>]*\bcx=["']([0-9]+)["'][^>]*\bcy=["']([0-9]+)["'][^>]*\/?>/u.exec(rawXml);
418
+ if (extentMatch) {
419
+ return {
420
+ widthEmu: Number.parseInt(extentMatch[1] ?? "0", 10),
421
+ heightEmu: Number.parseInt(extentMatch[2] ?? "0", 10),
422
+ };
423
+ }
424
+ const xfrmMatch = /<a:ext\b[^>]*\bcx=["']([0-9]+)["'][^>]*\bcy=["']([0-9]+)["'][^>]*\/?>/u.exec(rawXml);
425
+ if (!xfrmMatch) return undefined;
426
+ return {
427
+ widthEmu: Number.parseInt(xfrmMatch[1] ?? "0", 10),
428
+ heightEmu: Number.parseInt(xfrmMatch[2] ?? "0", 10),
429
+ };
430
+ }
431
+
432
+ function readVmlExtentEmu(
433
+ style: string | undefined,
434
+ ): { widthEmu: number; heightEmu: number } | undefined {
435
+ if (!style) return undefined;
436
+ const widthTwips = readCssLengthTwips(style, "width");
437
+ const heightTwips = readCssLengthTwips(style, "height");
438
+ if (widthTwips === undefined || heightTwips === undefined) return undefined;
439
+ return {
440
+ widthEmu: Math.round(widthTwips * 635),
441
+ heightEmu: Math.round(heightTwips * 635),
442
+ };
443
+ }
444
+
445
+ function readCssLengthTwips(style: string, property: "width" | "height"): number | undefined {
446
+ const match = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([0-9.]+)\\s*(pt|in|cm|mm|px)?`, "iu")
447
+ .exec(style);
448
+ if (!match) return undefined;
449
+ const value = Number.parseFloat(match[1] ?? "");
450
+ if (!Number.isFinite(value)) return undefined;
451
+ const unit = (match[2] ?? "pt").toLowerCase();
452
+ switch (unit) {
453
+ case "pt":
454
+ return Math.round(value * 20);
455
+ case "in":
456
+ return Math.round(value * 1440);
457
+ case "cm":
458
+ return Math.round(value * 567);
459
+ case "mm":
460
+ return Math.round(value * 56.7);
461
+ case "px":
462
+ return Math.round(value * 15);
463
+ default:
464
+ return undefined;
465
+ }
466
+ }
467
+
468
+ function collectRelationshipIds(node: XmlElementNode): string[] {
469
+ const ids = new Set<string>();
470
+ const visit = (current: XmlElementNode): void => {
471
+ const id =
472
+ current.attributes["r:id"] ??
473
+ current.attributes["r:embed"] ??
474
+ current.attributes.embed ??
475
+ current.attributes["r:link"] ??
476
+ current.attributes.link;
477
+ if (id) ids.add(id);
478
+ for (const child of current.children) {
479
+ if (child.type === "element") visit(child);
480
+ }
481
+ };
482
+ visit(node);
483
+ return [...ids].sort();
484
+ }
485
+
486
+ function hashString(value: string): string {
487
+ let hash = 2166136261;
488
+ for (let index = 0; index < value.length; index += 1) {
489
+ hash ^= value.charCodeAt(index);
490
+ hash = Math.imul(hash, 16777619);
491
+ }
492
+ return (hash >>> 0).toString(16).padStart(8, "0");
493
+ }
@@ -23,6 +23,7 @@ import type {
23
23
  TableStyleDefinition,
24
24
  TableStyleFormatting,
25
25
  TableWidth,
26
+ Mutable,
26
27
  } from "../../model/canonical-document.ts";
27
28
  import {
28
29
  readCellBorders,
@@ -372,8 +373,8 @@ function resolveStyleLinkReciprocals(
372
373
  );
373
374
  continue;
374
375
  }
375
- if (partner.linkedStyleId === undefined) {
376
- partner.linkedStyleId = style.styleId;
376
+ if ((partner as Mutable<typeof partner>).linkedStyleId === undefined) {
377
+ (partner as Mutable<typeof partner>).linkedStyleId = style.styleId;
377
378
  } else if (partner.linkedStyleId !== style.styleId) {
378
379
  diagnostics.push(
379
380
  `style ${label} "${style.styleId}" links to "${target}" but partner already links to "${partner.linkedStyleId}"; partner link retained`,
@@ -439,7 +440,7 @@ function readTableStyleFormatting(styleNode: XmlElementNode): TableStyleFormatti
439
440
  const cellProperties = findChildElementOptional(styleNode, "tcPr");
440
441
  const pPrNode = findChildElementOptional(styleNode, "pPr");
441
442
  const rPrNode = findChildElementOptional(styleNode, "rPr");
442
- const formatting: TableStyleFormatting = {};
443
+ const formatting: Mutable<TableStyleFormatting> = {};
443
444
 
444
445
  const paragraphProperties = readParagraphProperties(pPrNode);
445
446
  if (paragraphProperties) formatting.paragraphProperties = paragraphProperties;
@@ -454,11 +455,11 @@ function readTableStyleFormatting(styleNode: XmlElementNode): TableStyleFormatti
454
455
  const cellMargins = readTableCellMargins(tableProperties);
455
456
  const tblLook = readTableLook(tableProperties);
456
457
 
457
- if (width) table.width = width as TableWidth;
458
- if (alignment) table.alignment = alignment;
459
- if (borders) table.borders = borders as TableBorders;
460
- if (cellMargins) table.cellMargins = cellMargins as TableCellMargins;
461
- if (tblLook) table.tblLook = tblLook as TableLook;
458
+ if (width) (table as Mutable<typeof table>).width = width as TableWidth;
459
+ if (alignment) (table as Mutable<typeof table>).alignment = alignment;
460
+ if (borders) (table as Mutable<typeof table>).borders = borders as TableBorders;
461
+ if (cellMargins) (table as Mutable<typeof table>).cellMargins = cellMargins as TableCellMargins;
462
+ if (tblLook) (table as Mutable<typeof table>).tblLook = tblLook as TableLook;
462
463
 
463
464
  if (Object.keys(table).length > 0) {
464
465
  formatting.table = table;
@@ -471,9 +472,9 @@ function readTableStyleFormatting(styleNode: XmlElementNode): TableStyleFormatti
471
472
  const heightRule = readRowHeightRule(rowProperties);
472
473
  const isHeader = readRowIsHeader(rowProperties);
473
474
 
474
- if (height !== undefined) row.height = height;
475
- if (heightRule) row.heightRule = heightRule;
476
- if (isHeader !== undefined) row.isHeader = isHeader;
475
+ if (height !== undefined) (row as Mutable<typeof row>).height = height;
476
+ if (heightRule) (row as Mutable<typeof row>).heightRule = heightRule;
477
+ if (isHeader !== undefined) (row as Mutable<typeof row>).isHeader = isHeader;
477
478
 
478
479
  if (Object.keys(row).length > 0) {
479
480
  formatting.row = row;
@@ -487,10 +488,10 @@ function readTableStyleFormatting(styleNode: XmlElementNode): TableStyleFormatti
487
488
  const shading = readCellShading(cellProperties);
488
489
  const verticalAlign = readCellVerticalAlign(cellProperties);
489
490
 
490
- if (width) cell.width = width as TableWidth;
491
- if (borders) cell.borders = borders as TableCellBorders;
492
- if (shading) cell.shading = shading as CellShading;
493
- if (verticalAlign) cell.verticalAlign = verticalAlign;
491
+ if (width) (cell as Mutable<typeof cell>).width = width as TableWidth;
492
+ if (borders) (cell as Mutable<typeof cell>).borders = borders as TableCellBorders;
493
+ if (shading) (cell as Mutable<typeof cell>).shading = shading as CellShading;
494
+ if (verticalAlign) (cell as Mutable<typeof cell>).verticalAlign = verticalAlign;
494
495
 
495
496
  if (Object.keys(cell).length > 0) {
496
497
  formatting.cell = cell;
@@ -590,4 +591,3 @@ function findChildElementOptional(
590
591
  );
591
592
  }
592
593
 
593
-
@@ -5,6 +5,7 @@ import type {
5
5
  ResolvedTheme,
6
6
  CanonicalTheme,
7
7
  ClrSchemeMapping,
8
+ Mutable,
8
9
  } from "../../model/canonical-document.ts";
9
10
  import type { XmlElementNode } from "./xml-element.ts";
10
11
  import { parseXml } from "./xml-parser.ts";
@@ -61,7 +62,7 @@ export function parseThemeXml(xml: string): ThemeDefinition {
61
62
  findChildElementOptional(themeElements, "fontScheme"),
62
63
  );
63
64
 
64
- const result: ThemeDefinition = {};
65
+ const result: Mutable<ThemeDefinition> = {};
65
66
  if (themeName) {
66
67
  result.name = themeName;
67
68
  }
@@ -79,7 +80,7 @@ export function parseThemeXml(xml: string): ThemeDefinition {
79
80
  * Resolve a ThemeDefinition into flattened runtime theme inputs.
80
81
  * Maps OOXML theme slot names to CSS-usable color values and font families.
81
82
  */
82
- export function resolveTheme(theme: ThemeDefinition): ResolvedTheme {
83
+ export function resolveTheme(theme: Mutable<ThemeDefinition>): ResolvedTheme {
83
84
  const colors: Record<string, string> = {};
84
85
 
85
86
  if (theme.colorScheme?.colors) {
@@ -142,7 +143,7 @@ export const DEFAULT_CLR_SCHEME_MAPPING: ClrSchemeMapping = Object.freeze({
142
143
  * CLAUDE.md §3.
143
144
  */
144
145
  export function materializeCanonicalTheme(
145
- theme: ThemeDefinition,
146
+ theme: Mutable<ThemeDefinition>,
146
147
  clrMap: ClrSchemeMapping,
147
148
  ): CanonicalTheme {
148
149
  const clrScheme: ThemeColorScheme = theme.colorScheme ?? { name: "", colors: {} };
@@ -240,7 +241,7 @@ function parseFontScheme(
240
241
  ? extractFontTypeface(minorFontElement)
241
242
  : undefined;
242
243
 
243
- const result: ThemeFontScheme = { name: schemeName };
244
+ const result: Mutable<ThemeFontScheme> = { name: schemeName };
244
245
  if (majorFont) {
245
246
  result.majorFont = majorFont;
246
247
  }