@beyondwork/docx-react-component 1.0.36 → 1.0.38
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 +402 -1
- package/src/core/commands/index.ts +18 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- 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 +328 -50
- package/src/io/export/serialize-numbering.ts +114 -24
- package/src/io/export/serialize-tables.ts +87 -11
- package/src/io/export/table-properties-xml.ts +174 -20
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +20 -0
- package/src/io/ooxml/parse-footnotes.ts +62 -1
- package/src/io/ooxml/parse-headers-footers.ts +62 -1
- package/src/io/ooxml/parse-main-document.ts +158 -1
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +45 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +2 -306
- package/src/runtime/document-runtime.ts +287 -11
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +233 -0
- package/src/runtime/layout/inert-layout-facet.ts +59 -0
- package/src/runtime/layout/layout-engine-instance.ts +628 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/margin-preset-catalog.ts +178 -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-format-catalog.ts +233 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +452 -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 +921 -0
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +1398 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +99 -15
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +501 -0
- package/src/ui/headless/scoped-chrome-policy.ts +183 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
- 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 +176 -6
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +505 -144
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-format catalog — named page sizes that back the section page-size
|
|
3
|
+
* picker and the real-dimension page frame.
|
|
4
|
+
*
|
|
5
|
+
* The canonical storage of page geometry remains
|
|
6
|
+
* `SectionProperties.pageSize` in twips. This catalog adds a layer of
|
|
7
|
+
* semantic naming on top so the UI can render "A4" or "US Letter" and
|
|
8
|
+
* the render kernel can pick a sensible default per locale.
|
|
9
|
+
*
|
|
10
|
+
* Unit reference (OOXML):
|
|
11
|
+
* - 1 inch = 1440 twips
|
|
12
|
+
* - 1 mm = 56.6929 twips
|
|
13
|
+
*
|
|
14
|
+
* The catalog deliberately ships a single default match tolerance (1 twip)
|
|
15
|
+
* so legacy documents whose page sizes were round-tripped through third-
|
|
16
|
+
* party tools still identify as the named format they were authored at.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Public types
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export type PageFormatId =
|
|
24
|
+
| "letter"
|
|
25
|
+
| "legal"
|
|
26
|
+
| "tabloid"
|
|
27
|
+
| "executive"
|
|
28
|
+
| "a3"
|
|
29
|
+
| "a4"
|
|
30
|
+
| "a5"
|
|
31
|
+
| "b4-iso"
|
|
32
|
+
| "b5-iso"
|
|
33
|
+
| "custom";
|
|
34
|
+
|
|
35
|
+
export type PageFormatRegion = "us" | "iso" | "jp" | "custom";
|
|
36
|
+
|
|
37
|
+
export type PageFormatLocaleDefault = "en-us" | "en-gb" | "eu" | "jp";
|
|
38
|
+
|
|
39
|
+
export interface PageFormatDisplay {
|
|
40
|
+
inches?: string;
|
|
41
|
+
millimeters?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PageFormatDefinition {
|
|
45
|
+
/** Stable identifier. */
|
|
46
|
+
id: PageFormatId;
|
|
47
|
+
/** Human-facing label used in pickers. */
|
|
48
|
+
label: string;
|
|
49
|
+
/** Locale or region hint. */
|
|
50
|
+
region: PageFormatRegion;
|
|
51
|
+
/** Width in twips at portrait orientation. */
|
|
52
|
+
portraitWidthTwips: number;
|
|
53
|
+
/** Height in twips at portrait orientation. */
|
|
54
|
+
portraitHeightTwips: number;
|
|
55
|
+
/** Default-for-locale hint so consumers can pick a sensible default. */
|
|
56
|
+
localeDefault?: PageFormatLocaleDefault;
|
|
57
|
+
/** Pre-computed inches/millimeters strings for the picker. */
|
|
58
|
+
display: PageFormatDisplay;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ActivePageFormat {
|
|
62
|
+
sectionIndex: number;
|
|
63
|
+
format: PageFormatDefinition;
|
|
64
|
+
orientation: "portrait" | "landscape";
|
|
65
|
+
/** True when the section matches a catalog format within the match tolerance. */
|
|
66
|
+
matchesCatalog: boolean;
|
|
67
|
+
/** When matchesCatalog is false, the raw twips (custom format). */
|
|
68
|
+
customTwips?: { width: number; height: number };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Catalog
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pre-defined named page formats. Ordering intentionally puts the most
|
|
77
|
+
* common sizes (Letter, A4) first because UI dropdowns render in list order.
|
|
78
|
+
*/
|
|
79
|
+
export const PAGE_FORMAT_CATALOG: readonly PageFormatDefinition[] = Object.freeze([
|
|
80
|
+
{
|
|
81
|
+
id: "letter",
|
|
82
|
+
label: "US Letter",
|
|
83
|
+
region: "us",
|
|
84
|
+
portraitWidthTwips: 12240,
|
|
85
|
+
portraitHeightTwips: 15840,
|
|
86
|
+
localeDefault: "en-us",
|
|
87
|
+
display: { inches: "8.5 × 11 in", millimeters: "215.9 × 279.4 mm" },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "a4",
|
|
91
|
+
label: "A4",
|
|
92
|
+
region: "iso",
|
|
93
|
+
portraitWidthTwips: 11906,
|
|
94
|
+
portraitHeightTwips: 16838,
|
|
95
|
+
localeDefault: "eu",
|
|
96
|
+
display: { inches: "8.27 × 11.69 in", millimeters: "210 × 297 mm" },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "legal",
|
|
100
|
+
label: "US Legal",
|
|
101
|
+
region: "us",
|
|
102
|
+
portraitWidthTwips: 12240,
|
|
103
|
+
portraitHeightTwips: 20160,
|
|
104
|
+
display: { inches: "8.5 × 14 in", millimeters: "215.9 × 355.6 mm" },
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "tabloid",
|
|
108
|
+
label: "Tabloid / Ledger",
|
|
109
|
+
region: "us",
|
|
110
|
+
portraitWidthTwips: 15840,
|
|
111
|
+
portraitHeightTwips: 24480,
|
|
112
|
+
display: { inches: "11 × 17 in", millimeters: "279.4 × 431.8 mm" },
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "executive",
|
|
116
|
+
label: "Executive",
|
|
117
|
+
region: "us",
|
|
118
|
+
portraitWidthTwips: 10440,
|
|
119
|
+
portraitHeightTwips: 15120,
|
|
120
|
+
display: { inches: "7.25 × 10.5 in", millimeters: "184.1 × 266.7 mm" },
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "a3",
|
|
124
|
+
label: "A3",
|
|
125
|
+
region: "iso",
|
|
126
|
+
portraitWidthTwips: 16838,
|
|
127
|
+
portraitHeightTwips: 23811,
|
|
128
|
+
display: { inches: "11.69 × 16.54 in", millimeters: "297 × 420 mm" },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "a5",
|
|
132
|
+
label: "A5",
|
|
133
|
+
region: "iso",
|
|
134
|
+
portraitWidthTwips: 8391,
|
|
135
|
+
portraitHeightTwips: 11906,
|
|
136
|
+
display: { inches: "5.83 × 8.27 in", millimeters: "148 × 210 mm" },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "b4-iso",
|
|
140
|
+
label: "B4 (ISO)",
|
|
141
|
+
region: "iso",
|
|
142
|
+
portraitWidthTwips: 14173,
|
|
143
|
+
portraitHeightTwips: 20016,
|
|
144
|
+
display: { inches: "9.84 × 13.90 in", millimeters: "250 × 353 mm" },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "b5-iso",
|
|
148
|
+
label: "B5 (ISO)",
|
|
149
|
+
region: "iso",
|
|
150
|
+
portraitWidthTwips: 9977,
|
|
151
|
+
portraitHeightTwips: 14173,
|
|
152
|
+
display: { inches: "6.93 × 9.84 in", millimeters: "176 × 250 mm" },
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "custom",
|
|
156
|
+
label: "Custom",
|
|
157
|
+
region: "custom",
|
|
158
|
+
portraitWidthTwips: 0,
|
|
159
|
+
portraitHeightTwips: 0,
|
|
160
|
+
display: {},
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Match tolerance in twips. Historic round-trips through third-party tools
|
|
166
|
+
* sometimes drift by sub-twip amounts; a 1-twip window catches those while
|
|
167
|
+
* keeping intentionally custom sizes genuinely custom.
|
|
168
|
+
*/
|
|
169
|
+
const MATCH_TOLERANCE_TWIPS = 1;
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Matching
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
export interface MatchPageFormatInput {
|
|
176
|
+
sectionIndex: number;
|
|
177
|
+
widthTwips: number;
|
|
178
|
+
heightTwips: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Match a section's page dimensions against the catalog.
|
|
183
|
+
*
|
|
184
|
+
* The matcher tolerates either orientation — if the supplied (width, height)
|
|
185
|
+
* match a catalog entry rotated by 90°, the active format is reported as
|
|
186
|
+
* `landscape`. Custom sizes fall through to the `custom` entry.
|
|
187
|
+
*/
|
|
188
|
+
export function matchPageFormat(input: MatchPageFormatInput): ActivePageFormat {
|
|
189
|
+
const { sectionIndex, widthTwips, heightTwips } = input;
|
|
190
|
+
|
|
191
|
+
for (const format of PAGE_FORMAT_CATALOG) {
|
|
192
|
+
if (format.id === "custom") continue;
|
|
193
|
+
|
|
194
|
+
const portraitMatch =
|
|
195
|
+
near(widthTwips, format.portraitWidthTwips) &&
|
|
196
|
+
near(heightTwips, format.portraitHeightTwips);
|
|
197
|
+
const landscapeMatch =
|
|
198
|
+
near(widthTwips, format.portraitHeightTwips) &&
|
|
199
|
+
near(heightTwips, format.portraitWidthTwips);
|
|
200
|
+
|
|
201
|
+
if (portraitMatch || landscapeMatch) {
|
|
202
|
+
return {
|
|
203
|
+
sectionIndex,
|
|
204
|
+
format,
|
|
205
|
+
orientation: portraitMatch ? "portrait" : "landscape",
|
|
206
|
+
matchesCatalog: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const custom = PAGE_FORMAT_CATALOG.find((f) => f.id === "custom")!;
|
|
212
|
+
return {
|
|
213
|
+
sectionIndex,
|
|
214
|
+
format: custom,
|
|
215
|
+
orientation: widthTwips > heightTwips ? "landscape" : "portrait",
|
|
216
|
+
matchesCatalog: false,
|
|
217
|
+
customTwips: { width: widthTwips, height: heightTwips },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function near(a: number, b: number): boolean {
|
|
222
|
+
return Math.abs(a - b) <= MATCH_TOLERANCE_TWIPS;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Look up a format by id. Returns the `custom` entry for unknown ids so
|
|
227
|
+
* callers can chain `.format` reads without null-checking.
|
|
228
|
+
*/
|
|
229
|
+
export function getPageFormatById(id: string): PageFormatDefinition {
|
|
230
|
+
const found = PAGE_FORMAT_CATALOG.find((f) => f.id === id);
|
|
231
|
+
if (found) return found;
|
|
232
|
+
return PAGE_FORMAT_CATALOG.find((f) => f.id === "custom")!;
|
|
233
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageFragmentMapper — offset ↔ fragment ↔ region mapping.
|
|
3
|
+
*
|
|
4
|
+
* Per runtime-owned-paginated-layout-engine.md §6, this is the missing layer
|
|
5
|
+
* between `pm-position-map.ts` and `DocumentNavigationSnapshot`:
|
|
6
|
+
*
|
|
7
|
+
* runtime position → page fragment → visible page region
|
|
8
|
+
*
|
|
9
|
+
* The mapper is a pure query service over a RuntimePageGraph. It does not
|
|
10
|
+
* mutate state and does not consult DOM or PM.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { EditorStoryTarget, SelectionSnapshot } from "../../api/public-types";
|
|
14
|
+
import type {
|
|
15
|
+
RuntimeBlockFragment,
|
|
16
|
+
RuntimePageGraph,
|
|
17
|
+
RuntimePageNode,
|
|
18
|
+
} from "./page-graph.ts";
|
|
19
|
+
|
|
20
|
+
export interface PageFragmentLocation {
|
|
21
|
+
pageId: string;
|
|
22
|
+
pageIndex: number;
|
|
23
|
+
fragmentId: string;
|
|
24
|
+
regionKind: "body" | "header" | "footer" | "column" | "footnote-area";
|
|
25
|
+
/** Zero-based index of the line box within the fragment, if line boxes are available. */
|
|
26
|
+
lineBoxIndex?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PageSpan {
|
|
30
|
+
firstPageIndex: number;
|
|
31
|
+
lastPageIndex: number;
|
|
32
|
+
pageCount: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PageFragmentMapper {
|
|
36
|
+
mapOffsetToFragment(offset: number, story?: EditorStoryTarget): PageFragmentLocation | null;
|
|
37
|
+
mapFragmentToOffsetRange(fragmentId: string): { from: number; to: number } | null;
|
|
38
|
+
mapSelectionToPageSpan(selection: SelectionSnapshot): PageSpan | null;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a page-local anchor for the given offset. Returns the offset
|
|
41
|
+
* unchanged today; retained as a seam for future semantic anchors.
|
|
42
|
+
*/
|
|
43
|
+
getAnchorForOffset(offset: number, story?: EditorStoryTarget): { offset: number; pageId: string } | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createPageFragmentMapper(graph: RuntimePageGraph): PageFragmentMapper {
|
|
47
|
+
const fragmentsById = new Map<string, RuntimeBlockFragment>();
|
|
48
|
+
for (const fragment of graph.fragments) {
|
|
49
|
+
fragmentsById.set(fragment.fragmentId, fragment);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pagesById = new Map<string, RuntimePageNode>();
|
|
53
|
+
for (const page of graph.pages) {
|
|
54
|
+
pagesById.set(page.pageId, page);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findFragmentAt(offset: number): RuntimeBlockFragment | undefined {
|
|
58
|
+
// Fragments sort naturally by page then order; a linear scan is O(n) but
|
|
59
|
+
// page counts are small (legal docs rarely exceed a few hundred pages).
|
|
60
|
+
for (const fragment of graph.fragments) {
|
|
61
|
+
if (offset >= fragment.from && offset < fragment.to) {
|
|
62
|
+
return fragment;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// fall back to the last fragment that starts at or before the offset
|
|
66
|
+
let best: RuntimeBlockFragment | undefined;
|
|
67
|
+
for (const fragment of graph.fragments) {
|
|
68
|
+
if (fragment.from <= offset) {
|
|
69
|
+
if (!best || fragment.from > best.from) best = fragment;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return best;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
mapOffsetToFragment(offset, _story) {
|
|
77
|
+
const fragment = findFragmentAt(offset);
|
|
78
|
+
if (!fragment) {
|
|
79
|
+
// Fallback to coarse page lookup
|
|
80
|
+
const page = findPageForOffsetInternal(graph, offset);
|
|
81
|
+
if (!page) return null;
|
|
82
|
+
return {
|
|
83
|
+
pageId: page.pageId,
|
|
84
|
+
pageIndex: page.pageIndex,
|
|
85
|
+
fragmentId: `${page.pageId}-synthetic`,
|
|
86
|
+
regionKind: "body",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const page = pagesById.get(fragment.pageId);
|
|
90
|
+
if (!page) return null;
|
|
91
|
+
|
|
92
|
+
// Locate a line box if any
|
|
93
|
+
let lineBoxIndex: number | undefined;
|
|
94
|
+
for (let i = 0; i < page.lineBoxes.length; i += 1) {
|
|
95
|
+
if (page.lineBoxes[i]!.fragmentId === fragment.fragmentId) {
|
|
96
|
+
lineBoxIndex = i;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
pageId: page.pageId,
|
|
103
|
+
pageIndex: page.pageIndex,
|
|
104
|
+
fragmentId: fragment.fragmentId,
|
|
105
|
+
regionKind: fragment.regionKind,
|
|
106
|
+
...(lineBoxIndex !== undefined ? { lineBoxIndex } : {}),
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
mapFragmentToOffsetRange(fragmentId) {
|
|
111
|
+
const fragment = fragmentsById.get(fragmentId);
|
|
112
|
+
if (!fragment) return null;
|
|
113
|
+
return { from: fragment.from, to: fragment.to };
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
mapSelectionToPageSpan(selection) {
|
|
117
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
118
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
119
|
+
const firstPage = findPageForOffsetInternal(graph, from);
|
|
120
|
+
const lastPage = findPageForOffsetInternal(graph, to);
|
|
121
|
+
if (!firstPage || !lastPage) return null;
|
|
122
|
+
return {
|
|
123
|
+
firstPageIndex: firstPage.pageIndex,
|
|
124
|
+
lastPageIndex: lastPage.pageIndex,
|
|
125
|
+
pageCount: Math.max(1, lastPage.pageIndex - firstPage.pageIndex + 1),
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
getAnchorForOffset(offset, _story) {
|
|
130
|
+
const page = findPageForOffsetInternal(graph, offset);
|
|
131
|
+
if (!page) return null;
|
|
132
|
+
return { offset, pageId: page.pageId };
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function findPageForOffsetInternal(
|
|
138
|
+
graph: RuntimePageGraph,
|
|
139
|
+
offset: number,
|
|
140
|
+
): RuntimePageNode | undefined {
|
|
141
|
+
for (const page of graph.pages) {
|
|
142
|
+
if (!page.isBlankFiller && offset < page.endOffset) {
|
|
143
|
+
return page;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return graph.pages[graph.pages.length - 1];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Rebuild the mapper for a spliced graph.
|
|
151
|
+
*
|
|
152
|
+
* Correctness baseline: the simplest safe implementation is to fully rebuild
|
|
153
|
+
* the mapper from the new graph. The `prior` and `firstDirtyPage` arguments
|
|
154
|
+
* are accepted for forward compatibility with a future optimization that
|
|
155
|
+
* retains fragment-id indexes for unaffected pages. Today rebuilding is
|
|
156
|
+
* cheap (linear in fragment count) so the naive path is the right trade-off.
|
|
157
|
+
*
|
|
158
|
+
* Renamed from `updateMapperIncremental` (§6 E.5) now that the body is an
|
|
159
|
+
* unconditional full rebuild — the old name implied an incremental strategy
|
|
160
|
+
* that never actually existed in the shipped runtime.
|
|
161
|
+
*/
|
|
162
|
+
export function rebuildMapper(
|
|
163
|
+
prior: PageFragmentMapper,
|
|
164
|
+
splicedGraph: RuntimePageGraph,
|
|
165
|
+
firstDirtyPage: number,
|
|
166
|
+
): PageFragmentMapper {
|
|
167
|
+
// Parameters are intentionally unused by the current implementation;
|
|
168
|
+
// retained as a seam for a future bounded update path.
|
|
169
|
+
void prior;
|
|
170
|
+
void firstDirtyPage;
|
|
171
|
+
return createPageFragmentMapper(splicedGraph);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @deprecated Renamed to `rebuildMapper` in §6 E.5. Retained as a
|
|
176
|
+
* one-release compatibility alias so external consumers are not broken by
|
|
177
|
+
* the rename; schedule removal after the next published API pin refresh.
|
|
178
|
+
*/
|
|
179
|
+
export const updateMapperIncremental = rebuildMapper;
|