@beyondwork/docx-react-component 1.0.103 → 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.
- package/package.json +1 -1
- package/src/api/public-types.ts +63 -1
- package/src/api/v3/_runtime-handle.ts +2 -0
- package/src/api/v3/ai/outline.ts +2 -7
- package/src/api/v3/runtime/geometry.ts +79 -0
- package/src/io/ooxml/parse-drawing.ts +99 -1
- package/src/io/ooxml/parse-fields.ts +27 -6
- package/src/io/ooxml/parse-shapes.ts +130 -0
- package/src/model/canonical-document.ts +34 -3
- package/src/model/canonical-layout-inputs.ts +979 -0
- package/src/model/layout/index.ts +6 -0
- package/src/model/layout/page-graph-types.ts +61 -0
- package/src/model/layout/runtime-page-graph-types.ts +10 -0
- package/src/runtime/collab/runtime-collab-sync.ts +3 -3
- package/src/runtime/debug/build-debug-inspector-snapshot.ts +17 -4
- package/src/runtime/document-runtime.ts +30 -14
- package/src/runtime/event-refresh-hints.ts +3 -0
- package/src/runtime/formatting/formatting-context.ts +110 -9
- package/src/runtime/formatting/index.ts +2 -0
- package/src/runtime/formatting/layout-inputs.ts +67 -3
- package/src/runtime/geometry/caret-geometry.ts +82 -10
- package/src/runtime/geometry/geometry-facet.ts +36 -0
- package/src/runtime/geometry/geometry-index.ts +891 -0
- package/src/runtime/geometry/geometry-types.ts +221 -1
- package/src/runtime/geometry/index.ts +26 -0
- package/src/runtime/geometry/inert-geometry-facet.ts +3 -0
- package/src/runtime/geometry/replacement-envelope.ts +41 -2
- package/src/runtime/layout/layout-engine-version.ts +16 -1
- package/src/runtime/layout/page-graph.ts +191 -1
- package/src/runtime/prerender/graph-canonicalize.ts +30 -0
- package/src/runtime/surface-projection.ts +43 -3
- package/src/runtime/workflow/coordinator.ts +57 -11
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +3 -0
|
@@ -59,6 +59,12 @@
|
|
|
59
59
|
export type {
|
|
60
60
|
RuntimePageRegions,
|
|
61
61
|
RuntimePageRegion,
|
|
62
|
+
RuntimeTwipsRect,
|
|
63
|
+
RuntimeResolvedRegions,
|
|
64
|
+
RuntimeExclusionZone,
|
|
65
|
+
RuntimeLayoutDivergenceKind,
|
|
66
|
+
RuntimeLayoutDivergence,
|
|
67
|
+
RuntimePageFrame,
|
|
62
68
|
RuntimeBlockFragment,
|
|
63
69
|
RuntimeLineBox,
|
|
64
70
|
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,60 @@ 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
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface RuntimePageFrame {
|
|
113
|
+
frameId: string;
|
|
114
|
+
pageId: string;
|
|
115
|
+
pageIndex: number;
|
|
116
|
+
sectionIndex: number;
|
|
117
|
+
displayPageNumber: number;
|
|
118
|
+
physicalBoundsTwips: RuntimeTwipsRect;
|
|
119
|
+
regions: RuntimeResolvedRegions;
|
|
120
|
+
divergenceIds: string[];
|
|
121
|
+
signature: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
63
124
|
// ---------------------------------------------------------------------------
|
|
64
125
|
// Fragment types
|
|
65
126
|
// ---------------------------------------------------------------------------
|
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
import type {
|
|
18
18
|
RuntimeBlockFragment,
|
|
19
|
+
RuntimeLayoutDivergence,
|
|
19
20
|
RuntimeLineBox,
|
|
20
21
|
RuntimeNoteAllocation,
|
|
21
22
|
RuntimePageAnchor,
|
|
23
|
+
RuntimePageFrame,
|
|
22
24
|
RuntimePageRegions,
|
|
23
25
|
} from "./page-graph-types.ts";
|
|
24
26
|
import type {
|
|
@@ -63,6 +65,14 @@ export interface RuntimePageNode {
|
|
|
63
65
|
stories: ResolvedPageStories;
|
|
64
66
|
/** Sub-regions on the page. */
|
|
65
67
|
regions: RuntimePageRegions;
|
|
68
|
+
/**
|
|
69
|
+
* PE2 Slice 2 — page-level frame record for consumers that need a
|
|
70
|
+
* single immutable twips-space layout payload instead of reconstructing
|
|
71
|
+
* it from `layout` + `regions`.
|
|
72
|
+
*/
|
|
73
|
+
frame?: RuntimePageFrame;
|
|
74
|
+
/** Typed layout limitations or overlaps detected while building this page. */
|
|
75
|
+
divergences?: RuntimeLayoutDivergence[];
|
|
66
76
|
/** Line boxes rendered in the body region. */
|
|
67
77
|
lineBoxes: RuntimeLineBox[];
|
|
68
78
|
/** Footnote allocations reserved at the bottom of the page. */
|
|
@@ -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
|
|
579
|
+
const graphPageCount = safeCall(
|
|
580
580
|
runtime,
|
|
581
|
-
(r) => r.
|
|
581
|
+
(r) => r.getLayoutFacet().getPageCount(),
|
|
582
582
|
null,
|
|
583
583
|
fallbacks,
|
|
584
|
-
"page-
|
|
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:
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,9 @@ 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_visibility_policy_changed":
|
|
30
|
+
case "workflow_markup_mode_policy_changed":
|
|
28
31
|
return {
|
|
29
32
|
invalidate: [
|
|
30
33
|
"workflowScope",
|
|
@@ -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
|
-
|
|
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,
|
|
@@ -7,6 +7,10 @@ import type {
|
|
|
7
7
|
FieldRegistryEntry,
|
|
8
8
|
TabStop,
|
|
9
9
|
} from "../../model/canonical-document.ts";
|
|
10
|
+
import {
|
|
11
|
+
MAIN_STORY_KEY,
|
|
12
|
+
type CanonicalFieldRegionIdentity,
|
|
13
|
+
} from "../../model/canonical-layout-inputs.ts";
|
|
10
14
|
import type { ResolvedField } from "./field/resolver.ts";
|
|
11
15
|
import type {
|
|
12
16
|
EffectiveParagraphFormatting,
|
|
@@ -43,7 +47,11 @@ export interface NumberingLayoutInput {
|
|
|
43
47
|
|
|
44
48
|
export interface FieldLayoutInput {
|
|
45
49
|
readonly fieldId: string;
|
|
50
|
+
readonly regionId: string;
|
|
51
|
+
readonly regionKind: "field" | "toc-region";
|
|
52
|
+
readonly storyKey: string;
|
|
46
53
|
readonly fieldIndex: number;
|
|
54
|
+
readonly paragraphIndex: number;
|
|
47
55
|
readonly family: FieldFamily;
|
|
48
56
|
readonly instruction: string;
|
|
49
57
|
readonly cachedText: string;
|
|
@@ -58,6 +66,11 @@ export interface FieldLayoutInput {
|
|
|
58
66
|
|
|
59
67
|
export interface RevisionLayoutPosture {
|
|
60
68
|
readonly mode: "clean" | "simple" | "all" | "original" | "no-markup";
|
|
69
|
+
/**
|
|
70
|
+
* True only when the active revision display changes measured text
|
|
71
|
+
* for the paragraph. Paint-only redline posture is compositor data,
|
|
72
|
+
* not an L04 measurement input.
|
|
73
|
+
*/
|
|
61
74
|
readonly affectsMeasuredText: boolean;
|
|
62
75
|
readonly hiddenRevisionIds: readonly string[];
|
|
63
76
|
readonly visibleRevisionIds: readonly string[];
|
|
@@ -100,6 +113,27 @@ export function toLayoutTabStops(
|
|
|
100
113
|
.sort((a, b) => a.positionTwips - b.positionTwips);
|
|
101
114
|
}
|
|
102
115
|
|
|
116
|
+
export function mergeLayoutTabStops(
|
|
117
|
+
...groups: readonly (readonly LayoutTabStopInput[] | undefined)[]
|
|
118
|
+
): readonly LayoutTabStopInput[] {
|
|
119
|
+
const byKey = new Map<string, LayoutTabStopInput>();
|
|
120
|
+
for (const group of groups) {
|
|
121
|
+
for (const tabStop of group ?? []) {
|
|
122
|
+
const key = [
|
|
123
|
+
tabStop.source,
|
|
124
|
+
tabStop.positionTwips,
|
|
125
|
+
tabStop.align,
|
|
126
|
+
tabStop.leader ?? "",
|
|
127
|
+
].join(":");
|
|
128
|
+
byKey.set(key, tabStop);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...byKey.values()].sort((a, b) => {
|
|
132
|
+
if (a.positionTwips !== b.positionTwips) return a.positionTwips - b.positionTwips;
|
|
133
|
+
return a.source.localeCompare(b.source);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
103
137
|
export function toNumberingLayoutInput(
|
|
104
138
|
numbering: NumberingPrefixResult | null | undefined,
|
|
105
139
|
): NumberingLayoutInput | undefined {
|
|
@@ -138,10 +172,15 @@ export function toNumberingLayoutInput(
|
|
|
138
172
|
export function toFieldLayoutInput(
|
|
139
173
|
entry: FieldRegistryEntry,
|
|
140
174
|
resolved?: ResolvedField,
|
|
175
|
+
region?: CanonicalFieldRegionIdentity,
|
|
141
176
|
): FieldLayoutInput {
|
|
142
177
|
return {
|
|
143
178
|
fieldId: `field-${entry.fieldIndex}`,
|
|
179
|
+
regionId: region?.regionId ?? `field:${entry.fieldIndex}`,
|
|
180
|
+
regionKind: region?.kind ?? "field",
|
|
181
|
+
storyKey: region?.storyKey ?? entry.storyKey ?? MAIN_STORY_KEY,
|
|
144
182
|
fieldIndex: entry.fieldIndex,
|
|
183
|
+
paragraphIndex: region?.paragraphIndex ?? entry.paragraphIndex,
|
|
145
184
|
family: entry.fieldFamily,
|
|
146
185
|
instruction: entry.instruction,
|
|
147
186
|
cachedText: entry.displayText,
|
|
@@ -156,20 +195,32 @@ export function toFieldLayoutInput(
|
|
|
156
195
|
export function collectFieldLayoutInputs(
|
|
157
196
|
registry: FieldRegistry | undefined,
|
|
158
197
|
resolve?: (entry: FieldRegistryEntry) => ResolvedField | undefined,
|
|
198
|
+
regions: readonly CanonicalFieldRegionIdentity[] = [],
|
|
159
199
|
): readonly FieldLayoutInput[] {
|
|
160
200
|
if (!registry) return [];
|
|
161
201
|
const inputs: FieldLayoutInput[] = [];
|
|
162
202
|
const entries = [...registry.supported, ...registry.preserveOnly];
|
|
203
|
+
const fieldRegions = new Map(
|
|
204
|
+
regions
|
|
205
|
+
.filter((region) => region.kind === "field")
|
|
206
|
+
.map((region) => [region.fieldIndex, region]),
|
|
207
|
+
);
|
|
208
|
+
const tocRegions = new Map(
|
|
209
|
+
regions
|
|
210
|
+
.filter((region) => region.kind === "toc-region" && region.tocId !== undefined)
|
|
211
|
+
.map((region) => [region.tocId!, region]),
|
|
212
|
+
);
|
|
163
213
|
for (const entry of entries) {
|
|
164
|
-
inputs.push(toFieldLayoutInput(entry, resolve?.(entry)));
|
|
214
|
+
inputs.push(toFieldLayoutInput(entry, resolve?.(entry), fieldRegions.get(entry.fieldIndex)));
|
|
165
215
|
}
|
|
166
216
|
for (const region of registry.tocRegions ?? []) {
|
|
167
217
|
const source = registry.supported.find(
|
|
168
218
|
(entry) => entry.fieldIndex === region.sourceFieldIndex,
|
|
169
219
|
);
|
|
170
220
|
if (!source) continue;
|
|
221
|
+
const identity = tocRegions.get(region.tocId);
|
|
171
222
|
inputs.push({
|
|
172
|
-
...toFieldLayoutInput(source, resolve?.(source)),
|
|
223
|
+
...toFieldLayoutInput(source, resolve?.(source), identity),
|
|
173
224
|
fieldId: `toc-${region.tocId}`,
|
|
174
225
|
family: "TOC",
|
|
175
226
|
instruction: region.instruction.raw,
|
|
@@ -200,6 +251,18 @@ export function buildRevisionLayoutPosture(
|
|
|
200
251
|
};
|
|
201
252
|
}
|
|
202
253
|
|
|
254
|
+
export function toMeasuredRevisionLayoutPosture(
|
|
255
|
+
posture: RevisionLayoutPosture | undefined,
|
|
256
|
+
): RevisionLayoutPosture | undefined {
|
|
257
|
+
if (!posture?.affectsMeasuredText || posture.hiddenRevisionIds.length === 0) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...posture,
|
|
262
|
+
visibleRevisionIds: [],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
203
266
|
export function buildEffectiveLayoutFormatting(input: {
|
|
204
267
|
readonly paragraph?: EffectiveParagraphFormatting;
|
|
205
268
|
readonly runs?: readonly EffectiveRunFormatting[];
|
|
@@ -213,6 +276,7 @@ export function buildEffectiveLayoutFormatting(input: {
|
|
|
213
276
|
readonly compatFlags?: readonly string[];
|
|
214
277
|
readonly revisionPosture?: RevisionLayoutPosture;
|
|
215
278
|
}): EffectiveLayoutFormatting {
|
|
279
|
+
const revisionPosture = toMeasuredRevisionLayoutPosture(input.revisionPosture);
|
|
216
280
|
const withoutHash = {
|
|
217
281
|
...(input.paragraph ? { paragraph: input.paragraph } : {}),
|
|
218
282
|
runs: input.runs ?? [],
|
|
@@ -221,7 +285,7 @@ export function buildEffectiveLayoutFormatting(input: {
|
|
|
221
285
|
...(input.numbering ? { numbering: input.numbering } : {}),
|
|
222
286
|
tabs: input.tabs ?? [],
|
|
223
287
|
compatFlags: input.compatFlags ?? [],
|
|
224
|
-
...(
|
|
288
|
+
...(revisionPosture ? { revisionPosture } : {}),
|
|
225
289
|
};
|
|
226
290
|
return {
|
|
227
291
|
...withoutHash,
|