@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
|
@@ -372,6 +372,17 @@ export interface ParagraphNode {
|
|
|
372
372
|
bidi?: boolean;
|
|
373
373
|
suppressLineNumbers?: boolean;
|
|
374
374
|
cnfStyle?: string;
|
|
375
|
+
/**
|
|
376
|
+
* Preserved w14 extension identifiers for this paragraph.
|
|
377
|
+
* Round-trip (§2 A.7) requires these to survive import → export so the
|
|
378
|
+
* `w14:paraId` / `w14:textId` attributes that Word places on paragraph
|
|
379
|
+
* and run boundaries are re-emitted with the same values. Both ids are
|
|
380
|
+
* 8-hex uppercase strings per ECMA-376 Part 1 Appendix A.
|
|
381
|
+
*/
|
|
382
|
+
wordExtensionIds?: {
|
|
383
|
+
paraId?: string;
|
|
384
|
+
textId?: string;
|
|
385
|
+
};
|
|
375
386
|
children: InlineNode[];
|
|
376
387
|
}
|
|
377
388
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ScopeTagTouch } from "../../api/public-types.ts";
|
|
2
|
+
import type {
|
|
3
|
+
CanonicalAnchor,
|
|
4
|
+
CommentThread,
|
|
5
|
+
RevisionRecord,
|
|
6
|
+
} from "../../model/canonical-document.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Diff prior vs next review state and emit one `ScopeTagTouch` per changed
|
|
10
|
+
* comment or revision anchor. The predicted-text lane consumes these touches
|
|
11
|
+
* to classify a text command's ack as `equivalent` (no touches), `adjusted`
|
|
12
|
+
* (anchors shifted), or to feed decoration redraws without a PM rebuild.
|
|
13
|
+
*
|
|
14
|
+
* Notes
|
|
15
|
+
* - Newly created annotations show up as "extended" (revisions) or "split"
|
|
16
|
+
* (comments) so the lane knows to redraw them.
|
|
17
|
+
* - Detached annotations show up as "detached".
|
|
18
|
+
* - Anchors whose range is byte-identical across the diff are omitted.
|
|
19
|
+
*/
|
|
20
|
+
export function collectScopeTagTouches(
|
|
21
|
+
priorComments: Readonly<Record<string, CommentThread>>,
|
|
22
|
+
nextComments: Readonly<Record<string, CommentThread>>,
|
|
23
|
+
priorRevisions: Readonly<Record<string, RevisionRecord>>,
|
|
24
|
+
nextRevisions: Readonly<Record<string, RevisionRecord>>,
|
|
25
|
+
): ScopeTagTouch[] {
|
|
26
|
+
const touches: ScopeTagTouch[] = [];
|
|
27
|
+
|
|
28
|
+
for (const [id, prior] of Object.entries(priorComments)) {
|
|
29
|
+
const next = nextComments[id];
|
|
30
|
+
if (!next) {
|
|
31
|
+
touches.push({
|
|
32
|
+
tagType: "comment",
|
|
33
|
+
tagId: id,
|
|
34
|
+
behavior: "detached",
|
|
35
|
+
range: anchorRange(prior.anchor),
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const behavior = classifyAnchorChange(prior.anchor, next.anchor);
|
|
40
|
+
if (behavior !== "unchanged") {
|
|
41
|
+
touches.push({
|
|
42
|
+
tagType: "comment",
|
|
43
|
+
tagId: id,
|
|
44
|
+
behavior,
|
|
45
|
+
range: anchorRange(next.anchor),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const [id, next] of Object.entries(nextComments)) {
|
|
51
|
+
if (!priorComments[id]) {
|
|
52
|
+
touches.push({
|
|
53
|
+
tagType: "comment",
|
|
54
|
+
tagId: id,
|
|
55
|
+
behavior: "split",
|
|
56
|
+
range: anchorRange(next.anchor),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const [id, prior] of Object.entries(priorRevisions)) {
|
|
62
|
+
const next = nextRevisions[id];
|
|
63
|
+
if (!next) {
|
|
64
|
+
touches.push({
|
|
65
|
+
tagType: "revision",
|
|
66
|
+
tagId: id,
|
|
67
|
+
behavior: "detached",
|
|
68
|
+
range: anchorRange(prior.anchor),
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const behavior = classifyAnchorChange(prior.anchor, next.anchor);
|
|
73
|
+
if (behavior !== "unchanged") {
|
|
74
|
+
touches.push({
|
|
75
|
+
tagType: "revision",
|
|
76
|
+
tagId: id,
|
|
77
|
+
behavior,
|
|
78
|
+
range: anchorRange(next.anchor),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const [id, next] of Object.entries(nextRevisions)) {
|
|
84
|
+
if (!priorRevisions[id]) {
|
|
85
|
+
touches.push({
|
|
86
|
+
tagType: "revision",
|
|
87
|
+
tagId: id,
|
|
88
|
+
behavior: "extended",
|
|
89
|
+
range: anchorRange(next.anchor),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return touches;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function anchorRange(anchor: CanonicalAnchor): { from: number; to: number } {
|
|
98
|
+
if (anchor.kind === "range") {
|
|
99
|
+
return { from: anchor.range.from, to: anchor.range.to };
|
|
100
|
+
}
|
|
101
|
+
if (anchor.kind === "node") {
|
|
102
|
+
return { from: anchor.at, to: anchor.at };
|
|
103
|
+
}
|
|
104
|
+
return { from: anchor.lastKnownRange.from, to: anchor.lastKnownRange.to };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function classifyAnchorChange(
|
|
108
|
+
prior: CanonicalAnchor,
|
|
109
|
+
next: CanonicalAnchor,
|
|
110
|
+
): "unchanged" | "extended" | "trimmed" | "split" | "detached" {
|
|
111
|
+
if (prior.kind === "detached" && next.kind === "detached") {
|
|
112
|
+
const pr = prior.lastKnownRange;
|
|
113
|
+
const nr = next.lastKnownRange;
|
|
114
|
+
return pr.from === nr.from && pr.to === nr.to ? "unchanged" : "detached";
|
|
115
|
+
}
|
|
116
|
+
if (prior.kind !== "detached" && next.kind === "detached") {
|
|
117
|
+
return "detached";
|
|
118
|
+
}
|
|
119
|
+
if (prior.kind === "detached" && next.kind !== "detached") {
|
|
120
|
+
return "extended";
|
|
121
|
+
}
|
|
122
|
+
const priorR = anchorRange(prior);
|
|
123
|
+
const nextR = anchorRange(next);
|
|
124
|
+
if (priorR.from === nextR.from && priorR.to === nextR.to) return "unchanged";
|
|
125
|
+
const priorLen = priorR.to - priorR.from;
|
|
126
|
+
const nextLen = nextR.to - nextR.from;
|
|
127
|
+
if (nextLen > priorLen) return "extended";
|
|
128
|
+
if (nextLen < priorLen) return "trimmed";
|
|
129
|
+
return "split";
|
|
130
|
+
}
|
|
@@ -17,34 +17,20 @@ import type {
|
|
|
17
17
|
EditorStoryTarget,
|
|
18
18
|
EditorSurfaceSnapshot,
|
|
19
19
|
SurfaceBlockSnapshot,
|
|
20
|
-
SurfaceInlineSegment,
|
|
21
20
|
} from "../api/public-types";
|
|
22
|
-
import type {
|
|
23
|
-
FootnoteCollection,
|
|
24
|
-
} from "../model/canonical-document.ts";
|
|
25
21
|
import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
|
|
26
22
|
import { createSelectionSnapshot } from "../core/state/editor-state.ts";
|
|
27
23
|
import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
|
|
28
24
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
29
25
|
import {
|
|
30
|
-
estimateBlockHeight,
|
|
31
|
-
estimateParagraphHeight,
|
|
32
|
-
getUsableColumnMetrics,
|
|
33
|
-
getUsableColumnWidth,
|
|
34
|
-
getUsablePageHeight,
|
|
35
|
-
} from "./page-layout-estimation.ts";
|
|
36
|
-
import {
|
|
37
|
-
buildPageLayoutSnapshot,
|
|
38
26
|
buildResolvedSections,
|
|
39
27
|
findSectionForPosition as resolveSectionForPosition,
|
|
40
28
|
resolveSectionForStoryTarget,
|
|
41
29
|
type ResolvedDocumentSection,
|
|
42
30
|
} from "./document-layout.ts";
|
|
31
|
+
import { buildPageStack } from "./layout/paginated-layout-engine.ts";
|
|
43
32
|
import { findNoteReferencePosition } from "./view-state.ts";
|
|
44
33
|
|
|
45
|
-
const MIN_BLOCK_HEIGHT_TWIPS = 240;
|
|
46
|
-
const FOOTNOTE_REFERENCE_RESERVATION_TWIPS = 180;
|
|
47
|
-
|
|
48
34
|
interface NavigationBaseSnapshot {
|
|
49
35
|
mainSurface: EditorSurfaceSnapshot;
|
|
50
36
|
sections: ResolvedDocumentSection[];
|
|
@@ -152,186 +138,6 @@ export function findSectionForOffset(
|
|
|
152
138
|
return sections.length > 0 ? resolveSectionForPosition(sections, offset).index : 0;
|
|
153
139
|
}
|
|
154
140
|
|
|
155
|
-
function buildPageStack(
|
|
156
|
-
document: CanonicalDocumentEnvelope,
|
|
157
|
-
sections: ResolvedDocumentSection[],
|
|
158
|
-
mainSurface: EditorSurfaceSnapshot,
|
|
159
|
-
): DocumentPageSnapshot[] {
|
|
160
|
-
const pages: DocumentPageSnapshot[] = [];
|
|
161
|
-
let globalPageIndex = 0;
|
|
162
|
-
|
|
163
|
-
for (const section of sections) {
|
|
164
|
-
const layout = buildPageLayoutSnapshot(
|
|
165
|
-
section.index,
|
|
166
|
-
section.properties ?? document.subParts?.finalSectionProperties,
|
|
167
|
-
document.subParts,
|
|
168
|
-
);
|
|
169
|
-
const sectionBlocks = collectSectionBlocks(mainSurface.blocks, section.start, section.end);
|
|
170
|
-
const paginated = paginateSectionBlocks(
|
|
171
|
-
section,
|
|
172
|
-
sectionBlocks,
|
|
173
|
-
layout,
|
|
174
|
-
document.subParts?.footnoteCollection,
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
for (const page of paginated) {
|
|
178
|
-
pages.push({
|
|
179
|
-
...page,
|
|
180
|
-
pageIndex: globalPageIndex,
|
|
181
|
-
});
|
|
182
|
-
globalPageIndex += 1;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Guarantee at least one page
|
|
187
|
-
if (pages.length === 0) {
|
|
188
|
-
pages.push({
|
|
189
|
-
pageIndex: 0,
|
|
190
|
-
sectionIndex: 0,
|
|
191
|
-
pageInSection: 0,
|
|
192
|
-
startOffset: 0,
|
|
193
|
-
endOffset: mainSurface.storySize,
|
|
194
|
-
layout: buildPageLayoutSnapshot(
|
|
195
|
-
0,
|
|
196
|
-
document.subParts?.finalSectionProperties,
|
|
197
|
-
document.subParts,
|
|
198
|
-
),
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return pages;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function collectSectionBlocks(
|
|
206
|
-
blocks: readonly SurfaceBlockSnapshot[],
|
|
207
|
-
start: number,
|
|
208
|
-
end: number,
|
|
209
|
-
): SurfaceBlockSnapshot[] {
|
|
210
|
-
return blocks.filter((block) => block.to > start && block.from < end);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function paginateSectionBlocks(
|
|
214
|
-
section: ResolvedDocumentSection,
|
|
215
|
-
blocks: readonly SurfaceBlockSnapshot[],
|
|
216
|
-
layout: DocumentPageSnapshot["layout"],
|
|
217
|
-
footnotes: FootnoteCollection | undefined,
|
|
218
|
-
): Omit<DocumentPageSnapshot, "pageIndex">[] {
|
|
219
|
-
if (blocks.length === 0) {
|
|
220
|
-
return [
|
|
221
|
-
{
|
|
222
|
-
sectionIndex: section.index,
|
|
223
|
-
pageInSection: 0,
|
|
224
|
-
startOffset: section.start,
|
|
225
|
-
endOffset: section.end,
|
|
226
|
-
layout,
|
|
227
|
-
},
|
|
228
|
-
];
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const pages: Omit<DocumentPageSnapshot, "pageIndex">[] = [];
|
|
232
|
-
const usableHeight = getUsablePageHeight(layout);
|
|
233
|
-
const columnMetrics = getUsableColumnMetrics(layout);
|
|
234
|
-
const maxColumns = Math.max(1, columnMetrics.length);
|
|
235
|
-
let pageStart = section.start;
|
|
236
|
-
let columnHeight = 0;
|
|
237
|
-
let columnIndex = 0;
|
|
238
|
-
let pageInSection = 0;
|
|
239
|
-
let reservedNoteHeight = 0;
|
|
240
|
-
const reservedNotes = new Set<string>();
|
|
241
|
-
|
|
242
|
-
const pushPage = (endOffset: number): void => {
|
|
243
|
-
const boundedEnd = Math.max(pageStart, Math.min(endOffset, section.end));
|
|
244
|
-
if (boundedEnd === pageStart && pages.length > 0) {
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
pages.push({
|
|
248
|
-
sectionIndex: section.index,
|
|
249
|
-
pageInSection,
|
|
250
|
-
startOffset: pageStart,
|
|
251
|
-
endOffset: boundedEnd,
|
|
252
|
-
layout,
|
|
253
|
-
});
|
|
254
|
-
pageInSection += 1;
|
|
255
|
-
pageStart = boundedEnd;
|
|
256
|
-
columnHeight = 0;
|
|
257
|
-
columnIndex = 0;
|
|
258
|
-
reservedNoteHeight = 0;
|
|
259
|
-
reservedNotes.clear();
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
for (let index = 0; index < blocks.length; index += 1) {
|
|
263
|
-
const block = blocks[index]!;
|
|
264
|
-
const nextBoundary = blocks[index + 1]?.from ?? section.end;
|
|
265
|
-
while (true) {
|
|
266
|
-
const columnWidth =
|
|
267
|
-
columnMetrics[Math.min(columnIndex, columnMetrics.length - 1)]?.width ??
|
|
268
|
-
getUsableColumnWidth(layout);
|
|
269
|
-
const baseHeight = estimateBlockHeight(block, columnWidth);
|
|
270
|
-
const keepWithNextHeight =
|
|
271
|
-
block.kind === "paragraph" && block.keepNext
|
|
272
|
-
? baseHeight + estimateBlockHeight(blocks[index + 1], columnWidth)
|
|
273
|
-
: baseHeight;
|
|
274
|
-
const noteHeight = estimateFootnoteReservation(block, footnotes, columnWidth, reservedNotes);
|
|
275
|
-
const projectedHeight = columnHeight + keepWithNextHeight + reservedNoteHeight + noteHeight;
|
|
276
|
-
|
|
277
|
-
if (block.kind === "paragraph" && block.pageBreakBefore && pageStart < block.from) {
|
|
278
|
-
pushPage(block.from);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
if (projectedHeight > usableHeight && pageStart < block.from) {
|
|
282
|
-
if (columnIndex < maxColumns - 1) {
|
|
283
|
-
columnIndex += 1;
|
|
284
|
-
columnHeight = 0;
|
|
285
|
-
reservedNoteHeight = 0;
|
|
286
|
-
reservedNotes.clear();
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
pushPage(block.from);
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const effectiveNoteHeight = estimateFootnoteReservation(
|
|
294
|
-
block,
|
|
295
|
-
footnotes,
|
|
296
|
-
columnWidth,
|
|
297
|
-
reservedNotes,
|
|
298
|
-
);
|
|
299
|
-
columnHeight += baseHeight;
|
|
300
|
-
reservedNoteHeight += effectiveNoteHeight;
|
|
301
|
-
currentPageNoteIds(block).forEach((noteKey) => reservedNotes.add(noteKey));
|
|
302
|
-
|
|
303
|
-
if (hasColumnBreak(block)) {
|
|
304
|
-
if (columnIndex < maxColumns - 1) {
|
|
305
|
-
columnIndex += 1;
|
|
306
|
-
columnHeight = 0;
|
|
307
|
-
reservedNoteHeight = 0;
|
|
308
|
-
reservedNotes.clear();
|
|
309
|
-
} else {
|
|
310
|
-
pushPage(nextBoundary);
|
|
311
|
-
}
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (index === blocks.length - 1) {
|
|
316
|
-
pushPage(section.end);
|
|
317
|
-
}
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return pages.length > 0
|
|
323
|
-
? pages
|
|
324
|
-
: [
|
|
325
|
-
{
|
|
326
|
-
sectionIndex: section.index,
|
|
327
|
-
pageInSection: 0,
|
|
328
|
-
startOffset: section.start,
|
|
329
|
-
endOffset: section.end,
|
|
330
|
-
layout,
|
|
331
|
-
},
|
|
332
|
-
];
|
|
333
|
-
}
|
|
334
|
-
|
|
335
141
|
function resolveActiveNavigationContext(
|
|
336
142
|
document: CanonicalDocumentEnvelope,
|
|
337
143
|
pages: readonly DocumentPageSnapshot[],
|
|
@@ -383,116 +189,6 @@ function findFirstPageIndexForSection(
|
|
|
383
189
|
return match >= 0 ? match : 0;
|
|
384
190
|
}
|
|
385
191
|
|
|
386
|
-
function estimateFootnoteReservation(
|
|
387
|
-
block: SurfaceBlockSnapshot,
|
|
388
|
-
footnotes: FootnoteCollection | undefined,
|
|
389
|
-
columnWidth: number,
|
|
390
|
-
reservedNotes: ReadonlySet<string>,
|
|
391
|
-
): number {
|
|
392
|
-
if (!footnotes || block.kind !== "paragraph") {
|
|
393
|
-
return 0;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
let reservation = 0;
|
|
397
|
-
for (const noteKey of currentPageNoteIds(block)) {
|
|
398
|
-
if (reservedNotes.has(noteKey)) {
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const [noteKind, noteId] = noteKey.split(":");
|
|
403
|
-
const noteCollection =
|
|
404
|
-
noteKind === "endnote" ? footnotes.endnotes : footnotes.footnotes;
|
|
405
|
-
const note = noteCollection[noteId];
|
|
406
|
-
reservation += FOOTNOTE_REFERENCE_RESERVATION_TWIPS;
|
|
407
|
-
if (note) {
|
|
408
|
-
reservation += note.blocks.reduce(
|
|
409
|
-
(total, child) => total + estimateCanonicalNoteBlockHeight(child, columnWidth),
|
|
410
|
-
0,
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return reservation;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function estimateCanonicalNoteBlockHeight(
|
|
419
|
-
block: FootnoteCollection["footnotes"][string]["blocks"][number],
|
|
420
|
-
columnWidth: number,
|
|
421
|
-
): number {
|
|
422
|
-
switch (block.type) {
|
|
423
|
-
case "paragraph":
|
|
424
|
-
return estimateParagraphHeight(
|
|
425
|
-
{
|
|
426
|
-
blockId: "note",
|
|
427
|
-
kind: "paragraph",
|
|
428
|
-
from: 0,
|
|
429
|
-
to: 0,
|
|
430
|
-
...(block.styleId ? { styleId: block.styleId } : {}),
|
|
431
|
-
segments: createEstimatedNoteSegments(block.children),
|
|
432
|
-
},
|
|
433
|
-
columnWidth,
|
|
434
|
-
);
|
|
435
|
-
case "table":
|
|
436
|
-
return MIN_BLOCK_HEIGHT_TWIPS * Math.max(1, block.rows.length);
|
|
437
|
-
default:
|
|
438
|
-
return MIN_BLOCK_HEIGHT_TWIPS;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function createEstimatedNoteSegments(
|
|
443
|
-
children: Extract<FootnoteCollection["footnotes"][string]["blocks"][number], { type: "paragraph" }>["children"],
|
|
444
|
-
): SurfaceInlineSegment[] {
|
|
445
|
-
const segments: SurfaceInlineSegment[] = [];
|
|
446
|
-
|
|
447
|
-
children.forEach((child, index) => {
|
|
448
|
-
if (child.type === "text") {
|
|
449
|
-
segments.push({
|
|
450
|
-
segmentId: `note-${index}`,
|
|
451
|
-
kind: "text",
|
|
452
|
-
from: 0,
|
|
453
|
-
to: Array.from(child.text).length,
|
|
454
|
-
text: child.text,
|
|
455
|
-
});
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (child.type === "hard_break" || child.type === "tab") {
|
|
460
|
-
segments.push({
|
|
461
|
-
segmentId: `note-${index}`,
|
|
462
|
-
kind: child.type,
|
|
463
|
-
from: 0,
|
|
464
|
-
to: 1,
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
return segments;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function currentPageNoteIds(
|
|
473
|
-
block: SurfaceBlockSnapshot,
|
|
474
|
-
): Set<string> {
|
|
475
|
-
const notes = new Set<string>();
|
|
476
|
-
if (block.kind !== "paragraph") {
|
|
477
|
-
return notes;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
for (const segment of block.segments) {
|
|
481
|
-
if (segment.kind === "note_ref" && segment.noteId) {
|
|
482
|
-
notes.add(`${segment.noteKind}:${segment.noteId}`);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return notes;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function hasColumnBreak(block: SurfaceBlockSnapshot): boolean {
|
|
489
|
-
return block.kind === "paragraph" && block.segments.some(
|
|
490
|
-
(segment) =>
|
|
491
|
-
segment.kind === "opaque_inline" &&
|
|
492
|
-
segment.label === "Column break",
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
192
|
// ---------------------------------------------------------------------------
|
|
497
193
|
// Heading outline
|
|
498
194
|
// ---------------------------------------------------------------------------
|