@beyondwork/docx-react-component 1.0.109 → 1.0.110
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/model/layout/runtime-page-graph-types.ts +25 -0
- package/src/runtime/document-runtime.ts +46 -0
- package/src/runtime/geometry/adjacent-geometry-intake.ts +450 -13
- package/src/runtime/geometry/geometry-index.ts +17 -2
- package/src/runtime/layout/layout-engine-instance.ts +231 -4
- package/src/runtime/layout/layout-engine-version.ts +16 -1
- package/src/runtime/layout/page-graph.ts +7 -0
- package/src/runtime/layout/paginated-layout-engine.ts +34 -0
- package/src/runtime/layout/public-facet.ts +68 -9
- package/src/runtime/layout/resolve-page-previews.ts +46 -8
- package/src/ui-tailwind/chrome-overlay/tw-table-split-row-carry-overlay.tsx +62 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +1 -0
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +62 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +1 -0
|
@@ -106,6 +106,7 @@ export function projectGeometryIndexFromFrame(
|
|
|
106
106
|
let splitRowCarryCount = 0;
|
|
107
107
|
|
|
108
108
|
for (const page of frame.pages) {
|
|
109
|
+
if (!isPageGeometryMaterialized(page)) continue;
|
|
109
110
|
const pageMetadata = pageFrameMetadata(page);
|
|
110
111
|
pageFrameCompleteness[pageMetadata.frameCompleteness] += 1;
|
|
111
112
|
layoutDivergenceObjectCount += pageMetadata.layoutDivergenceObjectIds.length;
|
|
@@ -313,7 +314,7 @@ export function projectGeometryIndexFromFrame(
|
|
|
313
314
|
|
|
314
315
|
const objectHandles = finalizeObjectHandleEntries(objectHandleEntries);
|
|
315
316
|
const coverage: GeometryIndexCoverage = {
|
|
316
|
-
status:
|
|
317
|
+
status: coverageStatusForProjectedPages(frame, pages.length),
|
|
317
318
|
pageCount: pages.length,
|
|
318
319
|
pageFrameCompleteness,
|
|
319
320
|
regionCount: regions.length,
|
|
@@ -367,6 +368,7 @@ export function summarizeGeometryCoverageFromFrame(
|
|
|
367
368
|
let splitRowCarryCount = 0;
|
|
368
369
|
|
|
369
370
|
for (const page of frame.pages) {
|
|
371
|
+
if (!isPageGeometryMaterialized(page)) continue;
|
|
370
372
|
const pageMetadata = pageFrameMetadata(page);
|
|
371
373
|
pageCount += 1;
|
|
372
374
|
pageFrameCompleteness[pageMetadata.frameCompleteness] += 1;
|
|
@@ -407,7 +409,7 @@ export function summarizeGeometryCoverageFromFrame(
|
|
|
407
409
|
}
|
|
408
410
|
|
|
409
411
|
return {
|
|
410
|
-
status:
|
|
412
|
+
status: coverageStatusForProjectedPages(frame, pageCount),
|
|
411
413
|
pageCount,
|
|
412
414
|
pageFrameCompleteness,
|
|
413
415
|
regionCount,
|
|
@@ -433,6 +435,19 @@ interface PageFrameMetadata {
|
|
|
433
435
|
divergenceIdsByObjectId: ReadonlyMap<string, readonly string[]>;
|
|
434
436
|
}
|
|
435
437
|
|
|
438
|
+
function isPageGeometryMaterialized(page: RenderPage): boolean {
|
|
439
|
+
return page.page.materialization !== "unpaginated";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function coverageStatusForProjectedPages(
|
|
443
|
+
frame: RenderFrame,
|
|
444
|
+
projectedPageCount: number,
|
|
445
|
+
): GeometryRehydrationStatus {
|
|
446
|
+
return frame.pages.length > 0 && projectedPageCount === 0
|
|
447
|
+
? "unavailable"
|
|
448
|
+
: "realized";
|
|
449
|
+
}
|
|
450
|
+
|
|
436
451
|
function pageFrameMetadata(page: RenderPage): PageFrameMetadata {
|
|
437
452
|
const frame = page.page.frame;
|
|
438
453
|
const divergenceIds = frame?.divergenceIds ? [...frame.divergenceIds] : [];
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
buildPageStackFromWithSplits,
|
|
50
50
|
buildPageStackWithSplits,
|
|
51
51
|
type LayoutInvalidationReason,
|
|
52
|
+
type PageStackPaginationOptions,
|
|
52
53
|
} from "./paginated-layout-engine.ts";
|
|
53
54
|
import {
|
|
54
55
|
buildPageGraph,
|
|
@@ -102,6 +103,21 @@ export interface LayoutEngineViewState {
|
|
|
102
103
|
export interface LayoutEngineQueryInput {
|
|
103
104
|
document: CanonicalDocumentEnvelope;
|
|
104
105
|
viewState?: LayoutEngineViewState;
|
|
106
|
+
/**
|
|
107
|
+
* Optional page window for lazy pagination. L04 materializes this window
|
|
108
|
+
* plus its buffer and returns `unpaginated` placeholders for known pages
|
|
109
|
+
* outside it.
|
|
110
|
+
*/
|
|
111
|
+
viewportPageWindow?: LayoutViewportPageWindow;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface LayoutViewportPageWindow {
|
|
115
|
+
startPageIndex: number;
|
|
116
|
+
endPageIndex: number;
|
|
117
|
+
/** Extra pages to materialize before/after the requested window. */
|
|
118
|
+
bufferPages?: number;
|
|
119
|
+
/** Optional total page estimate supplied by the viewport/runtime layer. */
|
|
120
|
+
estimatedPageCount?: number;
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
export interface LayoutEngineEvent {
|
|
@@ -227,9 +243,206 @@ interface CacheKey {
|
|
|
227
243
|
content: CanonicalDocumentEnvelope["content"];
|
|
228
244
|
styles: CanonicalDocumentEnvelope["styles"];
|
|
229
245
|
subParts: CanonicalDocumentEnvelope["subParts"];
|
|
246
|
+
viewportWindowKey: string;
|
|
230
247
|
// Note: view state does not invalidate the graph itself (graph is global).
|
|
231
248
|
}
|
|
232
249
|
|
|
250
|
+
interface NormalizedViewportPageWindow {
|
|
251
|
+
startPageIndex: number;
|
|
252
|
+
endPageIndex: number;
|
|
253
|
+
estimatedPageCount?: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const FULL_VIEWPORT_WINDOW_KEY = "full";
|
|
257
|
+
const DEFAULT_VIEWPORT_PAGE_WINDOW_BUFFER = 1;
|
|
258
|
+
|
|
259
|
+
function normalizeViewportPageWindow(
|
|
260
|
+
window: LayoutViewportPageWindow | undefined,
|
|
261
|
+
): NormalizedViewportPageWindow | undefined {
|
|
262
|
+
if (!window) return undefined;
|
|
263
|
+
const buffer = Number.isFinite(window.bufferPages)
|
|
264
|
+
? Math.max(0, Math.floor(window.bufferPages ?? 0))
|
|
265
|
+
: DEFAULT_VIEWPORT_PAGE_WINDOW_BUFFER;
|
|
266
|
+
const rawStart = Number.isFinite(window.startPageIndex)
|
|
267
|
+
? Math.floor(window.startPageIndex)
|
|
268
|
+
: 0;
|
|
269
|
+
const rawEnd = Number.isFinite(window.endPageIndex)
|
|
270
|
+
? Math.floor(window.endPageIndex)
|
|
271
|
+
: rawStart;
|
|
272
|
+
const startPageIndex = Math.max(0, Math.min(rawStart, rawEnd) - buffer);
|
|
273
|
+
const endPageIndex = Math.max(
|
|
274
|
+
startPageIndex,
|
|
275
|
+
Math.max(rawStart, rawEnd) + buffer,
|
|
276
|
+
);
|
|
277
|
+
const estimatedPageCount =
|
|
278
|
+
window.estimatedPageCount !== undefined && Number.isFinite(window.estimatedPageCount)
|
|
279
|
+
? Math.max(0, Math.floor(window.estimatedPageCount))
|
|
280
|
+
: undefined;
|
|
281
|
+
return {
|
|
282
|
+
startPageIndex,
|
|
283
|
+
endPageIndex,
|
|
284
|
+
...(estimatedPageCount !== undefined ? { estimatedPageCount } : {}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function viewportWindowKey(
|
|
289
|
+
window: NormalizedViewportPageWindow | undefined,
|
|
290
|
+
): string {
|
|
291
|
+
if (!window) return FULL_VIEWPORT_WINDOW_KEY;
|
|
292
|
+
return [
|
|
293
|
+
"window",
|
|
294
|
+
window.startPageIndex,
|
|
295
|
+
window.endPageIndex,
|
|
296
|
+
window.estimatedPageCount ?? "unknown",
|
|
297
|
+
].join(":");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function pageStackOptionsForWindow(
|
|
301
|
+
window: NormalizedViewportPageWindow | undefined,
|
|
302
|
+
): PageStackPaginationOptions | undefined {
|
|
303
|
+
if (!window) return undefined;
|
|
304
|
+
return { maxPageCount: window.endPageIndex + 1 };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function applyViewportWindowMaterialization(
|
|
308
|
+
graph: RuntimePageGraph,
|
|
309
|
+
window: NormalizedViewportPageWindow | undefined,
|
|
310
|
+
priorGraph: RuntimePageGraph | null,
|
|
311
|
+
): RuntimePageGraph {
|
|
312
|
+
if (!window) {
|
|
313
|
+
return graph.materialization?.kind === "complete"
|
|
314
|
+
? graph
|
|
315
|
+
: { ...graph, materialization: { kind: "complete" } };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const estimatedPageCount = Math.max(
|
|
319
|
+
graph.pages.length,
|
|
320
|
+
window.endPageIndex + 1,
|
|
321
|
+
window.estimatedPageCount ?? 0,
|
|
322
|
+
priorGraph?.pages.length ?? 0,
|
|
323
|
+
);
|
|
324
|
+
const hiddenPageIds = new Set<string>();
|
|
325
|
+
const pages: RuntimePageNode[] = graph.pages.map((page) => {
|
|
326
|
+
const materialized =
|
|
327
|
+
page.pageIndex >= window.startPageIndex &&
|
|
328
|
+
page.pageIndex <= window.endPageIndex;
|
|
329
|
+
if (materialized) {
|
|
330
|
+
return { ...page, materialization: "paginated" };
|
|
331
|
+
}
|
|
332
|
+
hiddenPageIds.add(page.pageId);
|
|
333
|
+
return toUnpaginatedPlaceholder(page);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const seed = pages[pages.length - 1] ?? graph.pages[graph.pages.length - 1];
|
|
337
|
+
if (seed) {
|
|
338
|
+
for (let pageIndex = pages.length; pageIndex < estimatedPageCount; pageIndex += 1) {
|
|
339
|
+
const placeholder = createTrailingUnpaginatedPlaceholder(
|
|
340
|
+
graph.revision,
|
|
341
|
+
pageIndex,
|
|
342
|
+
seed,
|
|
343
|
+
);
|
|
344
|
+
hiddenPageIds.add(placeholder.pageId);
|
|
345
|
+
pages.push(placeholder);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const fragments = graph.fragments.filter((fragment) => !hiddenPageIds.has(fragment.pageId));
|
|
350
|
+
const anchors = graph.anchors.filter((anchor) => !hiddenPageIds.has(anchor.pageId));
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
...graph,
|
|
354
|
+
pages,
|
|
355
|
+
fragments,
|
|
356
|
+
anchors,
|
|
357
|
+
contentPageCount: pages.filter((page) => !page.isBlankFiller).length,
|
|
358
|
+
materialization: {
|
|
359
|
+
kind: "viewport-window",
|
|
360
|
+
requestedWindow: {
|
|
361
|
+
startPageIndex: window.startPageIndex,
|
|
362
|
+
endPageIndex: window.endPageIndex,
|
|
363
|
+
},
|
|
364
|
+
paginatedRange: {
|
|
365
|
+
startPageIndex: window.startPageIndex,
|
|
366
|
+
endPageIndex: Math.min(window.endPageIndex, Math.max(0, graph.pages.length - 1)),
|
|
367
|
+
},
|
|
368
|
+
estimatedPageCount,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function toUnpaginatedPlaceholder(page: RuntimePageNode): RuntimePageNode {
|
|
374
|
+
const { frame: _frame, divergences: _divergences, ...rest } = page;
|
|
375
|
+
void _frame;
|
|
376
|
+
void _divergences;
|
|
377
|
+
return {
|
|
378
|
+
...rest,
|
|
379
|
+
regions: buildUnpaginatedRegions(page.layout),
|
|
380
|
+
lineBoxes: [],
|
|
381
|
+
noteAllocations: [],
|
|
382
|
+
materialization: "unpaginated",
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function createTrailingUnpaginatedPlaceholder(
|
|
387
|
+
revision: number,
|
|
388
|
+
pageIndex: number,
|
|
389
|
+
seed: RuntimePageNode,
|
|
390
|
+
): RuntimePageNode {
|
|
391
|
+
const displayPageNumber =
|
|
392
|
+
seed.stories.displayPageNumber + Math.max(0, pageIndex - seed.pageIndex);
|
|
393
|
+
return {
|
|
394
|
+
pageId: `page-${revision}-${pageIndex}`,
|
|
395
|
+
pageIndex,
|
|
396
|
+
sectionIndex: seed.sectionIndex,
|
|
397
|
+
pageInSection:
|
|
398
|
+
seed.pageInSection >= 0
|
|
399
|
+
? seed.pageInSection + Math.max(0, pageIndex - seed.pageIndex)
|
|
400
|
+
: pageIndex,
|
|
401
|
+
startOffset: seed.endOffset,
|
|
402
|
+
endOffset: seed.endOffset,
|
|
403
|
+
layout: seed.layout,
|
|
404
|
+
stories: {
|
|
405
|
+
...seed.stories,
|
|
406
|
+
isFirstPage: false,
|
|
407
|
+
isEvenPage: displayPageNumber % 2 === 0,
|
|
408
|
+
displayPageNumber,
|
|
409
|
+
},
|
|
410
|
+
regions: buildUnpaginatedRegions(seed.layout),
|
|
411
|
+
lineBoxes: [],
|
|
412
|
+
noteAllocations: [],
|
|
413
|
+
isBlankFiller: false,
|
|
414
|
+
materialization: "unpaginated",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function buildUnpaginatedRegions(
|
|
419
|
+
layout: PageLayoutSnapshot,
|
|
420
|
+
): RuntimePageNode["regions"] {
|
|
421
|
+
const bodyWidth = Math.max(
|
|
422
|
+
0,
|
|
423
|
+
layout.pageWidth - layout.marginLeft - layout.marginRight - layout.gutter,
|
|
424
|
+
);
|
|
425
|
+
const bodyHeight = Math.max(
|
|
426
|
+
0,
|
|
427
|
+
layout.pageHeight - layout.marginTop - layout.marginBottom,
|
|
428
|
+
);
|
|
429
|
+
return {
|
|
430
|
+
body: {
|
|
431
|
+
kind: "body",
|
|
432
|
+
originTwips: layout.marginTop,
|
|
433
|
+
widthTwips: bodyWidth,
|
|
434
|
+
heightTwips: bodyHeight,
|
|
435
|
+
fragmentIds: [],
|
|
436
|
+
rectTwips: {
|
|
437
|
+
xTwips: layout.marginLeft,
|
|
438
|
+
yTwips: layout.marginTop,
|
|
439
|
+
widthTwips: bodyWidth,
|
|
440
|
+
heightTwips: bodyHeight,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
233
446
|
// ---------------------------------------------------------------------------
|
|
234
447
|
// Perf-probe helper (§6 E.7)
|
|
235
448
|
// ---------------------------------------------------------------------------
|
|
@@ -451,6 +664,7 @@ export function createLayoutEngine(
|
|
|
451
664
|
const recomputeStart = telemetryOn ? telemetryNow() : 0;
|
|
452
665
|
const pageCountBeforeRecompute = previousPageCount;
|
|
453
666
|
const document = input.document;
|
|
667
|
+
const viewportWindow = normalizeViewportPageWindow(input.viewportPageWindow);
|
|
454
668
|
const mainSurface = createEditorSurfaceSnapshot(
|
|
455
669
|
document,
|
|
456
670
|
createSelectionSnapshot(0, 0),
|
|
@@ -463,6 +677,7 @@ export function createLayoutEngine(
|
|
|
463
677
|
sections,
|
|
464
678
|
mainSurface,
|
|
465
679
|
measurementProvider,
|
|
680
|
+
pageStackOptionsForWindow(viewportWindow),
|
|
466
681
|
);
|
|
467
682
|
const pages = pageStack.pages;
|
|
468
683
|
const stories = resolvePageStories(pages);
|
|
@@ -486,7 +701,7 @@ export function createLayoutEngine(
|
|
|
486
701
|
const existing = fragmentsByPageIndex.get(pageIndex) ?? [];
|
|
487
702
|
fragmentsByPageIndex.set(pageIndex, [...existing, ...noteFragments]);
|
|
488
703
|
}
|
|
489
|
-
const
|
|
704
|
+
const measuredGraph = buildPageGraph({
|
|
490
705
|
pages,
|
|
491
706
|
sections,
|
|
492
707
|
stories,
|
|
@@ -495,6 +710,11 @@ export function createLayoutEngine(
|
|
|
495
710
|
noteAllocationsByPageIndex: pageStack.noteAllocationsByPageIndex,
|
|
496
711
|
subParts: document.subParts,
|
|
497
712
|
});
|
|
713
|
+
const graph = applyViewportWindowMaterialization(
|
|
714
|
+
measuredGraph,
|
|
715
|
+
viewportWindow,
|
|
716
|
+
cachedGraph,
|
|
717
|
+
);
|
|
498
718
|
|
|
499
719
|
// Field dirtiness diff from previous graph
|
|
500
720
|
const dirtyFamilies = computeFieldDirtiness(cachedGraph, graph);
|
|
@@ -504,7 +724,7 @@ export function createLayoutEngine(
|
|
|
504
724
|
|
|
505
725
|
const formatting = buildResolvedFormattingState(document, mainSurface);
|
|
506
726
|
|
|
507
|
-
const currentPageCount =
|
|
727
|
+
const currentPageCount = graph.contentPageCount;
|
|
508
728
|
// Compute the delta only; `previousPageCount` is mutated AFTER all
|
|
509
729
|
// emits so every emission reads the pre-commit value through the
|
|
510
730
|
// `pageCountDelta` local. `incrementalRelayout` mirrors this order
|
|
@@ -520,6 +740,7 @@ export function createLayoutEngine(
|
|
|
520
740
|
content: document.content,
|
|
521
741
|
styles: document.styles,
|
|
522
742
|
subParts: document.subParts,
|
|
743
|
+
viewportWindowKey: viewportWindowKey(viewportWindow),
|
|
523
744
|
};
|
|
524
745
|
cachedGraph = graph;
|
|
525
746
|
cachedFormatting = formatting;
|
|
@@ -752,6 +973,7 @@ export function createLayoutEngine(
|
|
|
752
973
|
content: document.content,
|
|
753
974
|
styles: document.styles,
|
|
754
975
|
subParts: document.subParts,
|
|
976
|
+
viewportWindowKey: FULL_VIEWPORT_WINDOW_KEY,
|
|
755
977
|
};
|
|
756
978
|
cachedGraph = splicedGraph;
|
|
757
979
|
cachedFormatting = formatting;
|
|
@@ -780,12 +1002,15 @@ export function createLayoutEngine(
|
|
|
780
1002
|
|
|
781
1003
|
function getGraphInternal(input: LayoutEngineQueryInput): RuntimePageGraph {
|
|
782
1004
|
const document = input.document;
|
|
1005
|
+
const normalizedWindow = normalizeViewportPageWindow(input.viewportPageWindow);
|
|
1006
|
+
const currentViewportWindowKey = viewportWindowKey(normalizedWindow);
|
|
783
1007
|
const keyEqual =
|
|
784
1008
|
cachedGraph !== null &&
|
|
785
1009
|
cachedKey !== null &&
|
|
786
1010
|
cachedKey.content === document.content &&
|
|
787
1011
|
cachedKey.styles === document.styles &&
|
|
788
|
-
cachedKey.subParts === document.subParts
|
|
1012
|
+
cachedKey.subParts === document.subParts &&
|
|
1013
|
+
cachedKey.viewportWindowKey === currentViewportWindowKey;
|
|
789
1014
|
|
|
790
1015
|
if (keyEqual && pendingInvalidation === null) {
|
|
791
1016
|
return cachedGraph!;
|
|
@@ -797,7 +1022,8 @@ export function createLayoutEngine(
|
|
|
797
1022
|
if (
|
|
798
1023
|
pending !== null &&
|
|
799
1024
|
pending.result.scope === "bounded" &&
|
|
800
|
-
cachedGraph !== null
|
|
1025
|
+
cachedGraph !== null &&
|
|
1026
|
+
normalizedWindow === undefined
|
|
801
1027
|
) {
|
|
802
1028
|
const spliced = incrementalRelayout(input, pending);
|
|
803
1029
|
if (spliced !== null) {
|
|
@@ -1009,6 +1235,7 @@ export function createLayoutEngine(
|
|
|
1009
1235
|
content: document.content,
|
|
1010
1236
|
styles: document.styles,
|
|
1011
1237
|
subParts: document.subParts,
|
|
1238
|
+
viewportWindowKey: FULL_VIEWPORT_WINDOW_KEY,
|
|
1012
1239
|
};
|
|
1013
1240
|
previousPageCount = graph.contentPageCount;
|
|
1014
1241
|
},
|
|
@@ -1233,8 +1233,23 @@
|
|
|
1233
1233
|
* history-block bumped to v81 in isolation; ship-preview's rollup advances
|
|
1234
1234
|
* to v87. Cache envelopes from v86 invalidate because table row ranges and
|
|
1235
1235
|
* continuation cursor payloads can change.
|
|
1236
|
+
*
|
|
1237
|
+
* 88 — Ship-side rollup: pe2 commit 327244cee ("Add L04 lazy pagination
|
|
1238
|
+
* placeholders") landed L04 lazy pagination — layout queries can now pass
|
|
1239
|
+
* a viewport page window; windowed reads bound pagination by the requested
|
|
1240
|
+
* high-water page when possible and emit `unpaginated` page placeholders
|
|
1241
|
+
* outside the materialized range. Pe2 commits 378e1b9c6 ("feat(05): consume
|
|
1242
|
+
* numbering readback joins") + 64e64567f ("feat(05): project adjacent
|
|
1243
|
+
* geometry into l04 frames") additionally brushed `src/runtime/geometry/**`,
|
|
1244
|
+
* and pe2 commit d5597ed33 ("feat(11): consume field ledgers and split row
|
|
1245
|
+
* carry") brushed `src/runtime/layout/public-facet.ts` +
|
|
1246
|
+
* `resolve-page-previews.ts`, all without separate pe2 bumps. The pe2
|
|
1247
|
+
* history-block bumped to v82 in isolation; ship-preview's rollup advances
|
|
1248
|
+
* to v88. Cache envelopes from v87 invalidate because page graph
|
|
1249
|
+
* materialization state, cache-key semantics, geometry projections, and
|
|
1250
|
+
* surface preview shapes can change.
|
|
1236
1251
|
*/
|
|
1237
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
1252
|
+
export const LAYOUT_ENGINE_VERSION = 88 as const;
|
|
1238
1253
|
|
|
1239
1254
|
/**
|
|
1240
1255
|
* Serialization schema version for the LayCache envelope and cached payload
|
|
@@ -83,6 +83,9 @@ export type {
|
|
|
83
83
|
|
|
84
84
|
export type {
|
|
85
85
|
RuntimePageGraph,
|
|
86
|
+
RuntimePageGraphMaterialization,
|
|
87
|
+
RuntimePageMaterialization,
|
|
88
|
+
RuntimePageWindowRange,
|
|
86
89
|
RuntimePageNode,
|
|
87
90
|
BuildPageGraphInput,
|
|
88
91
|
} from "../../model/layout/runtime-page-graph-types.ts";
|
|
@@ -253,6 +256,7 @@ export function buildPageGraph(
|
|
|
253
256
|
[],
|
|
254
257
|
noteAllocations: pageNoteAllocations,
|
|
255
258
|
isBlankFiller: page.pageInSection === -1,
|
|
259
|
+
materialization: "paginated",
|
|
256
260
|
};
|
|
257
261
|
pages.push(node);
|
|
258
262
|
|
|
@@ -1390,6 +1394,9 @@ function pageNodesStructurallyEqual(
|
|
|
1390
1394
|
if (a.startOffset !== b.startOffset) return false;
|
|
1391
1395
|
if (a.endOffset !== b.endOffset) return false;
|
|
1392
1396
|
if (a.isBlankFiller !== b.isBlankFiller) return false;
|
|
1397
|
+
if ((a.materialization ?? "paginated") !== (b.materialization ?? "paginated")) {
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1393
1400
|
if (a.regions.body.fragmentIds.length !== b.regions.body.fragmentIds.length) {
|
|
1394
1401
|
return false;
|
|
1395
1402
|
}
|
|
@@ -209,6 +209,14 @@ export interface PageStackResultWithSplits {
|
|
|
209
209
|
fragmentMeasurementsByPageIndex?: ReadonlyMap<number, ReadonlyMap<string, FragmentMeasurement>>;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
export interface PageStackPaginationOptions {
|
|
213
|
+
/**
|
|
214
|
+
* Stop after this many global pages have been emitted. Used by L04 lazy
|
|
215
|
+
* pagination so viewport reads can avoid walking the whole document tail.
|
|
216
|
+
*/
|
|
217
|
+
maxPageCount?: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
212
220
|
// ---------------------------------------------------------------------------
|
|
213
221
|
// Facade
|
|
214
222
|
// ---------------------------------------------------------------------------
|
|
@@ -248,6 +256,7 @@ export function buildPageStackWithSplits(
|
|
|
248
256
|
sections: ResolvedDocumentSection[],
|
|
249
257
|
mainSurface: EditorSurfaceSnapshot,
|
|
250
258
|
measurementProvider?: LayoutMeasurementProvider,
|
|
259
|
+
options?: PageStackPaginationOptions,
|
|
251
260
|
): PageStackResultWithSplits {
|
|
252
261
|
const defaultTabInterval = document.subParts?.settings?.defaultTabStop ?? 720;
|
|
253
262
|
// Theme-font fallback for L04 dominant-font resolution. Threaded into
|
|
@@ -288,8 +297,15 @@ export function buildPageStackWithSplits(
|
|
|
288
297
|
// reuses heights. The WeakMap frees memory automatically when the block
|
|
289
298
|
// snapshots go out of scope at the end of the call.
|
|
290
299
|
const cache = createMeasurementCache();
|
|
300
|
+
const maxPageCount =
|
|
301
|
+
options?.maxPageCount !== undefined
|
|
302
|
+
? Math.max(1, Math.floor(options.maxPageCount))
|
|
303
|
+
: undefined;
|
|
304
|
+
const hasReachedMaxPageCount = (): boolean =>
|
|
305
|
+
maxPageCount !== undefined && globalPageIndex >= maxPageCount;
|
|
291
306
|
|
|
292
307
|
for (let sectionIdx = 0; sectionIdx < sections.length; sectionIdx++) {
|
|
308
|
+
if (hasReachedMaxPageCount()) break;
|
|
293
309
|
const section = sections[sectionIdx]!;
|
|
294
310
|
const layout = buildPageLayoutSnapshot(
|
|
295
311
|
section.index,
|
|
@@ -347,6 +363,7 @@ export function buildPageStackWithSplits(
|
|
|
347
363
|
|
|
348
364
|
if (breakType === "evenPage" && globalPageIndex > 0) {
|
|
349
365
|
if (nextDisplayPage % 2 !== 0) {
|
|
366
|
+
if (hasReachedMaxPageCount()) break;
|
|
350
367
|
const prevLayout = pages[pages.length - 1]?.layout ?? layout;
|
|
351
368
|
pages.push({
|
|
352
369
|
pageIndex: globalPageIndex,
|
|
@@ -360,6 +377,7 @@ export function buildPageStackWithSplits(
|
|
|
360
377
|
}
|
|
361
378
|
} else if (breakType === "oddPage" && globalPageIndex > 0) {
|
|
362
379
|
if (nextDisplayPage % 2 === 0) {
|
|
380
|
+
if (hasReachedMaxPageCount()) break;
|
|
363
381
|
const prevLayout = pages[pages.length - 1]?.layout ?? layout;
|
|
364
382
|
pages.push({
|
|
365
383
|
pageIndex: globalPageIndex,
|
|
@@ -383,6 +401,9 @@ export function buildPageStackWithSplits(
|
|
|
383
401
|
defaultTabInterval,
|
|
384
402
|
nextColumnSeed,
|
|
385
403
|
themeFonts,
|
|
404
|
+
maxPageCount !== undefined
|
|
405
|
+
? { maxPageCount: Math.max(1, maxPageCount - globalPageIndex) }
|
|
406
|
+
: undefined,
|
|
386
407
|
);
|
|
387
408
|
const paginated = paginatedResult.pages;
|
|
388
409
|
|
|
@@ -421,6 +442,7 @@ export function buildPageStackWithSplits(
|
|
|
421
442
|
}
|
|
422
443
|
continue;
|
|
423
444
|
}
|
|
445
|
+
if (hasReachedMaxPageCount()) break;
|
|
424
446
|
pageInSectionToGlobal.set(page.pageInSection, globalPageIndex);
|
|
425
447
|
pages.push({
|
|
426
448
|
...page,
|
|
@@ -1612,6 +1634,7 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1612
1634
|
* `subParts.resolvedTheme` — threaded from `buildPageStackWithSplits`.
|
|
1613
1635
|
*/
|
|
1614
1636
|
themeFonts?: LayoutThemeFonts,
|
|
1637
|
+
options?: PageStackPaginationOptions,
|
|
1615
1638
|
): SectionPaginationResult {
|
|
1616
1639
|
if (blocks.length === 0) {
|
|
1617
1640
|
return {
|
|
@@ -1633,6 +1656,11 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1633
1656
|
}
|
|
1634
1657
|
|
|
1635
1658
|
const pages: Omit<DocumentPageSnapshot, "pageIndex">[] = [];
|
|
1659
|
+
const maxPageCount =
|
|
1660
|
+
options?.maxPageCount !== undefined
|
|
1661
|
+
? Math.max(1, Math.floor(options.maxPageCount))
|
|
1662
|
+
: undefined;
|
|
1663
|
+
let reachedPageLimit = false;
|
|
1636
1664
|
const splitsByBlock = new Map<string, SectionLocalSlice[]>();
|
|
1637
1665
|
const tableSplitsByBlock = new Map<string, SectionLocalTableSlice[]>();
|
|
1638
1666
|
const fragmentMeasurementsByPageInSection = new Map<number, Map<string, FragmentMeasurement>>();
|
|
@@ -1850,6 +1878,7 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1850
1878
|
};
|
|
1851
1879
|
|
|
1852
1880
|
const pushPage = (endOffset: number): void => {
|
|
1881
|
+
if (reachedPageLimit) return;
|
|
1853
1882
|
const boundedEnd = Math.max(pageStart, Math.min(endOffset, section.end));
|
|
1854
1883
|
if (boundedEnd === pageStart && pages.length > 0) {
|
|
1855
1884
|
return;
|
|
@@ -1877,12 +1906,17 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1877
1906
|
pendingNoteKeys.clear();
|
|
1878
1907
|
pendingNoteBlockFroms.clear();
|
|
1879
1908
|
pendingNoteColumnWidths.clear();
|
|
1909
|
+
if (maxPageCount !== undefined && pages.length >= maxPageCount) {
|
|
1910
|
+
reachedPageLimit = true;
|
|
1911
|
+
}
|
|
1880
1912
|
};
|
|
1881
1913
|
|
|
1882
1914
|
for (let index = 0; index < blocks.length; index += 1) {
|
|
1915
|
+
if (reachedPageLimit) break;
|
|
1883
1916
|
const block = blocks[index]!;
|
|
1884
1917
|
const nextBoundary = blocks[index + 1]?.from ?? section.end;
|
|
1885
1918
|
while (true) {
|
|
1919
|
+
if (reachedPageLimit) break;
|
|
1886
1920
|
const columnWidth =
|
|
1887
1921
|
columnMetrics[Math.min(columnIndex, columnMetrics.length - 1)]?.width ??
|
|
1888
1922
|
getUsableColumnWidth(layout);
|
|
@@ -204,6 +204,8 @@ export interface PublicPageNode {
|
|
|
204
204
|
endOffset: number;
|
|
205
205
|
/** Whether this page is a blank filler (e.g. from evenPage/oddPage). */
|
|
206
206
|
isBlankFiller: boolean;
|
|
207
|
+
/** Whether L04 has measured this page or it is a lazy-pagination placeholder. */
|
|
208
|
+
materialization: NonNullable<RuntimePageNode["materialization"]>;
|
|
207
209
|
/** Resolved display page number (1-based, honors section restarts). */
|
|
208
210
|
displayPageNumber: number;
|
|
209
211
|
/** Whether this is treated as the first page of its section (title page). */
|
|
@@ -1446,6 +1448,7 @@ function toPublicPageNode(
|
|
|
1446
1448
|
startOffset: node.startOffset,
|
|
1447
1449
|
endOffset: node.endOffset,
|
|
1448
1450
|
isBlankFiller: node.isBlankFiller,
|
|
1451
|
+
materialization: node.materialization ?? "paginated",
|
|
1449
1452
|
displayPageNumber: node.stories.displayPageNumber,
|
|
1450
1453
|
isFirstPage: node.stories.isFirstPage,
|
|
1451
1454
|
isEvenPage: node.stories.isEvenPage,
|
|
@@ -2178,6 +2181,10 @@ function resolveHeaderFooterRegionBlocks(
|
|
|
2178
2181
|
blockSnapshot,
|
|
2179
2182
|
node,
|
|
2180
2183
|
graph,
|
|
2184
|
+
{
|
|
2185
|
+
ledger: findPageLocalStoryFieldLedger(node, regionKind, storyTarget),
|
|
2186
|
+
ordinal: 0,
|
|
2187
|
+
},
|
|
2181
2188
|
);
|
|
2182
2189
|
return {
|
|
2183
2190
|
blockId: blockSnapshot.blockId,
|
|
@@ -2201,6 +2208,33 @@ const PAGE_INSTANCE_FIELD_FAMILIES = new Set([
|
|
|
2201
2208
|
"SECTIONPAGES",
|
|
2202
2209
|
]);
|
|
2203
2210
|
|
|
2211
|
+
type PageLocalFieldLedger = readonly {
|
|
2212
|
+
readonly family: string;
|
|
2213
|
+
readonly displayText: string;
|
|
2214
|
+
}[];
|
|
2215
|
+
|
|
2216
|
+
interface PageScopedFieldState {
|
|
2217
|
+
readonly ledger: PageLocalFieldLedger | undefined;
|
|
2218
|
+
ordinal: number;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
function findPageLocalStoryFieldLedger(
|
|
2222
|
+
page: RuntimePageNode,
|
|
2223
|
+
regionKind: "header" | "footer",
|
|
2224
|
+
storyTarget: EditorStoryTarget,
|
|
2225
|
+
): PageLocalFieldLedger | undefined {
|
|
2226
|
+
if (storyTarget.kind !== regionKind) return undefined;
|
|
2227
|
+
const pageLocalStory = page.frame?.pageLocalStories.find(
|
|
2228
|
+
(story) =>
|
|
2229
|
+
story.kind === storyTarget.kind &&
|
|
2230
|
+
story.relationshipId === storyTarget.relationshipId &&
|
|
2231
|
+
story.variant === storyTarget.variant &&
|
|
2232
|
+
(storyTarget.sectionIndex === undefined ||
|
|
2233
|
+
story.sectionIndex === storyTarget.sectionIndex),
|
|
2234
|
+
);
|
|
2235
|
+
return pageLocalStory?.resolvedFields;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2204
2238
|
function resolvePageScopedFieldsInBlocks(
|
|
2205
2239
|
blocks: readonly SurfaceBlockSnapshot[],
|
|
2206
2240
|
page: RuntimePageNode,
|
|
@@ -2208,7 +2242,10 @@ function resolvePageScopedFieldsInBlocks(
|
|
|
2208
2242
|
): readonly SurfaceBlockSnapshot[] {
|
|
2209
2243
|
let changed = false;
|
|
2210
2244
|
const resolvedBlocks = blocks.map((block) => {
|
|
2211
|
-
const resolved = resolvePageInstanceFieldsInBlock(block, page, graph
|
|
2245
|
+
const resolved = resolvePageInstanceFieldsInBlock(block, page, graph, {
|
|
2246
|
+
ledger: undefined,
|
|
2247
|
+
ordinal: 0,
|
|
2248
|
+
});
|
|
2212
2249
|
if (resolved !== block) changed = true;
|
|
2213
2250
|
return resolved;
|
|
2214
2251
|
});
|
|
@@ -2219,10 +2256,16 @@ function resolvePageInstanceFieldsInBlock(
|
|
|
2219
2256
|
block: SurfaceBlockSnapshot,
|
|
2220
2257
|
page: RuntimePageNode,
|
|
2221
2258
|
graph: RuntimePageGraph,
|
|
2259
|
+
fieldState: PageScopedFieldState,
|
|
2222
2260
|
): SurfaceBlockSnapshot {
|
|
2223
2261
|
switch (block.kind) {
|
|
2224
2262
|
case "paragraph": {
|
|
2225
|
-
const segments = resolvePageInstanceFieldsInSegments(
|
|
2263
|
+
const segments = resolvePageInstanceFieldsInSegments(
|
|
2264
|
+
block.segments,
|
|
2265
|
+
page,
|
|
2266
|
+
graph,
|
|
2267
|
+
fieldState,
|
|
2268
|
+
);
|
|
2226
2269
|
return segments === block.segments ? block : { ...block, segments };
|
|
2227
2270
|
}
|
|
2228
2271
|
case "table": {
|
|
@@ -2232,7 +2275,12 @@ function resolvePageInstanceFieldsInBlock(
|
|
|
2232
2275
|
const cells = row.cells.map((cell) => {
|
|
2233
2276
|
let cellChanged = false;
|
|
2234
2277
|
const content = cell.content.map((child) => {
|
|
2235
|
-
const resolved = resolvePageInstanceFieldsInBlock(
|
|
2278
|
+
const resolved = resolvePageInstanceFieldsInBlock(
|
|
2279
|
+
child,
|
|
2280
|
+
page,
|
|
2281
|
+
graph,
|
|
2282
|
+
fieldState,
|
|
2283
|
+
);
|
|
2236
2284
|
if (resolved !== child) cellChanged = true;
|
|
2237
2285
|
return resolved;
|
|
2238
2286
|
});
|
|
@@ -2249,7 +2297,12 @@ function resolvePageInstanceFieldsInBlock(
|
|
|
2249
2297
|
case "sdt_block": {
|
|
2250
2298
|
let changed = false;
|
|
2251
2299
|
const children = block.children.map((child) => {
|
|
2252
|
-
const resolved = resolvePageInstanceFieldsInBlock(
|
|
2300
|
+
const resolved = resolvePageInstanceFieldsInBlock(
|
|
2301
|
+
child,
|
|
2302
|
+
page,
|
|
2303
|
+
graph,
|
|
2304
|
+
fieldState,
|
|
2305
|
+
);
|
|
2253
2306
|
if (resolved !== child) changed = true;
|
|
2254
2307
|
return resolved;
|
|
2255
2308
|
});
|
|
@@ -2264,17 +2317,23 @@ function resolvePageInstanceFieldsInSegments(
|
|
|
2264
2317
|
segments: SurfaceInlineSegment[],
|
|
2265
2318
|
page: RuntimePageNode,
|
|
2266
2319
|
graph: RuntimePageGraph,
|
|
2320
|
+
fieldState: PageScopedFieldState,
|
|
2267
2321
|
): SurfaceInlineSegment[] {
|
|
2268
2322
|
let changed = false;
|
|
2269
2323
|
const resolvedSegments = segments.map((segment) => {
|
|
2270
2324
|
if (segment.kind !== "field_ref" || !PAGE_INSTANCE_FIELD_FAMILIES.has(segment.fieldFamily)) {
|
|
2271
2325
|
return segment;
|
|
2272
2326
|
}
|
|
2273
|
-
const
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2327
|
+
const ledgerField = fieldState.ledger?.[fieldState.ordinal];
|
|
2328
|
+
fieldState.ordinal += 1;
|
|
2329
|
+
const displayText =
|
|
2330
|
+
ledgerField?.family === segment.fieldFamily
|
|
2331
|
+
? ledgerField.displayText
|
|
2332
|
+
: resolvePageFieldDisplayText(
|
|
2333
|
+
segment.fieldFamily,
|
|
2334
|
+
segment.displayText ?? segment.label,
|
|
2335
|
+
{ page, graph },
|
|
2336
|
+
);
|
|
2278
2337
|
if (segment.displayText === displayText && segment.refreshStatus === "current") {
|
|
2279
2338
|
return segment;
|
|
2280
2339
|
}
|