@beyondwork/docx-react-component 1.0.56 → 1.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +157 -0
  3. package/src/compare/diff-engine.ts +3 -0
  4. package/src/core/commands/formatting-commands.ts +1 -0
  5. package/src/core/commands/index.ts +17 -11
  6. package/src/core/selection/mapping.ts +18 -1
  7. package/src/core/selection/review-anchors.ts +29 -18
  8. package/src/io/chart-preview-resolver.ts +175 -41
  9. package/src/io/docx-session.ts +57 -2
  10. package/src/io/export/serialize-main-document.ts +82 -0
  11. package/src/io/export/serialize-styles.ts +61 -3
  12. package/src/io/export/table-properties-xml.ts +19 -4
  13. package/src/io/normalize/normalize-text.ts +33 -0
  14. package/src/io/ooxml/parse-anchor.ts +182 -0
  15. package/src/io/ooxml/parse-drawing.ts +319 -0
  16. package/src/io/ooxml/parse-fields.ts +115 -2
  17. package/src/io/ooxml/parse-fill.ts +215 -0
  18. package/src/io/ooxml/parse-font-table.ts +190 -0
  19. package/src/io/ooxml/parse-footnotes.ts +52 -1
  20. package/src/io/ooxml/parse-main-document.ts +241 -1
  21. package/src/io/ooxml/parse-numbering.ts +96 -0
  22. package/src/io/ooxml/parse-picture.ts +107 -0
  23. package/src/io/ooxml/parse-settings.ts +34 -0
  24. package/src/io/ooxml/parse-shapes.ts +87 -0
  25. package/src/io/ooxml/parse-solid-fill.ts +11 -0
  26. package/src/io/ooxml/parse-styles.ts +74 -1
  27. package/src/io/ooxml/parse-theme.ts +60 -0
  28. package/src/io/paste/html-clipboard.ts +449 -0
  29. package/src/io/paste/word-clipboard.ts +5 -1
  30. package/src/legal/_document-root.ts +26 -0
  31. package/src/legal/bookmarks.ts +4 -3
  32. package/src/legal/cross-references.ts +3 -2
  33. package/src/legal/defined-terms.ts +2 -1
  34. package/src/legal/signature-blocks.ts +2 -1
  35. package/src/model/canonical-document.ts +415 -3
  36. package/src/runtime/chart/chart-model-store.ts +73 -10
  37. package/src/runtime/document-runtime.ts +693 -41
  38. package/src/runtime/edit-ops/index.ts +129 -0
  39. package/src/runtime/event-refresh-hints.ts +7 -0
  40. package/src/runtime/field-resolver.ts +341 -0
  41. package/src/runtime/footnote-resolver.ts +55 -0
  42. package/src/runtime/hyperlink-color-resolver.ts +13 -10
  43. package/src/runtime/object-grab/index.ts +51 -0
  44. package/src/runtime/paragraph-style-resolver.ts +105 -0
  45. package/src/runtime/resolved-numbering-geometry.ts +12 -0
  46. package/src/runtime/selection/cursor-ops.ts +186 -15
  47. package/src/runtime/selection/index.ts +17 -1
  48. package/src/runtime/structure-ops/index.ts +77 -0
  49. package/src/runtime/styles-cascade.ts +33 -0
  50. package/src/runtime/surface-projection.ts +186 -12
  51. package/src/runtime/theme-color-resolver.ts +189 -44
  52. package/src/runtime/units.ts +46 -0
  53. package/src/runtime/view-state.ts +13 -2
  54. package/src/ui/WordReviewEditor.tsx +168 -10
  55. package/src/ui/editor-runtime-boundary.ts +94 -1
  56. package/src/ui/editor-shell-view.tsx +1 -1
  57. package/src/ui/runtime-shortcut-dispatch.ts +17 -3
  58. package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
  59. package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
  60. package/src/ui-tailwind/chart/render/area.tsx +22 -4
  61. package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
  62. package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
  63. package/src/ui-tailwind/chart/render/combo.tsx +37 -4
  64. package/src/ui-tailwind/chart/render/line.tsx +28 -5
  65. package/src/ui-tailwind/chart/render/pie.tsx +36 -16
  66. package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
  67. package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
  68. package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
  69. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
  70. package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
  72. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
  73. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
  76. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
  77. package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
  78. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -0
  79. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
  80. package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
  81. package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
  82. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
  83. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
  85. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
  86. package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
  87. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
  88. package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
  89. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
  90. package/src/ui-tailwind/editor-surface/pm-schema.ts +188 -11
  91. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +28 -2
  92. package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
  93. package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
  94. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
  95. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
  96. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
  97. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
  98. package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
  99. package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
  100. package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
  101. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
  102. package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
  103. package/src/ui-tailwind/theme/editor-theme.css +1 -0
  104. package/src/ui-tailwind/theme/tokens.css +6 -0
  105. package/src/ui-tailwind/theme/tokens.ts +10 -0
  106. package/src/validation/compatibility-engine.ts +2 -0
  107. package/src/validation/docx-comment-proof.ts +12 -3
