@beyondwork/docx-react-component 1.0.41 → 1.0.42
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 +13 -1
- 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/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +347 -4
- 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 +568 -1
- package/src/index.ts +118 -1
- 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/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 +271 -0
- package/src/io/ooxml/workflow-payload.ts +146 -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 +280 -93
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +122 -12
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +230 -34
- package/src/runtime/layout/public-facet.ts +185 -13
- 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/resign-payload.ts +120 -0
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +587 -0
- package/src/ui/editor-runtime-boundary.ts +1 -0
- package/src/ui/editor-shell-view.tsx +11 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- 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/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 +58 -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/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 +15 -13
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -14
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +32 -0
- 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 +293 -34
|
@@ -45,35 +45,12 @@ export interface DocxFontLoader {
|
|
|
45
45
|
refresh(input: FontLoaderInput): void;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
interface MinimalFontFace {
|
|
49
|
-
load(): Promise<MinimalFontFace>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface MinimalFontFaceDescriptors {
|
|
53
|
-
weight?: string;
|
|
54
|
-
style?: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface MinimalFontFaceConstructor {
|
|
58
|
-
new (
|
|
59
|
-
family: string,
|
|
60
|
-
source: ArrayBuffer | ArrayBufferView | string,
|
|
61
|
-
descriptors?: MinimalFontFaceDescriptors,
|
|
62
|
-
): MinimalFontFace;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface MinimalFontFaceSet {
|
|
66
|
-
add(face: MinimalFontFace): void;
|
|
67
|
-
check(font: string): boolean;
|
|
68
|
-
ready: Promise<MinimalFontFaceSet>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
48
|
export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
72
|
-
const globalDocument = (globalThis as unknown as { document?: { fonts?: unknown } }).document;
|
|
73
49
|
const supported =
|
|
74
|
-
|
|
50
|
+
typeof document !== "undefined" &&
|
|
75
51
|
typeof (globalThis as { FontFace?: unknown }).FontFace !== "undefined" &&
|
|
76
|
-
|
|
52
|
+
// Guard against jsdom which exposes FontFace but not document.fonts
|
|
53
|
+
Boolean((document as Document & { fonts?: FontFaceSet }).fonts);
|
|
77
54
|
|
|
78
55
|
let current: FontLoaderInput = initial;
|
|
79
56
|
let readyPromise: Promise<void>;
|
|
@@ -81,7 +58,7 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
81
58
|
|
|
82
59
|
function run(input: FontLoaderInput): Promise<void> {
|
|
83
60
|
if (!supported) return Promise.resolve();
|
|
84
|
-
const fontSet =
|
|
61
|
+
const fontSet = (document as Document & { fonts?: FontFaceSet }).fonts;
|
|
85
62
|
if (!fontSet) return Promise.resolve();
|
|
86
63
|
|
|
87
64
|
const pending: Array<Promise<unknown>> = [];
|
|
@@ -93,8 +70,10 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
93
70
|
|
|
94
71
|
for (const [descriptor, data] of variantsOf(variants)) {
|
|
95
72
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
const FontFaceCtor = (globalThis as any).FontFace as {
|
|
75
|
+
new (family: string, source: BufferSource, descriptors?: FontFaceDescriptors): FontFace;
|
|
76
|
+
};
|
|
98
77
|
const face = new FontFaceCtor(family, data, descriptor);
|
|
99
78
|
pending.push(
|
|
100
79
|
face.load().then((loaded) => {
|
|
@@ -109,6 +88,8 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
109
88
|
}
|
|
110
89
|
}
|
|
111
90
|
|
|
91
|
+
// Mark declared families as registered if the browser already resolves
|
|
92
|
+
// them (e.g. system fonts like Calibri, Arial).
|
|
112
93
|
for (const family of input.families) {
|
|
113
94
|
try {
|
|
114
95
|
const probe = `12px "${family.replace(/"/g, "'")}", serif`;
|
|
@@ -146,7 +127,7 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
146
127
|
|
|
147
128
|
function* variantsOf(
|
|
148
129
|
variants: EmbeddedFontBytes,
|
|
149
|
-
): IterableIterator<[
|
|
130
|
+
): IterableIterator<[FontFaceDescriptors, ArrayBuffer]> {
|
|
150
131
|
if (variants.regular) {
|
|
151
132
|
yield [{ weight: "400", style: "normal" }, variants.regular];
|
|
152
133
|
}
|
|
@@ -35,6 +35,7 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
|
35
35
|
getDisplayPageNumber: () => null,
|
|
36
36
|
getLineBoxes: () => [],
|
|
37
37
|
getLineBoxesForRegion: () => [],
|
|
38
|
+
getStoryRegionsOnPage: () => [],
|
|
38
39
|
getFragmentsForPage: () => [],
|
|
39
40
|
getPageFormatCatalog: () => PAGE_FORMAT_CATALOG,
|
|
40
41
|
getActivePageFormat: () => null,
|
|
@@ -54,6 +55,7 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
|
54
55
|
whenMeasurementReady: () => Promise.resolve(),
|
|
55
56
|
getFirstPageIndexForBlock: () => null,
|
|
56
57
|
swapMeasurementProvider: () => undefined,
|
|
58
|
+
invalidateMeasurementCache: () => undefined,
|
|
57
59
|
getTableRenderPlan: () => null,
|
|
58
60
|
getDirtyFieldFamilies: () => [],
|
|
59
61
|
getFieldDirtinessReport: () => emptyReport,
|
|
@@ -104,7 +104,19 @@ export interface LayoutEngineEvent {
|
|
|
104
104
|
| "incremental_relayout"
|
|
105
105
|
| "page_count_changed"
|
|
106
106
|
| "page_field_dirtied"
|
|
107
|
-
| "measurement_backend_ready"
|
|
107
|
+
| "measurement_backend_ready"
|
|
108
|
+
/**
|
|
109
|
+
* P14.b — coalesced "the engine just finished a build" event. Emitted
|
|
110
|
+
* exactly once per `fullRebuild` / `incrementalRelayout` AFTER the
|
|
111
|
+
* granular events, carrying the union of dirty-field families, page-
|
|
112
|
+
* count delta, and page-range info. Subscribers that only need to
|
|
113
|
+
* react to "something layout-affecting changed" can listen to this
|
|
114
|
+
* single event and skip the multi-event subscription pattern that
|
|
115
|
+
* triggered N React re-renders per applyPatch. The granular events
|
|
116
|
+
* still fire for backward compat with consumers (TwStatusBar fidelity
|
|
117
|
+
* badge, etc.) that care about specific kinds.
|
|
118
|
+
*/
|
|
119
|
+
| "layout_committed";
|
|
108
120
|
revision: number;
|
|
109
121
|
previousPageCount?: number;
|
|
110
122
|
currentPageCount?: number;
|
|
@@ -113,6 +125,17 @@ export interface LayoutEngineEvent {
|
|
|
113
125
|
fidelity?: LayoutMeasurementProvider["fidelity"];
|
|
114
126
|
/** First dirty page index for incremental_relayout events. */
|
|
115
127
|
firstDirtyPageIndex?: number;
|
|
128
|
+
/**
|
|
129
|
+
* P14.b — page-count delta for `layout_committed`. Present when the
|
|
130
|
+
* commit produced a different total page count than the prior graph.
|
|
131
|
+
*/
|
|
132
|
+
pageCountDelta?: { previous: number; current: number };
|
|
133
|
+
/**
|
|
134
|
+
* P14.b — when `layout_committed` came from a bounded incremental
|
|
135
|
+
* relayout, the page range that was re-paginated. Absent for full
|
|
136
|
+
* rebuilds.
|
|
137
|
+
*/
|
|
138
|
+
pageRange?: { fromPageIndex: number; toPageIndex: number };
|
|
116
139
|
}
|
|
117
140
|
|
|
118
141
|
export interface LayoutEngineInstance {
|
|
@@ -149,6 +172,16 @@ export interface LayoutEngineInstance {
|
|
|
149
172
|
|
|
150
173
|
// ---- measurement plumbing --------------------------------------------
|
|
151
174
|
swapMeasurementProvider(provider: LayoutMeasurementProvider): void;
|
|
175
|
+
/**
|
|
176
|
+
* Invalidate the active measurement provider's internal caches (canvas
|
|
177
|
+
* glyph / run-width LRU) AND clear the engine's cached page graph so
|
|
178
|
+
* the next query re-paginates with fresh measurements. Host runtime
|
|
179
|
+
* calls this after `docxFontLoader.refresh(...)` registers new
|
|
180
|
+
* FontFace families — without this call the canvas backend's glyph
|
|
181
|
+
* cache keeps returning pre-refresh widths for already-measured
|
|
182
|
+
* glyphs, and the cached page graph keeps its stale page boundaries.
|
|
183
|
+
*/
|
|
184
|
+
invalidateMeasurementCache(): void;
|
|
152
185
|
}
|
|
153
186
|
|
|
154
187
|
// ---------------------------------------------------------------------------
|
|
@@ -284,14 +317,30 @@ export function createLayoutEngine(
|
|
|
284
317
|
const formatting = buildResolvedFormattingState(document, mainSurface);
|
|
285
318
|
|
|
286
319
|
const currentPageCount = resolveTotalPageCount(pages);
|
|
320
|
+
let pageCountDelta: { previous: number; current: number } | undefined;
|
|
287
321
|
if (currentPageCount !== previousPageCount) {
|
|
322
|
+
pageCountDelta = { previous: previousPageCount, current: currentPageCount };
|
|
323
|
+
previousPageCount = currentPageCount;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// MUST publish cache before emit: re-entrant getPageGraph() calls from
|
|
327
|
+
// subscribers during emit would otherwise trigger runaway rebuilds.
|
|
328
|
+
cachedKey = {
|
|
329
|
+
content: document.content,
|
|
330
|
+
styles: document.styles,
|
|
331
|
+
subParts: document.subParts,
|
|
332
|
+
};
|
|
333
|
+
cachedGraph = graph;
|
|
334
|
+
cachedFormatting = formatting;
|
|
335
|
+
cachedMapper = createPageFragmentMapper(graph);
|
|
336
|
+
|
|
337
|
+
if (pageCountDelta) {
|
|
288
338
|
emit({
|
|
289
339
|
kind: "page_count_changed",
|
|
290
340
|
revision: graph.revision,
|
|
291
|
-
previousPageCount,
|
|
292
|
-
currentPageCount,
|
|
341
|
+
previousPageCount: pageCountDelta.previous,
|
|
342
|
+
currentPageCount: pageCountDelta.current,
|
|
293
343
|
});
|
|
294
|
-
previousPageCount = currentPageCount;
|
|
295
344
|
}
|
|
296
345
|
|
|
297
346
|
if (dirtyFamilies.length > 0) {
|
|
@@ -308,14 +357,16 @@ export function createLayoutEngine(
|
|
|
308
357
|
...(reason ? { reason } : {}),
|
|
309
358
|
});
|
|
310
359
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
360
|
+
emit({
|
|
361
|
+
kind: "layout_committed",
|
|
362
|
+
revision: graph.revision,
|
|
363
|
+
...(reason ? { reason } : {}),
|
|
364
|
+
...(dirtyFamilies.length > 0
|
|
365
|
+
? { dirtyFieldFamilies: dirtyFamilies }
|
|
366
|
+
: {}),
|
|
367
|
+
...(pageCountDelta ? { pageCountDelta } : {}),
|
|
368
|
+
});
|
|
369
|
+
|
|
319
370
|
return graph;
|
|
320
371
|
}
|
|
321
372
|
|
|
@@ -388,7 +439,9 @@ export function createLayoutEngine(
|
|
|
388
439
|
const currentPageCount = resolveTotalPageCount(
|
|
389
440
|
deriveDocumentPageSnapshots(splicedGraph),
|
|
390
441
|
);
|
|
442
|
+
let pageCountDelta: { previous: number; current: number } | undefined;
|
|
391
443
|
if (currentPageCount !== previousPageCount) {
|
|
444
|
+
pageCountDelta = { previous: previousPageCount, current: currentPageCount };
|
|
392
445
|
emit({
|
|
393
446
|
kind: "page_count_changed",
|
|
394
447
|
revision: splicedGraph.revision,
|
|
@@ -413,6 +466,30 @@ export function createLayoutEngine(
|
|
|
413
466
|
firstDirtyPageIndex: firstDirty,
|
|
414
467
|
});
|
|
415
468
|
|
|
469
|
+
// P14.b — coalesced commit event for the bounded-incremental path.
|
|
470
|
+
//
|
|
471
|
+
// Page-range semantics: the current `incrementalRelayout` path uses
|
|
472
|
+
// `buildPageStackFromWithSplits` + `spliceGraph`, which always
|
|
473
|
+
// re-paginates from `firstDirty` through the document tail (we
|
|
474
|
+
// discard the prior tail and replace it with the freshly-paginated
|
|
475
|
+
// pages). So `toPageIndex = pages.length - 1` is correct for every
|
|
476
|
+
// commit produced by this path. Future bounded-middle splices
|
|
477
|
+
// (e.g., a middle-style change that doesn't touch the tail) would
|
|
478
|
+
// need to track an explicit upper bound — guard the assumption
|
|
479
|
+
// here so the contract drift becomes a test failure rather than a
|
|
480
|
+
// silent over-iteration in consumers (Chrome overlay, render kernel
|
|
481
|
+
// diff).
|
|
482
|
+
emit({
|
|
483
|
+
kind: "layout_committed",
|
|
484
|
+
revision: splicedGraph.revision,
|
|
485
|
+
reason: pending.reason,
|
|
486
|
+
pageRange: { fromPageIndex: firstDirty, toPageIndex: splicedGraph.pages.length - 1 },
|
|
487
|
+
...(dirtyFamilies.length > 0
|
|
488
|
+
? { dirtyFieldFamilies: dirtyFamilies }
|
|
489
|
+
: {}),
|
|
490
|
+
...(pageCountDelta ? { pageCountDelta } : {}),
|
|
491
|
+
});
|
|
492
|
+
|
|
416
493
|
cachedKey = {
|
|
417
494
|
content: document.content,
|
|
418
495
|
styles: document.styles,
|
|
@@ -605,13 +682,46 @@ export function createLayoutEngine(
|
|
|
605
682
|
},
|
|
606
683
|
|
|
607
684
|
swapMeasurementProvider(provider) {
|
|
685
|
+
const previousFidelity = measurementProvider.fidelity;
|
|
608
686
|
measurementProvider = provider;
|
|
687
|
+
// Hardening: a backend swap changes the measurement numerics the
|
|
688
|
+
// cached graph was built against. Empirical → canvas typically
|
|
689
|
+
// reduces line counts (canvas-accurate glyph widths pack more
|
|
690
|
+
// text per line); canvas → canvas-with-font-loading applies the
|
|
691
|
+
// correct FontFace metrics for embedded DOCX fonts. Either way,
|
|
692
|
+
// the cached graph is stale — invalidate so the next
|
|
693
|
+
// `getGraph()` query re-paginates with the new provider. Skip
|
|
694
|
+
// invalidation when fidelity is unchanged (e.g., an empirical
|
|
695
|
+
// → empirical swap, or a canvas fallback that resolved back to
|
|
696
|
+
// the same backend) so we don't churn.
|
|
697
|
+
if (previousFidelity !== provider.fidelity) {
|
|
698
|
+
cachedKey = null;
|
|
699
|
+
cachedGraph = null;
|
|
700
|
+
cachedFormatting = null;
|
|
701
|
+
cachedMapper = null;
|
|
702
|
+
}
|
|
609
703
|
emit({
|
|
610
704
|
kind: "measurement_backend_ready",
|
|
611
705
|
revision: cachedGraph?.revision ?? 0,
|
|
612
706
|
fidelity: provider.fidelity,
|
|
613
707
|
});
|
|
614
708
|
},
|
|
709
|
+
/**
|
|
710
|
+
* Invalidate the current measurement provider's internal glyph /
|
|
711
|
+
* run-width cache. Called by the host runtime after
|
|
712
|
+
* `fontLoader.refresh(...)` so canvas-backed measurements re-read
|
|
713
|
+
* the newly-registered FontFaces instead of returning stale widths
|
|
714
|
+
* from the pre-refresh glyph cache. The graph cache itself is
|
|
715
|
+
* also cleared because a font change can shift line breaks and
|
|
716
|
+
* therefore page boundaries.
|
|
717
|
+
*/
|
|
718
|
+
invalidateMeasurementCache() {
|
|
719
|
+
measurementProvider.invalidateCache();
|
|
720
|
+
cachedKey = null;
|
|
721
|
+
cachedGraph = null;
|
|
722
|
+
cachedFormatting = null;
|
|
723
|
+
cachedMapper = null;
|
|
724
|
+
},
|
|
615
725
|
};
|
|
616
726
|
}
|
|
617
727
|
|
|
@@ -73,6 +73,17 @@ export interface RuntimePageRegions {
|
|
|
73
73
|
* pages, the top-level body is used and `columns` is undefined.
|
|
74
74
|
*/
|
|
75
75
|
columns?: RuntimePageRegion[];
|
|
76
|
+
/**
|
|
77
|
+
* P4 — footnote regions reserved at the bottom of the page (above
|
|
78
|
+
* the footer band) when `noteAllocations` reserved space for one or
|
|
79
|
+
* more footnote bodies. Empty until the page-graph builder
|
|
80
|
+
* populates them, which lands alongside the P8 per-page region
|
|
81
|
+
* rendering work. The shape exists now so consumers
|
|
82
|
+
* (`getStoryRegionsOnPage` / `getLineBoxesForRegion("footnote-area")`)
|
|
83
|
+
* have a stable seam to read from when the builder catches up —
|
|
84
|
+
* pre-P8 reads return `[]` because no entries are populated.
|
|
85
|
+
*/
|
|
86
|
+
footnotes?: RuntimePageRegion[];
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
export interface RuntimePageRegion {
|
|
@@ -158,6 +169,14 @@ export interface RuntimeNoteAllocation {
|
|
|
158
169
|
noteId: string;
|
|
159
170
|
/** Twips reserved at the bottom of the page for this note's content. */
|
|
160
171
|
reservedHeightTwips: number;
|
|
172
|
+
/**
|
|
173
|
+
* P8 — fragment ID of this note's body. The corresponding
|
|
174
|
+
* `RuntimeBlockFragment` lives in `RuntimePageGraph.fragments` with
|
|
175
|
+
* `regionKind: "footnote-area"`. Undefined when the engine hasn't
|
|
176
|
+
* yet emitted a fragment for the allocation (back-compat for pre-P8
|
|
177
|
+
* callers).
|
|
178
|
+
*/
|
|
179
|
+
fragmentId?: string;
|
|
161
180
|
}
|
|
162
181
|
|
|
163
182
|
export interface RuntimePageAnchor {
|
|
@@ -196,8 +215,15 @@ export interface BuildPageGraphInput {
|
|
|
196
215
|
>;
|
|
197
216
|
/** Optional per-page line boxes. */
|
|
198
217
|
lineBoxes?: ReadonlyMap<string, RuntimeLineBox[]>;
|
|
199
|
-
/** Optional per-page note allocations. */
|
|
218
|
+
/** Optional per-page note allocations keyed by graph-assigned pageId. */
|
|
200
219
|
noteAllocations?: ReadonlyMap<string, RuntimeNoteAllocation[]>;
|
|
220
|
+
/**
|
|
221
|
+
* P8 — per-page note allocations keyed by pageIndex (0-based).
|
|
222
|
+
* Parallel to `fragmentsByPageIndex`; `buildPageGraph` uses the index to
|
|
223
|
+
* look up allocations without requiring callers to know the graph-internal
|
|
224
|
+
* `page-${revision}-${index}` pageId in advance.
|
|
225
|
+
*/
|
|
226
|
+
noteAllocationsByPageIndex?: ReadonlyMap<number, RuntimeNoteAllocation[]>;
|
|
201
227
|
}
|
|
202
228
|
|
|
203
229
|
export function buildPageGraph(input: BuildPageGraphInput): RuntimePageGraph;
|
|
@@ -244,10 +270,15 @@ export function buildPageGraph(
|
|
|
244
270
|
};
|
|
245
271
|
|
|
246
272
|
const pageFragments = aggregatedFragments.filter((f) => f.pageId === pageId);
|
|
247
|
-
|
|
273
|
+
// Split fragments into body-region ones (for the body fragmentIds list)
|
|
274
|
+
// and footnote-area ones (handled by buildRegions via noteAllocations).
|
|
275
|
+
const bodyPageFragments = pageFragments.filter(
|
|
276
|
+
(f) => f.regionKind !== "footnote-area",
|
|
277
|
+
);
|
|
278
|
+
const fragmentIds = bodyPageFragments.map((f) => f.fragmentId);
|
|
248
279
|
|
|
249
|
-
// If no fragments were supplied, synthesize a coarse body fragment so
|
|
250
|
-
// graph is still internally consistent.
|
|
280
|
+
// If no body fragments were supplied, synthesize a coarse body fragment so
|
|
281
|
+
// the graph is still internally consistent.
|
|
251
282
|
let bodyFragmentIds = fragmentIds;
|
|
252
283
|
if (fragmentIds.length === 0 && page.endOffset > page.startOffset) {
|
|
253
284
|
const coarse: RuntimeBlockFragment = {
|
|
@@ -264,6 +295,15 @@ export function buildPageGraph(
|
|
|
264
295
|
bodyFragmentIds = [coarse.fragmentId];
|
|
265
296
|
}
|
|
266
297
|
|
|
298
|
+
// Resolve per-page note allocations: prefer noteAllocationsByPageIndex
|
|
299
|
+
// (index-based, suitable when the caller doesn't know the graph-internal
|
|
300
|
+
// pageId) falling back to noteAllocations (pageId-keyed, used by the
|
|
301
|
+
// engine once it emits allocations).
|
|
302
|
+
const pageNoteAllocations: RuntimeNoteAllocation[] =
|
|
303
|
+
input.noteAllocationsByPageIndex?.get(index) ??
|
|
304
|
+
input.noteAllocations?.get(pageId) ??
|
|
305
|
+
[];
|
|
306
|
+
|
|
267
307
|
const node: RuntimePageNode = {
|
|
268
308
|
pageId,
|
|
269
309
|
pageIndex: page.pageIndex,
|
|
@@ -273,9 +313,9 @@ export function buildPageGraph(
|
|
|
273
313
|
endOffset: page.endOffset,
|
|
274
314
|
layout: page.layout,
|
|
275
315
|
stories,
|
|
276
|
-
regions: buildRegions(page.layout, bodyFragmentIds, stories),
|
|
316
|
+
regions: buildRegions(page.layout, bodyFragmentIds, stories, pageNoteAllocations),
|
|
277
317
|
lineBoxes: input.lineBoxes?.get(pageId) ?? [],
|
|
278
|
-
noteAllocations:
|
|
318
|
+
noteAllocations: pageNoteAllocations,
|
|
279
319
|
isBlankFiller: page.pageInSection === -1,
|
|
280
320
|
};
|
|
281
321
|
pages.push(node);
|
|
@@ -302,12 +342,40 @@ function buildRegions(
|
|
|
302
342
|
layout: PageLayoutSnapshot,
|
|
303
343
|
bodyFragmentIds: readonly string[],
|
|
304
344
|
stories: ResolvedPageStories,
|
|
345
|
+
noteAllocations: readonly RuntimeNoteAllocation[] = [],
|
|
305
346
|
): RuntimePageRegions {
|
|
306
347
|
const bodyWidth =
|
|
307
348
|
layout.pageWidth - layout.marginLeft - layout.marginRight;
|
|
308
|
-
|
|
349
|
+
let bodyHeight =
|
|
309
350
|
layout.pageHeight - layout.marginTop - layout.marginBottom;
|
|
310
351
|
|
|
352
|
+
// P8 — footnote area sits at the bottom of the page (above the footer
|
|
353
|
+
// band). Body height shrinks by the total reserved note height so
|
|
354
|
+
// band-aware consumers can stack bands without overflowing the paper frame.
|
|
355
|
+
let footnotesRegion: RuntimePageRegion | undefined;
|
|
356
|
+
if (noteAllocations.length > 0) {
|
|
357
|
+
const fragmentIds = noteAllocations
|
|
358
|
+
.filter(
|
|
359
|
+
(a): a is RuntimeNoteAllocation & { fragmentId: string } =>
|
|
360
|
+
Boolean(a.fragmentId),
|
|
361
|
+
)
|
|
362
|
+
.map((a) => a.fragmentId);
|
|
363
|
+
const totalNoteHeight = noteAllocations.reduce(
|
|
364
|
+
(sum, a) => sum + a.reservedHeightTwips,
|
|
365
|
+
0,
|
|
366
|
+
);
|
|
367
|
+
if (fragmentIds.length > 0) {
|
|
368
|
+
footnotesRegion = {
|
|
369
|
+
kind: "footnote-area",
|
|
370
|
+
originTwips: layout.pageHeight - layout.marginBottom - totalNoteHeight,
|
|
371
|
+
widthTwips: Math.max(0, bodyWidth),
|
|
372
|
+
heightTwips: Math.max(0, totalNoteHeight),
|
|
373
|
+
fragmentIds,
|
|
374
|
+
};
|
|
375
|
+
bodyHeight = Math.max(0, bodyHeight - totalNoteHeight);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
311
379
|
const body: RuntimePageRegion = {
|
|
312
380
|
kind: "body",
|
|
313
381
|
originTwips: layout.marginTop,
|
|
@@ -356,6 +424,10 @@ function buildRegions(
|
|
|
356
424
|
regions.columns = columns;
|
|
357
425
|
}
|
|
358
426
|
|
|
427
|
+
if (footnotesRegion) {
|
|
428
|
+
regions.footnotes = [footnotesRegion];
|
|
429
|
+
}
|
|
430
|
+
|
|
359
431
|
return regions;
|
|
360
432
|
}
|
|
361
433
|
|