@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
|
@@ -62,8 +62,12 @@ import {
|
|
|
62
62
|
type ScopeRailSegment,
|
|
63
63
|
} from "../workflow-rail-segments.ts";
|
|
64
64
|
import { createEditorSurfaceSnapshot } from "../surface-projection.ts";
|
|
65
|
+
import { storyTargetKey } from "../story-targeting.ts";
|
|
65
66
|
import { MAIN_STORY_TARGET } from "../../core/selection/mapping.ts";
|
|
66
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
createSelectionSnapshot,
|
|
69
|
+
type CanonicalDocumentEnvelope,
|
|
70
|
+
} from "../../core/state/editor-state.ts";
|
|
67
71
|
import { resolveTableStyleResolution } from "../table-style-resolver.ts";
|
|
68
72
|
import { buildTableRenderPlan } from "./table-render-plan.ts";
|
|
69
73
|
import { recordPerfSample } from "../../ui-tailwind/editor-surface/perf-probe.ts";
|
|
@@ -124,6 +128,13 @@ export interface PublicPageRegions {
|
|
|
124
128
|
header?: PublicPageRegion;
|
|
125
129
|
footer?: PublicPageRegion;
|
|
126
130
|
columns?: readonly PublicPageRegion[];
|
|
131
|
+
/**
|
|
132
|
+
* P8.3 — Footnote regions reserved at the bottom of the page (above the
|
|
133
|
+
* footer band). Mirrors `RuntimePageRegions.footnotes`. Present only
|
|
134
|
+
* when at least one allocation on this page produced a fragment — see
|
|
135
|
+
* `noteAllocations` for per-note metadata. Additive / back-compat safe.
|
|
136
|
+
*/
|
|
137
|
+
footnotes?: readonly PublicPageRegion[];
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
export interface PublicPageRegion {
|
|
@@ -134,6 +145,15 @@ export interface PublicPageRegion {
|
|
|
134
145
|
fragmentCount: number;
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
/**
|
|
149
|
+
* P8 — Region kind discriminator used by `PublicRegionBlock` and the
|
|
150
|
+
* `getStoryBlocksForRegion` API. Mirrors `PublicPageRegion["kind"]` plus
|
|
151
|
+
* `"endnote-area"`, which is only ever surfaced via
|
|
152
|
+
* `getDocumentEndnoteBlocks` (endnotes have document-end placement and
|
|
153
|
+
* never sit on a body page).
|
|
154
|
+
*/
|
|
155
|
+
export type PublicRegionKind = PublicPageRegion["kind"] | "endnote-area";
|
|
156
|
+
|
|
137
157
|
export interface PublicBlockFragment {
|
|
138
158
|
fragmentId: string;
|
|
139
159
|
blockId: string;
|
|
@@ -146,6 +166,29 @@ export interface PublicBlockFragment {
|
|
|
146
166
|
orderInRegion: number;
|
|
147
167
|
}
|
|
148
168
|
|
|
169
|
+
/**
|
|
170
|
+
* P8 — One block snapshot rendered into a region of a page. Returned by
|
|
171
|
+
* `WordReviewEditorLayoutFacet.getStoryBlocksForRegion` and
|
|
172
|
+
* `getDocumentEndnoteBlocks`.
|
|
173
|
+
*
|
|
174
|
+
* The `blockSnapshot` field is the same `SurfaceBlockSnapshot` consumed by
|
|
175
|
+
* the body block renderer (`tw-page-block-view`), so region renderers reuse
|
|
176
|
+
* body typography verbatim. For body fragments `runtimeFromOffset`/`To`
|
|
177
|
+
* mirror the underlying `RuntimeBlockFragment.from`/`to`. For header /
|
|
178
|
+
* footer / footnote-area blocks the offsets are local to the host story
|
|
179
|
+
* (header story, footer story, or note body).
|
|
180
|
+
*/
|
|
181
|
+
export interface PublicRegionBlock {
|
|
182
|
+
blockId: string;
|
|
183
|
+
fragmentId: string;
|
|
184
|
+
pageIndex: number;
|
|
185
|
+
regionKind: PublicRegionKind;
|
|
186
|
+
runtimeFromOffset: number;
|
|
187
|
+
runtimeToOffset: number;
|
|
188
|
+
heightTwips: number;
|
|
189
|
+
blockSnapshot: SurfaceBlockSnapshot;
|
|
190
|
+
}
|
|
191
|
+
|
|
149
192
|
export interface PublicLineBox {
|
|
150
193
|
fragmentId: string;
|
|
151
194
|
lineIndex: number;
|
|
@@ -276,6 +319,32 @@ export type LayoutFacetEvent =
|
|
|
276
319
|
kind: "zoom_changed";
|
|
277
320
|
revision: number;
|
|
278
321
|
zoom: RenderZoomSummary;
|
|
322
|
+
}
|
|
323
|
+
| {
|
|
324
|
+
/**
|
|
325
|
+
* P14.b — coalesced commit event. Fires exactly once per
|
|
326
|
+
* `applyPatch`-driven layout build (full or incremental) AFTER
|
|
327
|
+
* the granular events. Subscribers that only need to react
|
|
328
|
+
* to "the engine just finished a build" can listen here and
|
|
329
|
+
* skip the multi-event subscription pattern that triggered N
|
|
330
|
+
* React re-renders per applyPatch.
|
|
331
|
+
*
|
|
332
|
+
* Carries the union of: dirty-field families (TOC / PAGE /
|
|
333
|
+
* NUMPAGES etc.), page-count delta when the total changed, and
|
|
334
|
+
* the page range when the build was a bounded incremental
|
|
335
|
+
* relayout (absent for full rebuilds). The granular events
|
|
336
|
+
* (`layout_recomputed`, `incremental_relayout`,
|
|
337
|
+
* `page_count_changed`, `page_field_dirtied`) still fire for
|
|
338
|
+
* backward-compat with consumers that care about specific
|
|
339
|
+
* kinds (e.g., `TwStatusBar` measurement-fidelity badge keys
|
|
340
|
+
* off `measurement_backend_ready`).
|
|
341
|
+
*/
|
|
342
|
+
kind: "layout_committed";
|
|
343
|
+
revision: number;
|
|
344
|
+
reason?: LayoutFacetInvalidationReason;
|
|
345
|
+
dirtyFieldFamilies?: readonly string[];
|
|
346
|
+
pageCountDelta?: { previous: number; current: number };
|
|
347
|
+
pageRange?: { fromPageIndex: number; toPageIndex: number };
|
|
279
348
|
};
|
|
280
349
|
|
|
281
350
|
/**
|
|
@@ -338,8 +407,41 @@ export interface WordReviewEditorLayoutFacet {
|
|
|
338
407
|
region: PublicPageRegion["kind"],
|
|
339
408
|
options?: { columnIndex?: number },
|
|
340
409
|
): readonly PublicLineBox[];
|
|
410
|
+
/**
|
|
411
|
+
* P4 — every region present on the given page, in render order
|
|
412
|
+
* (header, body, columns…, footer, footnote-area). Each entry
|
|
413
|
+
* carries the region kind, twip dimensions, and fragment count.
|
|
414
|
+
* Consumers iterate this to enumerate which regions exist before
|
|
415
|
+
* calling `getLineBoxesForRegion(pageIndex, kind)` to fetch
|
|
416
|
+
* their per-line breakdowns. Returns an empty array when the
|
|
417
|
+
* pageIndex is out of range.
|
|
418
|
+
*/
|
|
419
|
+
getStoryRegionsOnPage(pageIndex: number): readonly PublicPageRegion[];
|
|
341
420
|
getFragmentsForPage(pageIndex: number): PublicBlockFragment[];
|
|
342
421
|
|
|
422
|
+
/**
|
|
423
|
+
* P8 — Returns per-region block snapshots for a page. Body resolves the
|
|
424
|
+
* page's body fragments to their `SurfaceBlockSnapshot`s; header / footer
|
|
425
|
+
* resolve via the page's active story target + `subParts.headers/footers`
|
|
426
|
+
* lookup; footnote-area resolves per-page `noteAllocations` against
|
|
427
|
+
* `subParts.footnoteCollection.footnotes`. Endnote-area is always empty
|
|
428
|
+
* per-page — document-end placement is handled by
|
|
429
|
+
* `getDocumentEndnoteBlocks`. Column falls through to body (multi-column
|
|
430
|
+
* splitting is a P10 follow-up). Cached per revision; busts on
|
|
431
|
+
* `graph.revision` change.
|
|
432
|
+
*/
|
|
433
|
+
getStoryBlocksForRegion(
|
|
434
|
+
pageIndex: number,
|
|
435
|
+
region: PublicPageRegion["kind"],
|
|
436
|
+
options?: { columnIndex?: number },
|
|
437
|
+
): readonly PublicRegionBlock[];
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* P8 — Returns endnote bodies in document order for document-end
|
|
441
|
+
* placement (per OOXML default `w:endnotePr/w:pos="docEnd"`).
|
|
442
|
+
*/
|
|
443
|
+
getDocumentEndnoteBlocks(): readonly PublicRegionBlock[];
|
|
444
|
+
|
|
343
445
|
// Page-format catalog --------------------------------------------------
|
|
344
446
|
getPageFormatCatalog(): readonly PageFormatDefinition[];
|
|
345
447
|
getActivePageFormat(sectionIndex: number): ActivePageFormat | null;
|
|
@@ -414,6 +516,14 @@ export interface WordReviewEditorLayoutFacet {
|
|
|
414
516
|
getMeasurementFidelity(): PublicMeasurementFidelity;
|
|
415
517
|
whenMeasurementReady(): Promise<void>;
|
|
416
518
|
swapMeasurementProvider(provider: LayoutMeasurementProvider): void;
|
|
519
|
+
/**
|
|
520
|
+
* Clear the active measurement provider's internal glyph / run-width
|
|
521
|
+
* cache AND the engine's cached page graph. Call after
|
|
522
|
+
* `docxFontLoader.refresh(...)` so the canvas backend re-reads the
|
|
523
|
+
* newly-registered `FontFace` metrics and pagination re-runs with
|
|
524
|
+
* the fresh widths. No-op on the inert facet.
|
|
525
|
+
*/
|
|
526
|
+
invalidateMeasurementCache(): void;
|
|
417
527
|
|
|
418
528
|
// Table render plan (P3e consumed by the render kernel, P4) ------------
|
|
419
529
|
/**
|
|
@@ -465,6 +575,13 @@ export interface WordReviewEditorLayoutFacet {
|
|
|
465
575
|
export interface CreateLayoutFacetInput {
|
|
466
576
|
engine: LayoutEngineInstance;
|
|
467
577
|
getQueryInput: () => LayoutEngineQueryInput;
|
|
578
|
+
/**
|
|
579
|
+
* P8 — Canonical document accessor used by `getStoryBlocksForRegion` /
|
|
580
|
+
* `getDocumentEndnoteBlocks` to resolve header / footer / footnote /
|
|
581
|
+
* endnote stories in `subParts`. When omitted the facet falls back to
|
|
582
|
+
* the document available on the engine query input.
|
|
583
|
+
*/
|
|
584
|
+
canonicalDocument?: () => CanonicalDocumentEnvelope;
|
|
468
585
|
/**
|
|
469
586
|
* Optional render-kernel accessor. When supplied, the facet exposes
|
|
470
587
|
* `getRenderFrame` + `getRenderZoom` that delegate to the kernel.
|
|
@@ -497,6 +614,15 @@ export interface CreateLayoutFacetInput {
|
|
|
497
614
|
| readonly import("../../api/public-types.ts").WorkflowMetadataMarkup[]
|
|
498
615
|
| null
|
|
499
616
|
| undefined;
|
|
617
|
+
/**
|
|
618
|
+
* R3 — optional suggestions snapshot accessor. Used by
|
|
619
|
+
* `getAllScopeCardModels` to attach `SuggestionGroup` entries whose
|
|
620
|
+
* `issueId` matches the scope's issue. Omit to skip group wiring.
|
|
621
|
+
*/
|
|
622
|
+
getSuggestionsSnapshot?: () =>
|
|
623
|
+
| import("../../api/public-types.ts").SuggestionsSnapshot
|
|
624
|
+
| null
|
|
625
|
+
| undefined;
|
|
500
626
|
}
|
|
501
627
|
|
|
502
628
|
export function createLayoutFacet(
|
|
@@ -540,6 +666,151 @@ export function createLayoutFacet(
|
|
|
540
666
|
// Keep the handle alive; the facet instance lives as long as the runtime.
|
|
541
667
|
void unsubscribeEngine;
|
|
542
668
|
|
|
669
|
+
// P8 — per-revision cache for getStoryBlocksForRegion. Identity is
|
|
670
|
+
// preserved within a single graph revision so consumers can `===`-compare
|
|
671
|
+
// results across band renders, and the cache flushes whenever the engine
|
|
672
|
+
// produces a fresh page graph.
|
|
673
|
+
let regionBlocksCache: {
|
|
674
|
+
revision: number;
|
|
675
|
+
byKey: Map<string, readonly PublicRegionBlock[]>;
|
|
676
|
+
} = { revision: -1, byKey: new Map() };
|
|
677
|
+
// P8.2.1 — secondary cache keyed by header/footer story-target identity.
|
|
678
|
+
// A 38-page doc with one default header projects the story once instead of
|
|
679
|
+
// 38 times; per-page `PublicRegionBlock[]`s still differ (each carries the
|
|
680
|
+
// page's `pageIndex`), but the underlying `SurfaceBlockSnapshot[]` and
|
|
681
|
+
// every `blockSnapshot` reference are shared. Busts together with
|
|
682
|
+
// `regionBlocksCache` on `graph.revision` change.
|
|
683
|
+
let storyProjectionCache: {
|
|
684
|
+
revision: number;
|
|
685
|
+
byKey: Map<string, readonly SurfaceBlockSnapshot[]>;
|
|
686
|
+
} = { revision: -1, byKey: new Map() };
|
|
687
|
+
// P8 — per-revision cache for getDocumentEndnoteBlocks (single key).
|
|
688
|
+
let endnoteBlocksCache: {
|
|
689
|
+
revision: number;
|
|
690
|
+
blocks: readonly PublicRegionBlock[] | null;
|
|
691
|
+
} = { revision: -1, blocks: null };
|
|
692
|
+
|
|
693
|
+
function resolveCanonicalDocument(): CanonicalDocumentEnvelope {
|
|
694
|
+
if (input.canonicalDocument) {
|
|
695
|
+
return input.canonicalDocument();
|
|
696
|
+
}
|
|
697
|
+
return getQueryInput().document;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Build the body story's `SurfaceBlockSnapshot[]` so body fragment
|
|
702
|
+
* `blockId`s can be resolved. Cached against the canonical document
|
|
703
|
+
* identity to avoid re-walking the body on every region read.
|
|
704
|
+
*/
|
|
705
|
+
let cachedBodySurface: {
|
|
706
|
+
document: CanonicalDocumentEnvelope;
|
|
707
|
+
blocks: readonly SurfaceBlockSnapshot[];
|
|
708
|
+
} | null = null;
|
|
709
|
+
function bodySurfaceBlocks(): readonly SurfaceBlockSnapshot[] {
|
|
710
|
+
const document = resolveCanonicalDocument();
|
|
711
|
+
if (cachedBodySurface && cachedBodySurface.document === document) {
|
|
712
|
+
return cachedBodySurface.blocks;
|
|
713
|
+
}
|
|
714
|
+
const surface = createEditorSurfaceSnapshot(
|
|
715
|
+
document,
|
|
716
|
+
createSelectionSnapshot(0, 0),
|
|
717
|
+
MAIN_STORY_TARGET,
|
|
718
|
+
);
|
|
719
|
+
cachedBodySurface = { document, blocks: surface.blocks };
|
|
720
|
+
return surface.blocks;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function findBodyBlockById(
|
|
724
|
+
blockId: string,
|
|
725
|
+
): SurfaceBlockSnapshot | undefined {
|
|
726
|
+
return bodySurfaceBlocks().find((b) => b.blockId === blockId);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function getRegionBlocksCached(
|
|
730
|
+
pageIndex: number,
|
|
731
|
+
region: PublicPageRegion["kind"],
|
|
732
|
+
columnIndex: number | undefined,
|
|
733
|
+
): readonly PublicRegionBlock[] {
|
|
734
|
+
const graph = currentGraph();
|
|
735
|
+
if (regionBlocksCache.revision !== graph.revision) {
|
|
736
|
+
regionBlocksCache = { revision: graph.revision, byKey: new Map() };
|
|
737
|
+
// P8.2.1 — bust the secondary story-projection cache in lock-step so
|
|
738
|
+
// shared `SurfaceBlockSnapshot`s never outlive their graph revision.
|
|
739
|
+
storyProjectionCache = { revision: graph.revision, byKey: new Map() };
|
|
740
|
+
// Body surface depends on the canonical document; the document
|
|
741
|
+
// identity check inside `bodySurfaceBlocks` keeps it warm across
|
|
742
|
+
// pure-layout invalidations (zoom, font, etc.).
|
|
743
|
+
}
|
|
744
|
+
const key = `${pageIndex}:${region}:${columnIndex ?? "_"}`;
|
|
745
|
+
const cached = regionBlocksCache.byKey.get(key);
|
|
746
|
+
if (cached) return cached;
|
|
747
|
+
const fresh = computeRegionBlocks(pageIndex, region, columnIndex, graph);
|
|
748
|
+
regionBlocksCache.byKey.set(key, fresh);
|
|
749
|
+
return fresh;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* P8.2.1 — shared projection for a header/footer story. Returns the same
|
|
754
|
+
* `SurfaceBlockSnapshot[]` array identity for repeated calls at the same
|
|
755
|
+
* revision so per-page `PublicRegionBlock[]`s can share every
|
|
756
|
+
* `blockSnapshot` reference.
|
|
757
|
+
*/
|
|
758
|
+
function getStoryProjectionCached(
|
|
759
|
+
storyTarget: EditorStoryTarget,
|
|
760
|
+
document: CanonicalDocumentEnvelope,
|
|
761
|
+
): readonly SurfaceBlockSnapshot[] {
|
|
762
|
+
const key = storyTargetKey(storyTarget);
|
|
763
|
+
const cached = storyProjectionCache.byKey.get(key);
|
|
764
|
+
if (cached) return cached;
|
|
765
|
+
const surface = createEditorSurfaceSnapshot(
|
|
766
|
+
document,
|
|
767
|
+
createSelectionSnapshot(0, 0),
|
|
768
|
+
storyTarget,
|
|
769
|
+
);
|
|
770
|
+
const fresh = surface.blocks;
|
|
771
|
+
storyProjectionCache.byKey.set(key, fresh);
|
|
772
|
+
return fresh;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function computeRegionBlocks(
|
|
776
|
+
pageIndex: number,
|
|
777
|
+
region: PublicPageRegion["kind"],
|
|
778
|
+
columnIndex: number | undefined,
|
|
779
|
+
graph: RuntimePageGraph,
|
|
780
|
+
): readonly PublicRegionBlock[] {
|
|
781
|
+
const node = graph.pages[pageIndex];
|
|
782
|
+
if (!node) return Object.freeze([]);
|
|
783
|
+
const document = resolveCanonicalDocument();
|
|
784
|
+
if (region === "body") {
|
|
785
|
+
return resolveBodyRegionBlocks(node, graph, findBodyBlockById);
|
|
786
|
+
}
|
|
787
|
+
if (region === "header" || region === "footer") {
|
|
788
|
+
const story =
|
|
789
|
+
region === "header" ? node.stories.header : node.stories.footer;
|
|
790
|
+
if (!story) return Object.freeze([]);
|
|
791
|
+
// P8.2.1 — share the projected `SurfaceBlockSnapshot[]` across every
|
|
792
|
+
// page that renders the same story target.
|
|
793
|
+
const projectedBlocks = getStoryProjectionCached(story, document);
|
|
794
|
+
return resolveHeaderFooterRegionBlocks(
|
|
795
|
+
node.pageIndex,
|
|
796
|
+
region,
|
|
797
|
+
story,
|
|
798
|
+
projectedBlocks,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
if (region === "footnote-area") {
|
|
802
|
+
return resolveFootnoteAreaRegionBlocks(node, document);
|
|
803
|
+
}
|
|
804
|
+
if (region === "column") {
|
|
805
|
+
// Multi-column body splitting is a P10 follow-up. For now reuse
|
|
806
|
+
// the body fragments for the indexed column.
|
|
807
|
+
void columnIndex;
|
|
808
|
+
return resolveBodyRegionBlocks(node, graph, findBodyBlockById);
|
|
809
|
+
}
|
|
810
|
+
// endnote-area: empty per-page; document-end via getDocumentEndnoteBlocks.
|
|
811
|
+
return Object.freeze([]);
|
|
812
|
+
}
|
|
813
|
+
|
|
543
814
|
return {
|
|
544
815
|
getPageCount() {
|
|
545
816
|
return currentGraph().pages.length;
|
|
@@ -635,6 +906,7 @@ export function createLayoutFacet(
|
|
|
635
906
|
return collectLineBoxesForRegion(
|
|
636
907
|
node,
|
|
637
908
|
region,
|
|
909
|
+
graph,
|
|
638
910
|
options?.columnIndex,
|
|
639
911
|
).map((box) => toPublicLineBox(box));
|
|
640
912
|
},
|
|
@@ -643,9 +915,102 @@ export function createLayoutFacet(
|
|
|
643
915
|
const graph = currentGraph();
|
|
644
916
|
const node = graph.pages[pageIndex];
|
|
645
917
|
if (!node) return [];
|
|
646
|
-
return collectLineBoxesForRegion(
|
|
647
|
-
|
|
648
|
-
|
|
918
|
+
return collectLineBoxesForRegion(
|
|
919
|
+
node,
|
|
920
|
+
region,
|
|
921
|
+
graph,
|
|
922
|
+
options?.columnIndex,
|
|
923
|
+
).map((box) => toPublicLineBox(box));
|
|
924
|
+
},
|
|
925
|
+
|
|
926
|
+
getStoryRegionsOnPage(pageIndex) {
|
|
927
|
+
const graph = currentGraph();
|
|
928
|
+
const node = graph.pages[pageIndex];
|
|
929
|
+
if (!node) return [];
|
|
930
|
+
const result: PublicPageRegion[] = [];
|
|
931
|
+
// Render order: header → body → columns → footer → footnote-area.
|
|
932
|
+
if (node.regions.header) {
|
|
933
|
+
result.push(toPublicPageRegion(node.regions.header));
|
|
934
|
+
}
|
|
935
|
+
result.push(toPublicPageRegion(node.regions.body));
|
|
936
|
+
if (node.regions.columns) {
|
|
937
|
+
for (const column of node.regions.columns) {
|
|
938
|
+
result.push(toPublicPageRegion(column));
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (node.regions.footer) {
|
|
942
|
+
result.push(toPublicPageRegion(node.regions.footer));
|
|
943
|
+
}
|
|
944
|
+
const footnoteRegions = node.regions.footnotes;
|
|
945
|
+
if (footnoteRegions) {
|
|
946
|
+
for (const footnote of footnoteRegions) {
|
|
947
|
+
result.push(toPublicPageRegion(footnote));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return result;
|
|
951
|
+
},
|
|
952
|
+
|
|
953
|
+
getStoryBlocksForRegion(pageIndex, region, options) {
|
|
954
|
+
return getRegionBlocksCached(pageIndex, region, options?.columnIndex);
|
|
955
|
+
},
|
|
956
|
+
|
|
957
|
+
getDocumentEndnoteBlocks() {
|
|
958
|
+
const graph = currentGraph();
|
|
959
|
+
if (endnoteBlocksCache.revision === graph.revision && endnoteBlocksCache.blocks) {
|
|
960
|
+
return endnoteBlocksCache.blocks;
|
|
961
|
+
}
|
|
962
|
+
const document = resolveCanonicalDocument();
|
|
963
|
+
const collection = document.subParts?.footnoteCollection;
|
|
964
|
+
const blocks: PublicRegionBlock[] = [];
|
|
965
|
+
if (collection) {
|
|
966
|
+
// P8.2.1 — batch every endnote body into one
|
|
967
|
+
// `createEditorSurfaceSnapshot` call. Previously we called the
|
|
968
|
+
// projector once per block (50 endnotes × 1 block → 50 full
|
|
969
|
+
// projections); now we project the flat concatenation once and
|
|
970
|
+
// slice the result back into per-note groups using the original
|
|
971
|
+
// block counts. `runtimeFromOffset` / `runtimeToOffset` inherit
|
|
972
|
+
// the projection's cumulative cursor (a stable "all-endnotes"
|
|
973
|
+
// meta-story offset); fragment ids still scope per-note so
|
|
974
|
+
// consumers can tell which endnote a block belongs to.
|
|
975
|
+
const groups: Array<{ noteId: string; count: number }> = [];
|
|
976
|
+
const flat: import("../../model/canonical-document.ts").BlockNode[] = [];
|
|
977
|
+
for (const noteId of Object.keys(collection.endnotes)) {
|
|
978
|
+
const note = collection.endnotes[noteId];
|
|
979
|
+
if (!note || note.blocks.length === 0) continue;
|
|
980
|
+
groups.push({ noteId, count: note.blocks.length });
|
|
981
|
+
for (const block of note.blocks) flat.push(block);
|
|
982
|
+
}
|
|
983
|
+
if (flat.length > 0) {
|
|
984
|
+
const surface = createEditorSurfaceSnapshot(
|
|
985
|
+
{
|
|
986
|
+
...document,
|
|
987
|
+
content: { type: "doc", children: flat },
|
|
988
|
+
} as CanonicalDocumentEnvelope,
|
|
989
|
+
createSelectionSnapshot(0, 0),
|
|
990
|
+
);
|
|
991
|
+
let cursor = 0;
|
|
992
|
+
for (const group of groups) {
|
|
993
|
+
for (let i = 0; i < group.count; i += 1) {
|
|
994
|
+
const blockSnapshot = surface.blocks[cursor + i];
|
|
995
|
+
if (!blockSnapshot) continue;
|
|
996
|
+
blocks.push({
|
|
997
|
+
blockId: blockSnapshot.blockId,
|
|
998
|
+
fragmentId: `endnote-${group.noteId}-${i}`,
|
|
999
|
+
pageIndex: -1,
|
|
1000
|
+
regionKind: "endnote-area",
|
|
1001
|
+
runtimeFromOffset: blockSnapshot.from,
|
|
1002
|
+
runtimeToOffset: blockSnapshot.to,
|
|
1003
|
+
heightTwips: 0,
|
|
1004
|
+
blockSnapshot,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
cursor += group.count;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const frozen = Object.freeze(blocks);
|
|
1012
|
+
endnoteBlocksCache = { revision: graph.revision, blocks: frozen };
|
|
1013
|
+
return frozen;
|
|
649
1014
|
},
|
|
650
1015
|
|
|
651
1016
|
getPageFormatCatalog() {
|
|
@@ -744,11 +1109,18 @@ export function createLayoutFacet(
|
|
|
744
1109
|
const kernel = input.renderKernel?.();
|
|
745
1110
|
const anchorIndex = kernel?.getRenderFrame?.()?.anchorIndex ?? null;
|
|
746
1111
|
const metadata = input.getWorkflowMarkupMetadata?.();
|
|
1112
|
+
const suggestions = input.getSuggestionsSnapshot?.() ?? null;
|
|
1113
|
+
// The workflow-markup entries carry BOTH issue and review-action
|
|
1114
|
+
// metadata. The projector filters to REVIEW_ACTION_METADATA_ID
|
|
1115
|
+
// internally, so we can just pass the whole list.
|
|
747
1116
|
return attachScopeCardModel({
|
|
748
1117
|
segments,
|
|
749
1118
|
scopes: railInput?.scopes ?? [],
|
|
750
1119
|
metadata: metadata ?? undefined,
|
|
751
1120
|
anchorIndex,
|
|
1121
|
+
suggestions,
|
|
1122
|
+
reviewActionMetadata: metadata ?? undefined,
|
|
1123
|
+
candidates: railInput?.candidates ?? undefined,
|
|
752
1124
|
});
|
|
753
1125
|
},
|
|
754
1126
|
|
|
@@ -816,6 +1188,10 @@ export function createLayoutFacet(
|
|
|
816
1188
|
engine.swapMeasurementProvider(provider);
|
|
817
1189
|
},
|
|
818
1190
|
|
|
1191
|
+
invalidateMeasurementCache() {
|
|
1192
|
+
engine.invalidateMeasurementCache();
|
|
1193
|
+
},
|
|
1194
|
+
|
|
819
1195
|
getTableRenderPlan(blockId, pageIndex) {
|
|
820
1196
|
const graph = currentGraph();
|
|
821
1197
|
const fragment = graph.fragments.find((f) => f.blockId === blockId);
|
|
@@ -910,6 +1286,9 @@ function toPublicPageRegions(regions: RuntimePageRegions): PublicPageRegions {
|
|
|
910
1286
|
...(regions.header ? { header: toPublicPageRegion(regions.header) } : {}),
|
|
911
1287
|
...(regions.footer ? { footer: toPublicPageRegion(regions.footer) } : {}),
|
|
912
1288
|
...(regions.columns ? { columns: regions.columns.map(toPublicPageRegion) } : {}),
|
|
1289
|
+
...(regions.footnotes && regions.footnotes.length > 0
|
|
1290
|
+
? { footnotes: regions.footnotes.map(toPublicPageRegion) }
|
|
1291
|
+
: {}),
|
|
913
1292
|
};
|
|
914
1293
|
}
|
|
915
1294
|
|
|
@@ -1077,6 +1456,17 @@ function toFacetEvent(
|
|
|
1077
1456
|
...(event.reason ? { reason: event.reason } : {}),
|
|
1078
1457
|
};
|
|
1079
1458
|
}
|
|
1459
|
+
case "layout_committed":
|
|
1460
|
+
return {
|
|
1461
|
+
kind: "layout_committed",
|
|
1462
|
+
revision: event.revision,
|
|
1463
|
+
...(event.reason ? { reason: event.reason } : {}),
|
|
1464
|
+
...(event.dirtyFieldFamilies && event.dirtyFieldFamilies.length > 0
|
|
1465
|
+
? { dirtyFieldFamilies: event.dirtyFieldFamilies }
|
|
1466
|
+
: {}),
|
|
1467
|
+
...(event.pageCountDelta ? { pageCountDelta: event.pageCountDelta } : {}),
|
|
1468
|
+
...(event.pageRange ? { pageRange: event.pageRange } : {}),
|
|
1469
|
+
};
|
|
1080
1470
|
default:
|
|
1081
1471
|
return null;
|
|
1082
1472
|
}
|
|
@@ -1173,24 +1563,90 @@ function findPageForOffsetAndStory(
|
|
|
1173
1563
|
/**
|
|
1174
1564
|
* Select line boxes that belong to a given region on a page.
|
|
1175
1565
|
*
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1566
|
+
* `body` returns the engine's per-page measured line boxes (one per
|
|
1567
|
+
* line of paginated body content).
|
|
1568
|
+
*
|
|
1569
|
+
* `header` / `footer` / `footnote-area` / `column` (P4): synthesizes
|
|
1570
|
+
* one line box per fragment in the region. Header/footer fragments
|
|
1571
|
+
* aren't paginated themselves — the engine reserves space for the
|
|
1572
|
+
* entire header story per page — so per-line measurements are not
|
|
1573
|
+
* available. The synthesized line box uses each fragment's
|
|
1574
|
+
* `heightTwips` as the line height with cumulative `baselineTwips`
|
|
1575
|
+
* from the region origin. This gives consumers (page stack, P8
|
|
1576
|
+
* region rendering) a uniform iteration shape across regions.
|
|
1577
|
+
*
|
|
1578
|
+
* `endnote-area` always returns empty: endnote bodies are emitted at
|
|
1579
|
+
* the document end as a separate region whose layout sits outside
|
|
1580
|
+
* the per-page accounting.
|
|
1183
1581
|
*/
|
|
1184
1582
|
function collectLineBoxesForRegion(
|
|
1185
1583
|
node: RuntimePageNode,
|
|
1186
1584
|
region: PublicPageRegion["kind"],
|
|
1187
|
-
|
|
1585
|
+
graph: RuntimePageGraph,
|
|
1586
|
+
columnIndex: number | undefined,
|
|
1188
1587
|
): readonly RuntimeLineBoxAlias[] {
|
|
1189
|
-
void _columnIndex;
|
|
1190
1588
|
if (region === "body") {
|
|
1191
1589
|
return node.lineBoxes;
|
|
1192
1590
|
}
|
|
1193
|
-
|
|
1591
|
+
// P4: synthesize line boxes from the region's fragments.
|
|
1592
|
+
const regionEntry = resolveRegionEntry(node, region, columnIndex);
|
|
1593
|
+
if (!regionEntry || regionEntry.fragmentIds.length === 0) {
|
|
1594
|
+
return EMPTY_LINE_BOXES;
|
|
1595
|
+
}
|
|
1596
|
+
const fragmentsById = new Map(
|
|
1597
|
+
graph.fragments.map((f) => [f.fragmentId, f] as const),
|
|
1598
|
+
);
|
|
1599
|
+
const result: RuntimeLineBoxAlias[] = [];
|
|
1600
|
+
let cursorTwips = 0;
|
|
1601
|
+
let lineIndex = 0;
|
|
1602
|
+
for (const fragmentId of regionEntry.fragmentIds) {
|
|
1603
|
+
const fragment = fragmentsById.get(fragmentId);
|
|
1604
|
+
if (!fragment) continue;
|
|
1605
|
+
const heightTwips = Math.max(1, fragment.heightTwips);
|
|
1606
|
+
result.push({
|
|
1607
|
+
fragmentId,
|
|
1608
|
+
lineIndex: lineIndex++,
|
|
1609
|
+
baselineTwips: cursorTwips + heightTwips,
|
|
1610
|
+
heightTwips,
|
|
1611
|
+
widthTwips: regionEntry.widthTwips,
|
|
1612
|
+
});
|
|
1613
|
+
cursorTwips += heightTwips;
|
|
1614
|
+
}
|
|
1615
|
+
return result;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function resolveRegionEntry(
|
|
1619
|
+
node: RuntimePageNode,
|
|
1620
|
+
region: PublicPageRegion["kind"],
|
|
1621
|
+
columnIndex: number | undefined,
|
|
1622
|
+
): RuntimePageRegion | undefined {
|
|
1623
|
+
switch (region) {
|
|
1624
|
+
case "header":
|
|
1625
|
+
return node.regions.header;
|
|
1626
|
+
case "footer":
|
|
1627
|
+
return node.regions.footer;
|
|
1628
|
+
case "column": {
|
|
1629
|
+
const columns = node.regions.columns ?? [];
|
|
1630
|
+
if (columns.length === 0) return undefined;
|
|
1631
|
+
const idx = columnIndex ?? 0;
|
|
1632
|
+
return columns[idx];
|
|
1633
|
+
}
|
|
1634
|
+
case "footnote-area": {
|
|
1635
|
+
// Footnote area sits at the bottom of the body region per
|
|
1636
|
+
// OOXML. `RuntimePageRegions.footnotes` is a stable seam
|
|
1637
|
+
// declared by P4; the page-graph builder populates entries
|
|
1638
|
+
// when P8 lands. Returns the first footnote region (page
|
|
1639
|
+
// layouts allocate one block of footnotes per page; multi-
|
|
1640
|
+
// block layouts come later).
|
|
1641
|
+
const footnoteRegionList = node.regions.footnotes;
|
|
1642
|
+
if (footnoteRegionList && footnoteRegionList.length > 0) {
|
|
1643
|
+
return footnoteRegionList[0];
|
|
1644
|
+
}
|
|
1645
|
+
return undefined;
|
|
1646
|
+
}
|
|
1647
|
+
default:
|
|
1648
|
+
return undefined;
|
|
1649
|
+
}
|
|
1194
1650
|
}
|
|
1195
1651
|
|
|
1196
1652
|
// Use a shared alias so the region helper doesn't import the runtime
|
|
@@ -1476,3 +1932,118 @@ function findCanonicalTableByBlockId(
|
|
|
1476
1932
|
}
|
|
1477
1933
|
return null;
|
|
1478
1934
|
}
|
|
1935
|
+
|
|
1936
|
+
// ---------------------------------------------------------------------------
|
|
1937
|
+
// P8 — region-block resolvers
|
|
1938
|
+
// ---------------------------------------------------------------------------
|
|
1939
|
+
|
|
1940
|
+
function resolveBodyRegionBlocks(
|
|
1941
|
+
node: RuntimePageNode,
|
|
1942
|
+
graph: RuntimePageGraph,
|
|
1943
|
+
findBodyBlockById: (blockId: string) => SurfaceBlockSnapshot | undefined,
|
|
1944
|
+
): readonly PublicRegionBlock[] {
|
|
1945
|
+
const fragmentsById = new Map<string, RuntimeBlockFragment>();
|
|
1946
|
+
for (const fragment of graph.fragments) {
|
|
1947
|
+
fragmentsById.set(fragment.fragmentId, fragment);
|
|
1948
|
+
}
|
|
1949
|
+
const blocks: PublicRegionBlock[] = [];
|
|
1950
|
+
for (const fragmentId of node.regions.body.fragmentIds) {
|
|
1951
|
+
const fragment = fragmentsById.get(fragmentId);
|
|
1952
|
+
if (!fragment) continue;
|
|
1953
|
+
const blockSnapshot = findBodyBlockById(fragment.blockId);
|
|
1954
|
+
if (!blockSnapshot) continue;
|
|
1955
|
+
blocks.push({
|
|
1956
|
+
blockId: fragment.blockId,
|
|
1957
|
+
fragmentId,
|
|
1958
|
+
pageIndex: node.pageIndex,
|
|
1959
|
+
regionKind: "body",
|
|
1960
|
+
runtimeFromOffset: fragment.from,
|
|
1961
|
+
runtimeToOffset: fragment.to,
|
|
1962
|
+
heightTwips: fragment.heightTwips,
|
|
1963
|
+
blockSnapshot,
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
return Object.freeze(blocks);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function resolveHeaderFooterRegionBlocks(
|
|
1970
|
+
pageIndex: number,
|
|
1971
|
+
regionKind: "header" | "footer",
|
|
1972
|
+
storyTarget: EditorStoryTarget,
|
|
1973
|
+
projectedBlocks: readonly SurfaceBlockSnapshot[],
|
|
1974
|
+
): readonly PublicRegionBlock[] {
|
|
1975
|
+
if (storyTarget.kind !== regionKind) return Object.freeze([]);
|
|
1976
|
+
if (projectedBlocks.length === 0) return Object.freeze([]);
|
|
1977
|
+
const fragmentBase = `story-${storyTarget.kind}-${storyTarget.relationshipId}-${storyTarget.variant}-${storyTarget.sectionIndex ?? "_"}`;
|
|
1978
|
+
return Object.freeze(
|
|
1979
|
+
projectedBlocks.map((blockSnapshot, index): PublicRegionBlock => ({
|
|
1980
|
+
blockId: blockSnapshot.blockId,
|
|
1981
|
+
fragmentId: `${fragmentBase}-${index}`,
|
|
1982
|
+
pageIndex,
|
|
1983
|
+
regionKind,
|
|
1984
|
+
runtimeFromOffset: blockSnapshot.from,
|
|
1985
|
+
runtimeToOffset: blockSnapshot.to,
|
|
1986
|
+
// Header/footer fragments aren't measured per-block — consumers read
|
|
1987
|
+
// the region's `heightTwips` from `getStoryRegionsOnPage`.
|
|
1988
|
+
heightTwips: 0,
|
|
1989
|
+
blockSnapshot,
|
|
1990
|
+
})),
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function resolveFootnoteAreaRegionBlocks(
|
|
1995
|
+
node: RuntimePageNode,
|
|
1996
|
+
document: CanonicalDocumentEnvelope,
|
|
1997
|
+
): readonly PublicRegionBlock[] {
|
|
1998
|
+
const collection = document.subParts?.footnoteCollection;
|
|
1999
|
+
if (!collection) return Object.freeze([]);
|
|
2000
|
+
// P8.2.1 — batch every allocation's body into one
|
|
2001
|
+
// `createEditorSurfaceSnapshot` call and slice the result back into
|
|
2002
|
+
// per-allocation groups. Amplification per-page is small (a handful of
|
|
2003
|
+
// footnotes), but this mirrors the endnote batching so both paths take
|
|
2004
|
+
// the same shape. The existence guard on `note` is sufficient — the
|
|
2005
|
+
// previous `void note;` marker is gone.
|
|
2006
|
+
const groups: Array<{
|
|
2007
|
+
allocation: RuntimeNoteAllocation;
|
|
2008
|
+
count: number;
|
|
2009
|
+
}> = [];
|
|
2010
|
+
const flat: import("../../model/canonical-document.ts").BlockNode[] = [];
|
|
2011
|
+
for (const allocation of node.noteAllocations) {
|
|
2012
|
+
if (allocation.noteKind !== "footnote") continue;
|
|
2013
|
+
const note = collection.footnotes[allocation.noteId];
|
|
2014
|
+
if (!note || note.blocks.length === 0) continue;
|
|
2015
|
+
groups.push({ allocation, count: note.blocks.length });
|
|
2016
|
+
for (const block of note.blocks) flat.push(block);
|
|
2017
|
+
}
|
|
2018
|
+
if (flat.length === 0) return Object.freeze([]);
|
|
2019
|
+
const surface = createEditorSurfaceSnapshot(
|
|
2020
|
+
{
|
|
2021
|
+
...document,
|
|
2022
|
+
content: { type: "doc", children: flat },
|
|
2023
|
+
} as CanonicalDocumentEnvelope,
|
|
2024
|
+
createSelectionSnapshot(0, 0),
|
|
2025
|
+
);
|
|
2026
|
+
const blocks: PublicRegionBlock[] = [];
|
|
2027
|
+
let cursor = 0;
|
|
2028
|
+
for (const group of groups) {
|
|
2029
|
+
const fragmentBase = group.allocation.fragmentId
|
|
2030
|
+
?? `note-${node.pageIndex}-${group.allocation.noteId}`;
|
|
2031
|
+
for (let i = 0; i < group.count; i += 1) {
|
|
2032
|
+
const blockSnapshot = surface.blocks[cursor + i];
|
|
2033
|
+
if (!blockSnapshot) continue;
|
|
2034
|
+
blocks.push({
|
|
2035
|
+
blockId: blockSnapshot.blockId,
|
|
2036
|
+
fragmentId: `${fragmentBase}-${i}`,
|
|
2037
|
+
pageIndex: node.pageIndex,
|
|
2038
|
+
regionKind: "footnote-area",
|
|
2039
|
+
runtimeFromOffset: blockSnapshot.from,
|
|
2040
|
+
runtimeToOffset: blockSnapshot.to,
|
|
2041
|
+
heightTwips: group.allocation.reservedHeightTwips,
|
|
2042
|
+
blockSnapshot,
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
cursor += group.count;
|
|
2046
|
+
}
|
|
2047
|
+
return Object.freeze(blocks);
|
|
2048
|
+
}
|
|
2049
|
+
|