@beyondwork/docx-react-component 1.0.36 → 1.0.38
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/README.md +103 -13
- package/package.json +1 -1
- package/src/api/package-version.ts +13 -0
- package/src/api/public-types.ts +402 -1
- package/src/core/commands/index.ts +18 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- package/src/core/selection/mapping.ts +6 -0
- package/src/io/docx-session.ts +24 -9
- package/src/io/export/build-app-properties-xml.ts +88 -0
- package/src/io/export/serialize-comments.ts +6 -1
- package/src/io/export/serialize-footnotes.ts +10 -9
- package/src/io/export/serialize-headers-footers.ts +11 -10
- package/src/io/export/serialize-main-document.ts +328 -50
- package/src/io/export/serialize-numbering.ts +114 -24
- package/src/io/export/serialize-tables.ts +87 -11
- package/src/io/export/table-properties-xml.ts +174 -20
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +20 -0
- package/src/io/ooxml/parse-footnotes.ts +62 -1
- package/src/io/ooxml/parse-headers-footers.ts +62 -1
- package/src/io/ooxml/parse-main-document.ts +158 -1
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +45 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +2 -306
- package/src/runtime/document-runtime.ts +287 -11
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +233 -0
- package/src/runtime/layout/inert-layout-facet.ts +59 -0
- package/src/runtime/layout/layout-engine-instance.ts +628 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
- package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +452 -0
- package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
- package/src/runtime/layout/page-story-resolver.ts +195 -0
- package/src/runtime/layout/paginated-layout-engine.ts +921 -0
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +1398 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +99 -15
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +501 -0
- package/src/ui/headless/scoped-chrome-policy.ts +183 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
- package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
- package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +505 -144
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LayoutEngineInstance — the stateful `PaginatedLayoutEngine` the architecture
|
|
3
|
+
* spec describes as `DocumentRuntime`-owned.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the stateless `buildPageStack` pipeline and adds:
|
|
6
|
+
* - cached graph keyed by (content, styles, subParts, viewState)
|
|
7
|
+
* - integrated `PageStoryResolver` on every graph build
|
|
8
|
+
* - integrated invalidation analysis (`analyzeInvalidation`)
|
|
9
|
+
* - field-dirtiness reporting (`computeFieldDirtiness`)
|
|
10
|
+
* - stable access to the enriched page graph + fragment mapper
|
|
11
|
+
* - pluggable `LayoutMeasurementProvider` (empirical by default, canvas on
|
|
12
|
+
* demand)
|
|
13
|
+
*
|
|
14
|
+
* This file intentionally stays additive. It does not refactor the existing
|
|
15
|
+
* `buildPageStack` / `computePageStack` exports — instead it consumes them
|
|
16
|
+
* and layers the graph/resolver/invalidation behavior on top.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
DocumentNavigationSnapshot,
|
|
21
|
+
DocumentPageSnapshot,
|
|
22
|
+
EditorStoryTarget,
|
|
23
|
+
EditorSurfaceSnapshot,
|
|
24
|
+
PageLayoutSnapshot,
|
|
25
|
+
SelectionSnapshot,
|
|
26
|
+
} from "../../api/public-types";
|
|
27
|
+
import type {
|
|
28
|
+
CanonicalDocumentEnvelope,
|
|
29
|
+
} from "../../core/state/editor-state.ts";
|
|
30
|
+
import { MAIN_STORY_TARGET } from "../../core/selection/mapping.ts";
|
|
31
|
+
import { createSelectionSnapshot } from "../../core/state/editor-state.ts";
|
|
32
|
+
import {
|
|
33
|
+
buildResolvedSections,
|
|
34
|
+
findSectionForPosition,
|
|
35
|
+
type ResolvedDocumentSection,
|
|
36
|
+
} from "../document-layout.ts";
|
|
37
|
+
import { findNoteReferencePosition } from "../view-state.ts";
|
|
38
|
+
import { createEditorSurfaceSnapshot } from "../surface-projection.ts";
|
|
39
|
+
import { buildHeadingOutline } from "../document-navigation.ts";
|
|
40
|
+
import {
|
|
41
|
+
analyzeInvalidation,
|
|
42
|
+
computeFieldDirtiness,
|
|
43
|
+
type InvalidationResult,
|
|
44
|
+
} from "./layout-invalidation.ts";
|
|
45
|
+
import {
|
|
46
|
+
buildPageStack,
|
|
47
|
+
buildPageStackFrom,
|
|
48
|
+
type LayoutInvalidationReason,
|
|
49
|
+
} from "./paginated-layout-engine.ts";
|
|
50
|
+
import {
|
|
51
|
+
buildPageGraph,
|
|
52
|
+
findPageNodeForOffset,
|
|
53
|
+
spliceGraph,
|
|
54
|
+
type RuntimePageGraph,
|
|
55
|
+
type RuntimePageNode,
|
|
56
|
+
} from "./page-graph.ts";
|
|
57
|
+
import { projectSurfaceBlocksToPageFragments } from "./project-block-fragments.ts";
|
|
58
|
+
import {
|
|
59
|
+
resolvePageStories,
|
|
60
|
+
resolveTotalPageCount,
|
|
61
|
+
} from "./page-story-resolver.ts";
|
|
62
|
+
import {
|
|
63
|
+
deriveActivePage,
|
|
64
|
+
deriveActivePageIndex,
|
|
65
|
+
deriveActiveSectionIndex,
|
|
66
|
+
deriveDocumentPageSnapshots,
|
|
67
|
+
derivePageLayoutSnapshotFromGraph,
|
|
68
|
+
} from "./page-layout-snapshot-adapter.ts";
|
|
69
|
+
import {
|
|
70
|
+
buildResolvedFormattingState,
|
|
71
|
+
type ResolvedFormattingState,
|
|
72
|
+
} from "./resolved-formatting-document.ts";
|
|
73
|
+
import {
|
|
74
|
+
createPageFragmentMapper,
|
|
75
|
+
rebuildMapper,
|
|
76
|
+
type PageFragmentMapper,
|
|
77
|
+
} from "./page-fragment-mapper.ts";
|
|
78
|
+
import {
|
|
79
|
+
createEmpiricalMeasurementProvider,
|
|
80
|
+
type LayoutMeasurementProvider,
|
|
81
|
+
} from "./layout-measurement-provider.ts";
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Types
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
export interface LayoutEngineViewState {
|
|
88
|
+
/** Active story that scopes story-aware queries. */
|
|
89
|
+
activeStory?: EditorStoryTarget;
|
|
90
|
+
workspaceMode?: "canvas" | "page";
|
|
91
|
+
zoomLevel?: number | "pageWidth" | "onePage";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface LayoutEngineQueryInput {
|
|
95
|
+
document: CanonicalDocumentEnvelope;
|
|
96
|
+
viewState?: LayoutEngineViewState;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface LayoutEngineEvent {
|
|
100
|
+
kind:
|
|
101
|
+
| "layout_recomputed"
|
|
102
|
+
| "incremental_relayout"
|
|
103
|
+
| "page_count_changed"
|
|
104
|
+
| "page_field_dirtied"
|
|
105
|
+
| "measurement_backend_ready";
|
|
106
|
+
revision: number;
|
|
107
|
+
previousPageCount?: number;
|
|
108
|
+
currentPageCount?: number;
|
|
109
|
+
dirtyFieldFamilies?: readonly string[];
|
|
110
|
+
reason?: LayoutInvalidationReason;
|
|
111
|
+
fidelity?: LayoutMeasurementProvider["fidelity"];
|
|
112
|
+
/** First dirty page index for incremental_relayout events. */
|
|
113
|
+
firstDirtyPageIndex?: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface LayoutEngineInstance {
|
|
117
|
+
/** Current measurement provider fidelity. */
|
|
118
|
+
readonly measurementFidelity: LayoutMeasurementProvider["fidelity"];
|
|
119
|
+
/** Await until the measurement provider is fully ready. */
|
|
120
|
+
whenMeasurementReady(): Promise<void>;
|
|
121
|
+
|
|
122
|
+
// ---- graph + snapshots ------------------------------------------------
|
|
123
|
+
getPageGraph(input: LayoutEngineQueryInput): RuntimePageGraph;
|
|
124
|
+
getResolvedFormattingState(input: LayoutEngineQueryInput): ResolvedFormattingState;
|
|
125
|
+
getPageStack(input: LayoutEngineQueryInput): DocumentPageSnapshot[];
|
|
126
|
+
getPageLayoutSnapshot(
|
|
127
|
+
input: LayoutEngineQueryInput,
|
|
128
|
+
sectionIndex: number,
|
|
129
|
+
): PageLayoutSnapshot | null;
|
|
130
|
+
getNavigationSnapshot(
|
|
131
|
+
input: LayoutEngineQueryInput,
|
|
132
|
+
selection: SelectionSnapshot,
|
|
133
|
+
activeStory: EditorStoryTarget,
|
|
134
|
+
): DocumentNavigationSnapshot;
|
|
135
|
+
|
|
136
|
+
// ---- fragment mapping -------------------------------------------------
|
|
137
|
+
getFragmentMapper(input: LayoutEngineQueryInput): PageFragmentMapper;
|
|
138
|
+
|
|
139
|
+
// ---- invalidation -----------------------------------------------------
|
|
140
|
+
invalidate(reason: LayoutInvalidationReason): void;
|
|
141
|
+
analyzeInvalidation(reason: LayoutInvalidationReason): InvalidationResult;
|
|
142
|
+
getDirtyFieldFamilies(): readonly string[];
|
|
143
|
+
clearDirtyFieldFamilies(families?: readonly string[]): void;
|
|
144
|
+
|
|
145
|
+
// ---- events -----------------------------------------------------------
|
|
146
|
+
subscribe(listener: (event: LayoutEngineEvent) => void): () => void;
|
|
147
|
+
|
|
148
|
+
// ---- measurement plumbing --------------------------------------------
|
|
149
|
+
swapMeasurementProvider(provider: LayoutMeasurementProvider): void;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Cache key
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
interface CacheKey {
|
|
157
|
+
content: CanonicalDocumentEnvelope["content"];
|
|
158
|
+
styles: CanonicalDocumentEnvelope["styles"];
|
|
159
|
+
subParts: CanonicalDocumentEnvelope["subParts"];
|
|
160
|
+
// Note: view state does not invalidate the graph itself (graph is global).
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Perf-probe helper (§6 E.7)
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Observation-only counter: every full rebuild bumps
|
|
169
|
+
* `layout.full.by_reason.<reason.kind>` on `window.__DOCX_REACT_PERF_PROBE__`
|
|
170
|
+
* so the CCEP typing flow can be instrumented without changing invalidation
|
|
171
|
+
* routing. Safe in Node environments (window guard), and no-op when the perf
|
|
172
|
+
* probe has not been enabled (`window.__DOCX_REACT_PERF_PROBE__.enabled`).
|
|
173
|
+
*/
|
|
174
|
+
function recordFullRebuildReason(reasonKind: string): void {
|
|
175
|
+
if (typeof window === "undefined") return;
|
|
176
|
+
const probe = (window as unknown as {
|
|
177
|
+
__DOCX_REACT_PERF_PROBE__?: {
|
|
178
|
+
enabled?: boolean;
|
|
179
|
+
invalidationCounts?: Record<string, number>;
|
|
180
|
+
};
|
|
181
|
+
}).__DOCX_REACT_PERF_PROBE__;
|
|
182
|
+
if (!probe?.enabled) return;
|
|
183
|
+
probe.invalidationCounts ??= {};
|
|
184
|
+
const totalKey = "layout.full.total";
|
|
185
|
+
const byReasonKey = `layout.full.by_reason.${reasonKind}`;
|
|
186
|
+
probe.invalidationCounts[totalKey] = (probe.invalidationCounts[totalKey] ?? 0) + 1;
|
|
187
|
+
probe.invalidationCounts[byReasonKey] =
|
|
188
|
+
(probe.invalidationCounts[byReasonKey] ?? 0) + 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Factory
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
export interface CreateLayoutEngineOptions {
|
|
196
|
+
/** Optional measurement provider. Defaults to empirical. */
|
|
197
|
+
measurementProvider?: LayoutMeasurementProvider;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function createLayoutEngine(
|
|
201
|
+
options: CreateLayoutEngineOptions = {},
|
|
202
|
+
): LayoutEngineInstance {
|
|
203
|
+
let measurementProvider: LayoutMeasurementProvider =
|
|
204
|
+
options.measurementProvider ?? createEmpiricalMeasurementProvider();
|
|
205
|
+
const dirtyFieldFamilies = new Set<string>();
|
|
206
|
+
const listeners = new Set<(event: LayoutEngineEvent) => void>();
|
|
207
|
+
let cachedKey: CacheKey | null = null;
|
|
208
|
+
let cachedGraph: RuntimePageGraph | null = null;
|
|
209
|
+
let cachedFormatting: ResolvedFormattingState | null = null;
|
|
210
|
+
let cachedMapper: PageFragmentMapper | null = null;
|
|
211
|
+
let previousPageCount = 0;
|
|
212
|
+
/**
|
|
213
|
+
* Invalidation reason stashed by `invalidate()` and consumed on the next
|
|
214
|
+
* `getGraphInternal()` call. For bounded scopes this is what lets the
|
|
215
|
+
* engine splice instead of rebuild.
|
|
216
|
+
*/
|
|
217
|
+
let pendingInvalidation: {
|
|
218
|
+
reason: LayoutInvalidationReason;
|
|
219
|
+
result: InvalidationResult;
|
|
220
|
+
} | null = null;
|
|
221
|
+
|
|
222
|
+
function emit(event: LayoutEngineEvent): void {
|
|
223
|
+
for (const listener of listeners) {
|
|
224
|
+
try {
|
|
225
|
+
listener(event);
|
|
226
|
+
} catch {
|
|
227
|
+
// never let a listener error interrupt the engine
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function fullRebuild(
|
|
233
|
+
input: LayoutEngineQueryInput,
|
|
234
|
+
reason?: LayoutInvalidationReason,
|
|
235
|
+
): RuntimePageGraph {
|
|
236
|
+
// §6 E.7 — observation-only perf probe. Attribute every full rebuild
|
|
237
|
+
// back to the invalidation reason that triggered it so we can later
|
|
238
|
+
// narrow specific kinds (numbering/section) without guessing.
|
|
239
|
+
recordFullRebuildReason(reason?.kind ?? "unknown");
|
|
240
|
+
const document = input.document;
|
|
241
|
+
const mainSurface = createEditorSurfaceSnapshot(
|
|
242
|
+
document,
|
|
243
|
+
createSelectionSnapshot(0, 0),
|
|
244
|
+
MAIN_STORY_TARGET,
|
|
245
|
+
);
|
|
246
|
+
const sections = buildResolvedSections(document);
|
|
247
|
+
const pages = buildPageStack(document, sections, mainSurface, measurementProvider);
|
|
248
|
+
const stories = resolvePageStories(pages);
|
|
249
|
+
const fragmentsByPageIndex = projectSurfaceBlocksToPageFragments(
|
|
250
|
+
mainSurface,
|
|
251
|
+
pages,
|
|
252
|
+
);
|
|
253
|
+
const graph = buildPageGraph({
|
|
254
|
+
pages,
|
|
255
|
+
sections,
|
|
256
|
+
stories,
|
|
257
|
+
fragmentsByPageIndex,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Field dirtiness diff from previous graph
|
|
261
|
+
const dirtyFamilies = computeFieldDirtiness(cachedGraph, graph);
|
|
262
|
+
for (const family of dirtyFamilies) {
|
|
263
|
+
dirtyFieldFamilies.add(family);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const formatting = buildResolvedFormattingState(document, mainSurface);
|
|
267
|
+
|
|
268
|
+
const currentPageCount = resolveTotalPageCount(pages);
|
|
269
|
+
if (currentPageCount !== previousPageCount) {
|
|
270
|
+
emit({
|
|
271
|
+
kind: "page_count_changed",
|
|
272
|
+
revision: graph.revision,
|
|
273
|
+
previousPageCount,
|
|
274
|
+
currentPageCount,
|
|
275
|
+
});
|
|
276
|
+
previousPageCount = currentPageCount;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (dirtyFamilies.length > 0) {
|
|
280
|
+
emit({
|
|
281
|
+
kind: "page_field_dirtied",
|
|
282
|
+
revision: graph.revision,
|
|
283
|
+
dirtyFieldFamilies: dirtyFamilies,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
emit({
|
|
288
|
+
kind: "layout_recomputed",
|
|
289
|
+
revision: graph.revision,
|
|
290
|
+
...(reason ? { reason } : {}),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
cachedKey = {
|
|
294
|
+
content: document.content,
|
|
295
|
+
styles: document.styles,
|
|
296
|
+
subParts: document.subParts,
|
|
297
|
+
};
|
|
298
|
+
cachedGraph = graph;
|
|
299
|
+
cachedFormatting = formatting;
|
|
300
|
+
cachedMapper = createPageFragmentMapper(graph);
|
|
301
|
+
return graph;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function incrementalRelayout(
|
|
305
|
+
input: LayoutEngineQueryInput,
|
|
306
|
+
pending: { reason: LayoutInvalidationReason; result: InvalidationResult },
|
|
307
|
+
): RuntimePageGraph | null {
|
|
308
|
+
const priorGraph = cachedGraph;
|
|
309
|
+
const range = pending.result.dirtyPageRange;
|
|
310
|
+
if (!priorGraph || !range) return null;
|
|
311
|
+
const firstDirty = range.firstPageIndex;
|
|
312
|
+
if (firstDirty < 0 || firstDirty >= priorGraph.pages.length) return null;
|
|
313
|
+
|
|
314
|
+
const document = input.document;
|
|
315
|
+
const mainSurface = createEditorSurfaceSnapshot(
|
|
316
|
+
document,
|
|
317
|
+
createSelectionSnapshot(0, 0),
|
|
318
|
+
MAIN_STORY_TARGET,
|
|
319
|
+
);
|
|
320
|
+
const sections = buildResolvedSections(document);
|
|
321
|
+
|
|
322
|
+
const dirtyPage = priorGraph.pages[firstDirty]!;
|
|
323
|
+
const freshSnapshots = buildPageStackFrom(
|
|
324
|
+
document,
|
|
325
|
+
sections,
|
|
326
|
+
mainSurface,
|
|
327
|
+
{
|
|
328
|
+
startPageIndex: firstDirty,
|
|
329
|
+
startOffset: dirtyPage.startOffset,
|
|
330
|
+
},
|
|
331
|
+
measurementProvider,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Convert fresh DocumentPageSnapshots into RuntimePageNodes via the
|
|
335
|
+
// standard buildPageGraph pipeline — this keeps region, story, and
|
|
336
|
+
// fragment synthesis consistent with the full-rebuild path. If the
|
|
337
|
+
// splice would not preserve the previously-held page count contract
|
|
338
|
+
// (e.g. the tail is empty because the edit deleted everything past
|
|
339
|
+
// firstDirty) we fall back to a full rebuild to stay safe.
|
|
340
|
+
if (freshSnapshots.length === 0 && firstDirty > 0) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const freshStories = resolvePageStories(freshSnapshots);
|
|
344
|
+
const freshGraph = buildPageGraph({
|
|
345
|
+
pages: freshSnapshots,
|
|
346
|
+
sections,
|
|
347
|
+
stories: freshStories,
|
|
348
|
+
});
|
|
349
|
+
const freshNodes = freshGraph.pages;
|
|
350
|
+
|
|
351
|
+
const splicedGraph = spliceGraph(priorGraph, freshNodes, firstDirty);
|
|
352
|
+
|
|
353
|
+
// Field dirtiness diff and resolved-formatting update run against the
|
|
354
|
+
// full spliced graph so NUMPAGES/PAGE tracking remains accurate.
|
|
355
|
+
const dirtyFamilies = computeFieldDirtiness(priorGraph, splicedGraph);
|
|
356
|
+
for (const family of dirtyFamilies) {
|
|
357
|
+
dirtyFieldFamilies.add(family);
|
|
358
|
+
}
|
|
359
|
+
const formatting = buildResolvedFormattingState(document, mainSurface);
|
|
360
|
+
|
|
361
|
+
const currentPageCount = resolveTotalPageCount(
|
|
362
|
+
deriveDocumentPageSnapshots(splicedGraph),
|
|
363
|
+
);
|
|
364
|
+
if (currentPageCount !== previousPageCount) {
|
|
365
|
+
emit({
|
|
366
|
+
kind: "page_count_changed",
|
|
367
|
+
revision: splicedGraph.revision,
|
|
368
|
+
previousPageCount,
|
|
369
|
+
currentPageCount,
|
|
370
|
+
});
|
|
371
|
+
previousPageCount = currentPageCount;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (dirtyFamilies.length > 0) {
|
|
375
|
+
emit({
|
|
376
|
+
kind: "page_field_dirtied",
|
|
377
|
+
revision: splicedGraph.revision,
|
|
378
|
+
dirtyFieldFamilies: dirtyFamilies,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
emit({
|
|
383
|
+
kind: "incremental_relayout",
|
|
384
|
+
revision: splicedGraph.revision,
|
|
385
|
+
reason: pending.reason,
|
|
386
|
+
firstDirtyPageIndex: firstDirty,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
cachedKey = {
|
|
390
|
+
content: document.content,
|
|
391
|
+
styles: document.styles,
|
|
392
|
+
subParts: document.subParts,
|
|
393
|
+
};
|
|
394
|
+
cachedGraph = splicedGraph;
|
|
395
|
+
cachedFormatting = formatting;
|
|
396
|
+
cachedMapper = rebuildMapper(
|
|
397
|
+
cachedMapper ?? createPageFragmentMapper(splicedGraph),
|
|
398
|
+
splicedGraph,
|
|
399
|
+
firstDirty,
|
|
400
|
+
);
|
|
401
|
+
return splicedGraph;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function getGraphInternal(input: LayoutEngineQueryInput): RuntimePageGraph {
|
|
405
|
+
const document = input.document;
|
|
406
|
+
const keyEqual =
|
|
407
|
+
cachedGraph !== null &&
|
|
408
|
+
cachedKey !== null &&
|
|
409
|
+
cachedKey.content === document.content &&
|
|
410
|
+
cachedKey.styles === document.styles &&
|
|
411
|
+
cachedKey.subParts === document.subParts;
|
|
412
|
+
|
|
413
|
+
if (keyEqual && pendingInvalidation === null) {
|
|
414
|
+
return cachedGraph!;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const pending = pendingInvalidation;
|
|
418
|
+
pendingInvalidation = null;
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
pending !== null &&
|
|
422
|
+
pending.result.scope === "bounded" &&
|
|
423
|
+
cachedGraph !== null
|
|
424
|
+
) {
|
|
425
|
+
const spliced = incrementalRelayout(input, pending);
|
|
426
|
+
if (spliced !== null) {
|
|
427
|
+
return spliced;
|
|
428
|
+
}
|
|
429
|
+
// Incremental path declined to produce a graph — fall through to full
|
|
430
|
+
// rebuild using the pending reason so listeners still see the cause.
|
|
431
|
+
return fullRebuild(input, pending.reason);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return fullRebuild(input, pending?.reason);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function getMapper(input: LayoutEngineQueryInput): PageFragmentMapper {
|
|
438
|
+
getGraphInternal(input);
|
|
439
|
+
return cachedMapper!;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function getFormatting(input: LayoutEngineQueryInput): ResolvedFormattingState {
|
|
443
|
+
getGraphInternal(input);
|
|
444
|
+
return cachedFormatting!;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
get measurementFidelity() {
|
|
449
|
+
return measurementProvider.fidelity;
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
whenMeasurementReady() {
|
|
453
|
+
return measurementProvider.whenReady();
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
getPageGraph(input) {
|
|
457
|
+
return getGraphInternal(input);
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
getResolvedFormattingState(input) {
|
|
461
|
+
return getFormatting(input);
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
getPageStack(input) {
|
|
465
|
+
return deriveDocumentPageSnapshots(getGraphInternal(input));
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
getPageLayoutSnapshot(input, sectionIndex) {
|
|
469
|
+
return derivePageLayoutSnapshotFromGraph(getGraphInternal(input), sectionIndex);
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
getNavigationSnapshot(input, selection, activeStory) {
|
|
473
|
+
const graph = getGraphInternal(input);
|
|
474
|
+
const selectionHead = selection.head;
|
|
475
|
+
const nav = buildNavigationFromGraph(
|
|
476
|
+
graph,
|
|
477
|
+
input.document,
|
|
478
|
+
activeStory,
|
|
479
|
+
selectionHead,
|
|
480
|
+
);
|
|
481
|
+
return nav;
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
getFragmentMapper(input) {
|
|
485
|
+
return getMapper(input);
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
invalidate(reason) {
|
|
489
|
+
const result = analyzeInvalidation(reason, cachedGraph);
|
|
490
|
+
for (const family of result.dirtyFieldFamilies) {
|
|
491
|
+
dirtyFieldFamilies.add(family);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (result.scope === "bounded") {
|
|
495
|
+
// Retain the cache — the next getGraphInternal() call will splice
|
|
496
|
+
// fresh pages into the preserved prefix. Only record the pending
|
|
497
|
+
// reason so the splice path can run.
|
|
498
|
+
pendingInvalidation = { reason, result };
|
|
499
|
+
} else {
|
|
500
|
+
// Full rebuild or field-only refresh: drop the cached graph as
|
|
501
|
+
// before. Field families were added above so downstream consumers
|
|
502
|
+
// still observe a refresh signal for field-only reasons.
|
|
503
|
+
cachedKey = null;
|
|
504
|
+
cachedGraph = null;
|
|
505
|
+
cachedFormatting = null;
|
|
506
|
+
cachedMapper = null;
|
|
507
|
+
pendingInvalidation = { reason, result };
|
|
508
|
+
}
|
|
509
|
+
// No event emitted here. fullRebuild() and incrementalRelayout() are
|
|
510
|
+
// the sole emitters of relayout events; they fire once each on the
|
|
511
|
+
// next getGraphInternal() call with the correct post-recompute
|
|
512
|
+
// revision.
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
analyzeInvalidation(reason) {
|
|
516
|
+
return analyzeInvalidation(reason, cachedGraph);
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
getDirtyFieldFamilies() {
|
|
520
|
+
return Array.from(dirtyFieldFamilies);
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
clearDirtyFieldFamilies(families) {
|
|
524
|
+
if (!families) {
|
|
525
|
+
dirtyFieldFamilies.clear();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
for (const family of families) {
|
|
529
|
+
dirtyFieldFamilies.delete(family);
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
subscribe(listener) {
|
|
534
|
+
listeners.add(listener);
|
|
535
|
+
return () => {
|
|
536
|
+
listeners.delete(listener);
|
|
537
|
+
};
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
swapMeasurementProvider(provider) {
|
|
541
|
+
measurementProvider = provider;
|
|
542
|
+
emit({
|
|
543
|
+
kind: "measurement_backend_ready",
|
|
544
|
+
revision: cachedGraph?.revision ?? 0,
|
|
545
|
+
fidelity: provider.fidelity,
|
|
546
|
+
});
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// Navigation derivation from graph
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
function buildNavigationFromGraph(
|
|
556
|
+
graph: RuntimePageGraph,
|
|
557
|
+
document: CanonicalDocumentEnvelope,
|
|
558
|
+
activeStory: EditorStoryTarget,
|
|
559
|
+
selectionHead: number,
|
|
560
|
+
): DocumentNavigationSnapshot {
|
|
561
|
+
const pages = deriveDocumentPageSnapshots(graph);
|
|
562
|
+
const sections = graph.sections;
|
|
563
|
+
const mainSurface = createEditorSurfaceSnapshot(
|
|
564
|
+
document,
|
|
565
|
+
createSelectionSnapshot(0, 0),
|
|
566
|
+
MAIN_STORY_TARGET,
|
|
567
|
+
);
|
|
568
|
+
const headings = buildHeadingOutline(document, mainSurface, sections, pages);
|
|
569
|
+
|
|
570
|
+
if (activeStory.kind === "main") {
|
|
571
|
+
const activePageIndex = deriveActivePageIndex(graph, selectionHead);
|
|
572
|
+
return {
|
|
573
|
+
pageCount: pages.length,
|
|
574
|
+
pages,
|
|
575
|
+
headings,
|
|
576
|
+
activePageIndex,
|
|
577
|
+
activeSectionIndex: deriveActiveSectionIndex(graph, selectionHead),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (activeStory.kind === "header" || activeStory.kind === "footer") {
|
|
582
|
+
const sectionIndex =
|
|
583
|
+
"sectionIndex" in activeStory && typeof activeStory.sectionIndex === "number"
|
|
584
|
+
? activeStory.sectionIndex
|
|
585
|
+
: 0;
|
|
586
|
+
const firstPage = graph.pages.findIndex((p) => p.sectionIndex === sectionIndex);
|
|
587
|
+
return {
|
|
588
|
+
pageCount: pages.length,
|
|
589
|
+
pages,
|
|
590
|
+
headings,
|
|
591
|
+
activePageIndex: firstPage >= 0 ? firstPage : 0,
|
|
592
|
+
activeSectionIndex: sectionIndex,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (activeStory.kind === "footnote" || activeStory.kind === "endnote") {
|
|
597
|
+
const referencePosition = findNoteReferencePosition(mainSurface, activeStory);
|
|
598
|
+
const activePageIndex = deriveActivePageIndex(graph, referencePosition);
|
|
599
|
+
return {
|
|
600
|
+
pageCount: pages.length,
|
|
601
|
+
pages,
|
|
602
|
+
headings,
|
|
603
|
+
activePageIndex,
|
|
604
|
+
activeSectionIndex:
|
|
605
|
+
graph.pages[activePageIndex]?.sectionIndex ??
|
|
606
|
+
findSectionForPosition(sections, referencePosition).index,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
pageCount: pages.length,
|
|
612
|
+
pages,
|
|
613
|
+
headings,
|
|
614
|
+
activePageIndex: 0,
|
|
615
|
+
activeSectionIndex: 0,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// Convenience: find the active page node directly
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
|
|
623
|
+
export function findActivePageNode(
|
|
624
|
+
graph: RuntimePageGraph,
|
|
625
|
+
selectionHead: number,
|
|
626
|
+
): RuntimePageNode | undefined {
|
|
627
|
+
return deriveActivePage(graph, selectionHead) ?? findPageNodeForOffset(graph, selectionHead);
|
|
628
|
+
}
|