@beyondwork/docx-react-component 1.0.103 → 1.0.105

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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +66 -1
  3. package/src/api/v3/_runtime-handle.ts +2 -0
  4. package/src/api/v3/ai/_pe2-evidence.ts +153 -0
  5. package/src/api/v3/ai/bundle.ts +13 -5
  6. package/src/api/v3/ai/inspect.ts +7 -1
  7. package/src/api/v3/ai/outline.ts +2 -7
  8. package/src/api/v3/ai/replacement.ts +113 -0
  9. package/src/api/v3/runtime/geometry.ts +79 -0
  10. package/src/api/v3/ui/_types.ts +86 -0
  11. package/src/api/v3/ui/index.ts +5 -0
  12. package/src/api/v3/ui/overlays.ts +104 -0
  13. package/src/io/ooxml/parse-drawing.ts +99 -1
  14. package/src/io/ooxml/parse-fields.ts +27 -6
  15. package/src/io/ooxml/parse-shapes.ts +130 -0
  16. package/src/model/canonical-document.ts +34 -3
  17. package/src/model/canonical-layout-inputs.ts +979 -0
  18. package/src/model/layout/index.ts +9 -0
  19. package/src/model/layout/page-graph-types.ts +150 -0
  20. package/src/model/layout/runtime-page-graph-types.ts +23 -0
  21. package/src/runtime/collab/runtime-collab-sync.ts +3 -3
  22. package/src/runtime/debug/build-debug-inspector-snapshot.ts +17 -4
  23. package/src/runtime/document-runtime.ts +30 -14
  24. package/src/runtime/event-refresh-hints.ts +35 -5
  25. package/src/runtime/formatting/formatting-context.ts +110 -9
  26. package/src/runtime/formatting/index.ts +2 -0
  27. package/src/runtime/formatting/layout-inputs.ts +67 -3
  28. package/src/runtime/geometry/caret-geometry.ts +82 -10
  29. package/src/runtime/geometry/geometry-facet.ts +44 -0
  30. package/src/runtime/geometry/geometry-index.ts +1268 -0
  31. package/src/runtime/geometry/geometry-types.ts +227 -1
  32. package/src/runtime/geometry/index.ts +26 -0
  33. package/src/runtime/geometry/inert-geometry-facet.ts +3 -0
  34. package/src/runtime/geometry/object-handles.ts +7 -4
  35. package/src/runtime/geometry/replacement-envelope.ts +41 -2
  36. package/src/runtime/layout/layout-engine-instance.ts +2 -0
  37. package/src/runtime/layout/layout-engine-version.ts +44 -1
  38. package/src/runtime/layout/page-graph.ts +877 -2
  39. package/src/runtime/layout/project-block-fragments.ts +101 -1
  40. package/src/runtime/layout/public-facet.ts +152 -0
  41. package/src/runtime/prerender/graph-canonicalize.ts +44 -0
  42. package/src/runtime/surface-projection.ts +43 -3
  43. package/src/runtime/workflow/coordinator.ts +57 -11
  44. package/src/ui/ui-controller-factory.ts +11 -0
  45. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +3 -0
