@beyondwork/docx-react-component 1.0.102 → 1.0.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +63 -1
  3. package/src/api/v3/_runtime-handle.ts +2 -0
  4. package/src/api/v3/ai/outline.ts +2 -7
  5. package/src/api/v3/runtime/geometry.ts +79 -0
  6. package/src/core/commands/formatting-commands.ts +8 -7
  7. package/src/core/commands/paragraph-layout-commands.ts +11 -10
  8. package/src/core/commands/section-layout-commands.ts +7 -6
  9. package/src/core/commands/style-commands.ts +3 -2
  10. package/src/io/normalize/normalize-text.ts +6 -5
  11. package/src/io/ooxml/parse-anchor.ts +15 -15
  12. package/src/io/ooxml/parse-drawing.ts +103 -5
  13. package/src/io/ooxml/parse-fields.ts +43 -21
  14. package/src/io/ooxml/parse-font-table.ts +2 -1
  15. package/src/io/ooxml/parse-footnotes.ts +3 -2
  16. package/src/io/ooxml/parse-headers-footers.ts +7 -6
  17. package/src/io/ooxml/parse-main-document.ts +41 -40
  18. package/src/io/ooxml/parse-numbering.ts +3 -2
  19. package/src/io/ooxml/parse-object.ts +6 -6
  20. package/src/io/ooxml/parse-paragraph-formatting.ts +12 -11
  21. package/src/io/ooxml/parse-picture.ts +16 -16
  22. package/src/io/ooxml/parse-run-formatting.ts +11 -10
  23. package/src/io/ooxml/parse-settings.ts +2 -1
  24. package/src/io/ooxml/parse-shapes.ts +148 -17
  25. package/src/io/ooxml/parse-styles.ts +16 -16
  26. package/src/io/ooxml/parse-theme.ts +5 -4
  27. package/src/model/canonical-document.ts +869 -836
  28. package/src/model/canonical-layout-inputs.ts +979 -0
  29. package/src/model/layout/index.ts +6 -0
  30. package/src/model/layout/page-graph-types.ts +61 -0
  31. package/src/model/layout/runtime-page-graph-types.ts +10 -0
  32. package/src/runtime/collab/runtime-collab-sync.ts +3 -3
  33. package/src/runtime/debug/build-debug-inspector-snapshot.ts +17 -4
  34. package/src/runtime/document-runtime.ts +30 -14
  35. package/src/runtime/event-refresh-hints.ts +3 -0
  36. package/src/runtime/formatting/document-lookup.ts +3 -2
  37. package/src/runtime/formatting/formatting-context.ts +176 -34
  38. package/src/runtime/formatting/index.ts +20 -0
  39. package/src/runtime/formatting/layout-inputs.ts +320 -0
  40. package/src/runtime/formatting/numbering/geometry.ts +13 -12
  41. package/src/runtime/formatting/style-cascade.ts +2 -1
  42. package/src/runtime/formatting/table-style-resolver.ts +8 -7
  43. package/src/runtime/geometry/caret-geometry.ts +82 -10
  44. package/src/runtime/geometry/geometry-facet.ts +36 -0
  45. package/src/runtime/geometry/geometry-index.ts +891 -0
  46. package/src/runtime/geometry/geometry-types.ts +221 -1
  47. package/src/runtime/geometry/index.ts +26 -0
  48. package/src/runtime/geometry/inert-geometry-facet.ts +3 -0
  49. package/src/runtime/geometry/replacement-envelope.ts +41 -2
  50. package/src/runtime/layout/layout-engine-version.ts +16 -1
  51. package/src/runtime/layout/page-graph.ts +191 -1
  52. package/src/runtime/prerender/graph-canonicalize.ts +30 -0
  53. package/src/runtime/surface-projection.ts +74 -39
  54. package/src/runtime/workflow/coordinator.ts +57 -11
  55. package/src/session/import/normalize.ts +2 -1
  56. package/src/session/import/source-package-evidence.ts +612 -1
  57. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +3 -0
