@beyondwork/docx-react-component 1.0.59 → 1.0.61

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 (46) hide show
  1. package/package.json +33 -44
  2. package/src/api/public-types.ts +43 -0
  3. package/src/core/state/editor-state.ts +2 -0
  4. package/src/io/docx-session.ts +167 -8
  5. package/src/io/export/serialize-footnotes.ts +36 -5
  6. package/src/io/export/serialize-headers-footers.ts +7 -0
  7. package/src/io/export/serialize-main-document.ts +25 -18
  8. package/src/io/export/serialize-paragraph-formatting.ts +6 -0
  9. package/src/io/export/serialize-settings.ts +130 -3
  10. package/src/io/normalize/normalize-text.ts +8 -4
  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-paragraph-formatting.ts +25 -1
  15. package/src/io/ooxml/parse-settings.ts +91 -1
  16. package/src/io/ooxml/workflow-payload.ts +6 -1
  17. package/src/model/canonical-document.ts +36 -2
  18. package/src/model/snapshot.ts +2 -0
  19. package/src/runtime/diagnostics/build-diagnostic.ts +2 -0
  20. package/src/runtime/diagnostics/code-metadata-table.ts +9 -0
  21. package/src/runtime/document-runtime.ts +770 -21
  22. package/src/runtime/footnote-resolver.ts +32 -8
  23. package/src/runtime/layout/layout-engine-version.ts +7 -1
  24. package/src/runtime/layout/measurement-backend-canvas.ts +1 -1
  25. package/src/runtime/layout/measurement-backend-empirical.ts +1 -1
  26. package/src/runtime/layout/paginated-layout-engine.ts +41 -8
  27. package/src/runtime/layout/resolved-formatting-document.ts +11 -9
  28. package/src/runtime/layout/resolved-formatting-state.ts +4 -0
  29. package/src/runtime/numbering-prefix.ts +26 -2
  30. package/src/runtime/query-scopes.ts +103 -2
  31. package/src/runtime/surface-projection.ts +75 -14
  32. package/src/runtime/table-schema.ts +26 -0
  33. package/src/ui/WordReviewEditor.tsx +25 -0
  34. package/src/ui/editor-runtime-boundary.ts +1 -0
  35. package/src/ui/editor-shell-view.tsx +8 -0
  36. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +514 -0
  37. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -0
  38. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +55 -6
  39. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  40. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +4 -0
  41. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -1
  42. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +16 -0
  43. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +319 -0
  44. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +248 -0
  45. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +4 -0
  46. package/src/ui-tailwind/tw-review-workspace.tsx +54 -3
@@ -62,6 +62,7 @@
62
62
  */
63
63
 
64
64
  import type {
65
+ ClrSchemeMappingSlot,
65
66
  CompatSetting,
66
67
  DocumentSettings,
67
68
  } from "../../model/canonical-document.ts";
@@ -81,6 +82,21 @@ export const WORD_SETTINGS_CONTENT_TYPE =
81
82
  const WORDPROCESSINGML_2006_MAIN_NS =
82
83
  "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
83
84
 
