@beyondwork/docx-react-component 1.0.41 → 1.0.43
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 +38 -37
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +541 -5
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +601 -9
- package/src/core/search/search-text.ts +15 -2
- package/src/index.ts +131 -1
- package/src/io/docx-session.ts +672 -2
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +83 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +367 -0
- package/src/io/ooxml/workflow-payload.ts +317 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +639 -124
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +4 -0
- package/src/runtime/layout/layout-engine-instance.ts +139 -14
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +441 -48
- package/src/runtime/layout/public-facet.ts +585 -14
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/surface-projection.ts +10 -5
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +80 -16
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +654 -45
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +111 -11
- package/src/ui/editor-shell-view.tsx +21 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +10 -256
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +37 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
|
@@ -41,6 +41,10 @@ import type {
|
|
|
41
41
|
FootnoteCollection,
|
|
42
42
|
} from "../../model/canonical-document.ts";
|
|
43
43
|
import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
|
|
44
|
+
import type {
|
|
45
|
+
RuntimeNoteAllocation,
|
|
46
|
+
RuntimeBlockFragment,
|
|
47
|
+
} from "./page-graph.ts";
|
|
44
48
|
import {
|
|
45
49
|
buildPageLayoutSnapshot,
|
|
46
50
|
buildResolvedSections,
|
|
@@ -63,6 +67,12 @@ import {
|
|
|
63
67
|
import { analyzeInvalidation as analyzeInvalidationFn } from "./layout-invalidation.ts";
|
|
64
68
|
import type { LayoutMeasurementProvider } from "./layout-measurement-provider.ts";
|
|
65
69
|
import { paginateParagraphLines } from "./paginate-paragraph-lines.ts";
|
|
70
|
+
import {
|
|
71
|
+
computeRepeatedHeaderHeight,
|
|
72
|
+
extractRowFlags,
|
|
73
|
+
findTableRowSplit,
|
|
74
|
+
measureTableRowHeights,
|
|
75
|
+
} from "./table-row-split.ts";
|
|
66
76
|
|
|
67
77
|
// ---------------------------------------------------------------------------
|
|
68
78
|
// Types
|
|
@@ -132,6 +142,19 @@ export interface BlockSplits {
|
|
|
132
142
|
export interface PageStackResultWithSplits {
|
|
133
143
|
pages: DocumentPageSnapshot[];
|
|
134
144
|
splits: BlockSplits;
|
|
145
|
+
/**
|
|
146
|
+
* P8.1b — per-page note allocations emitted by the engine.
|
|
147
|
+
* Keyed by zero-based global page index.
|
|
148
|
+
* Absent entries mean the page has no footnotes.
|
|
149
|
+
* Endnote allocations are deferred to P8 Task 7.
|
|
150
|
+
*/
|
|
151
|
+
noteAllocationsByPageIndex?: ReadonlyMap<number, RuntimeNoteAllocation[]>;
|
|
152
|
+
/**
|
|
153
|
+
* P8.1b — per-page note body fragments emitted by the engine.
|
|
154
|
+
* Each fragment has `regionKind: "footnote-area"`.
|
|
155
|
+
* Parallel to `noteAllocationsByPageIndex`.
|
|
156
|
+
*/
|
|
157
|
+
noteFragmentsByPageIndex?: ReadonlyMap<number, Array<Omit<RuntimeBlockFragment, "pageId">>>;
|
|
135
158
|
}
|
|
136
159
|
|
|
137
160
|
// ---------------------------------------------------------------------------
|
|
@@ -176,7 +199,19 @@ export function buildPageStackWithSplits(
|
|
|
176
199
|
): PageStackResultWithSplits {
|
|
177
200
|
const pages: DocumentPageSnapshot[] = [];
|
|
178
201
|
const splitsByBlock = new Map<string, ParagraphLineSlice[]>();
|
|
202
|
+
// P8.1b — aggregate note allocations and fragments across all sections,
|
|
203
|
+
// keyed by global page index.
|
|
204
|
+
const globalNoteAllocationsByPageIndex = new Map<number, RuntimeNoteAllocation[]>();
|
|
205
|
+
const globalNoteFragmentsByPageIndex = new Map<
|
|
206
|
+
number,
|
|
207
|
+
Array<Omit<RuntimeBlockFragment, "pageId">>
|
|
208
|
+
>();
|
|
179
209
|
let globalPageIndex = 0;
|
|
210
|
+
// A single cache lives for the whole pagination pass so cross-section
|
|
211
|
+
// re-measurement (rare but possible through keepNext heuristics) still
|
|
212
|
+
// reuses heights. The WeakMap frees memory automatically when the block
|
|
213
|
+
// snapshots go out of scope at the end of the call.
|
|
214
|
+
const cache = createMeasurementCache();
|
|
180
215
|
|
|
181
216
|
for (let sectionIdx = 0; sectionIdx < sections.length; sectionIdx++) {
|
|
182
217
|
const section = sections[sectionIdx]!;
|
|
@@ -234,6 +269,7 @@ export function buildPageStackWithSplits(
|
|
|
234
269
|
layout,
|
|
235
270
|
document.subParts?.footnoteCollection,
|
|
236
271
|
measurementProvider,
|
|
272
|
+
cache,
|
|
237
273
|
);
|
|
238
274
|
const paginated = paginatedResult.pages;
|
|
239
275
|
|
|
@@ -293,6 +329,21 @@ export function buildPageStackWithSplits(
|
|
|
293
329
|
}
|
|
294
330
|
if (existing.length > 0) splitsByBlock.set(blockId, existing);
|
|
295
331
|
}
|
|
332
|
+
|
|
333
|
+
// P8.1b — resolve per-section note allocations + fragments to global
|
|
334
|
+
// page index and merge into the global maps.
|
|
335
|
+
for (const [pageInSec, sectionAllocs] of paginatedResult.noteAllocationsByPageInSection) {
|
|
336
|
+
const globalPageIdx = pageInSectionToGlobal.get(pageInSec);
|
|
337
|
+
if (globalPageIdx === undefined) continue;
|
|
338
|
+
const existing = globalNoteAllocationsByPageIndex.get(globalPageIdx) ?? [];
|
|
339
|
+
globalNoteAllocationsByPageIndex.set(globalPageIdx, [...existing, ...sectionAllocs]);
|
|
340
|
+
}
|
|
341
|
+
for (const [pageInSec, sectionFrags] of paginatedResult.noteFragmentsByPageInSection) {
|
|
342
|
+
const globalPageIdx = pageInSectionToGlobal.get(pageInSec);
|
|
343
|
+
if (globalPageIdx === undefined) continue;
|
|
344
|
+
const existing = globalNoteFragmentsByPageIndex.get(globalPageIdx) ?? [];
|
|
345
|
+
globalNoteFragmentsByPageIndex.set(globalPageIdx, [...existing, ...sectionFrags]);
|
|
346
|
+
}
|
|
296
347
|
}
|
|
297
348
|
|
|
298
349
|
// Guarantee at least one page
|
|
@@ -316,6 +367,12 @@ export function buildPageStackWithSplits(
|
|
|
316
367
|
return {
|
|
317
368
|
pages,
|
|
318
369
|
splits: { byBlockId: splitsByBlock, tablesByBlockId: tableSplitsByBlock },
|
|
370
|
+
noteAllocationsByPageIndex: globalNoteAllocationsByPageIndex.size > 0
|
|
371
|
+
? globalNoteAllocationsByPageIndex
|
|
372
|
+
: undefined,
|
|
373
|
+
noteFragmentsByPageIndex: globalNoteFragmentsByPageIndex.size > 0
|
|
374
|
+
? globalNoteFragmentsByPageIndex
|
|
375
|
+
: undefined,
|
|
319
376
|
};
|
|
320
377
|
}
|
|
321
378
|
|
|
@@ -560,6 +617,59 @@ export function requiresFullRecompute(reason: LayoutInvalidationReason): boolean
|
|
|
560
617
|
const MIN_BLOCK_HEIGHT_TWIPS = 240;
|
|
561
618
|
const FOOTNOTE_REFERENCE_RESERVATION_TWIPS = 180;
|
|
562
619
|
|
|
620
|
+
/**
|
|
621
|
+
* Per-invocation measurement cache keyed by `(block, columnWidth)`.
|
|
622
|
+
*
|
|
623
|
+
* Pagination re-measures the same block more than once in the hot path:
|
|
624
|
+
* - `keepNext` checks both the current and the next block's height
|
|
625
|
+
* - Intra-paragraph splits re-ask for `measureParagraphLineCount`
|
|
626
|
+
* - Multi-pass pagination (splitRule, column advance) may loop over the
|
|
627
|
+
* same block with the same column width
|
|
628
|
+
*
|
|
629
|
+
* The cache is scoped to a single `buildPageStackWithSplits` call. Block
|
|
630
|
+
* references are stable during one pagination pass (the `blocks` array is
|
|
631
|
+
* frozen for the run), so a `WeakMap<Block, Map<columnWidth, height>>` is
|
|
632
|
+
* cheap and never outlives the call.
|
|
633
|
+
*
|
|
634
|
+
* Canvas-backed measurement is the expensive case; the empirical backend
|
|
635
|
+
* does its own work inline but the cache still saves redundant iteration.
|
|
636
|
+
*/
|
|
637
|
+
interface MeasurementCache {
|
|
638
|
+
getHeight(block: SurfaceBlockSnapshot, columnWidth: number): number | undefined;
|
|
639
|
+
setHeight(block: SurfaceBlockSnapshot, columnWidth: number, heightTwips: number): void;
|
|
640
|
+
getLineCount(block: SurfaceBlockSnapshot, columnWidth: number): number | undefined;
|
|
641
|
+
setLineCount(block: SurfaceBlockSnapshot, columnWidth: number, lineCount: number): void;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function createMeasurementCache(): MeasurementCache {
|
|
645
|
+
const heightByBlock = new WeakMap<SurfaceBlockSnapshot, Map<number, number>>();
|
|
646
|
+
const lineCountByBlock = new WeakMap<SurfaceBlockSnapshot, Map<number, number>>();
|
|
647
|
+
return {
|
|
648
|
+
getHeight(block, columnWidth) {
|
|
649
|
+
return heightByBlock.get(block)?.get(columnWidth);
|
|
650
|
+
},
|
|
651
|
+
setHeight(block, columnWidth, heightTwips) {
|
|
652
|
+
let map = heightByBlock.get(block);
|
|
653
|
+
if (!map) {
|
|
654
|
+
map = new Map();
|
|
655
|
+
heightByBlock.set(block, map);
|
|
656
|
+
}
|
|
657
|
+
map.set(columnWidth, heightTwips);
|
|
658
|
+
},
|
|
659
|
+
getLineCount(block, columnWidth) {
|
|
660
|
+
return lineCountByBlock.get(block)?.get(columnWidth);
|
|
661
|
+
},
|
|
662
|
+
setLineCount(block, columnWidth, lineCount) {
|
|
663
|
+
let map = lineCountByBlock.get(block);
|
|
664
|
+
if (!map) {
|
|
665
|
+
map = new Map();
|
|
666
|
+
lineCountByBlock.set(block, map);
|
|
667
|
+
}
|
|
668
|
+
map.set(columnWidth, lineCount);
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
563
673
|
/**
|
|
564
674
|
* Compute block height using resolved formatting when available.
|
|
565
675
|
* Uses improved table measurement for legal contracts.
|
|
@@ -567,42 +677,76 @@ const FOOTNOTE_REFERENCE_RESERVATION_TWIPS = 180;
|
|
|
567
677
|
* When `measurementProvider` is supplied, paragraph line counts are produced
|
|
568
678
|
* by `provider.measureLineFragments(...)`; otherwise the inline empirical
|
|
569
679
|
* path runs (which matches the empirical backend numerically).
|
|
680
|
+
*
|
|
681
|
+
* When `cache` is supplied, repeated measurements of the same
|
|
682
|
+
* `(block, columnWidth)` pair short-circuit to the cached value.
|
|
570
683
|
*/
|
|
571
684
|
function measureBlockHeight(
|
|
572
685
|
block: SurfaceBlockSnapshot | undefined,
|
|
573
686
|
columnWidth: number,
|
|
574
687
|
measurementProvider?: LayoutMeasurementProvider,
|
|
688
|
+
cache?: MeasurementCache,
|
|
575
689
|
): number {
|
|
576
690
|
if (!block) return 0;
|
|
577
691
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
692
|
+
const cached = cache?.getHeight(block, columnWidth);
|
|
693
|
+
if (cached !== undefined) return cached;
|
|
694
|
+
|
|
695
|
+
const compute = (): number => {
|
|
696
|
+
switch (block.kind) {
|
|
697
|
+
case "paragraph": {
|
|
698
|
+
const formatting = resolveBlockFormatting(block);
|
|
699
|
+
if (formatting) {
|
|
700
|
+
// Provider path: sum per-line heights so canvas-backed measurements
|
|
701
|
+
// that emit variable line heights (mixed inline font sizes, etc.)
|
|
702
|
+
// do not collapse to `lineCount * flatLineHeight`.
|
|
703
|
+
if (measurementProvider) {
|
|
704
|
+
const measured = measurementProvider.measureLineFragments({
|
|
705
|
+
block,
|
|
706
|
+
formatting,
|
|
707
|
+
runs: new Map(),
|
|
708
|
+
columnWidth,
|
|
709
|
+
});
|
|
710
|
+
cache?.setLineCount(block, columnWidth, measured.lineCount);
|
|
711
|
+
const contentHeight = measured.lineHeights.reduce(
|
|
712
|
+
(total, lineHeight) => total + lineHeight,
|
|
713
|
+
0,
|
|
714
|
+
);
|
|
715
|
+
const paragraphHeight =
|
|
716
|
+
contentHeight + formatting.spacingBefore + formatting.spacingAfter;
|
|
717
|
+
return Math.max(MIN_BLOCK_HEIGHT_TWIPS, paragraphHeight);
|
|
718
|
+
}
|
|
719
|
+
// Empirical-fallback path: flat per-line height × count.
|
|
720
|
+
const lineCount = measureParagraphLineCount(
|
|
721
|
+
block,
|
|
722
|
+
formatting,
|
|
723
|
+
columnWidth,
|
|
724
|
+
undefined,
|
|
725
|
+
);
|
|
726
|
+
return calculateParagraphHeight(formatting, lineCount);
|
|
727
|
+
}
|
|
728
|
+
return estimateBlockHeight(block, columnWidth);
|
|
589
729
|
}
|
|
590
|
-
|
|
730
|
+
case "table":
|
|
731
|
+
return measureTableHeight(block, columnWidth, measurementProvider, cache);
|
|
732
|
+
case "sdt_block":
|
|
733
|
+
return Math.max(
|
|
734
|
+
MIN_BLOCK_HEIGHT_TWIPS,
|
|
735
|
+
block.children.reduce(
|
|
736
|
+
(total, child) =>
|
|
737
|
+
total +
|
|
738
|
+
measureBlockHeight(child, columnWidth, measurementProvider, cache),
|
|
739
|
+
0,
|
|
740
|
+
),
|
|
741
|
+
);
|
|
742
|
+
case "opaque_block":
|
|
743
|
+
return block.label === "Section break" ? 0 : MIN_BLOCK_HEIGHT_TWIPS;
|
|
591
744
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
block.children.reduce(
|
|
598
|
-
(total, child) =>
|
|
599
|
-
total + measureBlockHeight(child, columnWidth, measurementProvider),
|
|
600
|
-
0,
|
|
601
|
-
),
|
|
602
|
-
);
|
|
603
|
-
case "opaque_block":
|
|
604
|
-
return block.label === "Section break" ? 0 : MIN_BLOCK_HEIGHT_TWIPS;
|
|
605
|
-
}
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const height = compute();
|
|
748
|
+
cache?.setHeight(block, columnWidth, height);
|
|
749
|
+
return height;
|
|
606
750
|
}
|
|
607
751
|
|
|
608
752
|
/**
|
|
@@ -621,6 +765,7 @@ function measureTableHeight(
|
|
|
621
765
|
block: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
|
|
622
766
|
columnWidth: number,
|
|
623
767
|
measurementProvider?: LayoutMeasurementProvider,
|
|
768
|
+
cache?: MeasurementCache,
|
|
624
769
|
): number {
|
|
625
770
|
const TABLE_ROW_PADDING_TWIPS = 120;
|
|
626
771
|
let totalHeight = 0;
|
|
@@ -665,6 +810,7 @@ function measureTableHeight(
|
|
|
665
810
|
child,
|
|
666
811
|
cellWidth,
|
|
667
812
|
measurementProvider,
|
|
813
|
+
cache,
|
|
668
814
|
);
|
|
669
815
|
}
|
|
670
816
|
contentHeight = Math.max(contentHeight, cellContentHeight + TABLE_ROW_PADDING_TWIPS);
|
|
@@ -883,6 +1029,10 @@ interface SectionLocalSlice {
|
|
|
883
1029
|
interface SectionPaginationResult {
|
|
884
1030
|
pages: Omit<DocumentPageSnapshot, "pageIndex">[];
|
|
885
1031
|
splits: { byBlockId: Map<string, SectionLocalSlice[]> };
|
|
1032
|
+
/** P8.1b — per-page note allocations keyed by pageInSection index. */
|
|
1033
|
+
noteAllocationsByPageInSection: Map<number, RuntimeNoteAllocation[]>;
|
|
1034
|
+
/** P8.1b — per-page note body fragments keyed by pageInSection index. */
|
|
1035
|
+
noteFragmentsByPageInSection: Map<number, Array<Omit<RuntimeBlockFragment, "pageId">>>;
|
|
886
1036
|
}
|
|
887
1037
|
|
|
888
1038
|
/**
|
|
@@ -896,6 +1046,7 @@ function paginateSectionBlocks(
|
|
|
896
1046
|
layout: DocumentPageSnapshot["layout"],
|
|
897
1047
|
footnotes: FootnoteCollection | undefined,
|
|
898
1048
|
measurementProvider?: LayoutMeasurementProvider,
|
|
1049
|
+
cache?: MeasurementCache,
|
|
899
1050
|
): Omit<DocumentPageSnapshot, "pageIndex">[] {
|
|
900
1051
|
return paginateSectionBlocksWithSplits(
|
|
901
1052
|
section,
|
|
@@ -903,15 +1054,17 @@ function paginateSectionBlocks(
|
|
|
903
1054
|
layout,
|
|
904
1055
|
footnotes,
|
|
905
1056
|
measurementProvider,
|
|
1057
|
+
cache,
|
|
906
1058
|
).pages;
|
|
907
1059
|
}
|
|
908
1060
|
|
|
909
|
-
function paginateSectionBlocksWithSplits(
|
|
1061
|
+
export function paginateSectionBlocksWithSplits(
|
|
910
1062
|
section: ResolvedDocumentSection,
|
|
911
1063
|
blocks: readonly SurfaceBlockSnapshot[],
|
|
912
1064
|
layout: DocumentPageSnapshot["layout"],
|
|
913
1065
|
footnotes: FootnoteCollection | undefined,
|
|
914
1066
|
measurementProvider?: LayoutMeasurementProvider,
|
|
1067
|
+
cache?: MeasurementCache,
|
|
915
1068
|
): SectionPaginationResult {
|
|
916
1069
|
if (blocks.length === 0) {
|
|
917
1070
|
return {
|
|
@@ -925,6 +1078,8 @@ function paginateSectionBlocksWithSplits(
|
|
|
925
1078
|
},
|
|
926
1079
|
],
|
|
927
1080
|
splits: { byBlockId: new Map() }, // section-local; global map includes tablesByBlockId via collectTableRowSlices
|
|
1081
|
+
noteAllocationsByPageInSection: new Map(),
|
|
1082
|
+
noteFragmentsByPageInSection: new Map(),
|
|
928
1083
|
};
|
|
929
1084
|
}
|
|
930
1085
|
|
|
@@ -939,15 +1094,109 @@ function paginateSectionBlocksWithSplits(
|
|
|
939
1094
|
let pageInSection = 0;
|
|
940
1095
|
let reservedNoteHeight = 0;
|
|
941
1096
|
const reservedNotes = new Set<string>();
|
|
1097
|
+
// P6.c: per-table progress when a table is being split row-by-row
|
|
1098
|
+
// across pages. Map<blockId, nextRowIndexToPlace>. Cleared once a
|
|
1099
|
+
// table is fully placed.
|
|
1100
|
+
const tableProgress = new Map<string, number>();
|
|
1101
|
+
|
|
1102
|
+
// P8.1b — per-page note tracking.
|
|
1103
|
+
// `pendingNoteKeys` parallels `reservedNotes` but is only snapshotted on
|
|
1104
|
+
// page-push (finalization), NOT on column break.
|
|
1105
|
+
// `pendingNoteBlockFroms` records the referencing block's `from` offset
|
|
1106
|
+
// for each note key, enabling hit-test via `RuntimeBlockFragment.from`.
|
|
1107
|
+
const noteAllocationsByPageInSection = new Map<number, RuntimeNoteAllocation[]>();
|
|
1108
|
+
const noteFragmentsByPageInSection = new Map<
|
|
1109
|
+
number,
|
|
1110
|
+
Array<Omit<RuntimeBlockFragment, "pageId">>
|
|
1111
|
+
>();
|
|
1112
|
+
const pendingNoteKeys = new Set<string>();
|
|
1113
|
+
const pendingNoteBlockFroms = new Map<string, { blockFrom: number; blockTo: number }>();
|
|
1114
|
+
// Track the columnWidth at the time each note was accumulated so the
|
|
1115
|
+
// measurement is reproducible at page-close time.
|
|
1116
|
+
const pendingNoteColumnWidths = new Map<string, number>();
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Snapshot the pending note state into `noteAllocationsByPageInSection` and
|
|
1120
|
+
* `noteFragmentsByPageInSection` for the page that is about to close.
|
|
1121
|
+
* Must be called BEFORE clearing `pendingNoteKeys`.
|
|
1122
|
+
*/
|
|
1123
|
+
const snapshotNoteAllocations = (closingPageInSection: number, columnWidth: number): void => {
|
|
1124
|
+
if (pendingNoteKeys.size === 0 || !footnotes) return;
|
|
1125
|
+
|
|
1126
|
+
const allocations: RuntimeNoteAllocation[] = [];
|
|
1127
|
+
const fragments: Array<Omit<RuntimeBlockFragment, "pageId">> = [];
|
|
1128
|
+
let orderInRegion = 0;
|
|
1129
|
+
|
|
1130
|
+
for (const noteKey of pendingNoteKeys) {
|
|
1131
|
+
const colonIdx = noteKey.indexOf(":");
|
|
1132
|
+
if (colonIdx === -1) continue;
|
|
1133
|
+
const noteKind = noteKey.slice(0, colonIdx) as "footnote" | "endnote";
|
|
1134
|
+
const noteId = noteKey.slice(colonIdx + 1);
|
|
1135
|
+
|
|
1136
|
+
// P8.1b: endnote allocations are deferred to P8 Task 7 (endnote area
|
|
1137
|
+
// component). The existing reservedNoteHeight math still reserves space
|
|
1138
|
+
// for endnotes, but we do not emit RuntimeNoteAllocation for them here.
|
|
1139
|
+
// See P8 plan, Task 7.
|
|
1140
|
+
if (noteKind === "endnote") continue;
|
|
1141
|
+
|
|
1142
|
+
const effectiveColumnWidth =
|
|
1143
|
+
pendingNoteColumnWidths.get(noteKey) ?? columnWidth;
|
|
1144
|
+
const { heightTwips } = measureNoteBody(
|
|
1145
|
+
noteKind,
|
|
1146
|
+
noteId,
|
|
1147
|
+
footnotes,
|
|
1148
|
+
effectiveColumnWidth,
|
|
1149
|
+
);
|
|
1150
|
+
|
|
1151
|
+
const fragmentId = `note-${closingPageInSection}-${noteKind}-${noteId}`;
|
|
1152
|
+
const refRange = pendingNoteBlockFroms.get(noteKey) ?? {
|
|
1153
|
+
blockFrom: pageStart,
|
|
1154
|
+
blockTo: pageStart + 1,
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
const allocation: RuntimeNoteAllocation = {
|
|
1158
|
+
noteKind,
|
|
1159
|
+
noteId,
|
|
1160
|
+
reservedHeightTwips: heightTwips + FOOTNOTE_REFERENCE_RESERVATION_TWIPS,
|
|
1161
|
+
fragmentId,
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
const fragment: Omit<RuntimeBlockFragment, "pageId"> = {
|
|
1165
|
+
fragmentId,
|
|
1166
|
+
blockId: `note-body-${noteKind}-${noteId}`,
|
|
1167
|
+
orderInRegion,
|
|
1168
|
+
regionKind: "footnote-area",
|
|
1169
|
+
from: refRange.blockFrom,
|
|
1170
|
+
to: refRange.blockTo,
|
|
1171
|
+
heightTwips: heightTwips + FOOTNOTE_REFERENCE_RESERVATION_TWIPS,
|
|
1172
|
+
kind: "whole",
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
allocations.push(allocation);
|
|
1176
|
+
fragments.push(fragment);
|
|
1177
|
+
orderInRegion += 1;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (allocations.length > 0) {
|
|
1181
|
+
noteAllocationsByPageInSection.set(closingPageInSection, allocations);
|
|
1182
|
+
noteFragmentsByPageInSection.set(closingPageInSection, fragments);
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
942
1185
|
|
|
943
1186
|
const pushPage = (endOffset: number): void => {
|
|
944
1187
|
const boundedEnd = Math.max(pageStart, Math.min(endOffset, section.end));
|
|
945
1188
|
if (boundedEnd === pageStart && pages.length > 0) {
|
|
946
1189
|
return;
|
|
947
1190
|
}
|
|
1191
|
+
const closingPageInSection = pageInSection;
|
|
1192
|
+
const columnWidth =
|
|
1193
|
+
columnMetrics[Math.min(columnIndex, columnMetrics.length - 1)]?.width ??
|
|
1194
|
+
getUsableColumnWidth(layout);
|
|
1195
|
+
// Snapshot note allocations for the page being closed BEFORE clearing state.
|
|
1196
|
+
snapshotNoteAllocations(closingPageInSection, columnWidth);
|
|
948
1197
|
pages.push({
|
|
949
1198
|
sectionIndex: section.index,
|
|
950
|
-
pageInSection,
|
|
1199
|
+
pageInSection: closingPageInSection,
|
|
951
1200
|
startOffset: pageStart,
|
|
952
1201
|
endOffset: boundedEnd,
|
|
953
1202
|
layout,
|
|
@@ -958,6 +1207,10 @@ function paginateSectionBlocksWithSplits(
|
|
|
958
1207
|
columnIndex = 0;
|
|
959
1208
|
reservedNoteHeight = 0;
|
|
960
1209
|
reservedNotes.clear();
|
|
1210
|
+
// P8.1b: also clear pending note tracking on page finalization.
|
|
1211
|
+
pendingNoteKeys.clear();
|
|
1212
|
+
pendingNoteBlockFroms.clear();
|
|
1213
|
+
pendingNoteColumnWidths.clear();
|
|
961
1214
|
};
|
|
962
1215
|
|
|
963
1216
|
for (let index = 0; index < blocks.length; index += 1) {
|
|
@@ -967,13 +1220,13 @@ function paginateSectionBlocksWithSplits(
|
|
|
967
1220
|
const columnWidth =
|
|
968
1221
|
columnMetrics[Math.min(columnIndex, columnMetrics.length - 1)]?.width ??
|
|
969
1222
|
getUsableColumnWidth(layout);
|
|
970
|
-
const baseHeight = measureBlockHeight(block, columnWidth, measurementProvider);
|
|
1223
|
+
const baseHeight = measureBlockHeight(block, columnWidth, measurementProvider, cache);
|
|
971
1224
|
|
|
972
1225
|
// keepNext: this paragraph must stay with the next one on the same page
|
|
973
1226
|
const keepWithNextHeight =
|
|
974
1227
|
block.kind === "paragraph" && block.keepNext
|
|
975
1228
|
? baseHeight +
|
|
976
|
-
measureBlockHeight(blocks[index + 1], columnWidth, measurementProvider)
|
|
1229
|
+
measureBlockHeight(blocks[index + 1], columnWidth, measurementProvider, cache)
|
|
977
1230
|
: baseHeight;
|
|
978
1231
|
|
|
979
1232
|
// keepLines: the entire paragraph must fit on one page.
|
|
@@ -990,13 +1243,100 @@ function paginateSectionBlocksWithSplits(
|
|
|
990
1243
|
continue;
|
|
991
1244
|
}
|
|
992
1245
|
|
|
1246
|
+
// P6.c: row-boundary split for tables that overflow the remaining
|
|
1247
|
+
// page space (or that, on a fresh page, exceed one full page).
|
|
1248
|
+
// Replaces the generic overflow path for table blocks. Tables fall
|
|
1249
|
+
// into one of four cases:
|
|
1250
|
+
//
|
|
1251
|
+
// 1. Remainder fits → place atomically, clear progress, break.
|
|
1252
|
+
// 2. Remainder overflows AND splittable → place rows that fit,
|
|
1253
|
+
// push at the row-boundary offset, continue (next iteration
|
|
1254
|
+
// resumes from `splitRowIndex`).
|
|
1255
|
+
// 3. Remainder overflows AND can't split AND has prior content →
|
|
1256
|
+
// push the whole remainder to the next page.
|
|
1257
|
+
// 4. Remainder overflows AND can't split AND on fresh page →
|
|
1258
|
+
// degrade to atomic placement (visual overflow, but offset
|
|
1259
|
+
// ranges stay clean — same as pre-P6.c behavior).
|
|
1260
|
+
if (block.kind === "table") {
|
|
1261
|
+
const startRow = tableProgress.get(block.blockId) ?? 0;
|
|
1262
|
+
const remainingForTable =
|
|
1263
|
+
usableHeight - columnHeight - reservedNoteHeight;
|
|
1264
|
+
const rowHeights = measureTableRowHeights({
|
|
1265
|
+
block,
|
|
1266
|
+
columnWidth,
|
|
1267
|
+
measurementProvider,
|
|
1268
|
+
});
|
|
1269
|
+
const { cantSplitFlags, isHeaderFlags } = extractRowFlags(block);
|
|
1270
|
+
const repeatedHeaderHeightTwips = computeRepeatedHeaderHeight(
|
|
1271
|
+
rowHeights,
|
|
1272
|
+
isHeaderFlags,
|
|
1273
|
+
);
|
|
1274
|
+
const headerReservation =
|
|
1275
|
+
startRow > 0 ? repeatedHeaderHeightTwips : 0;
|
|
1276
|
+
let remainderHeight = headerReservation;
|
|
1277
|
+
for (let r = startRow; r < rowHeights.length; r += 1) {
|
|
1278
|
+
remainderHeight += rowHeights[r] ?? 0;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Helper: best-effort offset for the start of row K. Falls back
|
|
1282
|
+
// to the table block's `from` when the row has no inner block.
|
|
1283
|
+
const rowOffset = (rowIndex: number): number => {
|
|
1284
|
+
const row = block.rows[rowIndex];
|
|
1285
|
+
const firstChild = row?.cells[0]?.content[0];
|
|
1286
|
+
return firstChild?.from ?? block.from;
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
// Case 1: remainder fits — place and break.
|
|
1290
|
+
if (remainderHeight <= remainingForTable) {
|
|
1291
|
+
columnHeight += startRow > 0 ? remainderHeight : baseHeight;
|
|
1292
|
+
if (startRow > 0) tableProgress.delete(block.blockId);
|
|
1293
|
+
if (index === blocks.length - 1) pushPage(section.end);
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Case 2: try a row-boundary split.
|
|
1298
|
+
const decision = findTableRowSplit({
|
|
1299
|
+
rowHeights,
|
|
1300
|
+
cantSplitFlags,
|
|
1301
|
+
isHeaderFlags,
|
|
1302
|
+
remainingHeightTwips: remainingForTable,
|
|
1303
|
+
repeatedHeaderHeightTwips,
|
|
1304
|
+
startRow,
|
|
1305
|
+
});
|
|
1306
|
+
if (decision.rowsOnCurrentPage > 0) {
|
|
1307
|
+
tableProgress.set(block.blockId, decision.splitRowIndex);
|
|
1308
|
+
pushPage(rowOffset(decision.splitRowIndex));
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// Case 3: can't split here. If there's content above (or we're
|
|
1313
|
+
// resuming), push everything from the resume point to the next
|
|
1314
|
+
// page so the next iteration starts fresh.
|
|
1315
|
+
if (columnHeight > 0 || startRow > 0) {
|
|
1316
|
+
pushPage(startRow > 0 ? rowOffset(startRow) : block.from);
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Case 4: degraded atomic placement (table on a fresh page that
|
|
1321
|
+
// it doesn't fit on, AND the first row alone exceeds page
|
|
1322
|
+
// height). Preserve pre-P6.c semantics so offsets stay clean.
|
|
1323
|
+
columnHeight += baseHeight;
|
|
1324
|
+
if (index === blocks.length - 1) pushPage(section.end);
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
993
1328
|
// Overflow check — paragraph doesn't fit on current page
|
|
994
1329
|
if (projectedHeight > usableHeight && pageStart < block.from) {
|
|
995
1330
|
if (columnIndex < maxColumns - 1) {
|
|
1331
|
+
// Advance to next column without a page break — do NOT snapshot.
|
|
996
1332
|
columnIndex += 1;
|
|
997
1333
|
columnHeight = 0;
|
|
998
1334
|
reservedNoteHeight = 0;
|
|
999
1335
|
reservedNotes.clear();
|
|
1336
|
+
// P8.1b: clear pending note state WITHOUT snapshotting.
|
|
1337
|
+
pendingNoteKeys.clear();
|
|
1338
|
+
pendingNoteBlockFroms.clear();
|
|
1339
|
+
pendingNoteColumnWidths.clear();
|
|
1000
1340
|
continue;
|
|
1001
1341
|
}
|
|
1002
1342
|
|
|
@@ -1017,12 +1357,18 @@ function paginateSectionBlocksWithSplits(
|
|
|
1017
1357
|
!block.keepNext
|
|
1018
1358
|
) {
|
|
1019
1359
|
const availableHeight = usableHeight - columnHeight - reservedNoteHeight;
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1360
|
+
const cachedLineCount = cache?.getLineCount(block, columnWidth);
|
|
1361
|
+
const totalLines =
|
|
1362
|
+
cachedLineCount ??
|
|
1363
|
+
measureParagraphLineCount(
|
|
1364
|
+
block,
|
|
1365
|
+
formatting,
|
|
1366
|
+
columnWidth,
|
|
1367
|
+
measurementProvider,
|
|
1368
|
+
);
|
|
1369
|
+
if (cachedLineCount === undefined) {
|
|
1370
|
+
cache?.setLineCount(block, columnWidth, totalLines);
|
|
1371
|
+
}
|
|
1026
1372
|
const availableLines =
|
|
1027
1373
|
formatting.lineHeight > 0
|
|
1028
1374
|
? Math.max(0, Math.floor(availableHeight / formatting.lineHeight))
|
|
@@ -1068,10 +1414,15 @@ function paginateSectionBlocksWithSplits(
|
|
|
1068
1414
|
// span the full page if it's truly larger than a page).
|
|
1069
1415
|
if (keepLinesActive && columnHeight > 0 && baseHeight > usableHeight - columnHeight && pageStart < block.from) {
|
|
1070
1416
|
if (columnIndex < maxColumns - 1) {
|
|
1417
|
+
// Column advance without page break — do NOT snapshot.
|
|
1071
1418
|
columnIndex += 1;
|
|
1072
1419
|
columnHeight = 0;
|
|
1073
1420
|
reservedNoteHeight = 0;
|
|
1074
1421
|
reservedNotes.clear();
|
|
1422
|
+
// P8.1b: clear pending note state WITHOUT snapshotting.
|
|
1423
|
+
pendingNoteKeys.clear();
|
|
1424
|
+
pendingNoteBlockFroms.clear();
|
|
1425
|
+
pendingNoteColumnWidths.clear();
|
|
1075
1426
|
continue;
|
|
1076
1427
|
}
|
|
1077
1428
|
pushPage(block.from);
|
|
@@ -1086,14 +1437,32 @@ function paginateSectionBlocksWithSplits(
|
|
|
1086
1437
|
);
|
|
1087
1438
|
columnHeight += baseHeight;
|
|
1088
1439
|
reservedNoteHeight += effectiveNoteHeight;
|
|
1089
|
-
currentPageNoteIds(block).forEach((noteKey) =>
|
|
1440
|
+
currentPageNoteIds(block).forEach((noteKey) => {
|
|
1441
|
+
reservedNotes.add(noteKey);
|
|
1442
|
+
// P8.1b: also track the referencing block range for hit-test.
|
|
1443
|
+
// Only record the first reference (earliest block.from) per noteKey
|
|
1444
|
+
// so the fragment's from/to points to the paragraph that introduced it.
|
|
1445
|
+
if (!pendingNoteKeys.has(noteKey)) {
|
|
1446
|
+
pendingNoteKeys.add(noteKey);
|
|
1447
|
+
pendingNoteBlockFroms.set(noteKey, { blockFrom: block.from, blockTo: block.to });
|
|
1448
|
+
pendingNoteColumnWidths.set(noteKey, columnWidth);
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1090
1451
|
|
|
1091
1452
|
if (hasColumnBreak(block)) {
|
|
1092
1453
|
if (columnIndex < maxColumns - 1) {
|
|
1454
|
+
// Column break within a multi-column layout: advance to next column.
|
|
1455
|
+
// DO NOT snapshot note allocations — only page-push triggers snapshotting.
|
|
1456
|
+
// Clear pending note state alongside reservedNotes so notes that only
|
|
1457
|
+
// appeared before the column break don't get double-counted.
|
|
1093
1458
|
columnIndex += 1;
|
|
1094
1459
|
columnHeight = 0;
|
|
1095
1460
|
reservedNoteHeight = 0;
|
|
1096
1461
|
reservedNotes.clear();
|
|
1462
|
+
// P8.1b: clear pending note state WITHOUT snapshotting.
|
|
1463
|
+
pendingNoteKeys.clear();
|
|
1464
|
+
pendingNoteBlockFroms.clear();
|
|
1465
|
+
pendingNoteColumnWidths.clear();
|
|
1097
1466
|
} else {
|
|
1098
1467
|
pushPage(nextBoundary);
|
|
1099
1468
|
}
|
|
@@ -1121,9 +1490,37 @@ function paginateSectionBlocksWithSplits(
|
|
|
1121
1490
|
},
|
|
1122
1491
|
],
|
|
1123
1492
|
splits: { byBlockId: splitsByBlock },
|
|
1493
|
+
noteAllocationsByPageInSection,
|
|
1494
|
+
noteFragmentsByPageInSection,
|
|
1124
1495
|
};
|
|
1125
1496
|
}
|
|
1126
1497
|
|
|
1498
|
+
/**
|
|
1499
|
+
* Measure the height consumed by one note's body blocks, plus return those
|
|
1500
|
+
* blocks for use in building a `RuntimeBlockFragment`.
|
|
1501
|
+
*
|
|
1502
|
+
* Factored out of `estimateFootnoteReservation` so both the reservation-math
|
|
1503
|
+
* path and the P8.1b allocation-emission path share the same measurement.
|
|
1504
|
+
*/
|
|
1505
|
+
function measureNoteBody(
|
|
1506
|
+
noteKind: "footnote" | "endnote",
|
|
1507
|
+
noteId: string,
|
|
1508
|
+
footnotes: FootnoteCollection,
|
|
1509
|
+
columnWidth: number,
|
|
1510
|
+
): { heightTwips: number } {
|
|
1511
|
+
const noteCollection =
|
|
1512
|
+
noteKind === "endnote" ? footnotes.endnotes : footnotes.footnotes;
|
|
1513
|
+
const note = noteCollection[noteId];
|
|
1514
|
+
if (!note) {
|
|
1515
|
+
return { heightTwips: 0 };
|
|
1516
|
+
}
|
|
1517
|
+
const heightTwips = note.blocks.reduce(
|
|
1518
|
+
(total, child) => total + estimateCanonicalNoteBlockHeight(child, columnWidth),
|
|
1519
|
+
0,
|
|
1520
|
+
);
|
|
1521
|
+
return { heightTwips };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1127
1524
|
function estimateFootnoteReservation(
|
|
1128
1525
|
block: SurfaceBlockSnapshot,
|
|
1129
1526
|
footnotes: FootnoteCollection | undefined,
|
|
@@ -1140,17 +1537,13 @@ function estimateFootnoteReservation(
|
|
|
1140
1537
|
continue;
|
|
1141
1538
|
}
|
|
1142
1539
|
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
const
|
|
1147
|
-
reservation
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
(total, child) => total + estimateCanonicalNoteBlockHeight(child, columnWidth),
|
|
1151
|
-
0,
|
|
1152
|
-
);
|
|
1153
|
-
}
|
|
1540
|
+
const colonIdx = noteKey.indexOf(":");
|
|
1541
|
+
if (colonIdx === -1) continue;
|
|
1542
|
+
const noteKind = noteKey.slice(0, colonIdx) as "footnote" | "endnote";
|
|
1543
|
+
const noteId = noteKey.slice(colonIdx + 1);
|
|
1544
|
+
// Use measureNoteBody so reservation math and emission share the same path.
|
|
1545
|
+
const { heightTwips } = measureNoteBody(noteKind, noteId, footnotes, columnWidth);
|
|
1546
|
+
reservation += FOOTNOTE_REFERENCE_RESERVATION_TWIPS + heightTwips;
|
|
1154
1547
|
}
|
|
1155
1548
|
|
|
1156
1549
|
return reservation;
|