@@ -7,7 +7,7 @@ import {
7
7
  type OpcPackagePart,
8
8
  type OpcRelationship,
9
9
  } from "../../io/ooxml/part-manifest.ts";
10
- import { parseXml } from "../../io/ooxml/xml-parser.ts";
10
+ import { parseXml, parseXmlWithOffsets } from "../../io/ooxml/xml-parser.ts";
11
11
  import type { XmlElementNode } from "../../io/ooxml/xml-element.ts";
12
12
  import { localName } from "../../io/ooxml/xml-attr-helpers.ts";
13
13
  import {
@@ -50,6 +50,95 @@ export interface SourcePackageRelationshipEvidence {
50
50
  targetExists: boolean;
51
51
  }
52
52
 
53
+ export interface SourcePackageLocationEvidence {
54
+ sourceId: string;
55
+ partPath: string;
56
+ storyKind: SourcePackageStoryKind;
57
+ element: string;
58
+ ordinal: number;
59
+ startOffset?: number;
60
+ endOffset?: number;
61
+ }
62
+
63
+ export interface SourcePackageStoryPartDescriptor {
64
+ sourceId: string;
65
+ partPath: string;
66
+ storyKind: SourcePackageStoryKind;
67
+ relationshipId?: string;
68
+ relationshipType?: string;
69
+ relationshipSourcePartPath?: string | null;
70
+ }
71
+
72
+ export interface SourcePackageHeaderFooterReferenceEvidence {
73
+ kind: "header" | "footer";
74
+ relationshipId: string;
75
+ type?: string;
76
+ resolvedTarget?: string;
77
+ targetExists?: boolean;
78
+ }
79
+
80
+ export interface SourcePackageSectionDescriptorEvidence
81
+ extends SourcePackageLocationEvidence {
82
+ headerFooterReferences: SourcePackageHeaderFooterReferenceEvidence[];
83
+ }
84
+
85
+ export interface SourcePackageObjectExtentEvidence
86
+ extends SourcePackageLocationEvidence {
87
+ kind: "drawing" | "vml-shape" | "object";
88
+ display: "inline" | "floating" | "unknown";
89
+ preserveOnly: true;
90
+ fallbackHint:
91
+ | "drawing-inline"
92
+ | "drawing-floating"
93
+ | "vml-shape"
94
+ | "ole-object"
95
+ | "opaque-object";
96
+ extentEmu?: { width: number; height: number };
97
+ extentTwips?: { width: number; height: number };
98
+ relationshipIds: string[];
99
+ shapeId?: string;
100
+ shapeType?: string;
101
+ vmlStyle?: string;
102
+ }
103
+
104
+ export interface SourcePackageRelationshipGroupEvidence {
105
+ headerFooters: SourcePackageRelationshipEvidence[];
106
+ media: SourcePackageRelationshipEvidence[];
107
+ charts: SourcePackageRelationshipEvidence[];
108
+ embedded: SourcePackageRelationshipEvidence[];
109
+ customXml: SourcePackageRelationshipEvidence[];
110
+ settings: SourcePackageRelationshipEvidence[];
111
+ numbering: SourcePackageRelationshipEvidence[];
112
+ comments: SourcePackageRelationshipEvidence[];
113
+ }
114
+
115
+ export interface SourcePackagePartGroupEvidence {
116
+ media: SourcePackageStoryPartDescriptor[];
117
+ images: SourcePackageStoryPartDescriptor[];
118
+ charts: SourcePackageStoryPartDescriptor[];
119
+ embedded: SourcePackageStoryPartDescriptor[];
120
+ customXml: SourcePackageStoryPartDescriptor[];
121
+ settings: SourcePackageStoryPartDescriptor[];
122
+ numbering: SourcePackageStoryPartDescriptor[];
123
+ comments: SourcePackageStoryPartDescriptor[];
124
+ themes: SourcePackageStoryPartDescriptor[];
125
+ }
126
+
127
+ export interface SourcePackageLayoutInventoryEvidence {
128
+ storyParts: SourcePackageStoryPartDescriptor[];
129
+ partGroups: SourcePackagePartGroupEvidence;
130
+ relationships: SourcePackageRelationshipGroupEvidence;
131
+ sections: SourcePackageSectionDescriptorEvidence[];
132
+ tables: SourcePackageLocationEvidence[];
133
+ fields: SourcePackageLocationEvidence[];
134
+ drawings: SourcePackageLocationEvidence[];
135
+ vmlShapes: SourcePackageLocationEvidence[];
136
+ contentControls: SourcePackageLocationEvidence[];
137
+ bookmarks: SourcePackageLocationEvidence[];
138
+ notes: SourcePackageLocationEvidence[];
139
+ objects: SourcePackageObjectExtentEvidence[];
140
+ }
141
+
53
142
  export interface SourcePackageEvidence {
54
143
  schemaVersion: 1;
55
144
  docId: string;
@@ -78,6 +167,7 @@ export interface SourcePackageEvidence {
78
167
  };
79
168
  parts: SourcePackagePartEvidence[];
80
169
  relationships: SourcePackageRelationshipEvidence[];
170
+ layoutInventory: SourcePackageLayoutInventoryEvidence;
81
171
  parseErrors: Array<{ partPath: string; message: string }>;
82
172
  }
83
173
 
@@ -162,6 +252,13 @@ export function createSourcePackageEvidence(
162
252
  (part) => part.storyKind !== "relationships" && part.storyKind !== "content-types",
163
253
  );
164
254
  const aggregateCounts = aggregateElementCounts(parts.map((part) => part.counts));
255
+ const layoutInventory = collectLayoutInventory(
256
+ sourcePackage,
257
+ parts,
258
+ relationships,
259
+ mainDocumentPartPath,
260
+ parseErrors,
261
+ );
165
262
 
166
263
  return {
167
264
  schemaVersion: 1,
@@ -192,6 +289,7 @@ export function createSourcePackageEvidence(
192
289
  },
193
290
  parts,
194
291
  relationships,
292
+ layoutInventory,
195
293
  parseErrors,
196
294
  };
197
295
  }