85
+ const CLRSCHEME_MAPPING_EMIT_ORDER: readonly ClrSchemeMappingSlot[] = [
86
+ "bg1",
87
+ "t1",
88
+ "bg2",
89
+ "t2",
90
+ "accent1",
91
+ "accent2",
92
+ "accent3",
93
+ "accent4",
94
+ "accent5",
95
+ "accent6",
96
+ "hlink",
97
+ "followedHyperlink",
98
+ ];
99
+
84
100
  /**
85
101
  * Render a complete `<w:settings>` XML document from canonical
86
102
  * `DocumentSettings`. The output is the standard XML declaration plus a
@@ -89,9 +105,13 @@ const WORDPROCESSINGML_2006_MAIN_NS =
89
105
  * Emit order is OOXML-schema-friendly to maximize Word's tolerance:
90
106
  * 1. <w:evenAndOddHeaders>
91
107
  * 2. <w:zoom>
92
- * 3. root-level compat-adjacent flags (e.g. <w:doNotEmbedSmartTags/>)
93
- * 4. <w:compat> wrapping flags then compatSetting triples
94
- * 5. <w:themeFontLang>
108
+ * 3. <w:defaultTabStop>
109
+ * 4. <w:footnotePr>
110
+ * 5. <w:endnotePr>
111
+ * 6. root-level compat-adjacent flags (e.g. <w:doNotEmbedSmartTags/>)
112
+ * 7. <w:compat> wrapping flags then compatSetting triples
113
+ * 8. <w:themeFontLang>
114
+ * 9. <w:clrSchemeMapping>
95
115
  *
96
116
  * Insertion order of `compatSettings` array entries and `compatFlags` /
97
117
  * `rootCompatFlags` / `themeFontLang` keys is preserved so a byte-stable
@@ -112,9 +132,13 @@ function synthesizeSettingsXml(settings: DocumentSettings): string {
112
132
  const parts: string[] = [];
113
133
  parts.push(emitEvenAndOddHeaders(settings));
114
134
  parts.push(emitZoom(settings));
135
+ parts.push(emitDefaultTabStop(settings));
136
+ parts.push(emitFootnoteLikeProperties("w:footnotePr", settings.footnotePr));
137
+ parts.push(emitFootnoteLikeProperties("w:endnotePr", settings.endnotePr));
115
138
  parts.push(emitRootCompatFlags(settings));
116
139
  parts.push(emitCompatBlock(settings));
117
140
  parts.push(emitThemeFontLang(settings));
141
+ parts.push(emitClrSchemeMapping(settings));
118
142
 
119
143
  const body = parts.filter((p) => p.length > 0).join("");
120
144
  return [
@@ -195,6 +219,24 @@ function graftSettingsXml(
195
219
  ) {
196
220
  appendedParts.push(emitZoom(settings));
197
221
  }
222
+ if (
223
+ !emittedTopLevel.has("defaultTabStop") &&
224
+ settings.defaultTabStop !== undefined
225
+ ) {
226
+ appendedParts.push(emitDefaultTabStop(settings));
227
+ }
228
+ if (
229
+ !emittedTopLevel.has("footnotePr") &&
230
+ settings.footnotePr !== undefined
231
+ ) {
232
+ appendedParts.push(emitFootnoteLikeProperties("w:footnotePr", settings.footnotePr));
233
+ }
234
+ if (
235
+ !emittedTopLevel.has("endnotePr") &&
236
+ settings.endnotePr !== undefined
237
+ ) {
238
+ appendedParts.push(emitFootnoteLikeProperties("w:endnotePr", settings.endnotePr));
239
+ }
198
240
  // Any rootCompatFlags entries that didn't have a source counterpart.
199
241
  for (const [name, value] of pendingRootFlags) {
200
242
  appendedParts.push(emitOnOffElement(name, value));
@@ -209,6 +251,12 @@ function graftSettingsXml(
209
251
  ) {
210
252
  appendedParts.push(emitThemeFontLang(settings));
211
253
  }
254
+ if (
255
+ !emittedTopLevel.has("clrSchemeMapping") &&
256
+ settings.clrSchemeMapping !== undefined
257
+ ) {
258
+ appendedParts.push(emitClrSchemeMapping(settings));
259
+ }
212
260
 
213
261
  return (
214
262
  blueprint.prelude +
@@ -229,8 +277,12 @@ function graftSettingsXml(
229
277
  const MODELLED_TOP_LEVEL_NAMES: ReadonlySet<string> = new Set([
230
278
  "evenAndOddHeaders",
231
279
  "zoom",
280
+ "defaultTabStop",
281
+ "footnotePr",
282
+ "endnotePr",
232
283
  "compat",
233
284
  "themeFontLang",
285
+ "clrSchemeMapping",
234
286
  ]);
235
287
 
236
288
  type ChildReplacement =
@@ -282,6 +334,36 @@ function computeChildReplacement(
282
334
  const xml = emitZoom(settings);
283
335
  return xml.length > 0 ? { kind: "replace", xml } : { kind: "drop" };
284
336
  }
337
+ case "defaultTabStop": {
338
+ if (
339
+ emitDefaultTabStop(parseModelledChild(child.rawXml)) ===
340
+ emitDefaultTabStop(settings)
341
+ ) {
342
+ return { kind: "keep" };
343
+ }
344
+ const xml = emitDefaultTabStop(settings);
345
+ return xml.length > 0 ? { kind: "replace", xml } : { kind: "drop" };
346
+ }
347
+ case "footnotePr": {
348
+ if (
349
+ emitFootnoteLikeProperties("w:footnotePr", parseModelledChild(child.rawXml).footnotePr) ===
350
+ emitFootnoteLikeProperties("w:footnotePr", settings.footnotePr)
351
+ ) {
352
+ return { kind: "keep" };
353
+ }
354
+ const xml = emitFootnoteLikeProperties("w:footnotePr", settings.footnotePr);
355
+ return xml.length > 0 ? { kind: "replace", xml } : { kind: "drop" };
356
+ }
357
+ case "endnotePr": {
358
+ if (
359
+ emitFootnoteLikeProperties("w:endnotePr", parseModelledChild(child.rawXml).endnotePr) ===
360
+ emitFootnoteLikeProperties("w:endnotePr", settings.endnotePr)
361
+ ) {
362
+ return { kind: "keep" };
363
+ }
364
+ const xml = emitFootnoteLikeProperties("w:endnotePr", settings.endnotePr);
365
+ return xml.length > 0 ? { kind: "replace", xml } : { kind: "drop" };
366
+ }
285
367
  case "compat": {
286
368
  if (
287
369
  emitCompatBlock(parseModelledChild(child.rawXml)) ===
@@ -302,6 +384,16 @@ function computeChildReplacement(
302
384
  const xml = emitThemeFontLang(settings);
303
385
  return xml.length > 0 ? { kind: "replace", xml } : { kind: "drop" };
304
386
  }
387
+ case "clrSchemeMapping": {
388
+ if (
389
+ emitClrSchemeMapping(parseModelledChild(child.rawXml)) ===
390
+ emitClrSchemeMapping(settings)
391
+ ) {
392
+ return { kind: "keep" };
393
+ }
394
+ const xml = emitClrSchemeMapping(settings);
395
+ return xml.length > 0 ? { kind: "replace", xml } : { kind: "drop" };
396
+ }
305
397
  }
306
398
  // Root compat flag?
307
399
  if (ROOT_COMPAT_FLAG_NAMES.has(child.localName)) {
@@ -361,6 +453,28 @@ function emitZoom(settings: DocumentSettings): string {
361
453
  return "";
362
454
  }
363
455
 
456
+ function emitDefaultTabStop(settings: DocumentSettings): string {
457
+ const { defaultTabStop } = settings;
458
+ if (defaultTabStop === undefined || !Number.isFinite(defaultTabStop)) return "";
459
+ return `<w:defaultTabStop w:val="${Math.round(defaultTabStop)}"/>`;
460
+ }
461
+
462
+ function emitFootnoteLikeProperties(
463
+ elementName: "w:footnotePr" | "w:endnotePr",
464
+ props: DocumentSettings["footnotePr"] | DocumentSettings["endnotePr"],
465
+ ): string {
466
+ if (!props) return "";
467
+ const parts: string[] = [];
468
+ if (props.pos) parts.push(`<w:pos w:val="${escapeXmlAttribute(props.pos)}"/>`);
469
+ if (props.numFmt) parts.push(`<w:numFmt w:val="${escapeXmlAttribute(props.numFmt)}"/>`);
470
+ if (props.numStart !== undefined && Number.isFinite(props.numStart)) {
471
+ parts.push(`<w:numStart w:val="${Math.round(props.numStart)}"/>`);
472
+ }
473
+ if (props.numRestart) parts.push(`<w:numRestart w:val="${escapeXmlAttribute(props.numRestart)}"/>`);
474
+ if (parts.length === 0) return "";
475
+ return `<${elementName}>${parts.join("")}</${elementName}>`;
476
+ }
477
+
364
478
  function emitRootCompatFlags(settings: DocumentSettings): string {
365
479
  const flags = settings.rootCompatFlags;
366
480
  if (!flags) return "";
@@ -410,6 +524,19 @@ function emitThemeFontLang(settings: DocumentSettings): string {
410
524
  return `<w:themeFontLang${attrs}/>`;
411
525
  }
412
526
 
527
+ function emitClrSchemeMapping(settings: DocumentSettings): string {
528
+ const mapping = settings.clrSchemeMapping;
529
+ if (!mapping) return "";
530
+ const attrs = CLRSCHEME_MAPPING_EMIT_ORDER
531
+ .map((slot) => {
532
+ const value = mapping[slot];
533
+ return value ? ` w:${slot}="${escapeXmlAttribute(value)}"` : "";
534
+ })
535
+ .join("");
536
+ if (attrs.length === 0) return "";
537
+ return `<w:clrSchemeMapping${attrs}/>`;
538
+ }
539
+
413
540
  /**
414
541
  * Emit a ST_OnOff element. true → bare self-closing tag; false → explicit
415
542
  * `w:val="false"` so the parser doesn't infer the default-true. Symmetric
@@ -260,13 +260,17 @@ function normalizeParagraph(
260
260
  : {}),
261
261
  ...(paragraph.indentation ? { indentation: paragraph.indentation } : {}),
262
262
  ...(paragraph.tabStops && paragraph.tabStops.length > 0 ? { tabStops: paragraph.tabStops } : {}),
263
- ...(paragraph.keepNext ? { keepNext: paragraph.keepNext } : {}),
264
- ...(paragraph.keepLines ? { keepLines: paragraph.keepLines } : {}),
263
+ ...(paragraph.keepNext !== undefined ? { keepNext: paragraph.keepNext } : {}),
264
+ ...(paragraph.keepLines !== undefined ? { keepLines: paragraph.keepLines } : {}),
265
265
  ...(paragraph.outlineLevel !== undefined ? { outlineLevel: paragraph.outlineLevel } : {}),
266
- ...(paragraph.pageBreakBefore ? { pageBreakBefore: paragraph.pageBreakBefore } : {}),
267
- ...(paragraph.bidi ? { bidi: paragraph.bidi } : {}),
266
+ ...(paragraph.pageBreakBefore !== undefined ? { pageBreakBefore: paragraph.pageBreakBefore } : {}),
267
+ ...(paragraph.widowControl !== undefined ? { widowControl: paragraph.widowControl } : {}),
268
+ ...(paragraph.bidi !== undefined ? { bidi: paragraph.bidi } : {}),
268
269
  ...(paragraph.borders ? { borders: paragraph.borders } : {}),
269
270
  ...(paragraph.shading ? { shading: paragraph.shading } : {}),
271
+ ...(paragraph.suppressLineNumbers !== undefined
272
+ ? { suppressLineNumbers: paragraph.suppressLineNumbers }
273
+ : {}),
270
274
  // A.7: preserve w14:paraId / w14:textId across import → export so
271
275
  // downstream tools that diff documents by paragraph id stay stable.
272
276
  ...(paragraph.wordExtensionIds
@@ -154,9 +154,13 @@ export function parseEndnotesXml(
154
154
  }
155
155
  }
156
156
 
157
+ const endnoteSeparators = parseFootnoteSeparators(xml);
158
+
157
159
  return {
158
160
  footnotes: existing?.footnotes ?? {},
159
161
  endnotes,
162
+ ...(existing?.footnoteSeparators ? { footnoteSeparators: existing.footnoteSeparators } : {}),
163
+ ...(Object.keys(endnoteSeparators).length > 0 ? { endnoteSeparators } : {}),
160
164
  };
161
165
  }
162
166
 
@@ -174,7 +178,9 @@ export function parseFootnoteSeparators(xml: string): FootnoteSeparators {
174
178
  if (!containerEl) return {};
175
179
 
176
180
  let separatorContent: string | undefined;
181
+ let separatorParagraphXml: string | undefined;
177
182
  let continuationSeparatorContent: string | undefined;
183
+ let continuationSeparatorParagraphXml: string | undefined;
178
184
 
179
185
  for (const child of containerEl.children) {
180
186
  if (child.type !== "element") continue;
@@ -186,6 +192,7 @@ export function parseFootnoteSeparators(xml: string): FootnoteSeparators {
186
192
 
187
193
  const paraEl = findChildElementOptional(child, "p");
188
194
  if (!paraEl) continue;
195
+ const paragraphXml = serializeElementToXml(paraEl);
189
196
 
190
197
  const runXml = paraEl.children
191
198
  .filter((c): c is XmlElementNode => c.type === "element" && localName(c.name) === "r")
@@ -194,14 +201,18 @@ export function parseFootnoteSeparators(xml: string): FootnoteSeparators {
194
201
 
195
202
  if (rawType === "separator") {
196
203
  separatorContent = runXml;
204
+ separatorParagraphXml = paragraphXml;
197
205
  } else {
198
206
  continuationSeparatorContent = runXml;
207
+ continuationSeparatorParagraphXml = paragraphXml;
199
208
  }
200
209
  }
201
210
 
202
211
  return {
203
212
  ...(separatorContent !== undefined ? { separatorContent } : {}),
213
+ ...(separatorParagraphXml !== undefined ? { separatorParagraphXml } : {}),
204
214
  ...(continuationSeparatorContent !== undefined ? { continuationSeparatorContent } : {}),
215
+ ...(continuationSeparatorParagraphXml !== undefined ? { continuationSeparatorParagraphXml } : {}),
205
216
  };
206
217
  }
207
218
 
@@ -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