@beyondwork/docx-react-component 1.0.102 → 1.0.104
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 +1 -1
- package/src/api/public-types.ts +63 -1
- package/src/api/v3/_runtime-handle.ts +2 -0
- package/src/api/v3/ai/outline.ts +2 -7
- package/src/api/v3/runtime/geometry.ts +79 -0
- package/src/core/commands/formatting-commands.ts +8 -7
- package/src/core/commands/paragraph-layout-commands.ts +11 -10
- package/src/core/commands/section-layout-commands.ts +7 -6
- package/src/core/commands/style-commands.ts +3 -2
- package/src/io/normalize/normalize-text.ts +6 -5
- package/src/io/ooxml/parse-anchor.ts +15 -15
- package/src/io/ooxml/parse-drawing.ts +103 -5
- package/src/io/ooxml/parse-fields.ts +43 -21
- package/src/io/ooxml/parse-font-table.ts +2 -1
- package/src/io/ooxml/parse-footnotes.ts +3 -2
- package/src/io/ooxml/parse-headers-footers.ts +7 -6
- package/src/io/ooxml/parse-main-document.ts +41 -40
- package/src/io/ooxml/parse-numbering.ts +3 -2
- package/src/io/ooxml/parse-object.ts +6 -6
- package/src/io/ooxml/parse-paragraph-formatting.ts +12 -11
- package/src/io/ooxml/parse-picture.ts +16 -16
- package/src/io/ooxml/parse-run-formatting.ts +11 -10
- package/src/io/ooxml/parse-settings.ts +2 -1
- package/src/io/ooxml/parse-shapes.ts +148 -17
- package/src/io/ooxml/parse-styles.ts +16 -16
- package/src/io/ooxml/parse-theme.ts +5 -4
- package/src/model/canonical-document.ts +869 -836
- package/src/model/canonical-layout-inputs.ts +979 -0
- package/src/model/layout/index.ts +6 -0
- package/src/model/layout/page-graph-types.ts +61 -0
- package/src/model/layout/runtime-page-graph-types.ts +10 -0
- package/src/runtime/collab/runtime-collab-sync.ts +3 -3
- package/src/runtime/debug/build-debug-inspector-snapshot.ts +17 -4
- package/src/runtime/document-runtime.ts +30 -14
- package/src/runtime/event-refresh-hints.ts +3 -0
- package/src/runtime/formatting/document-lookup.ts +3 -2
- package/src/runtime/formatting/formatting-context.ts +176 -34
- package/src/runtime/formatting/index.ts +20 -0
- package/src/runtime/formatting/layout-inputs.ts +320 -0
- package/src/runtime/formatting/numbering/geometry.ts +13 -12
- package/src/runtime/formatting/style-cascade.ts +2 -1
- package/src/runtime/formatting/table-style-resolver.ts +8 -7
- package/src/runtime/geometry/caret-geometry.ts +82 -10
- package/src/runtime/geometry/geometry-facet.ts +36 -0
- package/src/runtime/geometry/geometry-index.ts +891 -0
- package/src/runtime/geometry/geometry-types.ts +221 -1
- package/src/runtime/geometry/index.ts +26 -0
- package/src/runtime/geometry/inert-geometry-facet.ts +3 -0
- package/src/runtime/geometry/replacement-envelope.ts +41 -2
- package/src/runtime/layout/layout-engine-version.ts +16 -1
- package/src/runtime/layout/page-graph.ts +191 -1
- package/src/runtime/prerender/graph-canonicalize.ts +30 -0
- package/src/runtime/surface-projection.ts +74 -39
- package/src/runtime/workflow/coordinator.ts +57 -11
- package/src/session/import/normalize.ts +2 -1
- package/src/session/import/source-package-evidence.ts +612 -1
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +3 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CanonicalParagraphFormatting,
|
|
3
|
+
CanonicalRunFormatting,
|
|
4
|
+
FieldFamily,
|
|
5
|
+
FieldRefreshStatus,
|
|
6
|
+
FieldRegistry,
|
|
7
|
+
FieldRegistryEntry,
|
|
8
|
+
TabStop,
|
|
9
|
+
} from "../../model/canonical-document.ts";
|
|
10
|
+
import {
|
|
11
|
+
MAIN_STORY_KEY,
|
|
12
|
+
type CanonicalFieldRegionIdentity,
|
|
13
|
+
} from "../../model/canonical-layout-inputs.ts";
|
|
14
|
+
import type { ResolvedField } from "./field/resolver.ts";
|
|
15
|
+
import type {
|
|
16
|
+
EffectiveParagraphFormatting,
|
|
17
|
+
EffectiveRunFormatting,
|
|
18
|
+
EffectiveTableFormatting,
|
|
19
|
+
RevisionDisplayFlags,
|
|
20
|
+
} from "./formatting-types.ts";
|
|
21
|
+
import type { NumberingPrefixResult } from "./numbering/prefix.ts";
|
|
22
|
+
|
|
23
|
+
export type LayoutMarkerSuffix = "tab" | "space" | "none";
|
|
24
|
+
|
|
25
|
+
export interface LayoutTabStopInput {
|
|
26
|
+
readonly positionTwips: number;
|
|
27
|
+
readonly align: TabStop["align"];
|
|
28
|
+
readonly leader?: TabStop["leader"];
|
|
29
|
+
readonly source: "paragraph" | "numbering" | "toc";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface NumberingLayoutInput {
|
|
33
|
+
readonly markerText: string | null;
|
|
34
|
+
readonly markerRunFormatting?: CanonicalRunFormatting;
|
|
35
|
+
readonly markerSuffix: LayoutMarkerSuffix;
|
|
36
|
+
readonly markerLaneStartTwips?: number;
|
|
37
|
+
readonly markerLaneWidthTwips?: number;
|
|
38
|
+
readonly textColumnStartTwips?: number;
|
|
39
|
+
readonly hangingTwips?: number;
|
|
40
|
+
readonly associatedTabStops: readonly LayoutTabStopInput[];
|
|
41
|
+
readonly level: number;
|
|
42
|
+
readonly format: string;
|
|
43
|
+
readonly startAt: number;
|
|
44
|
+
readonly isLegalNumbering?: boolean;
|
|
45
|
+
readonly pictureBulletMediaId?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FieldLayoutInput {
|
|
49
|
+
readonly fieldId: string;
|
|
50
|
+
readonly regionId: string;
|
|
51
|
+
readonly regionKind: "field" | "toc-region";
|
|
52
|
+
readonly storyKey: string;
|
|
53
|
+
readonly fieldIndex: number;
|
|
54
|
+
readonly paragraphIndex: number;
|
|
55
|
+
readonly family: FieldFamily;
|
|
56
|
+
readonly instruction: string;
|
|
57
|
+
readonly cachedText: string;
|
|
58
|
+
readonly switches?: FieldRegistryEntry["switches"];
|
|
59
|
+
readonly targetBookmark?: string;
|
|
60
|
+
readonly refreshState: FieldRefreshStatus | "unresolved";
|
|
61
|
+
readonly resolvedText?: string;
|
|
62
|
+
readonly asHyperlink?: boolean;
|
|
63
|
+
readonly tocRegionId?: string;
|
|
64
|
+
readonly tocEntryCount?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RevisionLayoutPosture {
|
|
68
|
+
readonly mode: "clean" | "simple" | "all" | "original" | "no-markup";
|
|
69
|
+
/**
|
|
70
|
+
* True only when the active revision display changes measured text
|
|
71
|
+
* for the paragraph. Paint-only redline posture is compositor data,
|
|
72
|
+
* not an L04 measurement input.
|
|
73
|
+
*/
|
|
74
|
+
readonly affectsMeasuredText: boolean;
|
|
75
|
+
readonly hiddenRevisionIds: readonly string[];
|
|
76
|
+
readonly visibleRevisionIds: readonly string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface EffectiveLayoutFormatting {
|
|
80
|
+
readonly paragraph?: EffectiveParagraphFormatting;
|
|
81
|
+
readonly runs: readonly EffectiveRunFormatting[];
|
|
82
|
+
readonly table?: EffectiveTableFormatting;
|
|
83
|
+
readonly cell?: {
|
|
84
|
+
readonly paragraph: CanonicalParagraphFormatting;
|
|
85
|
+
readonly run: CanonicalRunFormatting;
|
|
86
|
+
};
|
|
87
|
+
readonly numbering?: NumberingLayoutInput;
|
|
88
|
+
readonly tabs: readonly LayoutTabStopInput[];
|
|
89
|
+
readonly compatFlags: readonly string[];
|
|
90
|
+
readonly revisionPosture?: RevisionLayoutPosture;
|
|
91
|
+
readonly structuralHash: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function normalizeNumberingMarkerSuffix(
|
|
95
|
+
suffix: NumberingPrefixResult["suffix"] | undefined,
|
|
96
|
+
): LayoutMarkerSuffix {
|
|
97
|
+
if (suffix === "tab" || suffix === "space") return suffix;
|
|
98
|
+
return "none";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function toLayoutTabStops(
|
|
102
|
+
tabStops: readonly TabStop[] | undefined,
|
|
103
|
+
source: LayoutTabStopInput["source"],
|
|
104
|
+
): readonly LayoutTabStopInput[] {
|
|
105
|
+
if (!tabStops || tabStops.length === 0) return [];
|
|
106
|
+
return tabStops
|
|
107
|
+
.map((tabStop) => ({
|
|
108
|
+
positionTwips: tabStop.position,
|
|
109
|
+
align: tabStop.align,
|
|
110
|
+
...(tabStop.leader ? { leader: tabStop.leader } : {}),
|
|
111
|
+
source,
|
|
112
|
+
}))
|
|
113
|
+
.sort((a, b) => a.positionTwips - b.positionTwips);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function mergeLayoutTabStops(
|
|
117
|
+
...groups: readonly (readonly LayoutTabStopInput[] | undefined)[]
|
|
118
|
+
): readonly LayoutTabStopInput[] {
|
|
119
|
+
const byKey = new Map<string, LayoutTabStopInput>();
|
|
120
|
+
for (const group of groups) {
|
|
121
|
+
for (const tabStop of group ?? []) {
|
|
122
|
+
const key = [
|
|
123
|
+
tabStop.source,
|
|
124
|
+
tabStop.positionTwips,
|
|
125
|
+
tabStop.align,
|
|
126
|
+
tabStop.leader ?? "",
|
|
127
|
+
].join(":");
|
|
128
|
+
byKey.set(key, tabStop);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...byKey.values()].sort((a, b) => {
|
|
132
|
+
if (a.positionTwips !== b.positionTwips) return a.positionTwips - b.positionTwips;
|
|
133
|
+
return a.source.localeCompare(b.source);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function toNumberingLayoutInput(
|
|
138
|
+
numbering: NumberingPrefixResult | null | undefined,
|
|
139
|
+
): NumberingLayoutInput | undefined {
|
|
140
|
+
if (!numbering) return undefined;
|
|
141
|
+
const markerLane = numbering.geometry.markerLane;
|
|
142
|
+
const textColumn = numbering.geometry.textColumn;
|
|
143
|
+
const hangingTwips =
|
|
144
|
+
textColumn?.hanging ??
|
|
145
|
+
numbering.geometry.indentation?.hanging ??
|
|
146
|
+
(typeof numbering.geometry.indentation?.firstLine === "number" &&
|
|
147
|
+
numbering.geometry.indentation.firstLine < 0
|
|
148
|
+
? Math.abs(numbering.geometry.indentation.firstLine)
|
|
149
|
+
: undefined);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
markerText: numbering.text,
|
|
153
|
+
...(numbering.markerRunProperties
|
|
154
|
+
? { markerRunFormatting: numbering.markerRunProperties }
|
|
155
|
+
: {}),
|
|
156
|
+
markerSuffix: normalizeNumberingMarkerSuffix(numbering.suffix),
|
|
157
|
+
...(markerLane ? { markerLaneStartTwips: markerLane.start } : {}),
|
|
158
|
+
...(markerLane ? { markerLaneWidthTwips: markerLane.width } : {}),
|
|
159
|
+
...(textColumn ? { textColumnStartTwips: textColumn.start } : {}),
|
|
160
|
+
...(hangingTwips !== undefined ? { hangingTwips } : {}),
|
|
161
|
+
associatedTabStops: toLayoutTabStops(numbering.geometry.tabStops, "numbering"),
|
|
162
|
+
level: numbering.level,
|
|
163
|
+
format: numbering.format,
|
|
164
|
+
startAt: numbering.startAt,
|
|
165
|
+
...(numbering.isLegalNumbering ? { isLegalNumbering: true } : {}),
|
|
166
|
+
...(numbering.picBulletMediaId
|
|
167
|
+
? { pictureBulletMediaId: numbering.picBulletMediaId }
|
|
168
|
+
: {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function toFieldLayoutInput(
|
|
173
|
+
entry: FieldRegistryEntry,
|
|
174
|
+
resolved?: ResolvedField,
|
|
175
|
+
region?: CanonicalFieldRegionIdentity,
|
|
176
|
+
): FieldLayoutInput {
|
|
177
|
+
return {
|
|
178
|
+
fieldId: `field-${entry.fieldIndex}`,
|
|
179
|
+
regionId: region?.regionId ?? `field:${entry.fieldIndex}`,
|
|
180
|
+
regionKind: region?.kind ?? "field",
|
|
181
|
+
storyKey: region?.storyKey ?? entry.storyKey ?? MAIN_STORY_KEY,
|
|
182
|
+
fieldIndex: entry.fieldIndex,
|
|
183
|
+
paragraphIndex: region?.paragraphIndex ?? entry.paragraphIndex,
|
|
184
|
+
family: entry.fieldFamily,
|
|
185
|
+
instruction: entry.instruction,
|
|
186
|
+
cachedText: entry.displayText,
|
|
187
|
+
...(entry.switches ? { switches: entry.switches } : {}),
|
|
188
|
+
...(entry.fieldTarget ? { targetBookmark: entry.fieldTarget } : {}),
|
|
189
|
+
refreshState: resolved?.refreshStatus ?? entry.refreshStatus ?? "unresolved",
|
|
190
|
+
...(resolved?.displayText !== undefined ? { resolvedText: resolved.displayText } : {}),
|
|
191
|
+
...(resolved?.asHyperlink ? { asHyperlink: true } : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function collectFieldLayoutInputs(
|
|
196
|
+
registry: FieldRegistry | undefined,
|
|
197
|
+
resolve?: (entry: FieldRegistryEntry) => ResolvedField | undefined,
|
|
198
|
+
regions: readonly CanonicalFieldRegionIdentity[] = [],
|
|
199
|
+
): readonly FieldLayoutInput[] {
|
|
200
|
+
if (!registry) return [];
|
|
201
|
+
const inputs: FieldLayoutInput[] = [];
|
|
202
|
+
const entries = [...registry.supported, ...registry.preserveOnly];
|
|
203
|
+
const fieldRegions = new Map(
|
|
204
|
+
regions
|
|
205
|
+
.filter((region) => region.kind === "field")
|
|
206
|
+
.map((region) => [region.fieldIndex, region]),
|
|
207
|
+
);
|
|
208
|
+
const tocRegions = new Map(
|
|
209
|
+
regions
|
|
210
|
+
.filter((region) => region.kind === "toc-region" && region.tocId !== undefined)
|
|
211
|
+
.map((region) => [region.tocId!, region]),
|
|
212
|
+
);
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
inputs.push(toFieldLayoutInput(entry, resolve?.(entry), fieldRegions.get(entry.fieldIndex)));
|
|
215
|
+
}
|
|
216
|
+
for (const region of registry.tocRegions ?? []) {
|
|
217
|
+
const source = registry.supported.find(
|
|
218
|
+
(entry) => entry.fieldIndex === region.sourceFieldIndex,
|
|
219
|
+
);
|
|
220
|
+
if (!source) continue;
|
|
221
|
+
const identity = tocRegions.get(region.tocId);
|
|
222
|
+
inputs.push({
|
|
223
|
+
...toFieldLayoutInput(source, resolve?.(source), identity),
|
|
224
|
+
fieldId: `toc-${region.tocId}`,
|
|
225
|
+
family: "TOC",
|
|
226
|
+
instruction: region.instruction.raw,
|
|
227
|
+
cachedText: region.cachedEntries.map((entry) => entry.displayText).join("\n"),
|
|
228
|
+
refreshState: region.status === "current" ? "current" : "stale",
|
|
229
|
+
tocRegionId: region.tocId,
|
|
230
|
+
tocEntryCount: region.cachedEntries.length + region.generatedEntries.length,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return inputs;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function buildRevisionLayoutPosture(
|
|
237
|
+
mode: RevisionLayoutPosture["mode"],
|
|
238
|
+
displays: readonly RevisionDisplayFlags[] = [],
|
|
239
|
+
): RevisionLayoutPosture {
|
|
240
|
+
const hiddenRevisionIds = displays
|
|
241
|
+
.filter((display) => display.hidden === true)
|
|
242
|
+
.map((display) => display.revisionId);
|
|
243
|
+
const visibleRevisionIds = displays
|
|
244
|
+
.filter((display) => display.hidden !== true)
|
|
245
|
+
.map((display) => display.revisionId);
|
|
246
|
+
return {
|
|
247
|
+
mode,
|
|
248
|
+
affectsMeasuredText: hiddenRevisionIds.length > 0,
|
|
249
|
+
hiddenRevisionIds,
|
|
250
|
+
visibleRevisionIds,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function toMeasuredRevisionLayoutPosture(
|
|
255
|
+
posture: RevisionLayoutPosture | undefined,
|
|
256
|
+
): RevisionLayoutPosture | undefined {
|
|
257
|
+
if (!posture?.affectsMeasuredText || posture.hiddenRevisionIds.length === 0) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...posture,
|
|
262
|
+
visibleRevisionIds: [],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function buildEffectiveLayoutFormatting(input: {
|
|
267
|
+
readonly paragraph?: EffectiveParagraphFormatting;
|
|
268
|
+
readonly runs?: readonly EffectiveRunFormatting[];
|
|
269
|
+
readonly table?: EffectiveTableFormatting;
|
|
270
|
+
readonly cell?: {
|
|
271
|
+
readonly paragraph: CanonicalParagraphFormatting;
|
|
272
|
+
readonly run: CanonicalRunFormatting;
|
|
273
|
+
};
|
|
274
|
+
readonly numbering?: NumberingLayoutInput;
|
|
275
|
+
readonly tabs?: readonly LayoutTabStopInput[];
|
|
276
|
+
readonly compatFlags?: readonly string[];
|
|
277
|
+
readonly revisionPosture?: RevisionLayoutPosture;
|
|
278
|
+
}): EffectiveLayoutFormatting {
|
|
279
|
+
const revisionPosture = toMeasuredRevisionLayoutPosture(input.revisionPosture);
|
|
280
|
+
const withoutHash = {
|
|
281
|
+
...(input.paragraph ? { paragraph: input.paragraph } : {}),
|
|
282
|
+
runs: input.runs ?? [],
|
|
283
|
+
...(input.table ? { table: input.table } : {}),
|
|
284
|
+
...(input.cell ? { cell: input.cell } : {}),
|
|
285
|
+
...(input.numbering ? { numbering: input.numbering } : {}),
|
|
286
|
+
tabs: input.tabs ?? [],
|
|
287
|
+
compatFlags: input.compatFlags ?? [],
|
|
288
|
+
...(revisionPosture ? { revisionPosture } : {}),
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
...withoutHash,
|
|
292
|
+
structuralHash: createStructuralHash(withoutHash),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function createStructuralHash(value: unknown): string {
|
|
297
|
+
const stable = stableStringify(value);
|
|
298
|
+
let hash = 0x811c9dc5;
|
|
299
|
+
for (let i = 0; i < stable.length; i += 1) {
|
|
300
|
+
hash ^= stable.charCodeAt(i);
|
|
301
|
+
hash = Math.imul(hash, 0x01000193);
|
|
302
|
+
}
|
|
303
|
+
return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function stableStringify(value: unknown): string {
|
|
307
|
+
if (value === null || typeof value !== "object") {
|
|
308
|
+
return JSON.stringify(value);
|
|
309
|
+
}
|
|
310
|
+
if (Array.isArray(value)) {
|
|
311
|
+
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
312
|
+
}
|
|
313
|
+
const record = value as Record<string, unknown>;
|
|
314
|
+
const keys = Object.keys(record)
|
|
315
|
+
.filter((key) => record[key] !== undefined)
|
|
316
|
+
.sort();
|
|
317
|
+
return `{${keys
|
|
318
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
319
|
+
.join(",")}}`;
|
|
320
|
+
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ParagraphNode,
|
|
9
9
|
ParagraphSpacing,
|
|
10
10
|
TabStop,
|
|
11
|
+
Mutable,
|
|
11
12
|
} from "../../../model/canonical-document.ts";
|
|
12
13
|
|
|
13
14
|
export const DEFAULT_NUMBERING_START_AT = 1;
|
|
@@ -52,7 +53,7 @@ export interface ResolvedNumberingDefinitionSet {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export function resolveNumberingDefinitionSet(
|
|
55
|
-
catalog: NumberingCatalog
|
|
56
|
+
catalog: Mutable<NumberingCatalog>,
|
|
56
57
|
numbering: ParagraphNode["numbering"] | undefined,
|
|
57
58
|
paragraph?: Pick<ParagraphNode, "spacing" | "indentation" | "tabStops">,
|
|
58
59
|
): ResolvedNumberingDefinitionSet | null {
|
|
@@ -145,7 +146,7 @@ function mergeLevelDefinition(
|
|
|
145
146
|
};
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
function withDefaultStartAt(level: NumberingLevelDefinition): NumberingLevelDefinition {
|
|
149
|
+
function withDefaultStartAt(level: Mutable<NumberingLevelDefinition>): NumberingLevelDefinition {
|
|
149
150
|
return {
|
|
150
151
|
...level,
|
|
151
152
|
startAt: level.startAt ?? DEFAULT_NUMBERING_START_AT,
|
|
@@ -156,7 +157,7 @@ function withDefaultStartAt(level: NumberingLevelDefinition): NumberingLevelDefi
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
function cloneLevelParagraphGeometry(
|
|
159
|
-
geometry: NumberingLevelParagraphGeometry
|
|
160
|
+
geometry: Mutable<NumberingLevelParagraphGeometry>,
|
|
160
161
|
): NumberingLevelParagraphGeometry {
|
|
161
162
|
return {
|
|
162
163
|
...(geometry.justification ? { justification: geometry.justification } : {}),
|
|
@@ -244,7 +245,7 @@ function mergeParagraphSpacing(
|
|
|
244
245
|
return undefined;
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
const spacing: ParagraphSpacing = {
|
|
248
|
+
const spacing: Mutable<ParagraphSpacing> = {
|
|
248
249
|
...(base ?? {}),
|
|
249
250
|
...(override ?? {}),
|
|
250
251
|
};
|
|
@@ -260,24 +261,24 @@ function mergeParagraphIndentation(
|
|
|
260
261
|
return undefined;
|
|
261
262
|
}
|
|
262
263
|
|
|
263
|
-
const out: ParagraphIndentation = {};
|
|
264
|
+
const out: Mutable<ParagraphIndentation> = {};
|
|
264
265
|
|
|
265
266
|
const left = override?.left ?? base?.left;
|
|
266
|
-
if (left !== undefined) out.left = left;
|
|
267
|
+
if (left !== undefined) (out as Mutable<typeof out>).left = left;
|
|
267
268
|
|
|
268
269
|
const right = override?.right ?? base?.right;
|
|
269
|
-
if (right !== undefined) out.right = right;
|
|
270
|
+
if (right !== undefined) (out as Mutable<typeof out>).right = right;
|
|
270
271
|
|
|
271
272
|
// ECMA-376 §17.3.1.12: firstLine and hanging are mutually exclusive on a single
|
|
272
273
|
// w:ind element. When override specifies either, it wins exclusively.
|
|
273
274
|
const overrideTouchesFLorH =
|
|
274
275
|
override?.firstLine !== undefined || override?.hanging !== undefined;
|
|
275
276
|
if (overrideTouchesFLorH) {
|
|
276
|
-
if (override?.hanging !== undefined) out.hanging = override.hanging;
|
|
277
|
-
else if (override?.firstLine !== undefined) out.firstLine = override.firstLine;
|
|
277
|
+
if (override?.hanging !== undefined) (out as Mutable<typeof out>).hanging = override.hanging;
|
|
278
|
+
else if (override?.firstLine !== undefined) (out as Mutable<typeof out>).firstLine = override.firstLine;
|
|
278
279
|
} else {
|
|
279
|
-
if (base?.hanging !== undefined) out.hanging = base.hanging;
|
|
280
|
-
else if (base?.firstLine !== undefined) out.firstLine = base.firstLine;
|
|
280
|
+
if (base?.hanging !== undefined) (out as Mutable<typeof out>).hanging = base.hanging;
|
|
281
|
+
else if (base?.firstLine !== undefined) (out as Mutable<typeof out>).firstLine = base.firstLine;
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
@@ -333,7 +334,7 @@ function deriveTextColumn(
|
|
|
333
334
|
};
|
|
334
335
|
}
|
|
335
336
|
|
|
336
|
-
function resolveHangingWidth(indentation: ParagraphIndentation): number | undefined {
|
|
337
|
+
function resolveHangingWidth(indentation: Mutable<ParagraphIndentation>): number | undefined {
|
|
337
338
|
if (indentation.hanging !== undefined) {
|
|
338
339
|
return indentation.hanging;
|
|
339
340
|
}
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
StylesCatalog,
|
|
29
29
|
TableStyleConditionalRegion,
|
|
30
30
|
TableStyleDefinition,
|
|
31
|
+
Mutable,
|
|
31
32
|
} from "../../model/canonical-document.ts";
|
|
32
33
|
import {
|
|
33
34
|
resolveCharacterStyleChain,
|
|
@@ -51,7 +52,7 @@ export function mergeParagraph(
|
|
|
51
52
|
over: CanonicalParagraphFormatting | undefined,
|
|
52
53
|
): CanonicalParagraphFormatting | undefined {
|
|
53
54
|
if (!base && !over) return undefined;
|
|
54
|
-
const merged: CanonicalParagraphFormatting = { ...(base ?? {}), ...(over ?? {}) };
|
|
55
|
+
const merged: Mutable<CanonicalParagraphFormatting> = { ...(base ?? {}), ...(over ?? {}) };
|
|
55
56
|
if (base?.spacing || over?.spacing) {
|
|
56
57
|
merged.spacing = { ...(base?.spacing ?? {}), ...(over?.spacing ?? {}) };
|
|
57
58
|
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
TableStyleDefinition,
|
|
11
11
|
TableStyleFormatting,
|
|
12
12
|
TableWidth,
|
|
13
|
+
Mutable,
|
|
13
14
|
} from "../../model/canonical-document.ts";
|
|
14
15
|
|
|
15
16
|
export interface ResolvedTableCellStyle {
|
|
@@ -58,7 +59,7 @@ export interface ResolvedTableStyleResolution {
|
|
|
58
59
|
}>;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
const DEFAULT_EFFECTIVE_TABLE_LOOK: TableLook = {
|
|
62
|
+
const DEFAULT_EFFECTIVE_TABLE_LOOK: Mutable<TableLook> = {
|
|
62
63
|
val: "04A0",
|
|
63
64
|
firstRow: true,
|
|
64
65
|
lastRow: false,
|
|
@@ -80,7 +81,7 @@ const CONDITIONAL_REGION_ORDER: TableStyleConditionalRegion[] = [
|
|
|
80
81
|
];
|
|
81
82
|
|
|
82
83
|
export function resolveTableStyleResolution(
|
|
83
|
-
table: TableNode
|
|
84
|
+
table: Mutable<TableNode>,
|
|
84
85
|
tableStyles: Record<string, TableStyleDefinition>,
|
|
85
86
|
): ResolvedTableStyleResolution {
|
|
86
87
|
const resolvedStyle = table.styleId ? resolveTableStyleDefinition(table.styleId, tableStyles) : {};
|
|
@@ -266,7 +267,7 @@ function decodeTableLookMask(val: string | undefined): TableLook | undefined {
|
|
|
266
267
|
function getActiveRowRegions(
|
|
267
268
|
rowIndex: number,
|
|
268
269
|
rowCount: number,
|
|
269
|
-
effectiveTblLook: TableLook
|
|
270
|
+
effectiveTblLook: Mutable<TableLook>,
|
|
270
271
|
): TableStyleConditionalRegion[] {
|
|
271
272
|
const active = new Set<TableStyleConditionalRegion>();
|
|
272
273
|
|
|
@@ -294,7 +295,7 @@ function getActiveCellRegions(
|
|
|
294
295
|
startColumn: number,
|
|
295
296
|
endColumn: number,
|
|
296
297
|
columnCount: number,
|
|
297
|
-
effectiveTblLook: TableLook
|
|
298
|
+
effectiveTblLook: Mutable<TableLook>,
|
|
298
299
|
): TableStyleConditionalRegion[] {
|
|
299
300
|
const active = new Set<TableStyleConditionalRegion>(rowRegions);
|
|
300
301
|
|
|
@@ -343,7 +344,7 @@ function mergeWholeStyleFormatting(
|
|
|
343
344
|
return undefined;
|
|
344
345
|
}
|
|
345
346
|
|
|
346
|
-
const merged: TableStyleFormatting = {};
|
|
347
|
+
const merged: Mutable<TableStyleFormatting> = {};
|
|
347
348
|
const table = mergeTableFormatting(base?.table, override?.table);
|
|
348
349
|
const row = mergeRowFormatting(base?.row, override?.row);
|
|
349
350
|
const cell = mergeCellFormatting(base?.cell, override?.cell);
|
|
@@ -429,14 +430,14 @@ function mergeBorderMap<T extends TableBorders | TableCellBorders>(
|
|
|
429
430
|
}
|
|
430
431
|
|
|
431
432
|
const sides = ["top", "left", "bottom", "right", "insideH", "insideV"] as const;
|
|
432
|
-
const merged = {} as T
|
|
433
|
+
const merged = {} as Mutable<T>;
|
|
433
434
|
for (const side of sides) {
|
|
434
435
|
const spec = mergePlainObject(base?.[side], override?.[side]) as BorderSpec | undefined;
|
|
435
436
|
if (spec) {
|
|
436
437
|
merged[side] = spec as T[typeof side];
|
|
437
438
|
}
|
|
438
439
|
}
|
|
439
|
-
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
440
|
+
return Object.keys(merged).length > 0 ? (merged as T) : undefined;
|
|
440
441
|
}
|
|
441
442
|
|
|
442
443
|
function mergePlainObject<T extends object>(base: T | undefined, override: T | undefined): T | undefined {
|
|
@@ -37,6 +37,7 @@ import type { EditorStoryTarget } from "../../api/public-types";
|
|
|
37
37
|
import type {
|
|
38
38
|
RenderFrame,
|
|
39
39
|
RenderFrameRect,
|
|
40
|
+
RenderStoryRegion,
|
|
40
41
|
} from "../render/index.ts";
|
|
41
42
|
import type {
|
|
42
43
|
CaretGeometry,
|
|
@@ -86,12 +87,14 @@ export function resolveCaretGeometry(
|
|
|
86
87
|
* Resolve `GeometryRect[]` for a selection range. Slice 4 produces:
|
|
87
88
|
* - an empty list when either endpoint can't be resolved
|
|
88
89
|
* - a single zero-width caret rect when `from === to`
|
|
90
|
+
* - one line rect per covered render line when line ranges are available
|
|
89
91
|
* - a single union rect otherwise (via `frame.anchorIndex.bySelection`)
|
|
90
92
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* keeps chrome surfaces that currently call
|
|
94
|
-
* "runtime-offset", … })[0]` in sync with the
|
|
93
|
+
* The line-aware path still tags rects `within-tolerance`: without per-run
|
|
94
|
+
* anchors we can identify covered lines but not the first/last glyph bounds.
|
|
95
|
+
* The single-rect fallback keeps chrome surfaces that currently call
|
|
96
|
+
* `getAnchorRects({ kind: "runtime-offset", … })[0]` in sync with the
|
|
97
|
+
* selection API when a frame lacks line boxes.
|
|
95
98
|
*/
|
|
96
99
|
export function resolveSelectionRects(
|
|
97
100
|
frame: RenderFrame | null,
|
|
@@ -119,14 +122,11 @@ export function resolveSelectionRects(
|
|
|
119
122
|
return [caret.rect];
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
const lineRects = resolveLineSelectionRects(frame, lo, hi, range.story);
|
|
126
|
+
if (lineRects.length > 0) return lineRects;
|
|
127
|
+
|
|
122
128
|
const union = frame.anchorIndex.bySelection(lo, hi, range.story);
|
|
123
129
|
if (!union) return [];
|
|
124
|
-
// Slice 7b (2026-04-22): the substrate returns ONE union rect for any
|
|
125
|
-
// non-collapsed range. That rect is geometrically correct at the
|
|
126
|
-
// block level but loses per-line detail — tag it `within-tolerance`
|
|
127
|
-
// so callers can gate per-line chrome work. When the render kernel
|
|
128
|
-
// ships per-run anchors (refactor/05 Slice 7 Task 2), this site
|
|
129
|
-
// upgrades to multiple `"exact"` rects automatically.
|
|
130
130
|
return [toGeometryRect(union, "within-tolerance")];
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -151,6 +151,78 @@ function toGeometryRect(
|
|
|
151
151
|
return out;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
function resolveLineSelectionRects(
|
|
155
|
+
frame: RenderFrame,
|
|
156
|
+
from: number,
|
|
157
|
+
to: number,
|
|
158
|
+
story?: EditorStoryTarget,
|
|
159
|
+
): readonly GeometryRect[] {
|
|
160
|
+
if (!Array.isArray(frame.pages)) return [];
|
|
161
|
+
const rects: GeometryRect[] = [];
|
|
162
|
+
for (const page of frame.pages) {
|
|
163
|
+
for (const region of collectRegions(page.regions)) {
|
|
164
|
+
if (!storyMatches(region.storyTarget, story)) continue;
|
|
165
|
+
for (const block of region.blocks) {
|
|
166
|
+
const blockFrom = block.fragment.from;
|
|
167
|
+
const blockTo = block.fragment.to;
|
|
168
|
+
if (blockTo <= from || blockFrom >= to) continue;
|
|
169
|
+
if (block.lines.length === 0) continue;
|
|
170
|
+
const lineCount = block.lines.length;
|
|
171
|
+
const span = Math.max(1, blockTo - blockFrom);
|
|
172
|
+
for (let i = 0; i < lineCount; i += 1) {
|
|
173
|
+
const line = block.lines[i]!;
|
|
174
|
+
const lineFrom =
|
|
175
|
+
blockFrom + Math.floor((span * i) / Math.max(1, lineCount));
|
|
176
|
+
const lineTo =
|
|
177
|
+
i === lineCount - 1
|
|
178
|
+
? blockTo
|
|
179
|
+
: blockFrom +
|
|
180
|
+
Math.floor((span * (i + 1)) / Math.max(1, lineCount));
|
|
181
|
+
if (lineTo <= from || lineFrom >= to) continue;
|
|
182
|
+
rects.push(toGeometryRect(line.frame, "within-tolerance"));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return rects;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function collectRegions(
|
|
191
|
+
regions: RenderFrame["pages"][number]["regions"],
|
|
192
|
+
): readonly RenderStoryRegion[] {
|
|
193
|
+
return [
|
|
194
|
+
regions.body,
|
|
195
|
+
regions.header,
|
|
196
|
+
regions.footer,
|
|
197
|
+
...(regions.columns ?? []),
|
|
198
|
+
regions.footnoteArea,
|
|
199
|
+
...(regions.footnotes ?? []),
|
|
200
|
+
].filter((region): region is RenderStoryRegion => Boolean(region));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function storyMatches(
|
|
204
|
+
actual: EditorStoryTarget,
|
|
205
|
+
expected: EditorStoryTarget | undefined,
|
|
206
|
+
): boolean {
|
|
207
|
+
if (!expected) return true;
|
|
208
|
+
if (actual.kind !== expected.kind) return false;
|
|
209
|
+
switch (expected.kind) {
|
|
210
|
+
case "main":
|
|
211
|
+
return true;
|
|
212
|
+
case "header":
|
|
213
|
+
case "footer":
|
|
214
|
+
return (
|
|
215
|
+
"relationshipId" in actual &&
|
|
216
|
+
actual.relationshipId === expected.relationshipId &&
|
|
217
|
+
actual.variant === expected.variant &&
|
|
218
|
+
actual.sectionIndex === expected.sectionIndex
|
|
219
|
+
);
|
|
220
|
+
case "footnote":
|
|
221
|
+
case "endnote":
|
|
222
|
+
return "noteId" in actual && actual.noteId === expected.noteId;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
154
226
|
/**
|
|
155
227
|
* Approximate the baseline offset within a line rect. The render frame
|
|
156
228
|
* does not yet carry per-anchor baseline metadata, so Slice 4 uses a
|
|
@@ -37,6 +37,8 @@ import type {
|
|
|
37
37
|
BlockGeometry,
|
|
38
38
|
CaretGeometry,
|
|
39
39
|
EnvelopeBundle,
|
|
40
|
+
GeometryIndex,
|
|
41
|
+
GeometryIndexCoverage,
|
|
40
42
|
GeometryRect,
|
|
41
43
|
HitTestPoint,
|
|
42
44
|
HitTestResult,
|
|
@@ -51,6 +53,11 @@ import {
|
|
|
51
53
|
import { resolveHitTest } from "./hit-test.ts";
|
|
52
54
|
import { resolveObjectHandles } from "./object-handles.ts";
|
|
53
55
|
import { resolveAnchorRects } from "./project-anchors.ts";
|
|
56
|
+
import {
|
|
57
|
+
createUnavailableGeometryCoverage,
|
|
58
|
+
projectGeometryIndexFromFrame,
|
|
59
|
+
summarizeGeometryCoverageFromFrame,
|
|
60
|
+
} from "./geometry-index.ts";
|
|
54
61
|
import {
|
|
55
62
|
resolveReplacementEnvelope,
|
|
56
63
|
type ReplacementScope,
|
|
@@ -67,6 +74,21 @@ import { createViewport, type ViewportHandle } from "./viewport.ts";
|
|
|
67
74
|
// ---------------------------------------------------------------------------
|
|
68
75
|
|
|
69
76
|
export interface GeometryFacet {
|
|
77
|
+
// PE2 index / coverage -------------------------------------------------
|
|
78
|
+
/**
|
|
79
|
+
* Renderer-neutral geometry index for the current frame. This is the PE2
|
|
80
|
+
* Slice-1 substrate: pages, regions, slices, lines, anchors, hit targets,
|
|
81
|
+
* and coverage metadata. Returns null when no render kernel/frame is warm.
|
|
82
|
+
*/
|
|
83
|
+
getGeometryIndex(): GeometryIndex | null;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Lightweight coverage summary for evidence/debug consumers. Unlike
|
|
87
|
+
* `getGeometryIndex`, this always returns a value; unwired/pre-paint states
|
|
88
|
+
* report `status: "unavailable"` with zero counts.
|
|
89
|
+
*/
|
|
90
|
+
getGeometryCoverage(): GeometryIndexCoverage;
|
|
91
|
+
|
|
70
92
|
// Hit-test -------------------------------------------------------------
|
|
71
93
|
/**
|
|
72
94
|
* Resolve a point in the shell's overlay coordinate space to a canonical
|
|
@@ -226,6 +248,20 @@ export function createGeometryFacet(
|
|
|
226
248
|
const viewport = input.viewport ?? createViewport();
|
|
227
249
|
|
|
228
250
|
return {
|
|
251
|
+
getGeometryIndex() {
|
|
252
|
+
const kernel = input.renderKernel?.();
|
|
253
|
+
if (!kernel) return null;
|
|
254
|
+
return projectGeometryIndexFromFrame(kernel.getRenderFrame(), {
|
|
255
|
+
canonicalDocument: input.getCanonicalDocument?.() ?? null,
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
getGeometryCoverage() {
|
|
260
|
+
const kernel = input.renderKernel?.();
|
|
261
|
+
if (!kernel) return createUnavailableGeometryCoverage();
|
|
262
|
+
return summarizeGeometryCoverageFromFrame(kernel.getRenderFrame());
|
|
263
|
+
},
|
|
264
|
+
|
|
229
265
|
hitTest(point) {
|
|
230
266
|
// Slice 6 wrapper-deletion (2026-04-22): the layout-facet fallback
|
|
231
267
|
// was deleted. `hitTest` now resolves only through the render-kernel
|