@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.
@@ -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: "realized",
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: "realized",
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 graph = buildPageGraph({
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 = resolveTotalPageCount(pages);
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 = 87 as const;
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(block.segments, page, graph);
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(child, page, graph);
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(child, page, graph);
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 displayText = resolvePageFieldDisplayText(
2274
- segment.fieldFamily,
2275
- segment.displayText ?? segment.label,
2276
- { page, graph },
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
  }