@beyondwork/docx-react-component 1.0.104 → 1.0.106

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +3 -0
  3. package/src/api/v3/_create.ts +9 -2
  4. package/src/api/v3/ai/_audit-reference.ts +28 -0
  5. package/src/api/v3/ai/_pe2-evidence.ts +419 -0
  6. package/src/api/v3/ai/attach.ts +22 -2
  7. package/src/api/v3/ai/bundle.ts +18 -6
  8. package/src/api/v3/ai/inspect.ts +12 -2
  9. package/src/api/v3/ai/replacement.ts +124 -0
  10. package/src/api/v3/index.ts +7 -0
  11. package/src/api/v3/ui/_types.ts +139 -0
  12. package/src/api/v3/ui/index.ts +9 -0
  13. package/src/api/v3/ui/overlays.ts +104 -0
  14. package/src/api/v3/ui/viewport.ts +97 -0
  15. package/src/model/layout/index.ts +3 -0
  16. package/src/model/layout/page-graph-types.ts +118 -0
  17. package/src/model/layout/runtime-page-graph-types.ts +13 -0
  18. package/src/runtime/document-runtime.ts +39 -18
  19. package/src/runtime/event-refresh-hints.ts +33 -6
  20. package/src/runtime/geometry/geometry-facet.ts +9 -1
  21. package/src/runtime/geometry/geometry-index.ts +461 -10
  22. package/src/runtime/geometry/geometry-types.ts +6 -0
  23. package/src/runtime/geometry/object-handles.ts +7 -4
  24. package/src/runtime/layout/layout-engine-instance.ts +2 -0
  25. package/src/runtime/layout/layout-engine-version.ts +36 -1
  26. package/src/runtime/layout/page-graph.ts +697 -10
  27. package/src/runtime/layout/paginated-layout-engine.ts +10 -0
  28. package/src/runtime/layout/project-block-fragments.ts +187 -8
  29. package/src/runtime/layout/public-facet.ts +236 -0
  30. package/src/runtime/prerender/graph-canonicalize.ts +14 -0
  31. package/src/runtime/workflow/index.ts +1 -0
  32. package/src/runtime/workflow/overlay-lanes.ts +228 -0
  33. package/src/ui/presence-overlay-lane.ts +131 -0
  34. package/src/ui/ui-controller-factory.ts +21 -0
@@ -21,6 +21,17 @@ import type {
21
21
  EditorStoryTarget,
22
22
  PageLayoutSnapshot,
23
23
  } from "../../api/public-types";
24
+ import type {
25
+ BlockNode,
26
+ FooterDocument,
27
+ HeaderDocument,
28
+ InlineNode,
29
+ PreserveOnlyObjectSizing,
30
+ } from "../../model/canonical-document.ts";
31
+ import {
32
+ formatPageNumber,
33
+ formatPageNumberWithChapter,
34
+ } from "../formatting/field/page-number-format.ts";
24
35
  import type {
25
36
  ResolvedPageStories,
26
37
  } from "./page-story-resolver.ts";