@@ -308,6 +406,365 @@ function countElements(root: XmlElementNode): SourcePackageElementCounts {
308
406
  return counts;
309
407
  }
310
408
 
409
+ function collectLayoutInventory(
410
+ sourcePackage: OpcPackage,
411
+ parts: SourcePackagePartEvidence[],
412
+ relationships: SourcePackageRelationshipEvidence[],
413
+ mainDocumentPartPath: string | undefined,
414
+ parseErrors: SourcePackageEvidence["parseErrors"],
415
+ ): SourcePackageLayoutInventoryEvidence {
416
+ const storyParts = collectStoryPartDescriptors(parts, relationships);
417
+ const partGroups = groupParts(storyParts, parts);
418
+ const relationshipGroups = groupRelationships(relationships);
419
+ const locations: Pick<
420
+ SourcePackageLayoutInventoryEvidence,
421
+ | "sections"
422
+ | "tables"
423
+ | "fields"
424
+ | "drawings"
425
+ | "vmlShapes"
426
+ | "contentControls"
427
+ | "bookmarks"
428
+ | "notes"
429
+ | "objects"
430
+ > = {
431
+ sections: [],
432
+ tables: [],
433
+ fields: [],
434
+ drawings: [],
435
+ vmlShapes: [],
436
+ contentControls: [],
437
+ bookmarks: [],
438
+ notes: [],
439
+ objects: [],
440
+ };
441
+
442
+ const partEvidenceByPath = new Map(parts.map((part) => [part.path, part]));
443
+ const sourceRelationshipByPart = relationships.reduce((map, relationship) => {
444
+ if (!relationship.sourcePartPath) return map;
445
+ const list = map.get(relationship.sourcePartPath) ?? [];
446
+ list.push(relationship);
447
+ map.set(relationship.sourcePartPath, list);
448
+ return map;
449
+ }, new Map<string, SourcePackageRelationshipEvidence[]>());
450
+
451
+ for (const part of sourcePackage.parts.values()) {
452
+ const partEvidence = partEvidenceByPath.get(part.path);
453
+ const storyKind = partEvidence?.storyKind ?? classifyPart(part, mainDocumentPartPath);
454
+ if (!shouldParseXmlPart(part, storyKind)) {
455
+ continue;
456
+ }
457
+
458
+ let root: XmlElementNode;
459
+ try {
460
+ root = parseXmlWithOffsets(new TextDecoder("utf-8").decode(part.bytes));
461
+ } catch (error) {
462
+ parseErrors.push({
463
+ partPath: part.path,
464
+ message: error instanceof Error ? error.message : String(error),
465
+ });
466
+ continue;
467
+ }
468
+
469
+ collectPartLocations(
470
+ root,
471
+ {
472
+ partPath: part.path,
473
+ storyKind,
474
+ relationships: sourceRelationshipByPart.get(part.path) ?? [],
475
+ },
476
+ locations,
477
+ );
478
+ }
479
+
480
+ return {
481
+ storyParts,
482
+ partGroups,
483
+ relationships: relationshipGroups,
484
+ ...locations,
485
+ };
486
+ }
487
+
488
+ function collectStoryPartDescriptors(
489
+ parts: SourcePackagePartEvidence[],
490
+ relationships: SourcePackageRelationshipEvidence[],
491
+ ): SourcePackageStoryPartDescriptor[] {
492
+ const descriptorByPath = new Map<string, SourcePackageStoryPartDescriptor>();
493
+ for (const part of parts) {
494
+ if (!isLayoutRelevantStoryKind(part.storyKind)) continue;
495
+ descriptorByPath.set(part.path, {
496
+ sourceId: partSourceId(part.path),
497
+ partPath: part.path,
498
+ storyKind: part.storyKind,
499
+ });
500
+ }
501
+
502
+ for (const relationship of relationships) {
503
+ if (relationship.targetMode !== "internal") continue;
504
+ const resolvedPath = normalizePartPath(relationship.resolvedTarget);
505
+ const descriptor = descriptorByPath.get(resolvedPath);
506
+ if (!descriptor) continue;
507
+ descriptorByPath.set(resolvedPath, {
508
+ ...descriptor,
509
+ relationshipId: relationship.relationshipId,
510
+ relationshipType: relationship.relationshipType,
511
+ relationshipSourcePartPath: relationship.sourcePartPath,
512
+ });
513
+ }
514
+
515
+ return [...descriptorByPath.values()].sort((left, right) =>
516
+ left.partPath.localeCompare(right.partPath),
517
+ );
518
+ }
519
+
520
+ function groupParts(
521
+ descriptors: SourcePackageStoryPartDescriptor[],
522
+ parts: SourcePackagePartEvidence[],
523
+ ): SourcePackagePartGroupEvidence {
524
+ const contentTypeByPath = new Map(parts.map((part) => [part.path, part.contentType]));
525
+ const byKind = (kind: SourcePackageStoryKind): SourcePackageStoryPartDescriptor[] =>
526
+ descriptors.filter((part) => part.storyKind === kind);
527
+
528
+ return {
529
+ media: byKind("media"),
530
+ images: byKind("media").filter((part) =>
531
+ contentTypeByPath.get(part.partPath)?.startsWith("image/") === true,
532
+ ),
533
+ charts: byKind("chart"),
534
+ embedded: byKind("embedded"),
535
+ customXml: byKind("custom-xml"),
536
+ settings: byKind("settings"),
537
+ numbering: byKind("numbering"),
538
+ comments: byKind("comments"),
539
+ themes: byKind("theme"),
540
+ };
541
+ }
542
+
543
+ function groupRelationships(
544
+ relationships: SourcePackageRelationshipEvidence[],
545
+ ): SourcePackageRelationshipGroupEvidence {
546
+ const byRole = (role: string): SourcePackageRelationshipEvidence[] =>
547
+ relationships.filter((relationship) => relationshipRole(relationship) === role);
548
+
549
+ return {
550
+ headerFooters: relationships.filter((relationship) => {
551
+ const role = relationshipRole(relationship);
552
+ return role === "header" || role === "footer";
553
+ }),
554
+ media: byRole("image"),
555
+ charts: byRole("chart"),
556
+ embedded: relationships.filter((relationship) => {
557
+ const role = relationshipRole(relationship);
558
+ return role === "oleObject" || role === "package";
559
+ }),
560
+ customXml: byRole("customXml"),
561
+ settings: byRole("settings"),
562
+ numbering: byRole("numbering"),
563
+ comments: byRole("comments"),
564
+ };
565
+ }
566
+
567
+ function collectPartLocations(
568
+ root: XmlElementNode,
569
+ context: {
570
+ partPath: string;
571
+ storyKind: SourcePackageStoryKind;
572
+ relationships: SourcePackageRelationshipEvidence[];
573
+ },
574
+ locations: Pick<
575
+ SourcePackageLayoutInventoryEvidence,
576
+ | "sections"
577
+ | "tables"
578
+ | "fields"
579
+ | "drawings"
580
+ | "vmlShapes"
581
+ | "contentControls"
582
+ | "bookmarks"
583
+ | "notes"
584
+ | "objects"
585
+ >,
586
+ ): void {
587
+ const ordinals = new Map<string, number>();
588
+ const nextOrdinal = (element: string): number => {
589
+ const next = (ordinals.get(element) ?? 0) + 1;
590
+ ordinals.set(element, next);
591
+ return next;
592
+ };
593
+
594
+ const visit = (node: XmlElementNode): void => {
595
+ const name = localName(node.name);
596
+ switch (name) {
597
+ case "sectPr":
598
+ locations.sections.push({
599
+ ...locationFor(context, node, name, nextOrdinal(name)),
600
+ headerFooterReferences: collectHeaderFooterReferences(node, context.relationships),
601
+ });
602
+ break;
603
+ case "tbl":
604
+ locations.tables.push(locationFor(context, node, name, nextOrdinal(name)));
605
+ break;
606
+ case "fldSimple":
607
+ case "fldChar":
608
+ case "instrText":
609
+ locations.fields.push(locationFor(context, node, name, nextOrdinal(name)));
610
+ break;
611
+ case "drawing": {
612
+ const ordinal = nextOrdinal(name);
613
+ locations.drawings.push(locationFor(context, node, name, ordinal));
614
+ break;
615
+ }
616
+ case "inline":
617
+ case "anchor": {
618
+ const ordinal = nextOrdinal(name);
619
+ locations.objects.push(drawingObjectFor(context, node, name, ordinal));
620
+ break;
621
+ }
622
+ case "shape": {
623
+ const ordinal = nextOrdinal(name);
624
+ locations.vmlShapes.push(locationFor(context, node, name, ordinal));
625
+ locations.objects.push(vmlObjectFor(context, node, ordinal));
626
+ break;
627
+ }
628
+ case "object": {
629
+ const ordinal = nextOrdinal(name);
630
+ locations.objects.push(objectFor(context, node, ordinal));
631
+ break;
632
+ }
633
+ case "sdt":
634
+ locations.contentControls.push(locationFor(context, node, name, nextOrdinal(name)));
635
+ break;
636
+ case "bookmarkStart":
637
+ locations.bookmarks.push(locationFor(context, node, name, nextOrdinal(name)));
638
+ break;
639
+ case "footnote":
640
+ case "endnote":
641
+ locations.notes.push(locationFor(context, node, name, nextOrdinal(name)));
642
+ break;
643
+ }
644
+
645
+ for (const child of node.children) {
646
+ if (child.type === "element") {
647
+ visit(child);
648
+ }
649
+ }
650
+ };
651
+
652
+ visit(root);
653
+ }
654
+
655
+ function locationFor(
656
+ context: {
657
+ partPath: string;
658
+ storyKind: SourcePackageStoryKind;
659
+ },
660
+ node: XmlElementNode,
661
+ element: string,
662
+ ordinal: number,
663
+ ): SourcePackageLocationEvidence {
664
+ return {
665
+ sourceId: elementSourceId(context.partPath, element, ordinal),
666
+ partPath: context.partPath,
667
+ storyKind: context.storyKind,
668
+ element,
669
+ ordinal,
670
+ ...(node.start !== undefined ? { startOffset: node.start } : {}),
671
+ ...(node.end !== undefined ? { endOffset: node.end } : {}),
672
+ };
673
+ }
674
+
675
+ function drawingObjectFor(
676
+ context: {
677
+ partPath: string;
678
+ storyKind: SourcePackageStoryKind;
679
+ },
680
+ node: XmlElementNode,
681
+ element: string,
682
+ ordinal: number,
683
+ ): SourcePackageObjectExtentEvidence {
684
+ const extentEmu = readDrawingExtentEmu(node);
685
+ return {
686
+ ...locationFor(context, node, element, ordinal),
687
+ kind: "drawing",
688
+ display: element === "anchor" ? "floating" : "inline",
689
+ preserveOnly: true,
690
+ fallbackHint: element === "anchor" ? "drawing-floating" : "drawing-inline",
691
+ ...(extentEmu ? { extentEmu, extentTwips: extentEmuToTwips(extentEmu) } : {}),
692
+ relationshipIds: collectRelationshipIds(node),
693
+ };
694
+ }
695
+
696
+ function vmlObjectFor(
697
+ context: {
698
+ partPath: string;
699
+ storyKind: SourcePackageStoryKind;
700
+ },
701
+ node: XmlElementNode,
702
+ ordinal: number,
703
+ ): SourcePackageObjectExtentEvidence {
704
+ const extentTwips = readVmlExtentTwips(node.attributes.style);
705
+ return {
706
+ ...locationFor(context, node, "shape", ordinal),
707
+ kind: "vml-shape",
708
+ display: "unknown",
709
+ preserveOnly: true,
710
+ fallbackHint: "vml-shape",
711
+ ...(extentTwips ? { extentTwips, extentEmu: extentTwipsToEmu(extentTwips) } : {}),
712
+ relationshipIds: collectRelationshipIds(node),
713
+ ...(node.attributes.id ? { shapeId: node.attributes.id } : {}),
714
+ ...(node.attributes.type ? { shapeType: node.attributes.type } : {}),
715
+ ...(node.attributes.style ? { vmlStyle: node.attributes.style } : {}),
716
+ };
717
+ }
718
+
719
+ function objectFor(
720
+ context: {
721
+ partPath: string;
722
+ storyKind: SourcePackageStoryKind;
723
+ },
724
+ node: XmlElementNode,
725
+ ordinal: number,
726
+ ): SourcePackageObjectExtentEvidence {
727
+ return {
728
+ ...locationFor(context, node, "object", ordinal),
729
+ kind: "object",
730
+ display: "unknown",
731
+ preserveOnly: true,
732
+ fallbackHint: hasDescendant(node, "OLEObject") ? "ole-object" : "opaque-object",
733
+ relationshipIds: collectRelationshipIds(node),
734
+ };
735
+ }
736
+
737
+ function collectHeaderFooterReferences(
738
+ node: XmlElementNode,
739
+ relationships: SourcePackageRelationshipEvidence[],
740
+ ): SourcePackageHeaderFooterReferenceEvidence[] {
741
+ const byId = new Map(relationships.map((relationship) => [relationship.relationshipId, relationship]));
742
+ const refs: SourcePackageHeaderFooterReferenceEvidence[] = [];
743
+ const visit = (current: XmlElementNode): void => {
744
+ const name = localName(current.name);
745
+ if (name === "headerReference" || name === "footerReference") {
746
+ const relationshipId = readRelationshipId(current);
747
+ if (relationshipId) {
748
+ const relationship = byId.get(relationshipId);
749
+ refs.push({
750
+ kind: name === "headerReference" ? "header" : "footer",
751
+ relationshipId,
752
+ ...(current.attributes["w:type"] ?? current.attributes.type
753
+ ? { type: current.attributes["w:type"] ?? current.attributes.type }
754
+ : {}),
755
+ ...(relationship ? { resolvedTarget: relationship.resolvedTarget } : {}),
756
+ ...(relationship ? { targetExists: relationship.targetExists } : {}),
757
+ });
758
+ }
759
+ }
760
+ for (const child of current.children) {
761
+ if (child.type === "element") visit(child);
762
+ }
763
+ };
764
+ visit(node);
765
+ return refs;
766
+ }
767
+
311
768
  function collectRelationships(
312
769
  sourcePackage: OpcPackage,
313
770
  ): SourcePackageRelationshipEvidence[] {
@@ -403,3 +860,157 @@ function countParts(
403
860
  ): number {
404
861
  return parts.filter((part) => part.storyKind === storyKind).length;
405
862
  }
863
+
864
+ function isLayoutRelevantStoryKind(storyKind: SourcePackageStoryKind): boolean {
865
+ return (
866
+ storyKind === "main-document" ||
867
+ storyKind === "header" ||
868
+ storyKind === "footer" ||
869
+ storyKind === "footnotes" ||
870
+ storyKind === "endnotes" ||
871
+ storyKind === "comments" ||
872
+ storyKind === "numbering" ||
873
+ storyKind === "settings" ||
874
+ storyKind === "theme" ||
875
+ storyKind === "chart" ||
876
+ storyKind === "media" ||
877
+ storyKind === "embedded" ||
878
+ storyKind === "custom-xml"
879
+ );
880
+ }
881
+
882
+ function relationshipRole(relationship: SourcePackageRelationshipEvidence): string {
883
+ const slashIndex = relationship.relationshipType.lastIndexOf("/");
884
+ return slashIndex >= 0
885
+ ? relationship.relationshipType.slice(slashIndex + 1)
886
+ : relationship.relationshipType;
887
+ }
888
+
889
+ function partSourceId(partPath: string): string {
890
+ return `part:${partPath}`;
891
+ }
892
+
893
+ function elementSourceId(partPath: string, element: string, ordinal: number): string {
894
+ return `${partSourceId(partPath)}#${element}:${ordinal}`;
895
+ }
896
+
897
+ function readDrawingExtentEmu(
898
+ node: XmlElementNode,
899
+ ): { width: number; height: number } | undefined {
900
+ const extent = findFirstDescendantByLocalName(node, "extent");
901
+ const width = extent ? readIntAttribute(extent, "cx") : undefined;
902
+ const height = extent ? readIntAttribute(extent, "cy") : undefined;
903
+ if (width !== undefined && height !== undefined) {
904
+ return { width, height };
905
+ }
906
+
907
+ const transform = findFirstDescendantByLocalName(node, "xfrm");
908
+ const ext = transform ? findFirstDescendantByLocalName(transform, "ext") : undefined;
909
+ const fallbackWidth = ext ? readIntAttribute(ext, "cx") : undefined;
910
+ const fallbackHeight = ext ? readIntAttribute(ext, "cy") : undefined;
911
+ if (fallbackWidth !== undefined && fallbackHeight !== undefined) {
912
+ return { width: fallbackWidth, height: fallbackHeight };
913
+ }
914
+ return undefined;
915
+ }
916
+
917
+ function readVmlExtentTwips(
918
+ style: string | undefined,
919
+ ): { width: number; height: number } | undefined {
920
+ if (!style) return undefined;
921
+ const width = readCssLengthTwips(style, "width");
922
+ const height = readCssLengthTwips(style, "height");
923
+ if (width !== undefined && height !== undefined) {
924
+ return { width, height };
925
+ }
926
+ return undefined;
927
+ }
928
+
929
+ function readCssLengthTwips(style: string, property: "width" | "height"): number | undefined {
930
+ const match = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([0-9.]+)\\s*(pt|in|cm|mm|px)?`, "iu")
931
+ .exec(style);
932
+ if (!match) return undefined;
933
+ const value = Number.parseFloat(match[1] ?? "");
934
+ if (!Number.isFinite(value)) return undefined;
935
+ const unit = (match[2] ?? "pt").toLowerCase();
936
+ switch (unit) {
937
+ case "pt":
938
+ return Math.round(value * 20);
939
+ case "in":
940
+ return Math.round(value * 1440);
941
+ case "cm":
942
+ return Math.round(value * 567);
943
+ case "mm":
944
+ return Math.round(value * 56.7);
945
+ case "px":
946
+ return Math.round(value * 15);
947
+ default:
948
+ return undefined;
949
+ }
950
+ }
951
+
952
+ function extentEmuToTwips(
953
+ extent: { width: number; height: number },
954
+ ): { width: number; height: number } {
955
+ return {
956
+ width: Math.round(extent.width / 635),
957
+ height: Math.round(extent.height / 635),
958
+ };
959
+ }
960
+
961
+ function extentTwipsToEmu(
962
+ extent: { width: number; height: number },
963
+ ): { width: number; height: number } {
964
+ return {
965
+ width: Math.round(extent.width * 635),
966
+ height: Math.round(extent.height * 635),
967
+ };
968
+ }
969
+
970
+ function collectRelationshipIds(node: XmlElementNode): string[] {
971
+ const ids = new Set<string>();
972
+ const visit = (current: XmlElementNode): void => {
973
+ const id = readRelationshipId(current);
974
+ if (id) ids.add(id);
975
+ for (const child of current.children) {
976
+ if (child.type === "element") visit(child);
977
+ }
978
+ };
979
+ visit(node);
980
+ return [...ids].sort();
981
+ }
982
+
983
+ function readRelationshipId(node: XmlElementNode): string | undefined {
984
+ return (
985
+ node.attributes["r:id"] ??
986
+ node.attributes.id ??
987
+ node.attributes["r:embed"] ??
988
+ node.attributes.embed ??
989
+ node.attributes["r:link"] ??
990
+ node.attributes.link
991
+ );
992
+ }
993
+
994
+ function findFirstDescendantByLocalName(
995
+ node: XmlElementNode,
996
+ name: string,
997
+ ): XmlElementNode | undefined {
998
+ for (const child of node.children) {
999
+ if (child.type !== "element") continue;
1000
+ if (localName(child.name) === name) return child;
1001
+ const nested = findFirstDescendantByLocalName(child, name);
1002
+ if (nested) return nested;
1003
+ }
1004
+ return undefined;
1005
+ }
1006
+
1007
+ function hasDescendant(node: XmlElementNode, name: string): boolean {
1008
+ return findFirstDescendantByLocalName(node, name) !== undefined;
1009
+ }
1010
+
1011
+ function readIntAttribute(node: XmlElementNode, name: string): number | undefined {
1012
+ const raw = node.attributes[name];
1013
+ if (raw === undefined) return undefined;
1014
+ const value = Number.parseInt(raw, 10);
1015
+ return Number.isFinite(value) ? value : undefined;
1016
+ }
@@ -839,6 +839,7 @@ export const TwPageStackOverlayLayer: React.FC<TwPageStackOverlayLayerProps> = (
839
839
  null,
840
840
  );
841
841
  if (geometryRects !== null) {
842
+ incrementInvalidationCounter("overlay.page.geometry.hit");
842
843
  setRectsIfChanged(reconcilePaperRectsWithFlow(geometryRects, pageCount));
843
844
  return;
844
845
  }
@@ -854,6 +855,7 @@ export const TwPageStackOverlayLayer: React.FC<TwPageStackOverlayLayerProps> = (
854
855
  // above via `geometryFacet` or the UI-API resolver. Lines below
855
856
  // fire only before the first render frame.
856
857
  if (origin) {
858
+ incrementInvalidationCounter("overlay.page.dom.degraded");
857
859
  const widgets = measureWidgetsViaBoundingRect(scrollRoot, origin, {
858
860
  pageCount,
859
861
  visiblePageIndexRange: null,
@@ -870,6 +872,7 @@ export const TwPageStackOverlayLayer: React.FC<TwPageStackOverlayLayerProps> = (
870
872
  });
871
873
  setRectsIfChanged(reconcilePaperRectsWithFlow(domRects, pageCount));
872
874
  } else {
875
+ incrementInvalidationCounter("overlay.page.dom.degraded");
873
876
  const widgets = measureWidgetsViaOffsetChain(scrollRoot, {
874
877
  pageCount,
875
878
  visiblePageIndexRange: null,