@beyondwork/docx-react-component 1.0.109 → 1.0.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +3 -0
  3. package/src/model/layout/page-graph-types.ts +33 -0
  4. package/src/model/layout/runtime-page-graph-types.ts +25 -0
  5. package/src/runtime/document-runtime.ts +46 -0
  6. package/src/runtime/geometry/adjacent-geometry-intake.ts +820 -15
  7. package/src/runtime/geometry/caret-geometry.ts +219 -7
  8. package/src/runtime/geometry/geometry-index.ts +52 -12
  9. package/src/runtime/geometry/object-handles.ts +42 -1
  10. package/src/runtime/layout/index.ts +3 -0
  11. package/src/runtime/layout/inert-layout-facet.ts +13 -0
  12. package/src/runtime/layout/layout-engine-instance.ts +233 -4
  13. package/src/runtime/layout/layout-engine-version.ts +47 -2
  14. package/src/runtime/layout/layout-facet-types.ts +3 -0
  15. package/src/runtime/layout/page-graph.ts +88 -7
  16. package/src/runtime/layout/paginated-layout-engine.ts +34 -0
  17. package/src/runtime/layout/project-block-fragments.ts +144 -1
  18. package/src/runtime/layout/public-facet.ts +228 -9
  19. package/src/runtime/layout/resolve-page-previews.ts +46 -8
  20. package/src/runtime/scopes/adjacent-geometry-evidence.ts +456 -0
  21. package/src/runtime/scopes/compile-scope-bundle.ts +8 -0
  22. package/src/runtime/scopes/evidence.ts +16 -0
  23. package/src/runtime/scopes/index.ts +13 -0
  24. package/src/runtime/scopes/semantic-scope-types.ts +67 -0
  25. package/src/ui-tailwind/chrome-overlay/tw-table-split-row-carry-overlay.tsx +62 -0
  26. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +104 -0
  27. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +50 -5
  28. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +27 -0
  29. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +62 -0
  30. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +1 -0
  31. package/src/README.md +0 -85
  32. package/src/api/README.md +0 -26
  33. package/src/api/v3/README.md +0 -91
  34. package/src/component-inventory.md +0 -99
  35. package/src/core/README.md +0 -10
  36. package/src/core/commands/README.md +0 -3
  37. package/src/core/schema/README.md +0 -3
  38. package/src/core/selection/README.md +0 -3
  39. package/src/core/state/README.md +0 -3
  40. package/src/io/README.md +0 -10
  41. package/src/io/export/README.md +0 -3
  42. package/src/io/normalize/README.md +0 -3
  43. package/src/io/ooxml/README.md +0 -3
  44. package/src/io/opc/README.md +0 -3
  45. package/src/model/README.md +0 -3
  46. package/src/preservation/README.md +0 -3
  47. package/src/review/README.md +0 -16
  48. package/src/review/store/README.md +0 -3
  49. package/src/runtime/README.md +0 -3
  50. package/src/ui/README.md +0 -30
  51. package/src/ui/comments/README.md +0 -3
  52. package/src/ui/compatibility/README.md +0 -3
  53. package/src/ui/editor-surface/README.md +0 -3
  54. package/src/ui/review/README.md +0 -3
  55. package/src/ui/status/README.md +0 -3
  56. package/src/ui/theme/README.md +0 -3
  57. package/src/ui/toolbar/README.md +0 -3
  58. package/src/ui-tailwind/debug/README.md +0 -22
  59. package/src/validation/README.md +0 -3
@@ -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);
@@ -23,6 +23,7 @@ import type {
23
23
  DocumentPageSnapshot,
24
24
  EditorSurfaceSnapshot,
25
25
  SurfaceBlockSnapshot,
26
+ SurfaceInlineSegment,
26
27
  } from "../../api/public-types";