@@ -54,6 +65,15 @@ export type {
54
65
  RuntimeLayoutDivergenceKind,
55
66
  RuntimeLayoutDivergence,
56
67
  RuntimePageFrame,
68
+ RuntimePageLocalStoryInstance,
69
+ RuntimeResolvedStoryField,
70
+ RuntimeStoryAnchoredObject,
71
+ RuntimeLayoutContinuationCursor,
72
+ RuntimeParagraphContinuationCursor,
73
+ RuntimeTableContinuationCursor,
74
+ RuntimeTableVerticalMergeCarry,
75
+ RuntimeFragmentLayoutObjectKind,
76
+ RuntimeFragmentLayoutObject,
57
77
  RuntimeBlockFragment,
58
78
  RuntimeLineBox,
59
79
  RuntimeNoteAllocation,
@@ -73,9 +93,11 @@ import type {
73
93
  RuntimeNoteAllocation,
74
94
  RuntimePageAnchor,
75
95
  RuntimePageFrame,
96
+ RuntimePageLocalStoryInstance,
76
97
  RuntimePageRegion,
77
98
  RuntimePageRegions,
78
99
  RuntimeResolvedRegions,
100
+ RuntimeStoryAnchoredObject,
79
101
  RuntimeTwipsRect,
80
102
  } from "../../model/layout/page-graph-types.ts";
81
103
  import type {
@@ -90,6 +112,19 @@ import type {
90
112
 
91
113
  let graphRevision = 0;
92
114
 
115
+ const PAGE_INSTANCE_FIELD_FAMILIES = new Set([
116
+ "PAGE",
117
+ "NUMPAGES",
118
+ "SECTIONPAGES",
119
+ ]);
120
+
121
+ const EMUS_PER_TWIP = 635;
122
+
123
+ type HeaderFooterStoryTarget = Extract<
124
+ EditorStoryTarget,
125
+ { kind: "header" | "footer" }
126
+ >;
127
+
93
128
  export function buildPageGraph(input: BuildPageGraphInput): RuntimePageGraph;
94
129
  export function buildPageGraph(
95
130
  pages: readonly DocumentPageSnapshot[],
@@ -114,6 +149,7 @@ export function buildPageGraph(
114
149
  const pages: RuntimePageNode[] = [];
115
150
  const aggregatedFragments: RuntimeBlockFragment[] = [...(input.fragments ?? [])];
116
151
  const pageIdByGlobalPageIndex = new Map<number, string>();
152
+ const pageFieldCounts = buildPageFieldCounts(input.pages);
117
153
  for (let index = 0; index < input.pages.length; index += 1) {
118
154
  const page = input.pages[index]!;
119
155
  pageIdByGlobalPageIndex.set(page.pageIndex, `page-${graphRevision}-${index}`);
@@ -180,17 +216,22 @@ export function buildPageGraph(
180
216
  stories.displayPageNumber,
181
217
  );
182
218
  const regions = buildRegions(page.layout, bodyPageFragments, stories, pageNoteAllocations);
183
- const divergences = detectFrameDivergences(frameId, regions);
184
- const frame = buildPageFrame({
219
+ const frameDivergences = detectFrameDivergences(frameId, regions);
220
+ const builtFrame = buildPageFrame({
185
221
  frameId,
186
222
  pageId,
187
223
  pageIndex: page.pageIndex,
188
224
  sectionIndex: page.sectionIndex,
189
225
  displayPageNumber: stories.displayPageNumber,
190
226
  layout: page.layout,
227
+ stories,
191
228
  regions,
192
- divergences,
229
+ divergences: frameDivergences,
230
+ subParts: input.subParts,
231
+ pageFieldCounts,
193
232
  });
233
+ const divergences = builtFrame.divergences;
234
+ const frame = builtFrame.frame;
194
235
 
195
236
  const node: RuntimePageNode = {
196
237
  pageId,
@@ -383,9 +424,12 @@ function buildPageFrame(input: {
383
424
  sectionIndex: number;
384
425
  displayPageNumber: number;
385
426
  layout: PageLayoutSnapshot;
427
+ stories: ResolvedPageStories;
386
428
  regions: RuntimePageRegions;
387
429
  divergences: readonly RuntimeLayoutDivergence[];
388
- }): RuntimePageFrame {
430
+ subParts?: BuildPageGraphInput["subParts"];
431
+ pageFieldCounts: PageFieldCounts;
432
+ }): { frame: RuntimePageFrame; divergences: RuntimeLayoutDivergence[] } {
389
433
  const regions: RuntimeResolvedRegions = {
390
434
  body: input.regions.body,
391
435
  exclusionZones: [],
@@ -394,18 +438,587 @@ function buildPageFrame(input: {
394
438
  ...(input.regions.columns ? { columns: input.regions.columns } : {}),
395
439
  ...(input.regions.footnotes ? { footnotes: input.regions.footnotes } : {}),
396
440
  };
397
- const divergenceIds = input.divergences.map((d) => d.divergenceId);
398
- return {
441
+ const pageLocalStoryResult = buildPageLocalStoryInstances({
399
442
  frameId: input.frameId,
400
443
  pageId: input.pageId,
401
444
  pageIndex: input.pageIndex,
402
445
  sectionIndex: input.sectionIndex,
403
446
  displayPageNumber: input.displayPageNumber,
404
- physicalBoundsTwips: rect(0, 0, input.layout.pageWidth, input.layout.pageHeight),
447
+ layout: input.layout,
448
+ stories: input.stories,
405
449
  regions,
406
- divergenceIds,
407
- signature: buildPageFrameSignature(input, divergenceIds),
450
+ subParts: input.subParts,
451
+ pageFieldCounts: input.pageFieldCounts,
452
+ });
453
+ const divergences = [...input.divergences, ...pageLocalStoryResult.divergences];
454
+ const pageLocalStories = pageLocalStoryResult.instances;
455
+ const divergenceIds = divergences.map((d) => d.divergenceId);
456
+ return {
457
+ frame: {
458
+ frameId: input.frameId,
459
+ pageId: input.pageId,
460
+ pageIndex: input.pageIndex,
461
+ sectionIndex: input.sectionIndex,
462
+ displayPageNumber: input.displayPageNumber,
463
+ physicalBoundsTwips: rect(0, 0, input.layout.pageWidth, input.layout.pageHeight),
464
+ regions,
465
+ pageLocalStories,
466
+ divergenceIds,
467
+ signature: buildPageFrameSignature(input, pageLocalStories, divergenceIds),
468
+ },
469
+ divergences,
470
+ };
471
+ }
472
+
473
+ function buildPageLocalStoryInstances(input: {
474
+ frameId: string;
475
+ pageId: string;
476
+ pageIndex: number;
477
+ sectionIndex: number;
478
+ displayPageNumber: number;
479
+ layout: PageLayoutSnapshot;
480
+ stories: ResolvedPageStories;
481
+ regions: RuntimeResolvedRegions;
482
+ subParts?: BuildPageGraphInput["subParts"];
483
+ pageFieldCounts: PageFieldCounts;
484
+ }): {
485
+ instances: RuntimePageLocalStoryInstance[];
486
+ divergences: RuntimeLayoutDivergence[];
487
+ } {
488
+ const instances: RuntimePageLocalStoryInstance[] = [];
489
+ const divergences: RuntimeLayoutDivergence[] = [];
490
+ if (isHeaderFooterStoryTarget(input.stories.header)) {
491
+ const built = buildPageLocalStoryInstance(
492
+ input.frameId,
493
+ input.pageId,
494
+ input.pageIndex,
495
+ input.sectionIndex,
496
+ input.displayPageNumber,
497
+ input.layout,
498
+ input.stories.header,
499
+ input.regions.header,
500
+ findHeaderFooterPart(input.subParts?.headers, input.stories.header),
501
+ input.pageFieldCounts,
502
+ );
503
+ instances.push(built.instance);
504
+ divergences.push(...built.divergences);
505
+ }
506
+ if (isHeaderFooterStoryTarget(input.stories.footer)) {
507
+ const built = buildPageLocalStoryInstance(
508
+ input.frameId,
509
+ input.pageId,
510
+ input.pageIndex,
511
+ input.sectionIndex,
512
+ input.displayPageNumber,
513
+ input.layout,
514
+ input.stories.footer,
515
+ input.regions.footer,
516
+ findHeaderFooterPart(input.subParts?.footers, input.stories.footer),
517
+ input.pageFieldCounts,
518
+ );
519
+ instances.push(built.instance);
520
+ divergences.push(...built.divergences);
521
+ }
522
+ return { instances, divergences };
523
+ }
524
+
525
+ function buildPageLocalStoryInstance(
526
+ frameId: string,
527
+ pageId: string,
528
+ pageIndex: number,
529
+ sectionIndex: number,
530
+ displayPageNumber: number,
531
+ layout: PageLayoutSnapshot,
532
+ target: HeaderFooterStoryTarget,
533
+ region: RuntimePageRegion | undefined,
534
+ source: HeaderDocument | FooterDocument | undefined,
535
+ pageFieldCounts: PageFieldCounts,
536
+ ): {
537
+ instance: RuntimePageLocalStoryInstance;
538
+ divergences: RuntimeLayoutDivergence[];
539
+ } {
540
+ const measuredFrameHeightTwips = region?.heightTwips ?? 0;
541
+ const sectionPart =
542
+ target.sectionIndex === undefined ? "section-unknown" : `section-${target.sectionIndex}`;
543
+ const instanceId = `${frameId}:${target.kind}:${target.variant}:${target.relationshipId}`;
544
+ const storyKey = `${target.kind}:${target.relationshipId}`;
545
+ const resolvedFields = source
546
+ ? collectResolvedStoryFields(source.blocks, {
547
+ storyKey,
548
+ pageIndex,
549
+ sectionIndex,
550
+ displayPageNumber,
551
+ layout,
552
+ pageFieldCounts,
553
+ })
554
+ : [];
555
+ const objectLedger = source
556
+ ? collectStoryAnchoredObjects(source.blocks, {
557
+ frameId,
558
+ storyKey,
559
+ kind: target.kind,
560
+ variant: target.variant,
561
+ relationshipId: target.relationshipId,
562
+ })
563
+ : { objects: [], divergences: [] };
564
+ const signature = buildPageLocalStorySignature({
565
+ kind: target.kind,
566
+ variant: target.variant,
567
+ relationshipId: target.relationshipId,
568
+ sectionPart,
569
+ measuredFrameHeightTwips,
570
+ resolvedFields,
571
+ anchoredObjects: objectLedger.objects,
572
+ });
573
+ return {
574
+ instance: {
575
+ instanceId,
576
+ storyKey,
577
+ pageId,
578
+ kind: target.kind,
579
+ variant: target.variant,
580
+ relationshipId: target.relationshipId,
581
+ ...(target.sectionIndex === undefined ? {} : { sectionIndex: target.sectionIndex }),
582
+ resolvedFields,
583
+ anchoredObjects: objectLedger.objects,
584
+ measuredFrameHeightTwips,
585
+ signature,
586
+ },
587
+ divergences: objectLedger.divergences,
588
+ };
589
+ }
590
+
591
+ function buildPageLocalStorySignature(input: {
592
+ kind: RuntimePageLocalStoryInstance["kind"];
593
+ variant: RuntimePageLocalStoryInstance["variant"];
594
+ relationshipId: string;
595
+ sectionPart: string;
596
+ measuredFrameHeightTwips: number;
597
+ resolvedFields: readonly RuntimePageLocalStoryInstance["resolvedFields"][number][];
598
+ anchoredObjects: readonly RuntimeStoryAnchoredObject[];
599
+ }): string {
600
+ return [
601
+ "page-local-story",
602
+ input.kind,
603
+ input.variant,
604
+ input.relationshipId,
605
+ input.sectionPart,
606
+ input.measuredFrameHeightTwips,
607
+ ...input.resolvedFields.map((field) =>
608
+ [field.fieldId, field.family, field.displayText].join(":"),
609
+ ),
610
+ ...input.anchoredObjects.map((object) =>
611
+ [
612
+ object.objectId,
613
+ object.sourceType,
614
+ object.display,
615
+ object.extentTwips?.widthTwips ?? "",
616
+ object.extentTwips?.heightTwips ?? "",
617
+ object.relationshipIds?.join(",") ?? "",
618
+ object.preserveOnly ? "preserve-only" : "renderable",
619
+ object.divergenceIds.join(","),
620
+ ].join(":"),
621
+ ),
622
+ ].join("|");
623
+ }
624
+
625
+ interface PageFieldCounts {
626
+ contentPageCount: number;
627
+ sectionContentPageCountByIndex: ReadonlyMap<number, number>;
628
+ }
629
+
630
+ interface PageFieldResolutionInput {
631
+ storyKey: string;
632
+ pageIndex: number;
633
+ sectionIndex: number;
634
+ displayPageNumber: number;
635
+ layout: PageLayoutSnapshot;
636
+ pageFieldCounts: PageFieldCounts;
637
+ }
638
+
639
+ function buildPageFieldCounts(
640
+ pages: readonly Pick<DocumentPageSnapshot, "pageInSection" | "sectionIndex">[],
641
+ ): PageFieldCounts {
642
+ const sectionContentPageCountByIndex = new Map<number, number>();
643
+ let contentPageCount = 0;
644
+ for (const page of pages) {
645
+ if (page.pageInSection === -1) continue;
646
+ contentPageCount += 1;
647
+ sectionContentPageCountByIndex.set(
648
+ page.sectionIndex,
649
+ (sectionContentPageCountByIndex.get(page.sectionIndex) ?? 0) + 1,
650
+ );
651
+ }
652
+ return { contentPageCount, sectionContentPageCountByIndex };
653
+ }
654
+
655
+ function findHeaderFooterPart<T extends HeaderDocument | FooterDocument>(
656
+ parts: ReadonlyArray<T> | undefined,
657
+ target: HeaderFooterStoryTarget,
658
+ ): T | undefined {
659
+ return parts?.find((part) => part.relationshipId === target.relationshipId);
660
+ }
661
+
662
+ function collectResolvedStoryFields(
663
+ blocks: readonly BlockNode[],
664
+ context: PageFieldResolutionInput,
665
+ ): RuntimePageLocalStoryInstance["resolvedFields"] {
666
+ const fields: RuntimePageLocalStoryInstance["resolvedFields"] = [];
667
+ let ordinal = 0;
668
+
669
+ const visitBlock = (block: BlockNode): void => {
670
+ switch (block.type) {
671
+ case "paragraph":
672
+ for (const child of block.children) visitInline(child);
673
+ break;
674
+ case "table":
675
+ for (const row of block.rows) {
676
+ for (const cell of row.cells) {
677
+ for (const child of cell.children) visitBlock(child);
678
+ }
679
+ }
680
+ break;
681
+ case "sdt":
682
+ for (const child of block.children) visitBlock(child);
683
+ break;
684
+ default:
685
+ break;
686
+ }
408
687
  };
688
+
689
+ const visitInline = (inline: InlineNode): void => {
690
+ switch (inline.type) {
691
+ case "field": {
692
+ const family = inline.fieldFamily ?? classifyFieldInstructionLocal(inline.instruction);
693
+ if (PAGE_INSTANCE_FIELD_FAMILIES.has(family)) {
694
+ const fieldId = `${context.storyKey}:field-${ordinal}:${family}`;
695
+ fields.push({
696
+ fieldId,
697
+ family,
698
+ displayText: resolvePageInstanceFieldDisplayText(
699
+ family,
700
+ flattenInline(inline.children),
701
+ context,
702
+ ),
703
+ });
704
+ ordinal += 1;
705
+ }
706
+ break;
707
+ }
708
+ case "hyperlink":
709
+ for (const child of inline.children) visitInline(child);
710
+ break;
711
+ default:
712
+ break;
713
+ }
714
+ };
715
+
716
+ for (const block of blocks) visitBlock(block);
717
+ return fields;
718
+ }
719
+
720
+ interface StoryObjectContext {
721
+ frameId: string;
722
+ storyKey: string;
723
+ kind: "header" | "footer";
724
+ variant: RuntimePageLocalStoryInstance["variant"];
725
+ relationshipId: string;
726
+ }
727
+
728
+ function collectStoryAnchoredObjects(
729
+ blocks: readonly BlockNode[],
730
+ context: StoryObjectContext,
731
+ ): {
732
+ objects: RuntimeStoryAnchoredObject[];
733
+ divergences: RuntimeLayoutDivergence[];
734
+ } {
735
+ const objects: RuntimeStoryAnchoredObject[] = [];
736
+ const divergences: RuntimeLayoutDivergence[] = [];
737
+ let ordinal = 0;
738
+
739
+ const pushObject = (
740
+ object: Omit<RuntimeStoryAnchoredObject, "objectId" | "divergenceIds"> & {
741
+ objectId?: string;
742
+ preserveHint?: PreserveOnlyObjectSizing;
743
+ wrapMode?: string;
744
+ },
745
+ ): void => {
746
+ const objectId =
747
+ object.objectId ?? `${context.storyKey}:object-${ordinal}:${object.sourceType}`;
748
+ const objectDivergenceIds: string[] = [];
749
+ const objectDisplay = object.display;
750
+
751
+ if (object.preserveOnly || object.preserveHint) {
752
+ const divergenceId = `${context.frameId}:preserve-only-placeholder:${objectId}`;
753
+ objectDivergenceIds.push(divergenceId);
754
+ divergences.push({
755
+ divergenceId,
756
+ kind: "preserve-only-placeholder",
757
+ source: "runtime",
758
+ severity: "info",
759
+ message: `Page-local ${context.kind} story contains preserve-only object ${objectId}`,
760
+ regionKinds: [context.kind],
761
+ objectIds: [objectId],
762
+ });
763
+ }
764
+
765
+ const wrapMode = object.wrapMode;
766
+ if (
767
+ objectDisplay === "floating" &&
768
+ wrapMode !== undefined &&
769
+ wrapMode !== "none" &&
770
+ wrapMode !== "topAndBottom"
771
+ ) {
772
+ const divergenceId = `${context.frameId}:unsupported-wrap:${objectId}:${wrapMode}`;
773
+ objectDivergenceIds.push(divergenceId);
774
+ divergences.push({
775
+ divergenceId,
776
+ kind: "unsupported-wrap",
777
+ source: "runtime",
778
+ severity: "warning",
779
+ message: `Page-local ${context.kind} story object ${objectId} uses unsupported wrap mode ${wrapMode}`,
780
+ regionKinds: [context.kind],
781
+ objectIds: [objectId],
782
+ });
783
+ }
784
+
785
+ const { preserveHint: _preserveHint, wrapMode: _wrapMode, ...ledger } = object;
786
+ objects.push({
787
+ ...ledger,
788
+ objectId,
789
+ divergenceIds: objectDivergenceIds,
790
+ });
791
+ ordinal += 1;
792
+ };
793
+
794
+ const visitBlock = (block: BlockNode): void => {
795
+ switch (block.type) {
796
+ case "paragraph":
797
+ for (const child of block.children) visitInline(child);
798
+ break;
799
+ case "table":
800
+ for (const row of block.rows) {
801
+ for (const cell of row.cells) {
802
+ for (const child of cell.children) visitBlock(child);
803
+ }
804
+ }
805
+ break;
806
+ case "sdt":
807
+ for (const child of block.children) visitBlock(child);
808
+ break;
809
+ default:
810
+ break;
811
+ }
812
+ };
813
+
814
+ const visitInline = (inline: InlineNode): void => {
815
+ switch (inline.type) {
816
+ case "image": {
817
+ pushObject({
818
+ objectId: inline.mediaId,
819
+ sourceType: "image",
820
+ display: inline.display === "floating" ? "floating" : "inline",
821
+ preserveOnly: false,
822
+ wrapMode: inline.floating?.wrap,
823
+ });
824
+ break;
825
+ }
826
+ case "drawing_frame": {
827
+ const preserveHint = getDrawingFramePreserveHint(inline);
828
+ const relationshipIds = collectDrawingRelationshipIds(inline);
829
+ const display = inline.anchor.display;
830
+ pushObject({
831
+ objectId: getDrawingFrameObjectId(inline, context.storyKey, ordinal),
832
+ sourceType: "drawing-frame",
833
+ display,
834
+ extentTwips: extentTwipsFromEmu(
835
+ inline.anchor.extent.widthEmu,
836
+ inline.anchor.extent.heightEmu,
837
+ ),
838
+ ...(relationshipIds.length > 0 ? { relationshipIds } : {}),
839
+ preserveOnly: Boolean(preserveHint),
840
+ ...(preserveHint ? { preserveHint } : {}),
841
+ wrapMode: inline.anchor.wrapMode,
842
+ });
843
+ if (inline.content.type === "shape") {
844
+ for (const child of inline.content.txbxBlocks ?? []) visitBlock(child);
845
+ }
846
+ break;
847
+ }
848
+ case "chart_preview":
849
+ case "smartart_preview":
850
+ case "shape":
851
+ case "wordart":
852
+ case "vml_shape": {
853
+ const preserveHint = inline.preserveOnlyObject;
854
+ pushObject({
855
+ objectId: getPreserveOnlyObjectId(inline, context.storyKey, ordinal),
856
+ sourceType: sourceTypeForInlineObject(inline.type),
857
+ display: preserveHint?.display ?? "unknown",
858
+ ...(preserveHint?.extentEmu
859
+ ? {
860
+ extentTwips: extentTwipsFromEmu(
861
+ preserveHint.extentEmu.widthEmu,
862
+ preserveHint.extentEmu.heightEmu,
863
+ ),
864
+ }
865
+ : {}),
866
+ ...(preserveHint?.relationshipIds
867
+ ? { relationshipIds: [...preserveHint.relationshipIds] }
868
+ : {}),
869
+ preserveOnly: Boolean(preserveHint),
870
+ ...(preserveHint ? { preserveHint } : {}),
871
+ });
872
+ if (inline.type === "shape") {
873
+ for (const child of inline.txbxBlocks ?? []) visitBlock(child);
874
+ }
875
+ break;
876
+ }
877
+ case "ole_embed":
878
+ pushObject({
879
+ objectId: inline.id,
880
+ sourceType: "ole-embed",
881
+ display: "unknown",
882
+ relationshipIds: [inline.relationshipId],
883
+ preserveOnly: true,
884
+ });
885
+ break;
886
+ case "opaque_inline":
887
+ pushObject({
888
+ objectId: inline.fragmentId,
889
+ sourceType: "opaque-inline",
890
+ display: "unknown",
891
+ preserveOnly: true,
892
+ });
893
+ break;
894
+ case "field":
895
+ for (const child of inline.children) visitInline(child);
896
+ break;
897
+ case "hyperlink":
898
+ for (const child of inline.children) visitInline(child);
899
+ break;
900
+ default:
901
+ break;
902
+ }
903
+ };
904
+
905
+ for (const block of blocks) visitBlock(block);
906
+ return { objects, divergences };
907
+ }
908
+
909
+ function getDrawingFramePreserveHint(
910
+ inline: Extract<InlineNode, { type: "drawing_frame" }>,
911
+ ): PreserveOnlyObjectSizing | undefined {
912
+ const content = inline.content;
913
+ if (content.type === "picture") return undefined;
914
+ return content.preserveOnlyObject;
915
+ }
916
+
917
+ function collectDrawingRelationshipIds(
918
+ inline: Extract<InlineNode, { type: "drawing_frame" }>,
919
+ ): string[] {
920
+ const content = inline.content;
921
+ if (content.type === "picture") return [content.blipRef];
922
+ const preserveIds = content.preserveOnlyObject?.relationshipIds ?? [];
923
+ return [...preserveIds];
924
+ }
925
+
926
+ function getDrawingFrameObjectId(
927
+ inline: Extract<InlineNode, { type: "drawing_frame" }>,
928
+ storyKey: string,
929
+ ordinal: number,
930
+ ): string {
931
+ if (inline.anchor.docPr?.id) return `${storyKey}:drawing-${inline.anchor.docPr.id}`;
932
+ if (inline.content.type === "picture") return `${storyKey}:picture-${inline.content.blipRef}`;
933
+ return `${storyKey}:drawing-${ordinal}`;
934
+ }
935
+
936
+ function getPreserveOnlyObjectId(
937
+ inline: Extract<
938
+ InlineNode,
939
+ { type: "chart_preview" | "smartart_preview" | "shape" | "wordart" | "vml_shape" }
940
+ >,
941
+ storyKey: string,
942
+ ordinal: number,
943
+ ): string {
944
+ return inline.preserveOnlyObject?.sourceId ?? `${storyKey}:${inline.type}-${ordinal}`;
945
+ }
946
+
947
+ function sourceTypeForInlineObject(
948
+ type: Extract<
949
+ InlineNode,
950
+ { type: "chart_preview" | "smartart_preview" | "shape" | "wordart" | "vml_shape" }
951
+ >["type"],
952
+ ): RuntimeStoryAnchoredObject["sourceType"] {
953
+ switch (type) {
954
+ case "chart_preview":
955
+ return "chart-preview";
956
+ case "smartart_preview":
957
+ return "smartart-preview";
958
+ case "shape":
959
+ return "shape";
960
+ case "wordart":
961
+ return "wordart";
962
+ case "vml_shape":
963
+ return "vml-shape";
964
+ }
965
+ }
966
+
967
+ function extentTwipsFromEmu(
968
+ widthEmu: number,
969
+ heightEmu: number,
970
+ ): RuntimeStoryAnchoredObject["extentTwips"] {
971
+ return {
972
+ widthTwips: Math.max(0, Math.round(widthEmu / EMUS_PER_TWIP)),
973
+ heightTwips: Math.max(0, Math.round(heightEmu / EMUS_PER_TWIP)),
974
+ };
975
+ }
976
+
977
+ function resolvePageInstanceFieldDisplayText(
978
+ family: string,
979
+ cachedDisplayText: string,
980
+ context: PageFieldResolutionInput,
981
+ ): string {
982
+ switch (family) {
983
+ case "PAGE":
984
+ return formatPageNumberWithChapter(
985
+ context.displayPageNumber,
986
+ context.layout.pageNumbering,
987
+ );
988
+ case "NUMPAGES":
989
+ return formatPageNumber(
990
+ context.pageFieldCounts.contentPageCount,
991
+ context.layout.pageNumbering?.format,
992
+ );
993
+ case "SECTIONPAGES":
994
+ return formatPageNumber(
995
+ context.pageFieldCounts.sectionContentPageCountByIndex.get(context.sectionIndex) ?? 0,
996
+ context.layout.pageNumbering?.format,
997
+ );
998
+ default:
999
+ return cachedDisplayText;
1000
+ }
1001
+ }
1002
+
1003
+ function flattenInline(inlines: ReadonlyArray<InlineNode> | undefined): string {
1004
+ if (!inlines) return "";
1005
+ let buf = "";
1006
+ for (const inline of inlines) {
1007
+ if (inline.type === "text") buf += inline.text;
1008
+ else if (inline.type === "hard_break" || inline.type === "tab") buf += " ";
1009
+ }
1010
+ return buf;
1011
+ }
1012
+
1013
+ function classifyFieldInstructionLocal(instr: string): string {
1014
+ const match = /^\s*(\w+)/.exec(instr);
1015
+ return match ? match[1]!.toUpperCase() : "UNKNOWN";
1016
+ }
1017
+
1018
+ function isHeaderFooterStoryTarget(
1019
+ target: EditorStoryTarget | undefined,
1020
+ ): target is HeaderFooterStoryTarget {
1021
+ return target?.kind === "header" || target?.kind === "footer";
409
1022
  }
410
1023
 
411
1024
  function buildPageFrameId(
@@ -424,6 +1037,7 @@ function buildPageFrameSignature(
424
1037
  layout: PageLayoutSnapshot;
425
1038
  regions: RuntimePageRegions;
426
1039
  },
1040
+ pageLocalStories: readonly RuntimePageLocalStoryInstance[],
427
1041
  divergenceIds: readonly string[],
428
1042
  ): string {
429
1043
  const regionParts = collectRegions(input.regions).map((region) => {
@@ -445,6 +1059,7 @@ function buildPageFrameSignature(
445
1059
  input.layout.pageWidth,
446
1060
  input.layout.pageHeight,
447
1061
  ...regionParts,
1062
+ ...pageLocalStories.map((story) => story.signature),
448
1063
  ...divergenceIds,
449
1064
  ].join("|");
450
1065
  }
@@ -654,10 +1269,11 @@ export function spliceGraph(
654
1269
  }));
655
1270
 
656
1271
  const contentPageCount = nextPages.filter((p) => !p.isBlankFiller).length;
1272
+ const normalizedPages = normalizePageLocalStoryFieldsForPages(nextPages);
657
1273
 
658
1274
  return {
659
1275
  revision: graphRevision,
660
- pages: nextPages,
1276
+ pages: normalizedPages,
661
1277
  fragments: mergedFragments,
662
1278
  anchors,
663
1279
  sections: [...prior.sections],
@@ -665,6 +1281,77 @@ export function spliceGraph(
665
1281
  };
666
1282
  }
667
1283
 
1284
+ function normalizePageLocalStoryFieldsForPages(
1285
+ pages: readonly RuntimePageNode[],
1286
+ ): RuntimePageNode[] {
1287
+ const pageFieldCounts = buildPageFieldCounts(pages);
1288
+ return pages.map((page) => {
1289
+ const frame = page.frame;
1290
+ if (!frame || frame.pageLocalStories.every((story) => story.resolvedFields.length === 0)) {
1291
+ return page;
1292
+ }
1293
+
1294
+ let changed = false;
1295
+ const pageLocalStories = frame.pageLocalStories.map((story) => {
1296
+ if (story.resolvedFields.length === 0) return story;
1297
+ let storyChanged = false;
1298
+ const resolvedFields = story.resolvedFields.map((field) => {
1299
+ const displayText = resolvePageInstanceFieldDisplayText(
1300
+ field.family,
1301
+ field.displayText,
1302
+ {
1303
+ storyKey: story.storyKey,
1304
+ pageIndex: page.pageIndex,
1305
+ sectionIndex: page.sectionIndex,
1306
+ displayPageNumber: page.stories.displayPageNumber,
1307
+ layout: page.layout,
1308
+ pageFieldCounts,
1309
+ },
1310
+ );
1311
+ if (displayText === field.displayText) return field;
1312
+ storyChanged = true;
1313
+ changed = true;
1314
+ return { ...field, displayText };
1315
+ });
1316
+ if (!storyChanged) return story;
1317
+ const sectionPart =
1318
+ story.sectionIndex === undefined ? "section-unknown" : `section-${story.sectionIndex}`;
1319
+ return {
1320
+ ...story,
1321
+ resolvedFields,
1322
+ signature: buildPageLocalStorySignature({
1323
+ kind: story.kind,
1324
+ variant: story.variant,
1325
+ relationshipId: story.relationshipId,
1326
+ sectionPart,
1327
+ measuredFrameHeightTwips: story.measuredFrameHeightTwips,
1328
+ resolvedFields,
1329
+ anchoredObjects: story.anchoredObjects,
1330
+ }),
1331
+ };
1332
+ });
1333
+
1334
+ if (!changed) return page;
1335
+ const divergenceIds = frame.divergenceIds;
1336
+ const nextFrame: RuntimePageFrame = {
1337
+ ...frame,
1338
+ pageLocalStories,
1339
+ signature: buildPageFrameSignature(
1340
+ {
1341
+ pageIndex: frame.pageIndex,
1342
+ sectionIndex: frame.sectionIndex,
1343
+ displayPageNumber: frame.displayPageNumber,
1344
+ layout: page.layout,
1345
+ regions: page.regions,
1346
+ },
1347
+ pageLocalStories,
1348
+ divergenceIds,
1349
+ ),
1350
+ };
1351
+ return { ...page, frame: nextFrame };
1352
+ });
1353
+ }
1354
+
668
1355
  /**
669
1356
  * Compare two `RuntimePageNode`s by the fields that matter for
670
1357
  * pagination identity — if these all match, the fresh node represents