@beyondwork/docx-react-component 1.0.58 → 1.0.59

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 (134) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -1
  3. package/src/api/awareness-identity-types.ts +4 -2
  4. package/src/api/comment-negotiation-types.ts +4 -1
  5. package/src/api/external-custody-types.ts +16 -0
  6. package/src/api/internal/build-ref-projections.ts +108 -0
  7. package/src/api/package-version.ts +1 -1
  8. package/src/api/participants-types.ts +11 -1
  9. package/src/api/public-types.ts +978 -10
  10. package/src/api/scope-metadata-resolver-types.ts +6 -0
  11. package/src/compare/diff-engine.ts +3 -0
  12. package/src/core/commands/formatting-commands.ts +1 -0
  13. package/src/core/commands/index.ts +225 -16
  14. package/src/core/commands/legacy-form-field-commands.ts +181 -0
  15. package/src/core/commands/table-structure-commands.ts +149 -31
  16. package/src/core/selection/mapping.ts +20 -0
  17. package/src/core/state/editor-state.ts +2 -1
  18. package/src/index.ts +28 -0
  19. package/src/io/docx-session.ts +22 -3
  20. package/src/io/export/export-session.ts +11 -7
  21. package/src/io/export/ooxml-namespaces.ts +47 -0
  22. package/src/io/export/reattach-preserved-parts.ts +4 -16
  23. package/src/io/export/serialize-comments.ts +3 -131
  24. package/src/io/export/serialize-ffdata.ts +89 -0
  25. package/src/io/export/serialize-headers-footers.ts +5 -0
  26. package/src/io/export/serialize-main-document.ts +224 -34
  27. package/src/io/export/serialize-numbering.ts +22 -2
  28. package/src/io/export/serialize-revisions.ts +99 -0
  29. package/src/io/export/serialize-tables.ts +9 -0
  30. package/src/io/export/split-review-boundaries.ts +1 -0
  31. package/src/io/export/table-properties-xml.ts +14 -0
  32. package/src/io/load-scheduler.ts +70 -28
  33. package/src/io/normalize/normalize-text.ts +13 -0
  34. package/src/io/ooxml/_mini-xml.ts +198 -0
  35. package/src/io/ooxml/canonicalize-payload.ts +1 -4
  36. package/src/io/ooxml/chart/chart-style-table.ts +4 -3
  37. package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
  38. package/src/io/ooxml/chart/parse-series.ts +2 -1
  39. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  40. package/src/io/ooxml/chart/types.ts +6 -434
  41. package/src/io/ooxml/comment-presentation-payload.ts +6 -5
  42. package/src/io/ooxml/highlight-colors.ts +8 -5
  43. package/src/io/ooxml/parse-anchor.ts +68 -53
  44. package/src/io/ooxml/parse-comments.ts +14 -142
  45. package/src/io/ooxml/parse-complex-content.ts +3 -106
  46. package/src/io/ooxml/parse-drawing.ts +100 -195
  47. package/src/io/ooxml/parse-ffdata.ts +93 -0
  48. package/src/io/ooxml/parse-fields.ts +7 -146
  49. package/src/io/ooxml/parse-fill.ts +88 -8
  50. package/src/io/ooxml/parse-font-table.ts +5 -105
  51. package/src/io/ooxml/parse-footnotes.ts +28 -152
  52. package/src/io/ooxml/parse-headers-footers.ts +106 -212
  53. package/src/io/ooxml/parse-inline-media.ts +3 -200
  54. package/src/io/ooxml/parse-main-document.ts +180 -217
  55. package/src/io/ooxml/parse-numbering.ts +154 -335
  56. package/src/io/ooxml/parse-object.ts +147 -0
  57. package/src/io/ooxml/parse-ole-relationship.ts +82 -0
  58. package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
  59. package/src/io/ooxml/parse-picture-sdt.ts +85 -0
  60. package/src/io/ooxml/parse-picture.ts +72 -42
  61. package/src/io/ooxml/parse-revisions.ts +285 -51
  62. package/src/io/ooxml/parse-settings.ts +6 -99
  63. package/src/io/ooxml/parse-shapes.ts +25 -140
  64. package/src/io/ooxml/parse-styles.ts +3 -218
  65. package/src/io/ooxml/parse-tables.ts +76 -256
  66. package/src/io/ooxml/parse-theme.ts +1 -4
  67. package/src/io/ooxml/property-grab-bag.ts +5 -47
  68. package/src/io/ooxml/xml-element-serialize.ts +32 -0
  69. package/src/io/ooxml/xml-parser.ts +183 -0
  70. package/src/legal/bookmarks.ts +1 -1
  71. package/src/legal/cross-references.ts +1 -1
  72. package/src/legal/defined-terms.ts +1 -1
  73. package/src/legal/{_document-root.ts → document-root.ts} +8 -0
  74. package/src/legal/signature-blocks.ts +1 -1
  75. package/src/model/canonical-document.ts +159 -6
  76. package/src/model/chart-types.ts +439 -0
  77. package/src/model/snapshot.ts +3 -1
  78. package/src/review/store/comment-remapping.ts +24 -11
  79. package/src/review/store/revision-actions.ts +482 -2
  80. package/src/review/store/revision-store.ts +15 -0
  81. package/src/review/store/revision-types.ts +76 -0
  82. package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
  83. package/src/runtime/collab/runtime-collab-sync.ts +33 -0
  84. package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
  85. package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
  86. package/src/runtime/document-runtime.ts +476 -34
  87. package/src/runtime/document-search.ts +115 -0
  88. package/src/runtime/edit-ops/index.ts +18 -2
  89. package/src/runtime/footnote-resolver.ts +130 -0
  90. package/src/runtime/layout/layout-engine-instance.ts +31 -4
  91. package/src/runtime/layout/layout-engine-version.ts +37 -1
  92. package/src/runtime/layout/page-graph.ts +14 -1
  93. package/src/runtime/layout/resolved-formatting-state.ts +21 -0
  94. package/src/runtime/numbering-prefix.ts +17 -0
  95. package/src/runtime/query-scopes.ts +5 -8
  96. package/src/runtime/resolved-numbering-geometry.ts +37 -6
  97. package/src/runtime/revision-runtime.ts +27 -1
  98. package/src/runtime/selection/post-edit-validator.ts +60 -6
  99. package/src/runtime/structure-ops/index.ts +20 -4
  100. package/src/runtime/surface-projection.ts +290 -21
  101. package/src/runtime/table-schema.ts +6 -0
  102. package/src/runtime/theme-color-resolver.ts +2 -2
  103. package/src/runtime/units.ts +9 -0
  104. package/src/runtime/workflow-rail-segments.ts +4 -0
  105. package/src/ui/WordReviewEditor.tsx +187 -43
  106. package/src/ui/editor-runtime-boundary.ts +10 -0
  107. package/src/ui/editor-shell-view.tsx +4 -1
  108. package/src/ui/headless/chrome-registry.ts +53 -0
  109. package/src/ui/headless/selection-tool-resolver.ts +11 -1
  110. package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
  111. package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
  112. package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
  113. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
  114. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
  115. package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
  116. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
  117. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
  118. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
  119. package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
  120. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
  121. package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
  122. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
  123. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
  124. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
  125. package/src/ui-tailwind/index.ts +9 -0
  126. package/src/ui-tailwind/page-chrome-model.ts +77 -5
  127. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
  128. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
  129. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
  130. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
  131. package/src/ui-tailwind/theme/tokens.ts +14 -0
  132. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
  133. package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
  134. package/src/validation/diagnostics.ts +1 -0