@@ -14,6 +14,7 @@ import type {
14
14
  TableIndent,
15
15
  TableLook,
16
16
  TableWidth,
17
+ FootnoteProperties,
17
18
  SectionProperties,
18
19
  PageSize,
19
20
  PageMargins,
@@ -24,6 +25,8 @@ import type {
24
25
  SectionDocumentGrid,
25
26
  SectionLineNumbering,
26
27
  SectionPageBorders,
28
+ DrawingFrameNode,
29
+ UnknownPropertyChild,
27
30
  } from "../../model/canonical-document.ts";
28
31
  import type { OpcRelationship } from "./part-manifest.ts";
29
32
  import { SCOPE_MARKER_BOOKMARK_PREFIX } from "./parse-scope-markers.ts";
@@ -34,6 +37,7 @@ import {
34
37
  import { toCanonicalNumberingInstanceId } from "./parse-numbering.ts";
35
38
  import { parseComplexContentXml, type ChartPartLookup } from "./parse-complex-content.ts";
36
39
  import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
40
+ import { parseDrawingFrame } from "./parse-drawing.ts";
37
41
  import { classifyFieldInstruction } from "./parse-fields.ts";
38
42
  import { resolveHighlightColor } from "./highlight-colors.ts";
39
43
  import {
@@ -83,7 +87,9 @@ import {
83
87
  const SECT_PR_MODELLED_CHILDREN: ReadonlySet<string> = new Set([
84
88
  "cols",
85
89
  "docGrid",
90
+ "endnotePr",
86
91
  "footerReference",
92
+ "footnotePr",
87
93
  "headerReference",
88
94
  "lnNumType",
89
95
  "pgBorders",
@@ -99,6 +105,111 @@ const SECT_PR_GRAB_BAG_DESCRIPTOR: PropertyGrabBagDescriptor = {
99
105
  modelledChildAttributes: new Map(),
100
106
  };
101
107
 
108
+ // Phase 7 Slice A — typed grab-bag descriptors for tblPr / trPr / tcPr.
109
+ // Mirrors what table-properties-xml.ts's TABLE/ROW/CELL_PROPERTY_STRIP_SPEC
110
+ // declared as "modelled" elements on the serializer side. Anything outside
111
+ // this set is captured as `unknownPropertyChildren` and re-emitted verbatim.
112
+ const TBL_PR_MODELLED_CHILDREN: ReadonlySet<string> = new Set([
113
+ "tblStyle", "tblW", "jc", "tblBorders", "tblCellMar",
114
+ "tblLook", "tblInd", "tblLayout", "tblCellSpacing",
115
+ "tblCaption", "tblDescription", "bidiVisual", "tblpPr", "tblOverlap",
116
+ ]);
117
+ const TR_PR_MODELLED_CHILDREN: ReadonlySet<string> = new Set([
118
+ "gridBefore", "wBefore", "gridAfter", "wAfter",
119
+ "trHeight", "tblHeader", "cantSplit", "jc", "cnfStyle",
120
+ ]);
121
+ const TC_PR_MODELLED_CHILDREN: ReadonlySet<string> = new Set([
122
+ "tcW", "gridSpan", "vMerge", "tcBorders", "tcMar", "shd",
123
+ "vAlign", "textDirection", "noWrap", "tcFitText", "cnfStyle",
124
+ ]);
125
+ // Slice B — modelled attributes per modelled table-container child.
126
+ const TABLE_WIDTH_ATTRS = new Set(["w:w", "w:type"]);
127
+ // TABLE_BORDER_ATTRS: the per-side attrs for tblBorders/tcBorders children
128
+ // ("w:val", "w:sz", "w:space", "w:color", "w:themeColor", ...) would populate
129
+ // the tblPr/tcPr descriptors if nested-side capture were in scope.
130
+ // Deferred: nested-side descriptors need a separate pass; see B.5 lane doc.
131
+ const TABLE_SHD_ATTRS = new Set([
132
+ "w:val", "w:fill", "w:color",
133
+ "w:themeColor", "w:themeFill", "w:themeShade", "w:themeTint",
134
+ "w:themeFillShade", "w:themeFillTint",
135
+ ]);
136
+ const TBL_PR_MODELLED_CHILD_ATTRIBUTES = new Map<string, ReadonlySet<string>>([
137
+ ["tblStyle", new Set(["w:val"])],
138
+ ["tblW", TABLE_WIDTH_ATTRS],
139
+ ["jc", new Set(["w:val"])],
140
+ ["tblLook", new Set([
141
+ "w:val", "w:firstRow", "w:lastRow", "w:firstColumn",
142
+ "w:lastColumn", "w:noHBand", "w:noVBand",
143
+ ])],
144
+ ["tblInd", TABLE_WIDTH_ATTRS],
145
+ ["tblLayout", new Set(["w:type"])],
146
+ ["tblCellSpacing", TABLE_WIDTH_ATTRS],
147
+ ["tblCaption", new Set(["w:val"])],
148
+ ["tblDescription", new Set(["w:val"])],
149
+ ["bidiVisual", new Set(["w:val"])],
150
+ ["tblpPr", new Set([
151
+ "w:leftFromText", "w:rightFromText", "w:topFromText", "w:bottomFromText",
152
+ "w:vertAnchor", "w:horzAnchor",
153
+ "w:tblpX", "w:tblpXSpec", "w:tblpY", "w:tblpYSpec",
154
+ ])],
155
+ ["tblOverlap", new Set(["w:val"])],
156
+ ]);
157
+ const TR_PR_MODELLED_CHILD_ATTRIBUTES = new Map<string, ReadonlySet<string>>([
158
+ ["gridBefore", new Set(["w:val"])],
159
+ ["gridAfter", new Set(["w:val"])],
160
+ ["wBefore", TABLE_WIDTH_ATTRS],
161
+ ["wAfter", TABLE_WIDTH_ATTRS],
162
+ ["trHeight", new Set(["w:val", "w:hRule"])],
163
+ ["tblHeader", new Set(["w:val"])],
164
+ ["cantSplit", new Set(["w:val"])],
165
+ ["jc", new Set(["w:val"])],
166
+ ["cnfStyle", new Set([
167
+ "w:val", "w:firstRow", "w:lastRow", "w:firstColumn", "w:lastColumn",
168
+ "w:oddVBand", "w:evenVBand", "w:oddHBand", "w:evenHBand",
169
+ "w:firstRowFirstColumn", "w:firstRowLastColumn",
170
+ "w:lastRowFirstColumn", "w:lastRowLastColumn",
171
+ ])],
172
+ ]);
173
+ const TC_PR_MODELLED_CHILD_ATTRIBUTES = new Map<string, ReadonlySet<string>>([
174
+ ["tcW", TABLE_WIDTH_ATTRS],
175
+ ["gridSpan", new Set(["w:val"])],
176
+ ["vMerge", new Set(["w:val"])],
177
+ ["shd", TABLE_SHD_ATTRS],
178
+ ["vAlign", new Set(["w:val"])],
179
+ ["textDirection", new Set(["w:val"])],
180
+ ["noWrap", new Set(["w:val"])],
181
+ ["tcFitText", new Set(["w:val"])],
182
+ ["cnfStyle", TR_PR_MODELLED_CHILD_ATTRIBUTES.get("cnfStyle")!],
183
+ // Border-side container w:tcBorders has nested side children (top/bottom/...);
184
+ // those each carry TABLE_BORDER_ATTRS but nested-attr capture is out of B
185
+ // scope (would require a separate descriptor).
186
+ ]);
187
+
188
+ const TBL_PR_GRAB_BAG_DESCRIPTOR: PropertyGrabBagDescriptor = {
189
+ modelledChildNames: TBL_PR_MODELLED_CHILDREN,
190
+ modelledChildAttributes: new Map(),
191
+ };
192
+ const TR_PR_GRAB_BAG_DESCRIPTOR: PropertyGrabBagDescriptor = {
193
+ modelledChildNames: TR_PR_MODELLED_CHILDREN,
194
+ modelledChildAttributes: new Map(),
195
+ };
196
+ const TC_PR_GRAB_BAG_DESCRIPTOR: PropertyGrabBagDescriptor = {
197
+ modelledChildNames: TC_PR_MODELLED_CHILDREN,
198
+ modelledChildAttributes: new Map(),
199
+ };
200
+
201
+ // Returns only the children slot (back-compat); table parsers that want both
202
+ // slots call captureGrabBagFullFromContainer instead.
203
+ function captureGrabBagFromContainer(
204
+ container: XmlElementNode,
205
+ descriptor: PropertyGrabBagDescriptor,
206
+ ) {
207
+ const sourceChildren = container.children
208
+ .filter((child): child is XmlElementNode => child.type === "element")
209
+ .map((child) => buildGrabBagSourceChildFromParsed(child));
210
+ return capturePropertyGrabBag(sourceChildren, descriptor);
211
+ }
212
+
102
213
  export interface ParsedMainDocument {
103
214
  blocks: ParsedBlockNode[];
104
215
  finalSectionProperties?: SectionProperties;
@@ -171,7 +282,8 @@ export type ParsedInlineNode =
171
282
  | ParsedFootnoteRefInlineNode
172
283
  | ParsedFieldInlineNode
173
284
  | ParsedPermStartInlineNode
174
- | ParsedPermEndInlineNode;
285
+ | ParsedPermEndInlineNode
286
+ | DrawingFrameNode;
175
287
 
176
288
  export interface ParsedTextNode {
177
289
  type: "text";
@@ -382,6 +494,7 @@ export interface ParsedTableBlockNode {
382
494
  styleId?: string;
383
495
  tblLook?: TableLook;
384
496
  propertiesXml?: string;
497
+ unknownPropertyChildren?: UnknownPropertyChild[];
385
498
  gridColumns: number[];
386
499
  rows: ParsedTableRowNode[];
387
500
  width?: TableWidth;
@@ -401,6 +514,7 @@ export interface ParsedTableBlockNode {
401
514
  export interface ParsedTableRowNode {
402
515
  type: "table_row";
403
516
  propertiesXml?: string;
517
+ unknownPropertyChildren?: UnknownPropertyChild[];
404
518
  cells: ParsedTableCellNode[];
405
519
  gridBefore?: number;
406
520
  widthBefore?: TableWidth;
@@ -418,6 +532,7 @@ export interface ParsedTableRowNode {
418
532
  export interface ParsedTableCellNode {
419
533
  type: "table_cell";
420
534
  propertiesXml?: string;
535
+ unknownPropertyChildren?: UnknownPropertyChild[];
421
536
  gridSpan?: number;
422
537
  verticalMerge?: "restart" | "continue";
423
538
  children: ParsedBlockNode[];
@@ -604,6 +719,42 @@ function rewriteScopeMarkerBookmarks(blocks: ParsedBlockNode[]): void {
604
719
  rewriteInPlace(blocks as unknown as { [key: string]: unknown }[]);
605
720
  }
606
721
 
722
+ /**
723
+ * CO4 F3.3 — parse a `w:txbxContent` (or any body-child stream) XML slice into
724
+ * ParsedBlockNode[]. Thin wrapper around the internal `parseBodyChild` for
725
+ * `parse-drawing.ts` to recursively parse shape text-box content.
726
+ *
727
+ * Input XML must be a root element whose children are body-child elements
728
+ * (w:p, w:tbl, w:sdt, etc.) — matches the structure of `w:txbxContent`.
729
+ */
730
+ export function parseBlockStreamFromXml(
731
+ xml: string,
732
+ ctx: {
733
+ relationships: readonly OpcRelationship[];
734
+ mediaParts: ReadonlyMap<string, InlineMediaPart>;
735
+ sourcePartPath: string;
736
+ },
737
+ ): ParsedBlockNode[] {
738
+ const root = parseXml(xml);
739
+ const relationshipMap = new Map(
740
+ ctx.relationships.map((r) => [r.id, r] as const),
741
+ );
742
+ const wrapper =
743
+ root.children.find((c): c is XmlElementNode => c.type === "element") ?? root;
744
+ return wrapper.children
745
+ .filter((n): n is XmlElementNode => n.type === "element")
746
+ .map((n) =>
747
+ parseBodyChild(
748
+ n,
749
+ xml,
750
+ relationshipMap,
751
+ ctx.relationships,
752
+ ctx.mediaParts,
753
+ ctx.sourcePartPath,
754
+ ),
755
+ );
756
+ }
757
+
607
758
  function parseBodyChild(
608
759
  node: XmlElementNode,
609
760
  sourceXml: string,
@@ -1032,6 +1183,7 @@ function parseTableElement(
1032
1183
  let styleId: string | undefined;
1033
1184
  let tblLook: TableLook | undefined;
1034
1185
  let propertiesXml: string | undefined;
1186
+ let unknownPropertyChildren: UnknownPropertyChild[] | undefined;
1035
1187
  let gridColumns: number[] = [];
1036
1188
  let width: TableWidth | undefined;
1037
1189
  let alignment: ParsedTableBlockNode["alignment"];
@@ -1052,6 +1204,7 @@ function parseTableElement(
1052
1204
  switch (localName(child.name)) {
1053
1205
  case "tblPr": {
1054
1206
  propertiesXml = sourceXml.slice(child.start, child.end);
1207
+ unknownPropertyChildren = captureGrabBagFromContainer(child, TBL_PR_GRAB_BAG_DESCRIPTOR);
1055
1208
  styleId = readTableStyleId(child);
1056
1209
  tblLook = readTableLook(child);
1057
1210
  width = readTableWidth(child);
@@ -1083,6 +1236,7 @@ function parseTableElement(
1083
1236
  ...(styleId ? { styleId } : {}),
1084
1237
  ...(tblLook ? { tblLook } : {}),
1085
1238
  ...(propertiesXml ? { propertiesXml } : {}),
1239
+ ...(unknownPropertyChildren ? { unknownPropertyChildren } : {}),
1086
1240
  gridColumns,
1087
1241
  rows,
1088
1242
  ...(width ? { width } : {}),
@@ -1109,6 +1263,7 @@ function parseTableRowElement(
1109
1263
  sourcePartPath: string,
1110
1264
  ): ParsedTableRowNode {
1111
1265
  let propertiesXml: string | undefined;
1266
+ let unknownPropertyChildren: UnknownPropertyChild[] | undefined;
1112
1267
  let gridBefore: number | undefined;
1113
1268
  let widthBefore: TableWidth | undefined;
1114
1269
  let gridAfter: number | undefined;
@@ -1127,6 +1282,7 @@ function parseTableRowElement(
1127
1282
  switch (localName(child.name)) {
1128
1283
  case "trPr":
1129
1284
  propertiesXml = sourceXml.slice(child.start, child.end);
1285
+ unknownPropertyChildren = captureGrabBagFromContainer(child, TR_PR_GRAB_BAG_DESCRIPTOR);
1130
1286
  gridBefore = readTableRowGridPosition(child, "gridBefore");
1131
1287
  widthBefore = readTableRowWidth(child, "wBefore");
1132
1288
  gridAfter = readTableRowGridPosition(child, "gridAfter");
@@ -1147,6 +1303,7 @@ function parseTableRowElement(
1147
1303
  return {
1148
1304
  type: "table_row",
1149
1305
  ...(propertiesXml ? { propertiesXml } : {}),
1306
+ ...(unknownPropertyChildren ? { unknownPropertyChildren } : {}),
1150
1307
  ...(gridBefore !== undefined ? { gridBefore } : {}),
1151
1308
  ...(widthBefore ? { widthBefore } : {}),
1152
1309
  ...(gridAfter !== undefined ? { gridAfter } : {}),
@@ -1171,6 +1328,7 @@ function parseTableCellElement(
1171
1328
  sourcePartPath: string,
1172
1329
  ): ParsedTableCellNode {
1173
1330
  let propertiesXml: string | undefined;
1331
+ let unknownPropertyChildren: UnknownPropertyChild[] | undefined;
1174
1332
  let gridSpan: number | undefined;
1175
1333
  let verticalMerge: "restart" | "continue" | undefined;
1176
1334
  let width: TableWidth | undefined;
@@ -1190,6 +1348,7 @@ function parseTableCellElement(
1190
1348
  switch (localName(child.name)) {
1191
1349
  case "tcPr": {
1192
1350
  propertiesXml = sourceXml.slice(child.start, child.end);
1351
+ unknownPropertyChildren = captureGrabBagFromContainer(child, TC_PR_GRAB_BAG_DESCRIPTOR);
1193
1352
  gridSpan = readCellGridSpan(child);
1194
1353
  verticalMerge = readCellVerticalMerge(child);
1195
1354
  width = readCellWidth(child);
@@ -1214,6 +1373,7 @@ function parseTableCellElement(
1214
1373
  return {
1215
1374
  type: "table_cell",
1216
1375
  ...(propertiesXml ? { propertiesXml } : {}),
1376
+ ...(unknownPropertyChildren ? { unknownPropertyChildren } : {}),
1217
1377
  ...(gridSpan ? { gridSpan } : {}),
1218
1378
  ...(verticalMerge ? { verticalMerge } : {}),
1219
1379
  children,
@@ -2048,6 +2208,28 @@ function parseRun(
2048
2208
  case "drawing": {
2049
2209
  const drawingXml = sourceXml.slice(child.start, child.end);
2050
2210
 
2211
+ // CO4: canonical DrawingFrame path — normalize all w:drawing under one intermediate
2212
+ try {
2213
+ const frame = parseDrawingFrame(drawingXml, {
2214
+ relationships,
2215
+ mediaParts,
2216
+ sourcePartPath,
2217
+ chartPartLookup: activeChartPartLookup,
2218
+ // F3.3: recursive block parser for w:txbxContent inside shape content.
2219
+ // ParsedBlockNode is structurally compatible with TxbxBlockParser's
2220
+ // `{ type: string; ... }` contract — each ParsedBlockNode variant
2221
+ // has a discriminant `type` field (paragraph | table | sdt | ...).
2222
+ blockParser: (xml) =>
2223
+ parseBlockStreamFromXml(xml, { relationships, mediaParts, sourcePartPath }) as unknown as ReadonlyArray<{ type: string; [key: string]: unknown }>,
2224
+ });
2225
+ if (frame) {
2226
+ result.push(frame);
2227
+ break;
2228
+ }
2229
+ } catch {
2230
+ // fall through to legacy chain
2231
+ }
2232
+
2051
2233
  // Try complex content (charts / SmartArt) first
2052
2234
  try {
2053
2235
  const complexContent = parseComplexContentXml(
@@ -3198,6 +3380,16 @@ export function parseSectionPropertiesFromElement(
3198
3380
  props.titlePage = val !== "false" && val !== "0";
3199
3381
  break;
3200
3382
  }
3383
+ case "footnotePr": {
3384
+ const parsed = readFootnoteLikeProperties(child);
3385
+ if (parsed) props.footnotePr = parsed;
3386
+ break;
3387
+ }
3388
+ case "endnotePr": {
3389
+ const parsed = readFootnoteLikeProperties(child);
3390
+ if (parsed) props.endnotePr = parsed;
3391
+ break;
3392
+ }
3201
3393
  }
3202
3394
  }
3203
3395
 
@@ -3220,6 +3412,54 @@ function safeParseInt(value: string | undefined): number | undefined {
3220
3412
  return Number.isFinite(n) ? n : undefined;
3221
3413
  }
3222
3414
 
3415
+ /**
3416
+ * Read the child-element shape shared by `<w:footnotePr>` and
3417
+ * `<w:endnotePr>` into a typed `FootnoteProperties` / `EndnoteProperties`.
3418
+ * Both elements carry the same four children (`w:pos`, `w:numFmt`,
3419
+ * `w:numStart`, `w:numRestart`) per ECMA-376 §17.11.11–.18.
3420
+ *
3421
+ * Returns `undefined` when none of the recognized children are present so
3422
+ * callers never stamp an empty `{}` onto `SectionProperties`.
3423
+ */
3424
+ function readFootnoteLikeProperties(
3425
+ node: XmlElementNode,
3426
+ ): FootnoteProperties | undefined {
3427
+ const result: FootnoteProperties = {};
3428
+
3429
+ for (const child of node.children) {
3430
+ if (child.type !== "element") continue;
3431
+ const name = localName(child.name);
3432
+ const val = child.attributes["w:val"];
3433
+
3434
+ if (name === "pos") {
3435
+ if (val === "pageBottom" || val === "beneathText" || val === "sectEnd" || val === "docEnd") {
3436
+ result.pos = val;
3437
+ }
3438
+ } else if (name === "numFmt") {
3439
+ if (
3440
+ val === "decimal" ||
3441
+ val === "upperRoman" ||
3442
+ val === "lowerRoman" ||
3443
+ val === "upperLetter" ||
3444
+ val === "lowerLetter" ||
3445
+ val === "chicago" ||
3446
+ val === "none"
3447
+ ) {
3448
+ result.numFmt = val;
3449
+ }
3450
+ } else if (name === "numStart") {
3451
+ const n = safeParseInt(val);
3452
+ if (n !== undefined) result.numStart = n;
3453
+ } else if (name === "numRestart") {
3454
+ if (val === "continuous" || val === "eachSect" || val === "eachPage") {
3455
+ result.numRestart = val;
3456
+ }
3457
+ }
3458
+ }
3459
+
3460
+ return Object.keys(result).length > 0 ? result : undefined;
3461
+ }
3462
+
3223
3463
  function parseBorderSpec(node: XmlElementNode): BorderSpec | undefined {
3224
3464
  const border: BorderSpec = {};
3225
3465
  const value = node.attributes["w:val"];
@@ -4,6 +4,7 @@ import type {
4
4
  NumberingLevelParagraphGeometry,
5
5
  NumberingLevelOverride,
6
6
  NumberingLevelOverrideDefinition,
7
+ NumPicBullet,
7
8
  ParagraphSpacing,
8
9
  ParagraphIndentation,
9
10
  TabStop,
@@ -35,12 +36,26 @@ export function parseNumberingXml(xml: string): NumberingCatalog {
35
36
  const numberingElement = findChildElement(root, "numbering");
36
37
  const abstractDefinitions: NumberingCatalog["abstractDefinitions"] = {};
37
38
  const instances: NumberingCatalog["instances"] = {};
39
+ const numPicBullets: NonNullable<NumberingCatalog["numPicBullets"]> = {};
38
40
 
39
41
  for (const child of numberingElement.children) {
40
42
  if (child.type !== "element") {
41
43
  continue;
42
44
  }
43
45
 
46
+ // ECMA-376 §17.9.19 — <w:numPicBullet w:numPicBulletId="N"> is a sibling
47
+ // of <w:abstractNum> / <w:num> at the top of the numbering part. Captured
48
+ // as a catalog entry keyed by numPicBulletId; raw XML is preserved for
49
+ // byte-identical round-trip.
50
+ if (localName(child.name) === "numPicBullet") {
51
+ const rawId =
52
+ child.attributes["w:numPicBulletId"] ?? child.attributes.numPicBulletId;
53
+ if (rawId) {
54
+ numPicBullets[rawId] = readNumPicBullet(child, rawId);
55
+ }
56
+ continue;
57
+ }
58
+
44
59
  switch (localName(child.name)) {
45
60
  case "abstractNum": {
46
61
  const rawId = child.attributes["w:abstractNumId"] ?? child.attributes.abstractNumId;
@@ -102,9 +117,86 @@ export function parseNumberingXml(xml: string): NumberingCatalog {
102
117
  return {
103
118
  abstractDefinitions,
104
119
  instances,
120
+ ...(Object.keys(numPicBullets).length > 0 ? { numPicBullets } : {}),
105
121
  };
106
122
  }
107
123
 
124
+ /**
125
+ * Read a `<w:numPicBullet>` element into a catalog entry. Extracts the
126
+ * inner `wp:extent` when present (drawingML path); the raw XML is
127
+ * preserved verbatim so the export round-trips byte-for-byte regardless
128
+ * of whether the bullet uses drawing or VML markup.
129
+ */
130
+ function readNumPicBullet(
131
+ node: XmlElementNode,
132
+ numPicBulletId: string,
133
+ ): NumPicBullet {
134
+ let widthEmu: number | undefined;
135
+ let heightEmu: number | undefined;
136
+
137
+ // Walk drawing → inline/anchor → extent for the EMU dimensions. VML
138
+ // paths skip this branch; rendering will fall back to a default size.
139
+ const drawing = findChildElementOptional(node, "drawing");
140
+ if (drawing) {
141
+ const inline = findChildElementOptional(drawing, "inline");
142
+ const anchor = findChildElementOptional(drawing, "anchor");
143
+ const envelope = inline ?? anchor;
144
+ if (envelope) {
145
+ const extent = findChildElementOptional(envelope, "extent");
146
+ if (extent) {
147
+ const cx = parseInteger(extent.attributes.cx ?? "");
148
+ const cy = parseInteger(extent.attributes.cy ?? "");
149
+ if (cx !== undefined) widthEmu = cx;
150
+ if (cy !== undefined) heightEmu = cy;
151
+ }
152
+ }
153
+ }
154
+
155
+ return {
156
+ numPicBulletId,
157
+ rawXml: reconstructElementXml(node),
158
+ ...(widthEmu !== undefined ? { widthEmu } : {}),
159
+ ...(heightEmu !== undefined ? { heightEmu } : {}),
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Best-effort reconstruction of the source XML for a `<w:numPicBullet>`.
165
+ * Mirrors `buildGrabBagSourceChildFromParsed` in property-grab-bag.ts —
166
+ * attribute/element semantic content round-trips, whitespace between
167
+ * elements + quote styles normalize. For the Lane 3b scope this is the
168
+ * right trade-off: picture-bullet catalog entries usually round-trip
169
+ * once-per-document and their mediaId lookup is what matters.
170
+ */
171
+ function reconstructElementXml(node: XmlElementNode): string {
172
+ const attrs = Object.entries(node.attributes)
173
+ .map(([name, value]) => ` ${name}="${escapeAttr(value)}"`)
174
+ .join("");
175
+ if (node.children.length === 0) {
176
+ return `<${node.name}${attrs}/>`;
177
+ }
178
+ const body = node.children
179
+ .map((child) => {
180
+ if (child.type === "text") return escapeText(child.text);
181
+ if (child.type === "element") return reconstructElementXml(child);
182
+ return "";
183
+ })
184
+ .join("");
185
+ return `<${node.name}${attrs}>${body}</${node.name}>`;
186
+ }
187
+
188
+ function escapeAttr(value: string): string {
189
+ return value
190
+ .replace(/&/gu, "&amp;")
191
+ .replace(/</gu, "&lt;")
192
+ .replace(/>/gu, "&gt;")
193
+ .replace(/"/gu, "&quot;");
194
+ }
195
+
196
+ function escapeText(value: string): string {
197
+ return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;");
198
+ }
199
+
108
200
  export function parseParagraphNumberingReferences(
109
201
  documentXml: string,
110
202
  ): ParsedParagraphNumberingReference[] {
@@ -248,6 +340,9 @@ function readLevelDefinition(
248
340
  const restartAfterLevel = rawRestart !== undefined ? parseInteger(rawRestart) : undefined;
249
341
  const rPrNode = findChildElementOptional(levelNode, "rPr");
250
342
  const runProperties = readRunProperties(rPrNode);
343
+ const lvlPicBulletNode = findChildElementOptional(levelNode, "lvlPicBulletId");
344
+ const picBulletId =
345
+ lvlPicBulletNode?.attributes["w:val"] ?? lvlPicBulletNode?.attributes.val;
251
346
 
252
347
  return {
253
348
  level,
@@ -260,6 +355,7 @@ function readLevelDefinition(
260
355
  ...(paragraphGeometry ? { paragraphGeometry } : {}),
261
356
  ...(runProperties ? { runProperties } : {}),
262
357
  ...(restartAfterLevel !== undefined ? { restartAfterLevel } : {}),
358
+ ...(picBulletId ? { picBulletId } : {}),
263
359
  };
264
360
  }
265
361
 
@@ -0,0 +1,107 @@
1
+ import type { PictureContent } from "../../model/canonical-document.ts";
2
+
3
+ interface XmlElementNode {
4
+ type: "element";
5
+ name: string;
6
+ attributes: Record<string, string>;
7
+ children: XmlNode[];
8
+ }
9
+
10
+ interface XmlTextNode {
11
+ type: "text";
12
+ text: string;
13
+ }
14
+
15
+ type XmlNode = XmlElementNode | XmlTextNode;
16
+
17
+ /**
18
+ * Parse a pic:pic element (child of a:graphicData) into PictureContent.
19
+ *
20
+ * srcRect values are 0..100000 = 0..100% of the source bitmap dimension.
21
+ * rotation is in 60000ths of a degree (same unit as EMU-based angle fields).
22
+ * LO reference: oox/source/drawingml/shape.cxx for xfrm + srcRect scaling.
23
+ */
24
+ export function parsePicture(graphicDataEl: XmlElementNode): PictureContent | null {
25
+ const pic = findFirstDescendant(graphicDataEl, "pic");
26
+ if (!pic) return null;
27
+
28
+ // blipFill/blip r:embed
29
+ const blipFill = findFirstChild(pic, "blipFill");
30
+ if (!blipFill) return null;
31
+ const blip = findFirstChild(blipFill, "blip");
32
+ if (!blip) return null;
33
+ const blipRef = blip.attributes["r:embed"] ?? blip.attributes.embed ?? "";
34
+ if (!blipRef) return null;
35
+
36
+ // srcRect (percentage crop: 0..100000 = 0..100%)
37
+ const srcRectEl = findFirstChild(blipFill, "srcRect");
38
+ const srcRect = srcRectEl
39
+ ? {
40
+ top: readPercentAttr(srcRectEl, "t"),
41
+ bottom: readPercentAttr(srcRectEl, "b"),
42
+ left: readPercentAttr(srcRectEl, "l"),
43
+ right: readPercentAttr(srcRectEl, "r"),
44
+ }
45
+ : undefined;
46
+
47
+ // stretch
48
+ const stretchEl = findFirstChild(blipFill, "stretch");
49
+ const stretch = stretchEl ? true : undefined;
50
+
51
+ // spPr/xfrm — rotation and flip
52
+ const spPr = findFirstChild(pic, "spPr");
53
+ const xfrm = spPr ? findFirstChild(spPr, "xfrm") : undefined;
54
+ const rotRaw = xfrm?.attributes.rot;
55
+ const rotation = rotRaw !== undefined ? parseInt(rotRaw, 10) || undefined : undefined;
56
+ const flipH = xfrm ? readBoolAttr(xfrm, "flipH") : undefined;
57
+ const flipV = xfrm ? readBoolAttr(xfrm, "flipV") : undefined;
58
+
59
+ // prstGeom
60
+ const prstGeom = spPr ? findFirstChild(spPr, "prstGeom") : undefined;
61
+ const presetGeom = prstGeom?.attributes.prst;
62
+
63
+ const result: PictureContent = { type: "picture", blipRef };
64
+ if (srcRect) result.srcRect = srcRect;
65
+ if (stretch !== undefined) result.stretch = stretch;
66
+ if (rotation !== undefined) result.rotation = rotation;
67
+ if (flipH !== undefined) result.flipH = flipH;
68
+ if (flipV !== undefined) result.flipV = flipV;
69
+ if (presetGeom) result.presetGeom = presetGeom;
70
+ return result;
71
+ }
72
+
73
+ function readPercentAttr(el: XmlElementNode, name: string): number {
74
+ const v = el.attributes[name];
75
+ if (v === undefined) return 0;
76
+ return parseInt(v, 10) || 0;
77
+ }
78
+
79
+ function readBoolAttr(el: XmlElementNode, name: string): boolean | undefined {
80
+ const v = el.attributes[name];
81
+ if (v === undefined) return undefined;
82
+ return v !== "0" && v !== "false";
83
+ }
84
+
85
+ function findFirstChild(node: XmlElementNode, local: string): XmlElementNode | undefined {
86
+ for (const child of node.children) {
87
+ if (child.type === "element" && localName(child.name) === local) return child;
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ function findFirstDescendant(node: XmlElementNode, local: string): XmlElementNode | undefined {
93
+ for (const child of node.children) {
94
+ if (child.type !== "element") continue;
95
+ if (localName(child.name) === local) return child;
96
+ const found = findFirstDescendant(child, local);
97
+ if (found) return found;
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ function localName(name: string): string {
103
+ const i = name.indexOf(":");
104
+ return i >= 0 ? name.slice(i + 1) : name;
105
+ }
106
+
107
+ export { type XmlElementNode as PictureXmlElement };