27
28
  import type {
28
29
  CanonicalFieldRegionIdentity,
@@ -31,7 +32,9 @@ import type { RuntimeBlockFragment } from "./page-graph.ts";
31
32
  import type {
32
33
  RuntimeFieldRegionLayoutFacts,
33
34
  RuntimeLineBox,
35
+ RuntimeLineRunAnchor,
34
36
  RuntimeNumberingLayoutFacts,
37
+ RuntimeTwipsRect,
35
38
  } from "../../model/layout/page-graph-types.ts";
36
39
  import type {
37
40
  BlockSplits,
@@ -185,8 +188,12 @@ export function projectLineBoxesForPageFragments(
185
188
  ReadonlyArray<Omit<RuntimeBlockFragment, "pageId">>
186
189
  >,
187
190
  fragmentMeasurementsByPageIndex?: ReadonlyMap<number, ReadonlyMap<string, FragmentMeasurement>>,
191
+ surface?: EditorSurfaceSnapshot,
188
192
  ): Map<number, RuntimeLineBox[]> {
189
193
  const byPage = new Map<number, RuntimeLineBox[]>();
194
+ const blocksById = surface
195
+ ? new Map(surface.blocks.map((block) => [block.blockId, block] as const))
196
+ : new Map<string, SurfaceBlockSnapshot>();
190
197
  for (const page of pages) {
191
198
  const fragments = [...(fragmentsByPageIndex.get(page.pageIndex) ?? [])]
192
199
  .filter((fragment) => fragment.regionKind === "body")
@@ -224,12 +231,33 @@ export function projectLineBoxesForPageFragments(
224
231
  page.layout.gutter,
225
232
  );
226
233
  for (let lineIndex = 0; lineIndex < lineCount; lineIndex += 1) {
234
+ const lineTopTwips = cursorTwips + lineIndex * lineHeight;
235
+ const rectTwips = lineRectForFragment(page, fragment, lineTopTwips, widthTwips, lineHeight);
236
+ const baselinePageYTwips = rectTwips.yTwips + Math.round(lineHeight * 0.8);
237
+ const block = blocksById.get(fragment.blockId);
238
+ const direction = block?.kind === "paragraph" && block.bidi === true ? "rtl" : "ltr";
227
239
  lines.push({
228
240
  fragmentId: fragment.fragmentId,
229
241
  lineIndex,
230
- baselineTwips: cursorTwips + lineIndex * lineHeight,
242
+ baselineTwips: lineTopTwips,
243
+ baselinePageYTwips,
231
244
  heightTwips: lineHeight,
232
245
  widthTwips,
246
+ rectTwips,
247
+ direction,
248
+ ...(block?.kind === "paragraph"
249
+ ? {
250
+ runAnchors: buildRunAnchorsForLine({
251
+ block,
252
+ fragment,
253
+ lineIndex,
254
+ lineCount,
255
+ rectTwips,
256
+ baselinePageYTwips,
257
+ direction,
258
+ }),
259
+ }
260
+ : {}),
233
261
  });
234
262
  }
235
263
  cursorTwips += Math.max(0, fragment.heightTwips);
@@ -241,6 +269,121 @@ export function projectLineBoxesForPageFragments(
241
269
  return byPage;
242
270
  }
243
271
 
272
+ function lineRectForFragment(
273
+ page: DocumentPageSnapshot,
274
+ fragment: Omit<RuntimeBlockFragment, "pageId">,
275
+ lineTopTwips: number,
276
+ widthTwips: number,
277
+ heightTwips: number,
278
+ ): RuntimeTwipsRect {
279
+ const bodyX = page.layout.marginLeft + page.layout.gutter;
280
+ const bodyY = page.layout.marginTop;
281
+ const columnIndex = Math.max(0, fragment.columnIndex ?? 0);
282
+ const columnOffset = columnIndex > 0 ? columnIndex * Math.max(0, widthTwips) : 0;
283
+ return {
284
+ xTwips: bodyX + columnOffset,
285
+ yTwips: bodyY + lineTopTwips,
286
+ widthTwips: Math.max(0, widthTwips),
287
+ heightTwips: Math.max(0, heightTwips),
288
+ };
289
+ }
290
+
291
+ function buildRunAnchorsForLine(input: {
292
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>;
293
+ fragment: Omit<RuntimeBlockFragment, "pageId">;
294
+ lineIndex: number;
295
+ lineCount: number;
296
+ rectTwips: RuntimeTwipsRect;
297
+ baselinePageYTwips: number;
298
+ direction: "ltr" | "rtl";
299
+ }): RuntimeLineRunAnchor[] {
300
+ const textSegments = input.block.segments.filter(
301
+ (segment): segment is Extract<SurfaceInlineSegment, { kind: "text" }> =>
302
+ segment.kind === "text" && segment.text.length > 0,
303
+ );
304
+ if (textSegments.length === 0) return [];
305
+
306
+ const totalChars = Math.max(
307
+ 1,
308
+ textSegments.reduce((sum, segment) => sum + Array.from(segment.text).length, 0),
309
+ );
310
+ const totalLineCount =
311
+ input.fragment.kind === "paragraph-slice" && input.fragment.paragraphLineRange
312
+ ? Math.max(1, input.fragment.paragraphLineRange.totalLines)
313
+ : Math.max(1, input.lineCount);
314
+ const globalLineIndex =
315
+ (input.fragment.kind === "paragraph-slice" && input.fragment.paragraphLineRange
316
+ ? input.fragment.paragraphLineRange.from
317
+ : 0) + input.lineIndex;
318
+ const charsPerLine = Math.max(1, Math.ceil(totalChars / totalLineCount));
319
+ const lineCharStart = globalLineIndex * charsPerLine;
320
+ const lineCharEnd = Math.min(totalChars, lineCharStart + charsPerLine);
321
+ if (lineCharEnd <= lineCharStart) return [];
322
+
323
+ const charWidth = Math.max(1, input.rectTwips.widthTwips / charsPerLine);
324
+ const anchors: RuntimeLineRunAnchor[] = [];
325
+ let charCursor = 0;
326
+ for (const segment of textSegments) {
327
+ const segmentLength = Array.from(segment.text).length;
328
+ const segmentStart = charCursor;
329
+ const segmentEnd = charCursor + segmentLength;
330
+ charCursor = segmentEnd;
331
+
332
+ const overlapStart = Math.max(segmentStart, lineCharStart);
333
+ const overlapEnd = Math.min(segmentEnd, lineCharEnd);
334
+ if (overlapEnd <= overlapStart) continue;
335
+
336
+ const startInLine = overlapStart - lineCharStart;
337
+ const glyphCount = Math.max(1, overlapEnd - overlapStart);
338
+ const runWidthTwips = Math.max(1, Math.round(glyphCount * charWidth));
339
+ const startOffsetTwips = Math.round(startInLine * charWidth);
340
+ const xTwips =
341
+ input.direction === "rtl"
342
+ ? input.rectTwips.xTwips + input.rectTwips.widthTwips - startOffsetTwips - runWidthTwips
343
+ : input.rectTwips.xTwips + startOffsetTwips;
344
+ const glyphWidthTwips = Math.max(1, Math.round(charWidth));
345
+ const runRectTwips: RuntimeTwipsRect = {
346
+ xTwips,
347
+ yTwips: input.rectTwips.yTwips,
348
+ widthTwips: runWidthTwips,
349
+ heightTwips: input.rectTwips.heightTwips,
350
+ };
351
+ const firstGlyphRectTwips: RuntimeTwipsRect =
352
+ input.direction === "rtl"
353
+ ? {
354
+ ...runRectTwips,
355
+ xTwips: xTwips + Math.max(0, runWidthTwips - glyphWidthTwips),
356
+ widthTwips: glyphWidthTwips,
357
+ }
358
+ : { ...runRectTwips, widthTwips: glyphWidthTwips };
359
+ const lastGlyphRectTwips: RuntimeTwipsRect =
360
+ input.direction === "rtl"
361
+ ? { ...runRectTwips, widthTwips: glyphWidthTwips }
362
+ : {
363
+ ...runRectTwips,
364
+ xTwips: xTwips + Math.max(0, runWidthTwips - glyphWidthTwips),
365
+ widthTwips: glyphWidthTwips,
366
+ };
367
+ const runId = `${input.block.blockId}:${segment.segmentId}`;
368
+ anchors.push({
369
+ anchorId: `${input.fragment.fragmentId}:line-${input.lineIndex}:${segment.segmentId}`,
370
+ runId,
371
+ segmentId: segment.segmentId,
372
+ blockId: input.block.blockId,
373
+ fragmentId: input.fragment.fragmentId,
374
+ lineIndex: input.lineIndex,
375
+ direction: input.direction,
376
+ baselinePageYTwips: input.baselinePageYTwips,
377
+ lineRectTwips: { ...input.rectTwips },
378
+ firstGlyphRectTwips,
379
+ lastGlyphRectTwips,
380
+ runRectTwips,
381
+ precision: "layout-estimate",
382
+ });
383
+ }
384
+ return anchors;
385
+ }
386
+
244
387
  /**
245
388
  * Emit one fragment per slice for a paragraph that pagination split across
246
389
  * pages. The source `block` offset range stays intact on every slice; the
@@ -34,6 +34,7 @@ import type {
34
34
  RuntimePageNode,
35
35
  RuntimePageRegion,
36
36
  RuntimePageRegions,
37
+ RuntimeLineRunAnchor,
37
38
  RuntimeStoryAnchoredObject,
38
39
  RuntimeTwipsRect,
39
40
  } from "./page-graph.ts";
@@ -189,6 +190,7 @@ export interface PublicStoryAnchoredObject {
189
190
  widthTwips: number;
190
191
  heightTwips: number;
191
192
  };
193
+ anchorRectTwips?: PublicTwipsRect;
192
194
  relationshipIds?: readonly string[];
193
195
  mediaIds?: readonly string[];
194
196
  preserveOnly: boolean;
@@ -204,6 +206,8 @@ export interface PublicPageNode {
204
206
  endOffset: number;
205
207
  /** Whether this page is a blank filler (e.g. from evenPage/oddPage). */
206
208
  isBlankFiller: boolean;
209
+ /** Whether L04 has measured this page or it is a lazy-pagination placeholder. */
210
+ materialization: NonNullable<RuntimePageNode["materialization"]>;
207
211
  /** Resolved display page number (1-based, honors section restarts). */
208
212
  displayPageNumber: number;
209
213
  /** Whether this is treated as the first page of its section (title page). */
@@ -323,8 +327,28 @@ export interface PublicLineBox {
323
327
  fragmentId: string;
324
328
  lineIndex: number;
325
329
  baselineTwips: number;
330
+ baselinePageYTwips?: number;
326
331
  heightTwips: number;
327
332
  widthTwips: number;
333
+ rectTwips?: PublicTwipsRect;
334
+ direction?: "ltr" | "rtl";
335
+ runAnchors?: readonly PublicLineRunAnchor[];
336
+ }
337
+
338
+ export interface PublicLineRunAnchor {
339
+ anchorId: string;
340
+ runId: string;
341
+ segmentId: string;
342
+ blockId: string;
343
+ fragmentId: string;
344
+ lineIndex: number;
345
+ direction: "ltr" | "rtl";
346
+ baselinePageYTwips: number;
347
+ lineRectTwips: PublicTwipsRect;
348
+ firstGlyphRectTwips: PublicTwipsRect;
349
+ lastGlyphRectTwips: PublicTwipsRect;
350
+ runRectTwips: PublicTwipsRect;
351
+ precision: RuntimeLineRunAnchor["precision"];
328
352
  }
329
353
 
330
354
  export interface PublicNoteAllocation {
@@ -348,6 +372,36 @@ export interface PublicPageSpan {
348
372
  pageCount: number;
349
373
  }
350
374
 
375
+ export interface PublicPagePaginationTelemetry {
376
+ pageId: string;
377
+ pageIndex: number;
378
+ startOffset: number;
379
+ endOffset: number;
380
+ isBlankFiller: boolean;
381
+ materialization: NonNullable<RuntimePageNode["materialization"]>;
382
+ bodyFragmentCount: number;
383
+ /**
384
+ * Unique body block ids referenced by this page. Split blocks count once
385
+ * on each page they occupy, which makes this the useful page-window
386
+ * estimator for block-index based viewport callers.
387
+ */
388
+ bodyBlockReferenceCount: number;
389
+ lineBoxCount: number;
390
+ noteAllocationCount: number;
391
+ }
392
+
393
+ export interface PublicPaginationTelemetry {
394
+ revision: number;
395
+ pageCount: number;
396
+ materializedPageCount: number;
397
+ unpaginatedPageCount: number;
398
+ bodyFragmentCount: number;
399
+ bodyBlockReferenceCount: number;
400
+ averageBodyFragmentsPerMaterializedPage: number;
401
+ averageBodyBlockReferencesPerMaterializedPage: number;
402
+ pages: readonly PublicPagePaginationTelemetry[];
403
+ }
404
+
351
405
  export interface PublicSectionNode {
352
406
  sectionIndex: number;
353
407
  startOffset: number;
@@ -556,6 +610,13 @@ export interface WordReviewEditorLayoutFacet {
556
610
  */
557
611
  getStoryRegionsOnPage(pageIndex: number): readonly PublicPageRegion[];
558
612
  getFragmentsForPage(pageIndex: number): PublicBlockFragment[];
613
+ /**
614
+ * Per-page materialization and body block/fragment counts derived from the
615
+ * current page graph. Consumers that only have block-index viewport ranges
616
+ * can use this to estimate page windows from the actual document instead
617
+ * of hardcoded blocks-per-page constants.
618
+ */
619
+ getPaginationTelemetry(): PublicPaginationTelemetry;
559
620
 
560
621
  /**
561
622
  * P8 — Returns per-region block snapshots for a page. Body resolves the
@@ -1229,6 +1290,10 @@ export function createLayoutFacet(
1229
1290
  .map((f) => toPublicBlockFragment(f, graph));
1230
1291
  },
1231
1292
 
1293
+ getPaginationTelemetry() {
1294
+ return toPublicPaginationTelemetry(currentGraph());
1295
+ },
1296
+
1232
1297
  getResolvedFormatting(blockId) {
1233
1298
  const state = currentFormatting();
1234
1299
  const formatting = state.paragraphs.get(blockId);
@@ -1446,6 +1511,7 @@ function toPublicPageNode(
1446
1511
  startOffset: node.startOffset,
1447
1512
  endOffset: node.endOffset,
1448
1513
  isBlankFiller: node.isBlankFiller,
1514
+ materialization: node.materialization ?? "paginated",
1449
1515
  displayPageNumber: node.stories.displayPageNumber,
1450
1516
  isFirstPage: node.stories.isFirstPage,
1451
1517
  isEvenPage: node.stories.isEvenPage,
@@ -1554,6 +1620,9 @@ function toPublicStoryAnchoredObject(
1554
1620
  },
1555
1621
  }
1556
1622
  : {}),
1623
+ ...(object.anchorRectTwips
1624
+ ? { anchorRectTwips: toPublicTwipsRect(object.anchorRectTwips) }
1625
+ : {}),
1557
1626
  ...(object.relationshipIds ? { relationshipIds: [...object.relationshipIds] } : {}),
1558
1627
  ...(object.mediaIds ? { mediaIds: [...object.mediaIds] } : {}),
1559
1628
  preserveOnly: object.preserveOnly,
@@ -1643,6 +1712,76 @@ function toPublicBlockFragment(
1643
1712
  };
1644
1713
  }
1645
1714
 
1715
+ function bodyFragmentsForPage(
1716
+ node: RuntimePageNode,
1717
+ graph: RuntimePageGraph,
1718
+ ): RuntimeBlockFragment[] {
1719
+ const bodyFragmentIds = new Set(node.regions.body.fragmentIds);
1720
+ return graph.fragments.filter((fragment) =>
1721
+ bodyFragmentIds.has(fragment.fragmentId),
1722
+ );
1723
+ }
1724
+
1725
+ function toPublicPagePaginationTelemetry(
1726
+ node: RuntimePageNode,
1727
+ graph: RuntimePageGraph,
1728
+ ): PublicPagePaginationTelemetry {
1729
+ const bodyFragments = bodyFragmentsForPage(node, graph);
1730
+ const bodyBlockIds = new Set(bodyFragments.map((fragment) => fragment.blockId));
1731
+ return {
1732
+ pageId: node.pageId,
1733
+ pageIndex: node.pageIndex,
1734
+ startOffset: node.startOffset,
1735
+ endOffset: node.endOffset,
1736
+ isBlankFiller: node.isBlankFiller,
1737
+ materialization: node.materialization ?? "paginated",
1738
+ bodyFragmentCount: bodyFragments.length,
1739
+ bodyBlockReferenceCount: bodyBlockIds.size,
1740
+ lineBoxCount: node.lineBoxes.length,
1741
+ noteAllocationCount: node.noteAllocations.length,
1742
+ };
1743
+ }
1744
+
1745
+ function average(count: number, divisor: number): number {
1746
+ return divisor > 0 ? count / divisor : 0;
1747
+ }
1748
+
1749
+ function toPublicPaginationTelemetry(
1750
+ graph: RuntimePageGraph,
1751
+ ): PublicPaginationTelemetry {
1752
+ const pages = graph.pages.map((node) =>
1753
+ toPublicPagePaginationTelemetry(node, graph),
1754
+ );
1755
+ const materializedPages = pages.filter(
1756
+ (page) => page.materialization === "paginated",
1757
+ );
1758
+ const bodyFragmentCount = materializedPages.reduce(
1759
+ (total, page) => total + page.bodyFragmentCount,
1760
+ 0,
1761
+ );
1762
+ const bodyBlockReferenceCount = materializedPages.reduce(
1763
+ (total, page) => total + page.bodyBlockReferenceCount,
1764
+ 0,
1765
+ );
1766
+ return {
1767
+ revision: graph.revision,
1768
+ pageCount: pages.length,
1769
+ materializedPageCount: materializedPages.length,
1770
+ unpaginatedPageCount: pages.length - materializedPages.length,
1771
+ bodyFragmentCount,
1772
+ bodyBlockReferenceCount,
1773
+ averageBodyFragmentsPerMaterializedPage: average(
1774
+ bodyFragmentCount,
1775
+ materializedPages.length,
1776
+ ),
1777
+ averageBodyBlockReferencesPerMaterializedPage: average(
1778
+ bodyBlockReferenceCount,
1779
+ materializedPages.length,
1780
+ ),
1781
+ pages,
1782
+ };
1783
+ }
1784
+
1646
1785
  function cloneContinuationCursor(
1647
1786
  cursor: RuntimeLayoutContinuationCursor,
1648
1787
  ): RuntimeLayoutContinuationCursor {
@@ -1721,8 +1860,32 @@ function toPublicLineBox(box: RuntimeLineBox): PublicLineBox {
1721
1860
  fragmentId: box.fragmentId,
1722
1861
  lineIndex: box.lineIndex,
1723
1862
  baselineTwips: box.baselineTwips,
1863
+ ...(box.baselinePageYTwips !== undefined
1864
+ ? { baselinePageYTwips: box.baselinePageYTwips }
1865
+ : {}),
1724
1866
  heightTwips: box.heightTwips,
1725
1867
  widthTwips: box.widthTwips,
1868
+ ...(box.rectTwips ? { rectTwips: toPublicTwipsRect(box.rectTwips) } : {}),
1869
+ ...(box.direction ? { direction: box.direction } : {}),
1870
+ ...(box.runAnchors ? { runAnchors: box.runAnchors.map(toPublicLineRunAnchor) } : {}),
1871
+ };
1872
+ }
1873
+
1874
+ function toPublicLineRunAnchor(anchor: RuntimeLineRunAnchor): PublicLineRunAnchor {
1875
+ return {
1876
+ anchorId: anchor.anchorId,
1877
+ runId: anchor.runId,
1878
+ segmentId: anchor.segmentId,
1879
+ blockId: anchor.blockId,
1880
+ fragmentId: anchor.fragmentId,
1881
+ lineIndex: anchor.lineIndex,
1882
+ direction: anchor.direction,
1883
+ baselinePageYTwips: anchor.baselinePageYTwips,
1884
+ lineRectTwips: toPublicTwipsRect(anchor.lineRectTwips),
1885
+ firstGlyphRectTwips: toPublicTwipsRect(anchor.firstGlyphRectTwips),
1886
+ lastGlyphRectTwips: toPublicTwipsRect(anchor.lastGlyphRectTwips),
1887
+ runRectTwips: toPublicTwipsRect(anchor.runRectTwips),
1888
+ precision: anchor.precision,
1726
1889
  };
1727
1890
  }
1728
1891
 
@@ -2178,6 +2341,10 @@ function resolveHeaderFooterRegionBlocks(
2178
2341
  blockSnapshot,
2179
2342
  node,
2180
2343
  graph,
2344
+ {
2345
+ ledger: findPageLocalStoryFieldLedger(node, regionKind, storyTarget),
2346
+ ordinal: 0,
2347
+ },
2181
2348
  );
2182
2349
  return {
2183
2350
  blockId: blockSnapshot.blockId,
@@ -2201,6 +2368,33 @@ const PAGE_INSTANCE_FIELD_FAMILIES = new Set([
2201
2368
  "SECTIONPAGES",
2202
2369
  ]);
2203
2370
 
2371
+ type PageLocalFieldLedger = readonly {
2372
+ readonly family: string;
2373
+ readonly displayText: string;
2374
+ }[];
2375
+
2376
+ interface PageScopedFieldState {
2377
+ readonly ledger: PageLocalFieldLedger | undefined;
2378
+ ordinal: number;
2379
+ }
2380
+
2381
+ function findPageLocalStoryFieldLedger(
2382
+ page: RuntimePageNode,
2383
+ regionKind: "header" | "footer",
2384
+ storyTarget: EditorStoryTarget,
2385
+ ): PageLocalFieldLedger | undefined {
2386
+ if (storyTarget.kind !== regionKind) return undefined;
2387
+ const pageLocalStory = page.frame?.pageLocalStories.find(
2388
+ (story) =>
2389
+ story.kind === storyTarget.kind &&
2390
+ story.relationshipId === storyTarget.relationshipId &&
2391
+ story.variant === storyTarget.variant &&
2392
+ (storyTarget.sectionIndex === undefined ||
2393
+ story.sectionIndex === storyTarget.sectionIndex),
2394
+ );
2395
+ return pageLocalStory?.resolvedFields;
2396
+ }
2397
+
2204
2398
  function resolvePageScopedFieldsInBlocks(
2205
2399
  blocks: readonly SurfaceBlockSnapshot[],
2206
2400
  page: RuntimePageNode,
@@ -2208,7 +2402,10 @@ function resolvePageScopedFieldsInBlocks(
2208
2402
  ): readonly SurfaceBlockSnapshot[] {
2209
2403
  let changed = false;
2210
2404
  const resolvedBlocks = blocks.map((block) => {
2211
- const resolved = resolvePageInstanceFieldsInBlock(block, page, graph);
2405
+ const resolved = resolvePageInstanceFieldsInBlock(block, page, graph, {
2406
+ ledger: undefined,
2407
+ ordinal: 0,
2408
+ });
2212
2409
  if (resolved !== block) changed = true;
2213
2410
  return resolved;
2214
2411
  });
@@ -2219,10 +2416,16 @@ function resolvePageInstanceFieldsInBlock(
2219
2416
  block: SurfaceBlockSnapshot,
2220
2417
  page: RuntimePageNode,
2221
2418
  graph: RuntimePageGraph,
2419
+ fieldState: PageScopedFieldState,
2222
2420
  ): SurfaceBlockSnapshot {
2223
2421
  switch (block.kind) {
2224
2422
  case "paragraph": {
2225
- const segments = resolvePageInstanceFieldsInSegments(block.segments, page, graph);
2423
+ const segments = resolvePageInstanceFieldsInSegments(
2424
+ block.segments,
2425
+ page,
2426
+ graph,
2427
+ fieldState,
2428
+ );
2226
2429
  return segments === block.segments ? block : { ...block, segments };
2227
2430
  }
2228
2431
  case "table": {
@@ -2232,7 +2435,12 @@ function resolvePageInstanceFieldsInBlock(
2232
2435
  const cells = row.cells.map((cell) => {
2233
2436
  let cellChanged = false;
2234
2437
  const content = cell.content.map((child) => {
2235
- const resolved = resolvePageInstanceFieldsInBlock(child, page, graph);
2438
+ const resolved = resolvePageInstanceFieldsInBlock(
2439
+ child,
2440
+ page,
2441
+ graph,
2442
+ fieldState,
2443
+ );
2236
2444
  if (resolved !== child) cellChanged = true;
2237
2445
  return resolved;
2238
2446
  });
@@ -2249,7 +2457,12 @@ function resolvePageInstanceFieldsInBlock(
2249
2457
  case "sdt_block": {
2250
2458
  let changed = false;
2251
2459
  const children = block.children.map((child) => {
2252
- const resolved = resolvePageInstanceFieldsInBlock(child, page, graph);
2460
+ const resolved = resolvePageInstanceFieldsInBlock(
2461
+ child,
2462
+ page,
2463
+ graph,
2464
+ fieldState,
2465
+ );
2253
2466
  if (resolved !== child) changed = true;
2254
2467
  return resolved;
2255
2468
  });
@@ -2264,17 +2477,23 @@ function resolvePageInstanceFieldsInSegments(
2264
2477
  segments: SurfaceInlineSegment[],
2265
2478
  page: RuntimePageNode,
2266
2479
  graph: RuntimePageGraph,
2480
+ fieldState: PageScopedFieldState,
2267
2481
  ): SurfaceInlineSegment[] {
2268
2482
  let changed = false;
2269
2483
  const resolvedSegments = segments.map((segment) => {
2270
2484
  if (segment.kind !== "field_ref" || !PAGE_INSTANCE_FIELD_FAMILIES.has(segment.fieldFamily)) {
2271
2485
  return segment;
2272
2486
  }
2273
- const displayText = resolvePageFieldDisplayText(
2274
- segment.fieldFamily,
2275
- segment.displayText ?? segment.label,
2276
- { page, graph },
2277
- );
2487
+ const ledgerField = fieldState.ledger?.[fieldState.ordinal];
2488
+ fieldState.ordinal += 1;
2489
+ const displayText =
2490
+ ledgerField?.family === segment.fieldFamily
2491
+ ? ledgerField.displayText
2492
+ : resolvePageFieldDisplayText(
2493
+ segment.fieldFamily,
2494
+ segment.displayText ?? segment.label,
2495
+ { page, graph },
2496
+ );
2278
2497
  if (segment.displayText === displayText && segment.refreshStatus === "current") {
2279
2498
  return segment;
2280
2499
  }