@@ -37,8 +37,10 @@ import {
37
37
  import { toCanonicalNumberingInstanceId } from "./parse-numbering.ts";
38
38
  import { parseComplexContentXml, type ChartPartLookup } from "./parse-complex-content.ts";
39
39
  import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
40
+ import { parseObject } from "./parse-object.ts";
40
41
  import { parseDrawingFrame } from "./parse-drawing.ts";
41
42
  import { classifyFieldInstruction } from "./parse-fields.ts";
43
+ import { parseFFDataFromFldChar } from "./parse-ffdata.ts";
42
44
  import { resolveHighlightColor } from "./highlight-colors.ts";
43
45
  import {
44
46
  readCellBorders as readSharedCellBorders,
@@ -76,6 +78,8 @@ import {
76
78
  capturePropertyGrabBag,
77
79
  type PropertyGrabBagDescriptor,
78
80
  } from "./property-grab-bag.ts";
81
+ import { parseXmlWithOffsets as parseXml } from "./xml-parser.ts";
82
+ import { localName } from "./xml-attr-helpers.ts";
79
83
 
80
84
  /**
81
85
  * Modelled direct children of `<w:sectPr>` that `parseSectionPropertiesFromElement`
@@ -277,6 +281,7 @@ export type ParsedInlineNode =
277
281
  | ParsedShapeInlineNode
278
282
  | ParsedWordArtInlineNode
279
283
  | ParsedVmlShapeInlineNode
284
+ | ParsedOleEmbedInlineNode
280
285
  | ParsedBookmarkStartInlineNode
281
286
  | ParsedBookmarkEndInlineNode
282
287
  | ParsedFootnoteRefInlineNode
@@ -393,6 +398,20 @@ export interface ParsedVmlShapeInlineNode {
393
398
  rawXml: string;
394
399
  }
395
400
 
401
+ export interface ParsedOleEmbedInlineNode {
402
+ type: "ole_embed";
403
+ id: string;
404
+ progId?: string;
405
+ embedType: "oleObject";
406
+ relationshipId: string;
407
+ metadata: {
408
+ originalFilename?: string;
409
+ classId?: string;
410
+ shapeId?: string;
411
+ };
412
+ rawXml: string;
413
+ }
414
+
396
415
  export interface ParsedBookmarkStartInlineNode {
397
416
  type: "bookmark_start";
398
417
  bookmarkId: string;
@@ -419,6 +438,7 @@ export interface ParsedFieldInlineNode {
419
438
  contentXml?: string;
420
439
  children?: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
421
440
  rawXml: string;
441
+ legacyFormField?: import("../../model/canonical-document.ts").LegacyFormFieldNode;
422
442
  }
423
443
 
424
444
  export interface ParsedPermStartInlineNode {
@@ -580,14 +600,21 @@ const HYPERLINK_RELATIONSHIP_TYPE =
580
600
  "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
581
601
 
582
602
  /**
583
- * Request-scoped chart-part lookup. Set by `parseMainDocumentXml` for
584
- * the duration of a single top-level parse; read by `parseRun` where
585
- * the `<w:drawing>` → `parseComplexContentXml` call site lives. Using a
586
- * module variable instead of threading the callback through ~8
587
- * intermediate function signatures keeps the call sites readable; the
588
- * try/finally in `parseMainDocumentXml` ensures the variable never
589
- * leaks across concurrent parses (Node.js is single-threaded; no
590
- * re-entrancy since the parser is fully synchronous).
603
+ * Request-scoped chart-part lookup. Set by `parseMainDocumentXml` (and
604
+ * `parseSdtXml` for isolated SDT parsing) for the duration of a single
605
+ * top-level parse; read by `parseRun` where the `<w:drawing>` →
606
+ * `parseComplexContentXml` call site lives. Using a module variable instead
607
+ * of threading the callback through ~8 intermediate function signatures keeps
608
+ * the call sites readable; the try/finally in each entry point ensures the
609
+ * variable never leaks across calls.
610
+ *
611
+ * **Re-entrancy invariant:** Node.js is single-threaded and the parser is
612
+ * fully synchronous, so no two top-level entry points can interleave.
613
+ * `parseSdtXml` must NEVER be called from within a `blockParser` callback
614
+ * that is itself executing inside a `parseMainDocumentXml` session — doing
615
+ * so would clobber the outer lookup and clear it on the inner finally, leaving
616
+ * the outer session with `undefined`. In the current codebase this path does
617
+ * not exist; `parseSdtXml` is only callable externally via `parsePictureSdt`.
591
618
  */
592
619
  let activeChartPartLookup: ChartPartLookup | undefined;
593
620
 
@@ -606,13 +633,49 @@ export function parseMainDocumentXml(
606
633
  }
607
634
  }
608
635
 
636
+ /**
637
+ * CO4.5 — Parse a raw `<w:sdt>` XML fragment in isolation, with full drawing-
638
+ * frame support. Used by `parse-picture-sdt.ts` and tests.
639
+ *
640
+ * Falls back to `{ type: "opaque_block", rawXml }` on any parse failure
641
+ * (malformed XML, no `<w:sdt>` root, etc.) so callers never need to
642
+ * try/catch. The `activeChartPartLookup` module variable is guarded via
643
+ * try/catch/finally so it is always cleared even on error — see the
644
+ * re-entrancy invariant comment on the variable declaration above.
645
+ */
646
+ export function parseSdtXml(
647
+ rawXml: string,
648
+ relationships: readonly OpcRelationship[] = [],
649
+ mediaParts: ReadonlyMap<string, InlineMediaPart> = new Map(),
650
+ sourcePartPath = "/word/document.xml",
651
+ chartPartLookup?: ChartPartLookup,
652
+ ): ParsedBlockNode {
653
+ activeChartPartLookup = chartPartLookup;
654
+ try {
655
+ const root = parseXml(rawXml) as XmlElementNode;
656
+ const sdtEl = root.children.find(
657
+ (c): c is XmlElementNode =>
658
+ c.type === "element" && localName(c.name) === "sdt",
659
+ );
660
+ if (!sdtEl) {
661
+ return { type: "opaque_block", rawXml };
662
+ }
663
+ const relationshipMap = new Map(relationships.map((r) => [r.id, r]));
664
+ return parseSdtElement(sdtEl, rawXml, relationshipMap, relationships, mediaParts, sourcePartPath);
665
+ } catch {
666
+ return { type: "opaque_block", rawXml };
667
+ } finally {
668
+ activeChartPartLookup = undefined;
669
+ }
670
+ }
671
+
609
672
  function parseMainDocumentXmlInner(
610
673
  xml: string,
611
674
  relationships: readonly OpcRelationship[],
612
675
  mediaParts: ReadonlyMap<string, InlineMediaPart>,
613
676
  sourcePartPath: string,
614
677
  ): ParsedMainDocument {
615
- const root = parseXml(xml);
678
+ const root = parseXml(xml) as XmlElementNode;
616
679
  const documentElement = findChildElement(root, "document");
617
680
  const bodyElement = findChildElement(documentElement, "body");
618
681
  const relationshipMap = new Map(relationships.map((relationship) => [relationship.id, relationship]));
@@ -727,32 +790,68 @@ function rewriteScopeMarkerBookmarks(blocks: ParsedBlockNode[]): void {
727
790
  * Input XML must be a root element whose children are body-child elements
728
791
  * (w:p, w:tbl, w:sdt, etc.) — matches the structure of `w:txbxContent`.
729
792
  */
793
+ /**
794
+ * Phase 4.3 G3 — maximum recursion depth for nested
795
+ * `w:drawing → w:txbxContent → w:drawing → …` chains. OOXML nesting past 3
796
+ * is already pathological; 8 is a generous ceiling that guards against stack
797
+ * overflow on malicious / corrupt inputs.
798
+ */
799
+ const TXBX_BLOCK_STREAM_MAX_DEPTH = 8;
800
+
801
+ /**
802
+ * Module-local counter set by `parseBlockStreamFromXml` during its sync scope.
803
+ * Consumed by the blockParser closure in `case "drawing"` to forward the
804
+ * current depth into the recursive call. Parsing is synchronous, so a simple
805
+ * scalar is safe (no concurrent calls on the same stack).
806
+ */
807
+ let activeTxbxBlockStreamDepth = 0;
808
+
730
809
  export function parseBlockStreamFromXml(
731
810
  xml: string,
732
811
  ctx: {
733
812
  relationships: readonly OpcRelationship[];
734
813
  mediaParts: ReadonlyMap<string, InlineMediaPart>;
735
814
  sourcePartPath: string;
815
+ /** Recursion depth — incremented each time blockParser is invoked from
816
+ * inside parseDrawingFrame's txbxContent handling. Default 0. */
817
+ depth?: number;
736
818
  },
737
819
  ): ParsedBlockNode[] {
738
- const root = parseXml(xml);
820
+ const depth = ctx.depth ?? 0;
821
+ if (depth > TXBX_BLOCK_STREAM_MAX_DEPTH) {
822
+ // Depth limit reached — return empty to halt infinite recursion. The
823
+ // outer ShapeContent.txbxContentXml still preserves the raw XML for
824
+ // round-trip, so no data is lost, only deep-nested structural parse.
825
+ return [];
826
+ }
827
+ const root = parseXml(xml) as XmlElementNode;
739
828
  const relationshipMap = new Map(
740
829
  ctx.relationships.map((r) => [r.id, r] as const),
741
830
  );
742
831
  const wrapper =
743
832
  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
- );
833
+
834
+ // Set module-local depth for the duration of the sync walk so the drawing
835
+ // case's blockParser closure sees our depth and can bump it further.
836
+ const priorDepth = activeTxbxBlockStreamDepth;
837
+ activeTxbxBlockStreamDepth = depth;
838
+ try {
839
+ return wrapper.children
840
+ .filter((n): n is XmlElementNode => n.type === "element")
841
+ .map((n) =>
842
+ parseBodyChild(
843
+ n,
844
+ xml,
845
+ relationshipMap,
846
+ ctx.relationships,
847
+ ctx.mediaParts,
848
+ ctx.sourcePartPath,
849
+ depth,
850
+ ),
851
+ );
852
+ } finally {
853
+ activeTxbxBlockStreamDepth = priorDepth;
854
+ }
756
855
  }
757
856
 
758
857
  function parseBodyChild(
@@ -762,6 +861,10 @@ function parseBodyChild(
762
861
  relationships: readonly OpcRelationship[],
763
862
  mediaParts: ReadonlyMap<string, InlineMediaPart>,
764
863
  sourcePartPath: string,
864
+ /** Phase 4.3 G3 — unused marker (depth tracked via module-local
865
+ * activeTxbxBlockStreamDepth); kept for interface compatibility with
866
+ * parseBlockStreamFromXml's mapping callback. */
867
+ _depth = 0,
765
868
  ): ParsedBlockNode {
766
869
  const nodeType = localName(node.name);
767
870
 
@@ -845,6 +948,7 @@ function parseBodyChild(
845
948
  instruction: string;
846
949
  children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
847
950
  mode: "instruction" | "result";
951
+ legacyFormField?: import("../../model/canonical-document.ts").LegacyFormFieldNode;
848
952
  } | null = null;
849
953
 
850
954
  for (const child of node.children) {
@@ -1071,6 +1175,7 @@ function appendParagraphRunNodes(
1071
1175
  instruction: string;
1072
1176
  children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
1073
1177
  mode: "instruction" | "result";
1178
+ legacyFormField?: import("../../model/canonical-document.ts").LegacyFormFieldNode;
1074
1179
  } | null,
1075
1180
  sourceXml: string,
1076
1181
  relationships: readonly OpcRelationship[],
@@ -1080,6 +1185,7 @@ function appendParagraphRunNodes(
1080
1185
  instruction: string;
1081
1186
  children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
1082
1187
  mode: "instruction" | "result";
1188
+ legacyFormField?: import("../../model/canonical-document.ts").LegacyFormFieldNode;
1083
1189
  } | null {
1084
1190
  const hasFieldMarkers = node.children.some(
1085
1191
  (child) =>
@@ -1115,7 +1221,13 @@ function appendParagraphRunNodes(
1115
1221
  if (name === "fldChar") {
1116
1222
  const fldType = child.attributes["w:fldCharType"] ?? child.attributes.fldCharType;
1117
1223
  if (fldType === "begin") {
1118
- activeComplexField = { instruction: "", children: [], mode: "instruction" };
1224
+ const legacyFormField = parseFFDataFromFldChar(child, sourceXml);
1225
+ activeComplexField = {
1226
+ instruction: "",
1227
+ children: [],
1228
+ mode: "instruction",
1229
+ ...(legacyFormField ? { legacyFormField } : {}),
1230
+ };
1119
1231
  } else if (fldType === "separate" && activeComplexField) {
1120
1232
  activeComplexField.mode = "result";
1121
1233
  } else if (fldType === "end" && activeComplexField) {
@@ -1156,6 +1268,7 @@ function flushActiveComplexField(
1156
1268
  instruction: string;
1157
1269
  children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
1158
1270
  mode: "instruction" | "result";
1271
+ legacyFormField?: import("../../model/canonical-document.ts").LegacyFormFieldNode;
1159
1272
  } | null,
1160
1273
  ): void {
1161
1274
  if (!activeComplexField || activeComplexField.instruction.trim().length === 0) {
@@ -1168,6 +1281,7 @@ function flushActiveComplexField(
1168
1281
  instruction: activeComplexField.instruction,
1169
1282
  children: activeComplexField.children,
1170
1283
  rawXml: "",
1284
+ ...(activeComplexField.legacyFormField ? { legacyFormField: activeComplexField.legacyFormField } : {}),
1171
1285
  });
1172
1286
  reset();
1173
1287
  }
@@ -2220,7 +2334,15 @@ function parseRun(
2220
2334
  // `{ type: string; ... }` contract — each ParsedBlockNode variant
2221
2335
  // has a discriminant `type` field (paragraph | table | sdt | ...).
2222
2336
  blockParser: (xml) =>
2223
- parseBlockStreamFromXml(xml, { relationships, mediaParts, sourcePartPath }) as unknown as ReadonlyArray<{ type: string; [key: string]: unknown }>,
2337
+ parseBlockStreamFromXml(xml, {
2338
+ relationships,
2339
+ mediaParts,
2340
+ sourcePartPath,
2341
+ // Phase 4.3 G3 — forward module-local recursion counter so
2342
+ // pathological nested chains (drawing → txbx → drawing → txbx
2343
+ // → …) terminate at TXBX_BLOCK_STREAM_MAX_DEPTH.
2344
+ depth: activeTxbxBlockStreamDepth + 1,
2345
+ }) as unknown as ReadonlyArray<{ type: string; [key: string]: unknown }>,
2224
2346
  });
2225
2347
  if (frame) {
2226
2348
  result.push(frame);
@@ -2323,6 +2445,30 @@ function parseRun(
2323
2445
  });
2324
2446
  break;
2325
2447
  }
2448
+ case "object": {
2449
+ // Lane 7c Slice 7c.1 — attempt OLE-object extraction. Falls
2450
+ // through to the generic opaque-fragment path when the element
2451
+ // does not contain a resolvable <o:OLEObject> relationship
2452
+ // (legacy VML <w:object>, unknown ProgIDs without a relationship
2453
+ // target, etc.), preserving the existing opaque-fragment
2454
+ // semantics.
2455
+ const objectXml = sourceXml.slice(child.start, child.end);
2456
+ try {
2457
+ const oleNode = parseObject(child, objectXml, relationships);
2458
+ if (oleNode) {
2459
+ result.push(oleNode);
2460
+ break;
2461
+ }
2462
+ } catch {
2463
+ // Fall through to opaque on any unexpected parse-time error.
2464
+ }
2465
+ encounteredUnsupportedChild = true;
2466
+ result.push({
2467
+ type: "opaque_inline",
2468
+ rawXml: objectXml,
2469
+ });
2470
+ break;
2471
+ }
2326
2472
  case "commentReference":
2327
2473
  break;
2328
2474
  case "footnoteReference": {
@@ -2934,10 +3080,6 @@ function findChildElement(node: XmlElementNode, childLocalName: string): XmlElem
2934
3080
  return child;
2935
3081
  }
2936
3082
 
2937
- function localName(name: string): string {
2938
- const separatorIndex = name.indexOf(":");
2939
- return separatorIndex >= 0 ? name.slice(separatorIndex + 1) : name;
2940
- }
2941
3083
 
2942
3084
  function readOptionalAttribute(node: XmlElementNode, name: string): string | undefined {
2943
3085
  return node.attributes[`w:${name}`]
@@ -2945,194 +3087,6 @@ function readOptionalAttribute(node: XmlElementNode, name: string): string | und
2945
3087
  ?? node.attributes[name];
2946
3088
  }
2947
3089
 
2948
- function parseXml(xml: string): XmlElementNode {
2949
- const root: XmlElementNode = {
2950
- type: "element",
2951
- name: "__root__",
2952
- attributes: {},
2953
- children: [],
2954
- start: 0,
2955
- end: xml.length,
2956
- };
2957
- const stack: XmlElementNode[] = [root];
2958
- let cursor = 0;
2959
-
2960
- while (cursor < xml.length) {
2961
- if (xml.startsWith("<!--", cursor)) {
2962
- const end = xml.indexOf("-->", cursor);
2963
- cursor = end >= 0 ? end + 3 : xml.length;
2964
- continue;
2965
- }
2966
-
2967
- if (xml.startsWith("<?", cursor)) {
2968
- const end = xml.indexOf("?>", cursor);
2969
- cursor = end >= 0 ? end + 2 : xml.length;
2970
- continue;
2971
- }
2972
-
2973
- if (xml.startsWith("<![CDATA[", cursor)) {
2974
- const end = xml.indexOf("]]>", cursor);
2975
- const textEnd = end >= 0 ? end : xml.length;
2976
- stack[stack.length - 1]?.children.push({
2977
- type: "text",
2978
- text: xml.slice(cursor + 9, textEnd),
2979
- start: cursor,
2980
- end: end >= 0 ? end + 3 : xml.length,
2981
- });
2982
- cursor = end >= 0 ? end + 3 : xml.length;
2983
- continue;
2984
- }
2985
-
2986
- const currentChar = xml[cursor];
2987
- if (currentChar !== "<") {
2988
- const nextTag = xml.indexOf("<", cursor);
2989
- const end = nextTag >= 0 ? nextTag : xml.length;
2990
- const text = decodeXmlEntities(xml.slice(cursor, end));
2991
- if (text.length > 0) {
2992
- stack[stack.length - 1]?.children.push({
2993
- type: "text",
2994
- text,
2995
- start: cursor,
2996
- end,
2997
- });
2998
- }
2999
- cursor = end;
3000
- continue;
3001
- }
3002
-
3003
- if (xml[cursor + 1] === "/") {
3004
- const end = xml.indexOf(">", cursor);
3005
- if (end < 0) {
3006
- throw new Error("Malformed XML: missing closing >.");
3007
- }
3008
-
3009
- const name = xml.slice(cursor + 2, end).trim();
3010
- const current = stack.pop();
3011
- if (!current || localName(current.name) !== localName(name)) {
3012
- throw new Error(`Malformed XML: unexpected closing tag </${name}>.`);
3013
- }
3014
- current.end = end + 1;
3015
- cursor = end + 1;
3016
- continue;
3017
- }
3018
-
3019
- const tagEnd = findTagEnd(xml, cursor);
3020
- const tagBody = xml.slice(cursor + 1, tagEnd);
3021
- const selfClosing = /\/\s*$/.test(tagBody);
3022
- const { name, attributes } = parseTag(tagBody.replace(/\/\s*$/, "").trim());
3023
- const element: XmlElementNode = {
3024
- type: "element",
3025
- name,
3026
- attributes,
3027
- children: [],
3028
- start: cursor,
3029
- end: tagEnd + 1,
3030
- };
3031
- stack[stack.length - 1]?.children.push(element);
3032
-
3033
- if (!selfClosing) {
3034
- stack.push(element);
3035
- }
3036
-
3037
- cursor = tagEnd + 1;
3038
- }
3039
-
3040
- if (stack.length !== 1) {
3041
- throw new Error("Malformed XML: unclosed element in main document XML.");
3042
- }
3043
-
3044
- return root;
3045
- }
3046
-
3047
- function parseTag(tagBody: string): { name: string; attributes: Record<string, string> } {
3048
- let cursor = 0;
3049
- while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
3050
- cursor += 1;
3051
- }
3052
-
3053
- const nameStart = cursor;
3054
- while (cursor < tagBody.length && !/\s/.test(tagBody[cursor] ?? "")) {
3055
- cursor += 1;
3056
- }
3057
- const name = tagBody.slice(nameStart, cursor);
3058
- const attributes: Record<string, string> = {};
3059
-
3060
- while (cursor < tagBody.length) {
3061
- while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
3062
- cursor += 1;
3063
- }
3064
- if (cursor >= tagBody.length) {
3065
- break;
3066
- }
3067
-
3068
- const keyStart = cursor;
3069
- while (cursor < tagBody.length && !/[\s=]/.test(tagBody[cursor] ?? "")) {
3070
- cursor += 1;
3071
- }
3072
- const key = tagBody.slice(keyStart, cursor);
3073
-
3074
- while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
3075
- cursor += 1;
3076
- }
3077
-
3078
- if (tagBody[cursor] !== "=") {
3079
- attributes[key] = "";
3080
- continue;
3081
- }
3082
- cursor += 1;
3083
-
3084
- while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
3085
- cursor += 1;
3086
- }
3087
-
3088
- const quote = tagBody[cursor];
3089
- if (quote !== `"` && quote !== `'`) {
3090
- throw new Error(`Malformed XML attribute ${key}.`);
3091
- }
3092
- cursor += 1;
3093
-
3094
- const valueStart = cursor;
3095
- while (cursor < tagBody.length && tagBody[cursor] !== quote) {
3096
- cursor += 1;
3097
- }
3098
- const rawValue = tagBody.slice(valueStart, cursor);
3099
- attributes[key] = decodeXmlEntities(rawValue);
3100
- cursor += 1;
3101
- }
3102
-
3103
- return { name, attributes };
3104
- }
3105
-
3106
- function findTagEnd(xml: string, start: number): number {
3107
- let cursor = start + 1;
3108
- let quote: string | null = null;
3109
-
3110
- while (cursor < xml.length) {
3111
- const current = xml[cursor];
3112
- if (quote) {
3113
- if (current === quote) {
3114
- quote = null;
3115
- }
3116
- cursor += 1;
3117
- continue;
3118
- }
3119
-
3120
- if (current === `"` || current === `'`) {
3121
- quote = current;
3122
- cursor += 1;
3123
- continue;
3124
- }
3125
-
3126
- if (current === ">") {
3127
- return cursor;
3128
- }
3129
-
3130
- cursor += 1;
3131
- }
3132
-
3133
- throw new Error("Malformed XML: missing >.");
3134
- }
3135
-
3136
3090
  function decodeXmlEntities(value: string): string {
3137
3091
  return value.replace(/&(#x[0-9a-fA-F]+|#\d+|amp|lt|gt|quot|apos);/g, (match, entity) => {
3138
3092
  switch (entity) {
@@ -3442,14 +3396,23 @@ function readFootnoteLikeProperties(
3442
3396
  val === "lowerRoman" ||
3443
3397
  val === "upperLetter" ||
3444
3398
  val === "lowerLetter" ||
3399
+ val === "ordinal" ||
3400
+ val === "cardinalText" ||
3401
+ val === "ordinalText" ||
3402
+ val === "hex" ||
3445
3403
  val === "chicago" ||
3404
+ val === "bullet" ||
3405
+ val === "ideographDigital" ||
3406
+ val === "japaneseCounting" ||
3407
+ val === "arabicAbjad" ||
3408
+ val === "arabicAlpha" ||
3446
3409
  val === "none"
3447
3410
  ) {
3448
3411
  result.numFmt = val;
3449
3412
  }
3450
3413
  } else if (name === "numStart") {
3451
3414
  const n = safeParseInt(val);
3452
- if (n !== undefined) result.numStart = n;
3415
+ if (n !== undefined) result.numStart = Math.max(1, n);
3453
3416
  } else if (name === "numRestart") {
3454
3417
  if (val === "continuous" || val === "eachSect" || val === "eachPage") {
3455
3418
  result.numRestart = val;