@@ -59,6 +59,15 @@
59
59
  export type {
60
60
  RuntimePageRegions,
61
61
  RuntimePageRegion,
62
+ RuntimeTwipsRect,
63
+ RuntimeResolvedRegions,
64
+ RuntimeExclusionZone,
65
+ RuntimeLayoutDivergenceKind,
66
+ RuntimeLayoutDivergence,
67
+ RuntimePageFrame,
68
+ RuntimePageLocalStoryInstance,
69
+ RuntimeResolvedStoryField,
70
+ RuntimeStoryAnchoredObject,
62
71
  RuntimeBlockFragment,
63
72
  RuntimeLineBox,
64
73
  RuntimeNoteAllocation,
@@ -48,6 +48,17 @@ export interface RuntimePageRegions {
48
48
  footnotes?: RuntimePageRegion[];
49
49
  }
50
50
 
51
+ export interface RuntimeTwipsRect {
52
+ /** Twips offset from the physical page left edge. */
53
+ xTwips: number;
54
+ /** Twips offset from the physical page top edge. */
55
+ yTwips: number;
56
+ /** Width in twips. */
57
+ widthTwips: number;
58
+ /** Height in twips. */
59
+ heightTwips: number;
60
+ }
61
+
51
62
  export interface RuntimePageRegion {
52
63
  kind: "body" | "header" | "footer" | "column" | "footnote-area";
53
64
  /** Twips offset from page top (header) or similar region-specific origin. */
@@ -56,10 +67,141 @@ export interface RuntimePageRegion {
56
67
  widthTwips: number;
57
68
  /** Height in twips. */
58
69
  heightTwips: number;
70
+ /**
71
+ * PE2 Slice 2 — explicit twips-space frame for this region.
72
+ * This is semantic layout geometry, not rendered pixel geometry.
73
+ */
74
+ rectTwips?: RuntimeTwipsRect;
59
75
  /** IDs of block fragments rendered in this region, in order. */
60
76
  fragmentIds: string[];
61
77
  }
62
78
 
79
+ export interface RuntimeResolvedRegions {
80
+ body: RuntimePageRegion;
81
+ header?: RuntimePageRegion;
82
+ footer?: RuntimePageRegion;
83
+ columns?: RuntimePageRegion[];
84
+ footnotes?: RuntimePageRegion[];
85
+ exclusionZones: RuntimeExclusionZone[];
86
+ }
87
+
88
+ export interface RuntimeExclusionZone {
89
+ rectTwips: RuntimeTwipsRect;
90
+ objectId: string;
91
+ wrapMode: string;
92
+ layoutInCell?: boolean;
93
+ }
94
+
95
+ export type RuntimeLayoutDivergenceKind =
96
+ | "frame-collision"
97
+ | "intentional-overflow"
98
+ | "unsupported-wrap"
99
+ | "stale-field-label"
100
+ | "preserve-only-placeholder";
101
+
102
+ export interface RuntimeLayoutDivergence {
103
+ divergenceId: string;
104
+ kind: RuntimeLayoutDivergenceKind;
105
+ source: "runtime";
106
+ severity: "info" | "warning" | "error";
107
+ message: string;
108
+ regionKinds?: RuntimePageRegion["kind"][];
109
+ fragmentIds?: string[];
110
+ objectIds?: string[];
111
+ }
112
+
113
+ export interface RuntimePageFrame {
114
+ frameId: string;
115
+ pageId: string;
116
+ pageIndex: number;
117
+ sectionIndex: number;
118
+ displayPageNumber: number;
119
+ physicalBoundsTwips: RuntimeTwipsRect;
120
+ regions: RuntimeResolvedRegions;
121
+ pageLocalStories: RuntimePageLocalStoryInstance[];
122
+ divergenceIds: string[];
123
+ signature: string;
124
+ }
125
+
126
+ export interface RuntimePageLocalStoryInstance {
127
+ instanceId: string;
128
+ storyKey: string;
129
+ pageId: string;
130
+ kind: "header" | "footer";
131
+ variant: "default" | "first" | "even" | "odd";
132
+ relationshipId: string;
133
+ sectionIndex?: number;
134
+ resolvedFields: RuntimeResolvedStoryField[];
135
+ anchoredObjects: RuntimeStoryAnchoredObject[];
136
+ measuredFrameHeightTwips: number;
137
+ signature: string;
138
+ }
139
+
140
+ export interface RuntimeResolvedStoryField {
141
+ fieldId: string;
142
+ family: string;
143
+ displayText: string;
144
+ }
145
+
146
+ export interface RuntimeStoryAnchoredObject {
147
+ objectId: string;
148
+ sourceType:
149
+ | "image"
150
+ | "drawing-frame"
151
+ | "chart-preview"
152
+ | "smartart-preview"
153
+ | "shape"
154
+ | "wordart"
155
+ | "vml-shape"
156
+ | "ole-embed"
157
+ | "opaque-inline";
158
+ display: "inline" | "floating" | "unknown";
159
+ extentTwips?: {
160
+ widthTwips: number;
161
+ heightTwips: number;
162
+ };
163
+ relationshipIds?: readonly string[];
164
+ preserveOnly: boolean;
165
+ divergenceIds: string[];
166
+ }
167
+
168
+ export type RuntimeLayoutContinuationCursor =
169
+ | RuntimeParagraphContinuationCursor
170
+ | RuntimeTableContinuationCursor;
171
+
172
+ export interface RuntimeParagraphContinuationCursor {
173
+ kind: "paragraph";
174
+ sequenceIndex: number;
175
+ sliceCount: number;
176
+ lineRange: {
177
+ from: number;
178
+ to: number;
179
+ totalLines: number;
180
+ };
181
+ continuesFromPreviousPage: boolean;
182
+ continuesToNextPage: boolean;
183
+ }
184
+
185
+ export interface RuntimeTableContinuationCursor {
186
+ kind: "table";
187
+ sequenceIndex: number;
188
+ sliceCount: number;
189
+ rowRange: {
190
+ from: number;
191
+ to: number;
192
+ totalRows: number;
193
+ };
194
+ continuesFromPreviousPage: boolean;
195
+ continuesToNextPage: boolean;
196
+ repeatedHeaderRowIndexes: readonly number[];
197
+ verticalMergeCarry: readonly RuntimeTableVerticalMergeCarry[];
198
+ }
199
+
200
+ export interface RuntimeTableVerticalMergeCarry {
201
+ columnIndex: number;
202
+ restartRowIndex: number;
203
+ }
204
+
63
205
  // ---------------------------------------------------------------------------
64
206
  // Fragment types
65
207
  // ---------------------------------------------------------------------------
@@ -115,6 +257,14 @@ export interface RuntimeBlockFragment {
115
257
  to: number;
116
258
  totalRows: number;
117
259
  };
260
+ /**
261
+ * PE2 Slice 3 — typed continuation cursor for fragments that render a
262
+ * logical block over multiple page/column frames. Existing slice fields
263
+ * remain for compatibility; this cursor makes repeated-header,
264
+ * line/row-range, and vertical-merge carry explicit for debug/render
265
+ * consumers without reading DOM or canonical tables again.
266
+ */
267
+ continuation?: RuntimeLayoutContinuationCursor;
118
268
  /**
119
269
  * Slice 5 — opaque style-chain ref derived from the block's `styleId`.
120
270
  * Used by `analyzeStylesChange` to bound invalidation to the first page
@@ -14,11 +14,17 @@
14
14
  * layer (`src/runtime/layout/page-graph.ts`).
15
15
  */
16
16
 
17
+ import type {
18
+ FooterDocument,
19
+ HeaderDocument,
20
+ } from "../canonical-document.ts";
17
21
  import type {
18
22
  RuntimeBlockFragment,
23
+ RuntimeLayoutDivergence,
19
24
  RuntimeLineBox,
20
25
  RuntimeNoteAllocation,
21
26
  RuntimePageAnchor,
27
+ RuntimePageFrame,
22
28
  RuntimePageRegions,
23
29
  } from "./page-graph-types.ts";
24
30
  import type {
@@ -63,6 +69,14 @@ export interface RuntimePageNode {
63
69
  stories: ResolvedPageStories;
64
70
  /** Sub-regions on the page. */
65
71
  regions: RuntimePageRegions;
72
+ /**
73
+ * PE2 Slice 2 — page-level frame record for consumers that need a
74
+ * single immutable twips-space layout payload instead of reconstructing
75
+ * it from `layout` + `regions`.
76
+ */
77
+ frame?: RuntimePageFrame;
78
+ /** Typed layout limitations or overlaps detected while building this page. */
79
+ divergences?: RuntimeLayoutDivergence[];
66
80
  /** Line boxes rendered in the body region. */
67
81
  lineBoxes: RuntimeLineBox[];
68
82
  /** Footnote allocations reserved at the bottom of the page. */
@@ -105,4 +119,13 @@ export interface BuildPageGraphInput {
105
119
  * `page-${revision}-${index}` pageId in advance.
106
120
  */
107
121
  noteAllocationsByPageIndex?: ReadonlyMap<number, RuntimeNoteAllocation[]>;
122
+ /**
123
+ * Optional canonical header/footer parts. When present, `buildPageGraph`
124
+ * resolves page-instance fields inside page-local header/footer story
125
+ * records without making render/UI consumers re-walk subparts.
126
+ */
127
+ subParts?: {
128
+ headers?: ReadonlyArray<HeaderDocument>;
129
+ footers?: ReadonlyArray<FooterDocument>;
130
+ };
108
131
  }
@@ -473,11 +473,11 @@ export function createRuntimeCollabSync(
473
473
  // For a late joiner it may already be populated — the seed ensures the
474
474
  // runtime reflects pre-existing shared state at attach time (i.e. if a
475
475
  // peer already set `lockedMode`, the new peer starts out locked).
476
- runtime.setSharedWorkflowState(workflowShared.get());
476
+ runtime.setSharedWorkflowState(workflowShared.get(), { source: "collab" });
477
477
 
478
478
  const workflowUnsub = workflowShared.subscribe((state) => {
479
479
  if (readOnly) return; // don't propagate while in read-only (post-mismatch)
480
- runtime.setSharedWorkflowState(state);
480
+ runtime.setSharedWorkflowState(state, { source: "collab" });
481
481
  });
482
482
 
483
483
  const unsubscribeCommandApplied = commandAppliedBridge.subscribe((command, _transaction, context, meta) => {
@@ -607,7 +607,7 @@ export function createRuntimeCollabSync(
607
607
  yCheckpoints.unobserve(onCheckpointsChange);
608
608
  workflowUnsub();
609
609
  workflowShared.destroy();
610
- runtime.setSharedWorkflowState(null);
610
+ runtime.setSharedWorkflowState(null, { source: "runtime" });
611
611
  listeners.clear();
612
612
  // R3 — abort the current remote-activity signal so any lingering
613
613
  // idle consumers detect the shutdown.
@@ -576,13 +576,23 @@ function projectLayout(
576
576
  runtime: DocumentRuntime,
577
577
  fallbacks: string[],
578
578
  ): DebugInspectorLayoutSection {
579
- const pageLayout = safeCall(
579
+ const graphPageCount = safeCall(
580
580
  runtime,
581
- (r) => r.getPageLayoutSnapshot(),
581
+ (r) => r.getLayoutFacet().getPageCount(),
582
582
  null,
583
583
  fallbacks,
584
- "page-layout-summary",
584
+ "layout-page-count",
585
585
  );
586
+ const pageLayout =
587
+ graphPageCount === null
588
+ ? safeCall(
589
+ runtime,
590
+ (r) => r.getPageLayoutSnapshot(),
591
+ null,
592
+ fallbacks,
593
+ "page-layout-summary",
594
+ )
595
+ : null;
586
596
  const sections = safeCall(runtime, (r) => r.getSections(), [], fallbacks, "sections");
587
597
  const stories = safeCall(
588
598
  runtime,
@@ -592,7 +602,10 @@ function projectLayout(
592
602
  "document-text-stream",
593
603
  );
594
604
  return {
595
- pageCount: (pageLayout as { pages?: unknown[] } | null)?.pages?.length ?? null,
605
+ pageCount:
606
+ graphPageCount ??
607
+ (pageLayout as { pages?: unknown[] } | null)?.pages?.length ??
608
+ null,
596
609
  sectionCount: sections.length,
597
610
  storyCount: stories.length,
598
611
  };
@@ -82,6 +82,7 @@ import type {
82
82
  WorkflowCandidateRange,
83
83
  WorkflowCandidateRangeOptions,
84
84
  WorkflowBlockedCommandReason,
85
+ WorkflowEventOrigin,
85
86
  WorkflowMetadataDefinition,
86
87
  OverlayKind,
87
88
  OverlayVisibilityPolicy,
@@ -764,7 +765,10 @@ export interface DocumentRuntime {
764
765
  setWorkflowOverlay(overlay: WorkflowOverlay): void;
765
766
  clearWorkflowOverlay(): void;
766
767
  getWorkflowOverlay(): WorkflowOverlay | null;
767
- setSharedWorkflowState(state: SharedWorkflowState | null): void;
768
+ setSharedWorkflowState(
769
+ state: SharedWorkflowState | null,
770
+ origin?: WorkflowEventOrigin,
771
+ ): void;
768
772
  getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
769
773
  getInteractionGuardSnapshot(): InteractionGuardSnapshot;
770
774
  getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
@@ -1880,6 +1884,19 @@ export function createDocumentRuntime(
1880
1884
  nextActiveStory: EditorStoryTarget,
1881
1885
  ): DocumentNavigationSnapshot {
1882
1886
  const activeStoryKey = storyTargetKey(nextActiveStory);
1887
+ const buildSnapshot = (): DocumentNavigationSnapshot =>
1888
+ layoutEngine.getNavigationSnapshot(
1889
+ {
1890
+ document: nextState.document,
1891
+ viewState: {
1892
+ activeStory: nextActiveStory,
1893
+ workspaceMode: viewState.workspaceMode,
1894
+ zoomLevel: viewState.zoomLevel,
1895
+ },
1896
+ },
1897
+ nextState.selection,
1898
+ nextActiveStory,
1899
+ );
1883
1900
  if (
1884
1901
  cachedNavigation &&
1885
1902
  cachedNavigation.revisionToken === nextState.revisionToken &&
@@ -1889,11 +1906,7 @@ export function createDocumentRuntime(
1889
1906
  return cachedNavigation.snapshot;
1890
1907
  }
1891
1908
 
1892
- const snapshot = createDocumentNavigationSnapshot(
1893
- nextState.document,
1894
- nextState.selection.head,
1895
- nextActiveStory,
1896
- );
1909
+ const snapshot = buildSnapshot();
1897
1910
  if (
1898
1911
  snapshot.activePageIndex === cachedNavigation.snapshot.activePageIndex &&
1899
1912
  snapshot.activeSectionIndex === cachedNavigation.snapshot.activeSectionIndex
@@ -1915,11 +1928,7 @@ export function createDocumentRuntime(
1915
1928
  return snapshot;
1916
1929
  }
1917
1930
 
1918
- const snapshot = createDocumentNavigationSnapshot(
1919
- nextState.document,
1920
- nextState.selection.head,
1921
- nextActiveStory,
1922
- );
1931
+ const snapshot = buildSnapshot();
1923
1932
  recordPerfSample("snapshot.navigation");
1924
1933
  incrementInvalidationCounter("runtime.snapshot.navigationMisses");
1925
1934
  cachedNavigation = {
@@ -4560,6 +4569,7 @@ export function createDocumentRuntime(
4560
4569
  // P5 — TOC entries print Word's display number (honors page-
4561
4570
  // number restarts), not the raw 0-based pageIndex+1.
4562
4571
  (pageIndex: number) => layoutFacet.getDisplayPageNumber(pageIndex),
4572
+ getCachedDocumentNavigationSnapshot(state, activeStory),
4563
4573
  );
4564
4574
  if (refreshed.changed) {
4565
4575
  this.dispatch({
@@ -4709,8 +4719,8 @@ export function createDocumentRuntime(
4709
4719
  getWorkflowOverlay() {
4710
4720
  return workflowCoordinator.getWorkflowOverlay();
4711
4721
  },
4712
- setSharedWorkflowState(sharedState) {
4713
- workflowCoordinator.setSharedWorkflowState(sharedState);
4722
+ setSharedWorkflowState(sharedState, origin) {
4723
+ workflowCoordinator.setSharedWorkflowState(sharedState, origin);
4714
4724
  },
4715
4725
  getWorkflowScopeSnapshot() {
4716
4726
  return workflowCoordinator.getWorkflowScopeSnapshot();
@@ -5935,6 +5945,7 @@ export function createDocumentRuntime(
5935
5945
  activeStory,
5936
5946
  undefined,
5937
5947
  (pageIndex: number) => layoutFacet.getDisplayPageNumber(pageIndex),
5948
+ getCachedDocumentNavigationSnapshot(state, activeStory),
5938
5949
  );
5939
5950
  perfCounters.increment("toc.autoRefresh.us", Math.round((performance.now() - t) * 1000));
5940
5951
  if (!refreshed.changed) {
@@ -6144,6 +6155,9 @@ export function createDocumentRuntime(
6144
6155
  case "story_changed":
6145
6156
  case "workflow_overlay_changed":
6146
6157
  case "workflow_active_work_item_changed":
6158
+ case "workflow_shared_state_changed":
6159
+ case "workflow_visibility_policy_changed":
6160
+ case "workflow_markup_mode_policy_changed":
6147
6161
  case "change_authored":
6148
6162
  case "change_accepted":
6149
6163
  case "change_rejected":
@@ -7482,6 +7496,7 @@ function refreshDocumentTableOfContents(
7482
7496
  activeStory: EditorStoryTarget,
7483
7497
  options?: TocRefreshOptions,
7484
7498
  resolveDisplayPageNumber?: (pageIndex: number) => number | null,
7499
+ navigationSnapshot?: DocumentNavigationSnapshot,
7485
7500
  ): {
7486
7501
  document: CanonicalDocumentEnvelope;
7487
7502
  result: TocRefreshResult;
@@ -7511,7 +7526,8 @@ function refreshDocumentTableOfContents(
7511
7526
  };
7512
7527
  }
7513
7528
 
7514
- const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
7529
+ const navigation =
7530
+ navigationSnapshot ?? createDocumentNavigationSnapshot(document, selectionHead, activeStory);
7515
7531
  // Build a single O(N) map from paragraph offset → bookmark name so the
7516
7532
  // per-heading lookup below is O(1) instead of O(N) per heading.
7517
7533
  const bookmarkNameByOffset = new Map<number, string>();
@@ -25,6 +25,8 @@ export function describeEventImpact(
25
25
  };
26
26
  case "workflow_overlay_changed":
27
27
  case "workflow_active_work_item_changed":
28
+ case "workflow_shared_state_changed":
29
+ case "workflow_markup_mode_policy_changed":
28
30
  return {
29
31
  invalidate: [
30
32
  "workflowScope",
@@ -37,9 +39,27 @@ export function describeEventImpact(
37
39
  staleTargets: ["anchors"],
38
40
  changeKinds: ["workflow"],
39
41
  };
42
+ case "workflow_visibility_policy_changed":
43
+ return {
44
+ invalidate: [
45
+ "workflowScope",
46
+ "workflowMarkup",
47
+ "reviewWork",
48
+ "chunks",
49
+ "contextAnalytics",
50
+ ],
51
+ staleTargets: ["none"],
52
+ changeKinds: ["workflow"],
53
+ };
40
54
  case "workflow_metadata_changed":
41
55
  return {
42
- invalidate: ["workflowMarkup", "render", "locations", "reviewWork", "chunks"],
56
+ invalidate: [
57
+ "workflowMarkup",
58
+ "locations",
59
+ "reviewWork",
60
+ "chunks",
61
+ "contextAnalytics",
62
+ ],
43
63
  staleTargets: ["anchors"],
44
64
  changeKinds: ["workflow"],
45
65
  };
@@ -47,10 +67,6 @@ export function describeEventImpact(
47
67
  case "change_accepted":
48
68
  case "change_rejected":
49
69
  case "suggestion_authored":
50
- case "suggestion_updated":
51
- case "comment_added":
52
- case "comment_resolved":
53
- case "comments_changed":
54
70
  return {
55
71
  invalidate: [
56
72
  "render",
@@ -69,6 +85,20 @@ export function describeEventImpact(
69
85
  staleTargets: ["search_results", "anchors"],
70
86
  changeKinds: ["content", "review", "structure"],
71
87
  };
88
+ case "suggestion_updated":
89
+ return {
90
+ invalidate: ["trackedChanges", "reviewWork", "chunks", "contextAnalytics"],
91
+ staleTargets: ["none"],
92
+ changeKinds: ["review"],
93
+ };
94
+ case "comment_added":
95
+ case "comment_resolved":
96
+ case "comments_changed":
97
+ return {
98
+ invalidate: ["comments", "reviewWork", "chunks", "contextAnalytics"],
99
+ staleTargets: ["none"],
100
+ changeKinds: ["review"],
101
+ };
72
102
  case "warning_added":
73
103
  case "warning_cleared":
74
104
  case "error":
@@ -46,6 +46,7 @@ import type {
46
46
  TableStyleConditionalRegion,
47
47
  Mutable,
48
48
  } from "../../model/canonical-document.ts";
49
+ import { collectCanonicalFieldRegionIdentities } from "../../model/canonical-layout-inputs.ts";
49
50
  import type { FieldPageGraph, FieldResolver, ResolvedField } from "./field/resolver.ts";
50
51
  import { createFieldResolver } from "./field/resolver.ts";
51
52
  import { ThemeColorResolver, concretizeThemeColors } from "./theme-color.ts";
@@ -68,13 +69,21 @@ import type {
68
69
  EffectiveNumbering,
69
70
  EffectiveParagraphFormatting,
70
71
  EffectiveRunFormatting,
72
+ RevisionDisplayFlags,
71
73
  } from "./formatting-types.ts";
72
74
  import {
75
+ buildEffectiveLayoutFormatting,
76
+ buildRevisionLayoutPosture,
73
77
  collectFieldLayoutInputs,
78
+ mergeLayoutTabStops,
74
79
  toFieldLayoutInput,
80
+ toLayoutTabStops,
75
81
  toNumberingLayoutInput,
82
+ type EffectiveLayoutFormatting,
76
83
  type FieldLayoutInput,
84
+ type LayoutTabStopInput,
77
85
  type NumberingLayoutInput,
86
+ type RevisionLayoutPosture,
78
87
  } from "./layout-inputs.ts";
79
88
 
80
89
  /**
@@ -342,6 +351,23 @@ export interface FormattingContext {
342
351
  /** Resolve every field/TOC registry entry into PE2 field-layout inputs. */
343
352
  collectFieldLayoutInputs(): readonly FieldLayoutInput[];
344
353
 
354
+ /** Resolve paragraph tab stops into L04-ready twip/leader inputs. */
355
+ resolveParagraphTabStops(para: ParagraphNode): readonly LayoutTabStopInput[];
356
+
357
+ /**
358
+ * Consolidated PE2 paragraph formatting bundle. This is the L03-owned
359
+ * handoff shape for L04 measurement: paragraph cascade, run formatting,
360
+ * numbering input, paragraph/numbering tabs, compatibility flags, revision
361
+ * posture, and a stable structural hash are resolved in one context call.
362
+ */
363
+ resolveParagraphLayoutFormatting(input: {
364
+ readonly paragraph: ParagraphNode;
365
+ readonly runs?: readonly EffectiveRunFormatting[];
366
+ readonly revisionDisplays?: readonly RevisionDisplayFlags[];
367
+ readonly revisionMode?: RevisionLayoutPosture["mode"];
368
+ readonly compatFlags?: readonly string[];
369
+ }): EffectiveLayoutFormatting;
370
+
345
371
  /** Resolve the effective font family for a run following
346
372
  * ECMA-376 §17.3.2.26 precedence + theme-minor fallback. */
347
373
  resolveFontFamily(input: RunResolveInput, themeMinorFont?: string): string | undefined;
@@ -637,9 +663,54 @@ class FormattingContextImpl implements FormattingContext {
637
663
  collectFieldLayoutInputs(): readonly FieldLayoutInput[] {
638
664
  return collectFieldLayoutInputs(this.doc.fieldRegistry, (entry) =>
639
665
  this.resolveField(entry),
666
+ collectCanonicalFieldRegionIdentities(this.doc),
640
667
  );
641
668
  }
642
669
 
670
+ resolveParagraphTabStops(para: ParagraphNode): readonly LayoutTabStopInput[] {
671
+ return toLayoutTabStops(this.resolveParagraphCascade(para).tabStops, "paragraph");
672
+ }
673
+
674
+ resolveParagraphLayoutFormatting(input: {
675
+ readonly paragraph: ParagraphNode;
676
+ readonly runs?: readonly EffectiveRunFormatting[];
677
+ readonly revisionDisplays?: readonly RevisionDisplayFlags[];
678
+ readonly revisionMode?: RevisionLayoutPosture["mode"];
679
+ readonly compatFlags?: readonly string[];
680
+ }): EffectiveLayoutFormatting {
681
+ const numberingDetail = this.resolveParagraphNumbering(input.paragraph, { advance: true });
682
+ const numbering = toNumberingLayoutInput(numberingDetail);
683
+ const paragraph = buildEffectiveParagraphFormatting(
684
+ this.resolveParagraphCascade(input.paragraph),
685
+ numberingDetail,
686
+ this.theme,
687
+ );
688
+ const runs = input.runs ?? [];
689
+ const revisionDisplays =
690
+ input.revisionDisplays ??
691
+ runs
692
+ .map((run) => run.revisionDisplay)
693
+ .filter((display): display is RevisionDisplayFlags => display !== undefined);
694
+ const revisionMode = input.revisionMode ?? revisionDisplays[0]?.markupMode;
695
+ const revisionPosture = revisionMode
696
+ ? buildRevisionLayoutPosture(revisionMode, revisionDisplays)
697
+ : undefined;
698
+ return buildEffectiveLayoutFormatting({
699
+ paragraph,
700
+ runs,
701
+ ...(numbering ? { numbering } : {}),
702
+ tabs: mergeLayoutTabStops(
703
+ this.resolveParagraphTabStops(input.paragraph),
704
+ numbering?.associatedTabStops,
705
+ ),
706
+ compatFlags: mergeCompatFlags(
707
+ collectParagraphCompatFlags(paragraph),
708
+ input.compatFlags ?? [],
709
+ ),
710
+ ...(revisionPosture ? { revisionPosture } : {}),
711
+ });
712
+ }
713
+
643
714
  resolveFontFamily(
644
715
  input: RunResolveInput,
645
716
  themeMinorFont?: string,
@@ -655,17 +726,11 @@ class FormattingContextImpl implements FormattingContext {
655
726
 
656
727
  resolveParagraph(para: ParagraphNode): EffectiveParagraphFormatting {
657
728
  const cascade = this.resolveParagraphCascade(para);
658
- const numbering = buildEffectiveNumbering(
729
+ return buildEffectiveParagraphFormatting(
730
+ cascade,
659
731
  this.resolveParagraphNumbering(para, { advance: true }),
732
+ this.theme,
660
733
  );
661
- const paragraphMarkRun = cascade.paragraphMarkRunProperties && this.theme
662
- ? (concretizeThemeColors(cascade.paragraphMarkRunProperties, this.theme) as EffectiveRunFormatting)
663
- : (cascade.paragraphMarkRunProperties as EffectiveRunFormatting | undefined);
664
- return {
665
- ...cascade,
666
- ...(numbering ? { numbering } : {}),
667
- ...(paragraphMarkRun ? { paragraphMarkRun } : {}),
668
- };
669
734
  }
670
735
 
671
736
  resolveRunWithProvenance(input: {
@@ -910,6 +975,42 @@ function buildEffectiveNumbering(
910
975
  return result;
911
976
  }
912
977
 
978
+ function buildEffectiveParagraphFormatting(
979
+ cascade: CanonicalParagraphFormatting,
980
+ numberingDetail: NumberingResolution | null,
981
+ theme: ThemeColorResolver | undefined,
982
+ ): EffectiveParagraphFormatting {
983
+ const numbering = buildEffectiveNumbering(numberingDetail);
984
+ const paragraphMarkRun = cascade.paragraphMarkRunProperties && theme
985
+ ? (concretizeThemeColors(cascade.paragraphMarkRunProperties, theme) as EffectiveRunFormatting)
986
+ : (cascade.paragraphMarkRunProperties as EffectiveRunFormatting | undefined);
987
+ return {
988
+ ...cascade,
989
+ ...(numbering ? { numbering } : {}),
990
+ ...(paragraphMarkRun ? { paragraphMarkRun } : {}),
991
+ };
992
+ }
993
+
994
+ function collectParagraphCompatFlags(
995
+ paragraph: EffectiveParagraphFormatting,
996
+ ): readonly string[] {
997
+ const flags: string[] = [];
998
+ if (paragraph.keepNext === true) flags.push("keepNext");
999
+ if (paragraph.keepLines === true) flags.push("keepLines");
1000
+ if (paragraph.pageBreakBefore === true) flags.push("pageBreakBefore");
1001
+ if (paragraph.contextualSpacing === true) flags.push("contextualSpacing");
1002
+ if (paragraph.widowControl === false) flags.push("widowControlDisabled");
1003
+ if (paragraph.bidi === true) flags.push("bidi");
1004
+ if (paragraph.suppressLineNumbers === true) flags.push("suppressLineNumbers");
1005
+ return flags;
1006
+ }
1007
+
1008
+ function mergeCompatFlags(
1009
+ ...groups: readonly (readonly string[] | undefined)[]
1010
+ ): readonly string[] {
1011
+ return [...new Set(groups.flatMap((group) => group ?? []))].sort();
1012
+ }
1013
+
913
1014
  // Type-only re-export of HyperlinkNode so callers can type their walk.
914
1015
  export type { HyperlinkNode, InlineNode };
915
1016
 
@@ -109,7 +109,9 @@ export {
109
109
  buildRevisionLayoutPosture,
110
110
  collectFieldLayoutInputs,
111
111
  createStructuralHash,
112
+ mergeLayoutTabStops,
112
113
  normalizeNumberingMarkerSuffix,
114
+ toMeasuredRevisionLayoutPosture,
113
115
  toFieldLayoutInput,
114
116
  toLayoutTabStops,
115
117
  toNumberingLayoutInput,