@beyondwork/docx-react-component 1.0.110 → 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 (52) 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/runtime/geometry/adjacent-geometry-intake.ts +373 -5
  5. package/src/runtime/geometry/caret-geometry.ts +219 -7
  6. package/src/runtime/geometry/geometry-index.ts +35 -10
  7. package/src/runtime/geometry/object-handles.ts +42 -1
  8. package/src/runtime/layout/index.ts +3 -0
  9. package/src/runtime/layout/inert-layout-facet.ts +13 -0
  10. package/src/runtime/layout/layout-engine-instance.ts +2 -0
  11. package/src/runtime/layout/layout-engine-version.ts +32 -2
  12. package/src/runtime/layout/layout-facet-types.ts +3 -0
  13. package/src/runtime/layout/page-graph.ts +81 -7
  14. package/src/runtime/layout/project-block-fragments.ts +144 -1
  15. package/src/runtime/layout/public-facet.ts +160 -0
  16. package/src/runtime/scopes/adjacent-geometry-evidence.ts +456 -0
  17. package/src/runtime/scopes/compile-scope-bundle.ts +8 -0
  18. package/src/runtime/scopes/evidence.ts +16 -0
  19. package/src/runtime/scopes/index.ts +13 -0
  20. package/src/runtime/scopes/semantic-scope-types.ts +67 -0
  21. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +104 -0
  22. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +50 -5
  23. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +26 -0
  24. package/src/README.md +0 -85
  25. package/src/api/README.md +0 -26
  26. package/src/api/v3/README.md +0 -91
  27. package/src/component-inventory.md +0 -99
  28. package/src/core/README.md +0 -10
  29. package/src/core/commands/README.md +0 -3
  30. package/src/core/schema/README.md +0 -3
  31. package/src/core/selection/README.md +0 -3
  32. package/src/core/state/README.md +0 -3
  33. package/src/io/README.md +0 -10
  34. package/src/io/export/README.md +0 -3
  35. package/src/io/normalize/README.md +0 -3
  36. package/src/io/ooxml/README.md +0 -3
  37. package/src/io/opc/README.md +0 -3
  38. package/src/model/README.md +0 -3
  39. package/src/preservation/README.md +0 -3
  40. package/src/review/README.md +0 -16
  41. package/src/review/store/README.md +0 -3
  42. package/src/runtime/README.md +0 -3
  43. package/src/ui/README.md +0 -30
  44. package/src/ui/comments/README.md +0 -3
  45. package/src/ui/compatibility/README.md +0 -3
  46. package/src/ui/editor-surface/README.md +0 -3
  47. package/src/ui/review/README.md +0 -3
  48. package/src/ui/status/README.md +0 -3
  49. package/src/ui/theme/README.md +0 -3
  50. package/src/ui/toolbar/README.md +0 -3
  51. package/src/ui-tailwind/debug/README.md +0 -22
  52. package/src/validation/README.md +0 -3
@@ -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;
@@ -325,8 +327,28 @@ export interface PublicLineBox {
325
327
  fragmentId: string;
326
328
  lineIndex: number;
327
329
  baselineTwips: number;
330
+ baselinePageYTwips?: number;
328
331
  heightTwips: number;
329
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"];
330
352
  }
331
353
 
332
354
  export interface PublicNoteAllocation {
@@ -350,6 +372,36 @@ export interface PublicPageSpan {
350
372
  pageCount: number;
351
373
  }
352
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
+
353
405
  export interface PublicSectionNode {
354
406
  sectionIndex: number;
355
407
  startOffset: number;
@@ -558,6 +610,13 @@ export interface WordReviewEditorLayoutFacet {
558
610
  */
559
611
  getStoryRegionsOnPage(pageIndex: number): readonly PublicPageRegion[];
560
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;
561
620
 
562
621
  /**
563
622
  * P8 — Returns per-region block snapshots for a page. Body resolves the
@@ -1231,6 +1290,10 @@ export function createLayoutFacet(
1231
1290
  .map((f) => toPublicBlockFragment(f, graph));
1232
1291
  },
1233
1292
 
1293
+ getPaginationTelemetry() {
1294
+ return toPublicPaginationTelemetry(currentGraph());
1295
+ },
1296
+
1234
1297
  getResolvedFormatting(blockId) {
1235
1298
  const state = currentFormatting();
1236
1299
  const formatting = state.paragraphs.get(blockId);
@@ -1557,6 +1620,9 @@ function toPublicStoryAnchoredObject(
1557
1620
  },
1558
1621
  }
1559
1622
  : {}),
1623
+ ...(object.anchorRectTwips
1624
+ ? { anchorRectTwips: toPublicTwipsRect(object.anchorRectTwips) }
1625
+ : {}),
1560
1626
  ...(object.relationshipIds ? { relationshipIds: [...object.relationshipIds] } : {}),
1561
1627
  ...(object.mediaIds ? { mediaIds: [...object.mediaIds] } : {}),
1562
1628
  preserveOnly: object.preserveOnly,
@@ -1646,6 +1712,76 @@ function toPublicBlockFragment(
1646
1712
  };
1647
1713
  }
1648
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
+
1649
1785
  function cloneContinuationCursor(
1650
1786
  cursor: RuntimeLayoutContinuationCursor,
1651
1787
  ): RuntimeLayoutContinuationCursor {
@@ -1724,8 +1860,32 @@ function toPublicLineBox(box: RuntimeLineBox): PublicLineBox {
1724
1860
  fragmentId: box.fragmentId,
1725
1861
  lineIndex: box.lineIndex,
1726
1862
  baselineTwips: box.baselineTwips,
1863
+ ...(box.baselinePageYTwips !== undefined
1864
+ ? { baselinePageYTwips: box.baselinePageYTwips }
1865
+ : {}),
1727
1866
  heightTwips: box.heightTwips,
1728
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,
1729
1889
  };
1730
1890
  }
1731
1891