@beyondwork/docx-react-component 1.0.35 → 1.0.37
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/README.md +103 -13
- package/package.json +1 -1
- package/src/api/package-version.ts +13 -0
- package/src/api/public-types.ts +84 -1
- package/src/core/commands/index.ts +19 -2
- package/src/core/selection/mapping.ts +6 -0
- package/src/io/docx-session.ts +24 -9
- package/src/io/export/build-app-properties-xml.ts +88 -0
- package/src/io/export/serialize-comments.ts +6 -1
- package/src/io/export/serialize-footnotes.ts +10 -9
- package/src/io/export/serialize-headers-footers.ts +11 -10
- package/src/io/export/serialize-main-document.ts +337 -50
- package/src/io/export/serialize-numbering.ts +115 -24
- package/src/io/export/serialize-tables.ts +13 -11
- package/src/io/export/table-properties-xml.ts +35 -16
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +5 -0
- package/src/io/ooxml/parse-footnotes.ts +2 -1
- package/src/io/ooxml/parse-headers-footers.ts +2 -1
- package/src/io/ooxml/parse-main-document.ts +21 -1
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +11 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-navigation.ts +1 -305
- package/src/runtime/document-runtime.ts +178 -16
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +188 -0
- package/src/runtime/layout/inert-layout-facet.ts +45 -0
- package/src/runtime/layout/layout-engine-instance.ts +618 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
- package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +433 -0
- package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
- package/src/runtime/layout/page-story-resolver.ts +195 -0
- package/src/runtime/layout/paginated-layout-engine.ts +788 -0
- package/src/runtime/layout/public-facet.ts +705 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/session-capabilities.ts +7 -4
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/ui/WordReviewEditor.tsx +15 -0
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +235 -0
- package/src/ui/headless/scoped-chrome-policy.ts +164 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +333 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +89 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +21 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +8 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +73 -13
- package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
- package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +173 -6
- package/src/ui-tailwind/theme/editor-theme.css +40 -14
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +235 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -1
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public Layout Facet — the ergonomic surface exposed on
|
|
3
|
+
* `WordReviewEditorRef.layout`.
|
|
4
|
+
*
|
|
5
|
+
* Per the plan (Phase 7), this provides a single coherent object tree for
|
|
6
|
+
* consumers to walk the layout graph, resolve positions, inspect measurement
|
|
7
|
+
* state, and observe layout events — without needing to deal with the legacy
|
|
8
|
+
* opaque JSON snapshots.
|
|
9
|
+
*
|
|
10
|
+
* Design rules:
|
|
11
|
+
* - Every returned value is a deep-cloned read model. Internal graph
|
|
12
|
+
* identities never escape the facet.
|
|
13
|
+
* - Story-aware. `EditorStoryTarget` is accepted anywhere a position is
|
|
14
|
+
* interpreted so header/footer/note queries work cleanly.
|
|
15
|
+
* - No DOM. No PM. No canonical model leakage.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
EditorStoryTarget,
|
|
20
|
+
PageLayoutSnapshot,
|
|
21
|
+
SelectionSnapshot,
|
|
22
|
+
} from "../../api/public-types";
|
|
23
|
+
import type {
|
|
24
|
+
ResolvedPageStories,
|
|
25
|
+
} from "./page-story-resolver.ts";
|
|
26
|
+
import type {
|
|
27
|
+
RuntimeBlockFragment,
|
|
28
|
+
RuntimeLineBox,
|
|
29
|
+
RuntimeNoteAllocation,
|
|
30
|
+
RuntimePageGraph,
|
|
31
|
+
RuntimePageNode,
|
|
32
|
+
RuntimePageRegion,
|
|
33
|
+
RuntimePageRegions,
|
|
34
|
+
} from "./page-graph.ts";
|
|
35
|
+
import type {
|
|
36
|
+
ResolvedFormattingState,
|
|
37
|
+
ResolvedRunFormatting,
|
|
38
|
+
} from "./resolved-formatting-document.ts";
|
|
39
|
+
import type { ResolvedParagraphFormatting } from "./resolved-formatting-state.ts";
|
|
40
|
+
import type {
|
|
41
|
+
LayoutEngineEvent,
|
|
42
|
+
LayoutEngineInstance,
|
|
43
|
+
LayoutEngineQueryInput,
|
|
44
|
+
} from "./layout-engine-instance.ts";
|
|
45
|
+
import type { PageFragmentMapper } from "./page-fragment-mapper.ts";
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Public read model types (shape-stable, cloned at the facet boundary)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
export interface PublicPageNode {
|
|
52
|
+
pageId: string;
|
|
53
|
+
pageIndex: number;
|
|
54
|
+
sectionIndex: number;
|
|
55
|
+
pageInSection: number;
|
|
56
|
+
startOffset: number;
|
|
57
|
+
endOffset: number;
|
|
58
|
+
/** Whether this page is a blank filler (e.g. from evenPage/oddPage). */
|
|
59
|
+
isBlankFiller: boolean;
|
|
60
|
+
/** Resolved display page number (1-based, honors section restarts). */
|
|
61
|
+
displayPageNumber: number;
|
|
62
|
+
/** Whether this is treated as the first page of its section (title page). */
|
|
63
|
+
isFirstPage: boolean;
|
|
64
|
+
/** Whether the displayed page number is even. */
|
|
65
|
+
isEvenPage: boolean;
|
|
66
|
+
/** Section-derived page layout geometry. */
|
|
67
|
+
layout: PageLayoutSnapshot;
|
|
68
|
+
/** Resolved header/footer/note stories active on this page. */
|
|
69
|
+
stories: PublicResolvedPageStories;
|
|
70
|
+
/** Sub-regions rendered on the page. */
|
|
71
|
+
regions: PublicPageRegions;
|
|
72
|
+
/** Number of line boxes rendered in the body region. */
|
|
73
|
+
lineBoxCount: number;
|
|
74
|
+
/** Footnotes reserved at the bottom of the page, if any. */
|
|
75
|
+
noteAllocations: readonly PublicNoteAllocation[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PublicResolvedPageStories {
|
|
79
|
+
header?: EditorStoryTarget;
|
|
80
|
+
footer?: EditorStoryTarget;
|
|
81
|
+
isFirstPage: boolean;
|
|
82
|
+
isEvenPage: boolean;
|
|
83
|
+
displayPageNumber: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface PublicPageRegions {
|
|
87
|
+
body: PublicPageRegion;
|
|
88
|
+
header?: PublicPageRegion;
|
|
89
|
+
footer?: PublicPageRegion;
|
|
90
|
+
columns?: readonly PublicPageRegion[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface PublicPageRegion {
|
|
94
|
+
kind: "body" | "header" | "footer" | "column" | "footnote-area";
|
|
95
|
+
originTwips: number;
|
|
96
|
+
widthTwips: number;
|
|
97
|
+
heightTwips: number;
|
|
98
|
+
fragmentCount: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface PublicBlockFragment {
|
|
102
|
+
fragmentId: string;
|
|
103
|
+
blockId: string;
|
|
104
|
+
pageId: string;
|
|
105
|
+
pageIndex: number;
|
|
106
|
+
regionKind: PublicPageRegion["kind"];
|
|
107
|
+
from: number;
|
|
108
|
+
to: number;
|
|
109
|
+
heightTwips: number;
|
|
110
|
+
orderInRegion: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface PublicLineBox {
|
|
114
|
+
fragmentId: string;
|
|
115
|
+
lineIndex: number;
|
|
116
|
+
baselineTwips: number;
|
|
117
|
+
heightTwips: number;
|
|
118
|
+
widthTwips: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface PublicNoteAllocation {
|
|
122
|
+
noteKind: "footnote" | "endnote";
|
|
123
|
+
noteId: string;
|
|
124
|
+
reservedHeightTwips: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface PublicPageAnchor {
|
|
128
|
+
offset: number;
|
|
129
|
+
pageId: string;
|
|
130
|
+
pageIndex: number;
|
|
131
|
+
fragmentId?: string;
|
|
132
|
+
regionKind?: PublicPageRegion["kind"];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface PublicPageSpan {
|
|
136
|
+
firstPageIndex: number;
|
|
137
|
+
lastPageIndex: number;
|
|
138
|
+
pageCount: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface PublicSectionNode {
|
|
142
|
+
sectionIndex: number;
|
|
143
|
+
startOffset: number;
|
|
144
|
+
endOffset: number;
|
|
145
|
+
firstPageIndex: number;
|
|
146
|
+
lastPageIndex: number;
|
|
147
|
+
pageCount: number;
|
|
148
|
+
layout: PageLayoutSnapshot;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface PublicResolvedParagraphFormatting {
|
|
152
|
+
blockId: string;
|
|
153
|
+
spacingBefore: number;
|
|
154
|
+
spacingAfter: number;
|
|
155
|
+
lineHeight: number;
|
|
156
|
+
lineRule: "auto" | "exact" | "atLeast";
|
|
157
|
+
indentLeft: number;
|
|
158
|
+
indentRight: number;
|
|
159
|
+
firstLineIndent: number;
|
|
160
|
+
hangingIndent: number;
|
|
161
|
+
fontSizeHalfPoints: number;
|
|
162
|
+
averageCharWidthTwips: number;
|
|
163
|
+
tabStops: readonly {
|
|
164
|
+
positionTwips: number;
|
|
165
|
+
alignment: string;
|
|
166
|
+
leader?: string;
|
|
167
|
+
}[];
|
|
168
|
+
keepNext: boolean;
|
|
169
|
+
keepLines: boolean;
|
|
170
|
+
pageBreakBefore: boolean;
|
|
171
|
+
widowControl: boolean;
|
|
172
|
+
contextualSpacing: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface PublicResolvedRunFormatting {
|
|
176
|
+
runId: string;
|
|
177
|
+
blockId: string;
|
|
178
|
+
fontFamily?: string;
|
|
179
|
+
fontSizeHalfPoints?: number;
|
|
180
|
+
bold: boolean;
|
|
181
|
+
italic: boolean;
|
|
182
|
+
underline: boolean;
|
|
183
|
+
strikethrough: boolean;
|
|
184
|
+
color?: string;
|
|
185
|
+
highlight?: string;
|
|
186
|
+
verticalAlign: "baseline" | "superscript" | "subscript";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface PublicBlockMeasurement {
|
|
190
|
+
blockId: string;
|
|
191
|
+
lineCount: number;
|
|
192
|
+
/** Total height the block occupies in twips. */
|
|
193
|
+
heightTwips: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export type PublicMeasurementFidelity =
|
|
197
|
+
| "empirical"
|
|
198
|
+
| "canvas"
|
|
199
|
+
| "canvas-with-font-loading";
|
|
200
|
+
|
|
201
|
+
export interface PublicFieldDirtinessReport {
|
|
202
|
+
families: readonly string[];
|
|
203
|
+
revision: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type LayoutFacetEvent =
|
|
207
|
+
| {
|
|
208
|
+
kind: "layout_recomputed";
|
|
209
|
+
revision: number;
|
|
210
|
+
reason?: LayoutFacetInvalidationReason;
|
|
211
|
+
}
|
|
212
|
+
| {
|
|
213
|
+
kind: "page_count_changed";
|
|
214
|
+
previous: number;
|
|
215
|
+
current: number;
|
|
216
|
+
revision: number;
|
|
217
|
+
}
|
|
218
|
+
| {
|
|
219
|
+
kind: "page_field_dirtied";
|
|
220
|
+
families: readonly string[];
|
|
221
|
+
revision: number;
|
|
222
|
+
}
|
|
223
|
+
| {
|
|
224
|
+
kind: "measurement_backend_ready";
|
|
225
|
+
fidelity: PublicMeasurementFidelity;
|
|
226
|
+
revision: number;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export type LayoutFacetInvalidationReason =
|
|
230
|
+
| { kind: "content-edit"; from: number; to: number }
|
|
231
|
+
| { kind: "section-change"; sectionIndex: number }
|
|
232
|
+
| { kind: "styles-change" }
|
|
233
|
+
| { kind: "theme-change" }
|
|
234
|
+
| { kind: "numbering-change"; numberingInstanceId?: string }
|
|
235
|
+
| { kind: "field-refresh"; family?: string }
|
|
236
|
+
| { kind: "full" };
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Facet interface
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
export interface WordReviewEditorLayoutFacet {
|
|
243
|
+
// Structure ------------------------------------------------------------
|
|
244
|
+
getPageCount(): number;
|
|
245
|
+
getPage(pageIndex: number): PublicPageNode | null;
|
|
246
|
+
getPages(options?: { sectionIndex?: number }): PublicPageNode[];
|
|
247
|
+
getSection(sectionIndex: number): PublicSectionNode | null;
|
|
248
|
+
getSections(): PublicSectionNode[];
|
|
249
|
+
|
|
250
|
+
// Offset / selection navigation ---------------------------------------
|
|
251
|
+
getPageForOffset(
|
|
252
|
+
offset: number,
|
|
253
|
+
story?: EditorStoryTarget,
|
|
254
|
+
): PublicPageNode | null;
|
|
255
|
+
getPageSpanForSelection(selection: SelectionSnapshot): PublicPageSpan | null;
|
|
256
|
+
getFragmentForOffset(
|
|
257
|
+
offset: number,
|
|
258
|
+
story?: EditorStoryTarget,
|
|
259
|
+
): PublicBlockFragment | null;
|
|
260
|
+
getAnchorForOffset(
|
|
261
|
+
offset: number,
|
|
262
|
+
story?: EditorStoryTarget,
|
|
263
|
+
): PublicPageAnchor | null;
|
|
264
|
+
|
|
265
|
+
// Per-page semantic reads ---------------------------------------------
|
|
266
|
+
getActiveStoriesOnPage(pageIndex: number): PublicResolvedPageStories | null;
|
|
267
|
+
getDisplayPageNumber(pageIndex: number): number | null;
|
|
268
|
+
getLineBoxes(
|
|
269
|
+
pageIndex: number,
|
|
270
|
+
options?: { region?: "body" | "header" | "footer" },
|
|
271
|
+
): PublicLineBox[];
|
|
272
|
+
getFragmentsForPage(pageIndex: number): PublicBlockFragment[];
|
|
273
|
+
|
|
274
|
+
// Measurement exposure -------------------------------------------------
|
|
275
|
+
getResolvedFormatting(blockId: string): PublicResolvedParagraphFormatting | null;
|
|
276
|
+
getResolvedRunFormatting(runId: string): PublicResolvedRunFormatting | null;
|
|
277
|
+
getMeasurement(blockId: string): PublicBlockMeasurement | null;
|
|
278
|
+
getMeasurementFidelity(): PublicMeasurementFidelity;
|
|
279
|
+
whenMeasurementReady(): Promise<void>;
|
|
280
|
+
|
|
281
|
+
// Fields ---------------------------------------------------------------
|
|
282
|
+
getDirtyFieldFamilies(): readonly string[];
|
|
283
|
+
getFieldDirtinessReport(): PublicFieldDirtinessReport;
|
|
284
|
+
|
|
285
|
+
// Events ---------------------------------------------------------------
|
|
286
|
+
subscribe(listener: (event: LayoutFacetEvent) => void): () => void;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Factory
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
export interface CreateLayoutFacetInput {
|
|
294
|
+
engine: LayoutEngineInstance;
|
|
295
|
+
getQueryInput: () => LayoutEngineQueryInput;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function createLayoutFacet(
|
|
299
|
+
input: CreateLayoutFacetInput,
|
|
300
|
+
): WordReviewEditorLayoutFacet {
|
|
301
|
+
const { engine, getQueryInput } = input;
|
|
302
|
+
|
|
303
|
+
function currentGraph(): RuntimePageGraph {
|
|
304
|
+
return engine.getPageGraph(getQueryInput());
|
|
305
|
+
}
|
|
306
|
+
function currentMapper(): PageFragmentMapper {
|
|
307
|
+
return engine.getFragmentMapper(getQueryInput());
|
|
308
|
+
}
|
|
309
|
+
function currentFormatting(): ResolvedFormattingState {
|
|
310
|
+
return engine.getResolvedFormattingState(getQueryInput());
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const listeners = new Set<(event: LayoutFacetEvent) => void>();
|
|
314
|
+
const unsubscribeEngine = engine.subscribe((event: LayoutEngineEvent) => {
|
|
315
|
+
const facetEvent = toFacetEvent(event);
|
|
316
|
+
if (!facetEvent) return;
|
|
317
|
+
for (const listener of listeners) {
|
|
318
|
+
try {
|
|
319
|
+
listener(facetEvent);
|
|
320
|
+
} catch {
|
|
321
|
+
// never let listener errors break the engine
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
// Keep the handle alive; the facet instance lives as long as the runtime.
|
|
326
|
+
void unsubscribeEngine;
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
getPageCount() {
|
|
330
|
+
return currentGraph().pages.length;
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
getPage(pageIndex) {
|
|
334
|
+
const graph = currentGraph();
|
|
335
|
+
const node = graph.pages[pageIndex];
|
|
336
|
+
return node ? toPublicPageNode(node, graph) : null;
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
getPages(options) {
|
|
340
|
+
const graph = currentGraph();
|
|
341
|
+
const filtered = options?.sectionIndex !== undefined
|
|
342
|
+
? graph.pages.filter((p) => p.sectionIndex === options.sectionIndex)
|
|
343
|
+
: graph.pages;
|
|
344
|
+
return filtered.map((node) => toPublicPageNode(node, graph));
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
getSection(sectionIndex) {
|
|
348
|
+
const graph = currentGraph();
|
|
349
|
+
return toPublicSectionNode(graph, sectionIndex);
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
getSections() {
|
|
353
|
+
const graph = currentGraph();
|
|
354
|
+
return graph.sections
|
|
355
|
+
.map((section) => toPublicSectionNode(graph, section.index))
|
|
356
|
+
.filter((node): node is PublicSectionNode => node !== null);
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
getPageForOffset(offset, _story) {
|
|
360
|
+
const graph = currentGraph();
|
|
361
|
+
for (const page of graph.pages) {
|
|
362
|
+
if (!page.isBlankFiller && offset < page.endOffset) {
|
|
363
|
+
return toPublicPageNode(page, graph);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const last = graph.pages[graph.pages.length - 1];
|
|
367
|
+
return last ? toPublicPageNode(last, graph) : null;
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
getPageSpanForSelection(selection) {
|
|
371
|
+
return currentMapper().mapSelectionToPageSpan(selection);
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
getFragmentForOffset(offset, story) {
|
|
375
|
+
const mapper = currentMapper();
|
|
376
|
+
const graph = currentGraph();
|
|
377
|
+
const location = mapper.mapOffsetToFragment(offset, story);
|
|
378
|
+
if (!location) return null;
|
|
379
|
+
const fragment = graph.fragments.find(
|
|
380
|
+
(f) => f.fragmentId === location.fragmentId,
|
|
381
|
+
);
|
|
382
|
+
if (!fragment) return null;
|
|
383
|
+
return toPublicBlockFragment(fragment, graph);
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
getAnchorForOffset(offset, story) {
|
|
387
|
+
const mapper = currentMapper();
|
|
388
|
+
const graph = currentGraph();
|
|
389
|
+
const anchor = mapper.getAnchorForOffset(offset, story);
|
|
390
|
+
if (!anchor) return null;
|
|
391
|
+
const page = graph.pages.find((p) => p.pageId === anchor.pageId);
|
|
392
|
+
if (!page) return null;
|
|
393
|
+
const fragmentLocation = mapper.mapOffsetToFragment(offset, story);
|
|
394
|
+
return {
|
|
395
|
+
offset: anchor.offset,
|
|
396
|
+
pageId: anchor.pageId,
|
|
397
|
+
pageIndex: page.pageIndex,
|
|
398
|
+
...(fragmentLocation?.fragmentId !== undefined
|
|
399
|
+
? { fragmentId: fragmentLocation.fragmentId }
|
|
400
|
+
: {}),
|
|
401
|
+
...(fragmentLocation?.regionKind !== undefined
|
|
402
|
+
? { regionKind: fragmentLocation.regionKind }
|
|
403
|
+
: {}),
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
getActiveStoriesOnPage(pageIndex) {
|
|
408
|
+
const graph = currentGraph();
|
|
409
|
+
const node = graph.pages[pageIndex];
|
|
410
|
+
if (!node) return null;
|
|
411
|
+
return toPublicResolvedPageStories(node.stories);
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
getDisplayPageNumber(pageIndex) {
|
|
415
|
+
const graph = currentGraph();
|
|
416
|
+
const node = graph.pages[pageIndex];
|
|
417
|
+
return node ? node.stories.displayPageNumber : null;
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
getLineBoxes(pageIndex, options) {
|
|
421
|
+
const graph = currentGraph();
|
|
422
|
+
const node = graph.pages[pageIndex];
|
|
423
|
+
if (!node) return [];
|
|
424
|
+
const region = options?.region ?? "body";
|
|
425
|
+
// Today all line boxes live on the body. The region filter is
|
|
426
|
+
// defensive for when header/footer metrics are also populated.
|
|
427
|
+
return node.lineBoxes
|
|
428
|
+
.filter(() => region === "body")
|
|
429
|
+
.map((box) => toPublicLineBox(box));
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
getFragmentsForPage(pageIndex) {
|
|
433
|
+
const graph = currentGraph();
|
|
434
|
+
const node = graph.pages[pageIndex];
|
|
435
|
+
if (!node) return [];
|
|
436
|
+
return graph.fragments
|
|
437
|
+
.filter((f) => f.pageId === node.pageId)
|
|
438
|
+
.map((f) => toPublicBlockFragment(f, graph));
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
getResolvedFormatting(blockId) {
|
|
442
|
+
const state = currentFormatting();
|
|
443
|
+
const formatting = state.paragraphs.get(blockId);
|
|
444
|
+
if (!formatting) return null;
|
|
445
|
+
return toPublicParagraphFormatting(blockId, formatting);
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
getResolvedRunFormatting(runId) {
|
|
449
|
+
const state = currentFormatting();
|
|
450
|
+
const run = state.runs.get(runId);
|
|
451
|
+
if (!run) return null;
|
|
452
|
+
const blockId = runId.split(":")[0] ?? "";
|
|
453
|
+
return toPublicRunFormatting(runId, blockId, run);
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
getMeasurement(blockId) {
|
|
457
|
+
const graph = currentGraph();
|
|
458
|
+
const fragments = graph.fragments.filter((f) => f.blockId === blockId);
|
|
459
|
+
if (fragments.length === 0) return null;
|
|
460
|
+
const lineCount = graph.pages.reduce((total, page) => {
|
|
461
|
+
return (
|
|
462
|
+
total +
|
|
463
|
+
page.lineBoxes.filter((line) =>
|
|
464
|
+
fragments.some((f) => f.fragmentId === line.fragmentId),
|
|
465
|
+
).length
|
|
466
|
+
);
|
|
467
|
+
}, 0);
|
|
468
|
+
const heightTwips = fragments.reduce((t, f) => t + f.heightTwips, 0);
|
|
469
|
+
return {
|
|
470
|
+
blockId,
|
|
471
|
+
lineCount,
|
|
472
|
+
heightTwips,
|
|
473
|
+
};
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
getMeasurementFidelity() {
|
|
477
|
+
return engine.measurementFidelity;
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
whenMeasurementReady() {
|
|
481
|
+
return engine.whenMeasurementReady();
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
getDirtyFieldFamilies() {
|
|
485
|
+
return engine.getDirtyFieldFamilies();
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
getFieldDirtinessReport() {
|
|
489
|
+
const graph = currentGraph();
|
|
490
|
+
return {
|
|
491
|
+
families: engine.getDirtyFieldFamilies(),
|
|
492
|
+
revision: graph.revision,
|
|
493
|
+
};
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
subscribe(listener) {
|
|
497
|
+
listeners.add(listener);
|
|
498
|
+
return () => {
|
|
499
|
+
listeners.delete(listener);
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Internal: graph → public clones
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
function toPublicPageNode(
|
|
510
|
+
node: RuntimePageNode,
|
|
511
|
+
graph: RuntimePageGraph,
|
|
512
|
+
): PublicPageNode {
|
|
513
|
+
return {
|
|
514
|
+
pageId: node.pageId,
|
|
515
|
+
pageIndex: node.pageIndex,
|
|
516
|
+
sectionIndex: node.sectionIndex,
|
|
517
|
+
pageInSection: node.pageInSection,
|
|
518
|
+
startOffset: node.startOffset,
|
|
519
|
+
endOffset: node.endOffset,
|
|
520
|
+
isBlankFiller: node.isBlankFiller,
|
|
521
|
+
displayPageNumber: node.stories.displayPageNumber,
|
|
522
|
+
isFirstPage: node.stories.isFirstPage,
|
|
523
|
+
isEvenPage: node.stories.isEvenPage,
|
|
524
|
+
layout: { ...node.layout },
|
|
525
|
+
stories: toPublicResolvedPageStories(node.stories),
|
|
526
|
+
regions: toPublicPageRegions(node.regions),
|
|
527
|
+
lineBoxCount: node.lineBoxes.length,
|
|
528
|
+
noteAllocations: node.noteAllocations.map(toPublicNoteAllocation),
|
|
529
|
+
};
|
|
530
|
+
void graph; // reserved for future cross-page derivations
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function toPublicResolvedPageStories(
|
|
534
|
+
stories: ResolvedPageStories,
|
|
535
|
+
): PublicResolvedPageStories {
|
|
536
|
+
return {
|
|
537
|
+
...(stories.header ? { header: { ...stories.header } } : {}),
|
|
538
|
+
...(stories.footer ? { footer: { ...stories.footer } } : {}),
|
|
539
|
+
isFirstPage: stories.isFirstPage,
|
|
540
|
+
isEvenPage: stories.isEvenPage,
|
|
541
|
+
displayPageNumber: stories.displayPageNumber,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function toPublicPageRegions(regions: RuntimePageRegions): PublicPageRegions {
|
|
546
|
+
return {
|
|
547
|
+
body: toPublicPageRegion(regions.body),
|
|
548
|
+
...(regions.header ? { header: toPublicPageRegion(regions.header) } : {}),
|
|
549
|
+
...(regions.footer ? { footer: toPublicPageRegion(regions.footer) } : {}),
|
|
550
|
+
...(regions.columns ? { columns: regions.columns.map(toPublicPageRegion) } : {}),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function toPublicPageRegion(region: RuntimePageRegion): PublicPageRegion {
|
|
555
|
+
return {
|
|
556
|
+
kind: region.kind,
|
|
557
|
+
originTwips: region.originTwips,
|
|
558
|
+
widthTwips: region.widthTwips,
|
|
559
|
+
heightTwips: region.heightTwips,
|
|
560
|
+
fragmentCount: region.fragmentIds.length,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function toPublicBlockFragment(
|
|
565
|
+
fragment: RuntimeBlockFragment,
|
|
566
|
+
graph: RuntimePageGraph,
|
|
567
|
+
): PublicBlockFragment {
|
|
568
|
+
const page = graph.pages.find((p) => p.pageId === fragment.pageId);
|
|
569
|
+
return {
|
|
570
|
+
fragmentId: fragment.fragmentId,
|
|
571
|
+
blockId: fragment.blockId,
|
|
572
|
+
pageId: fragment.pageId,
|
|
573
|
+
pageIndex: page?.pageIndex ?? -1,
|
|
574
|
+
regionKind: fragment.regionKind,
|
|
575
|
+
from: fragment.from,
|
|
576
|
+
to: fragment.to,
|
|
577
|
+
heightTwips: fragment.heightTwips,
|
|
578
|
+
orderInRegion: fragment.orderInRegion,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function toPublicLineBox(box: RuntimeLineBox): PublicLineBox {
|
|
583
|
+
return {
|
|
584
|
+
fragmentId: box.fragmentId,
|
|
585
|
+
lineIndex: box.lineIndex,
|
|
586
|
+
baselineTwips: box.baselineTwips,
|
|
587
|
+
heightTwips: box.heightTwips,
|
|
588
|
+
widthTwips: box.widthTwips,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function toPublicNoteAllocation(note: RuntimeNoteAllocation): PublicNoteAllocation {
|
|
593
|
+
return {
|
|
594
|
+
noteKind: note.noteKind,
|
|
595
|
+
noteId: note.noteId,
|
|
596
|
+
reservedHeightTwips: note.reservedHeightTwips,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function toPublicSectionNode(
|
|
601
|
+
graph: RuntimePageGraph,
|
|
602
|
+
sectionIndex: number,
|
|
603
|
+
): PublicSectionNode | null {
|
|
604
|
+
const section = graph.sections.find((s) => s.index === sectionIndex);
|
|
605
|
+
if (!section) return null;
|
|
606
|
+
const sectionPages = graph.pages.filter(
|
|
607
|
+
(p) => p.sectionIndex === sectionIndex,
|
|
608
|
+
);
|
|
609
|
+
const firstPage = sectionPages[0];
|
|
610
|
+
const lastPage = sectionPages[sectionPages.length - 1];
|
|
611
|
+
if (!firstPage || !lastPage) return null;
|
|
612
|
+
return {
|
|
613
|
+
sectionIndex,
|
|
614
|
+
startOffset: section.start,
|
|
615
|
+
endOffset: section.end,
|
|
616
|
+
firstPageIndex: firstPage.pageIndex,
|
|
617
|
+
lastPageIndex: lastPage.pageIndex,
|
|
618
|
+
pageCount: sectionPages.length,
|
|
619
|
+
layout: { ...firstPage.layout },
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function toPublicParagraphFormatting(
|
|
624
|
+
blockId: string,
|
|
625
|
+
formatting: ResolvedParagraphFormatting,
|
|
626
|
+
): PublicResolvedParagraphFormatting {
|
|
627
|
+
return {
|
|
628
|
+
blockId,
|
|
629
|
+
spacingBefore: formatting.spacingBefore,
|
|
630
|
+
spacingAfter: formatting.spacingAfter,
|
|
631
|
+
lineHeight: formatting.lineHeight,
|
|
632
|
+
lineRule: formatting.lineRule,
|
|
633
|
+
indentLeft: formatting.indentLeft,
|
|
634
|
+
indentRight: formatting.indentRight,
|
|
635
|
+
firstLineIndent: formatting.firstLineIndent,
|
|
636
|
+
hangingIndent: formatting.hangingIndent,
|
|
637
|
+
fontSizeHalfPoints: formatting.fontSizeHalfPoints,
|
|
638
|
+
averageCharWidthTwips: formatting.averageCharWidthTwips,
|
|
639
|
+
tabStops: formatting.tabStops.map((tab) => ({
|
|
640
|
+
positionTwips: tab.position,
|
|
641
|
+
alignment: tab.align,
|
|
642
|
+
...(tab.leader ? { leader: tab.leader } : {}),
|
|
643
|
+
})),
|
|
644
|
+
keepNext: formatting.keepNext,
|
|
645
|
+
keepLines: formatting.keepLines,
|
|
646
|
+
pageBreakBefore: formatting.pageBreakBefore,
|
|
647
|
+
widowControl: formatting.widowControl,
|
|
648
|
+
contextualSpacing: formatting.contextualSpacing,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function toPublicRunFormatting(
|
|
653
|
+
runId: string,
|
|
654
|
+
blockId: string,
|
|
655
|
+
run: ResolvedRunFormatting,
|
|
656
|
+
): PublicResolvedRunFormatting {
|
|
657
|
+
return {
|
|
658
|
+
runId,
|
|
659
|
+
blockId,
|
|
660
|
+
...(run.fontFamily ? { fontFamily: run.fontFamily } : {}),
|
|
661
|
+
...(run.fontSizeHalfPoints !== undefined
|
|
662
|
+
? { fontSizeHalfPoints: run.fontSizeHalfPoints }
|
|
663
|
+
: {}),
|
|
664
|
+
bold: run.bold,
|
|
665
|
+
italic: run.italic,
|
|
666
|
+
underline: run.underline,
|
|
667
|
+
strikethrough: run.strikethrough,
|
|
668
|
+
...(run.color ? { color: run.color } : {}),
|
|
669
|
+
...(run.highlight ? { highlight: run.highlight } : {}),
|
|
670
|
+
verticalAlign: run.verticalAlign,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function toFacetEvent(event: LayoutEngineEvent): LayoutFacetEvent | null {
|
|
675
|
+
switch (event.kind) {
|
|
676
|
+
case "layout_recomputed":
|
|
677
|
+
return {
|
|
678
|
+
kind: "layout_recomputed",
|
|
679
|
+
revision: event.revision,
|
|
680
|
+
...(event.reason ? { reason: event.reason } : {}),
|
|
681
|
+
};
|
|
682
|
+
case "page_count_changed":
|
|
683
|
+
return {
|
|
684
|
+
kind: "page_count_changed",
|
|
685
|
+
previous: event.previousPageCount ?? 0,
|
|
686
|
+
current: event.currentPageCount ?? 0,
|
|
687
|
+
revision: event.revision,
|
|
688
|
+
};
|
|
689
|
+
case "page_field_dirtied":
|
|
690
|
+
return {
|
|
691
|
+
kind: "page_field_dirtied",
|
|
692
|
+
families: event.dirtyFieldFamilies ?? [],
|
|
693
|
+
revision: event.revision,
|
|
694
|
+
};
|
|
695
|
+
case "measurement_backend_ready":
|
|
696
|
+
return {
|
|
697
|
+
kind: "measurement_backend_ready",
|
|
698
|
+
fidelity:
|
|
699
|
+
(event.fidelity as PublicMeasurementFidelity | undefined) ?? "empirical",
|
|
700
|
+
revision: event.revision,
|
|
701
|
+
};
|
|
702
|
+
default:
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|