@beyondwork/docx-react-component 1.0.28 → 1.0.30
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 +26 -37
- package/src/api/public-types.ts +531 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/index.ts +201 -79
- package/src/core/commands/table-structure-commands.ts +138 -5
- package/src/core/state/text-transaction.ts +370 -3
- package/src/index.ts +41 -0
- package/src/io/docx-session.ts +318 -25
- package/src/io/export/serialize-footnotes.ts +41 -46
- package/src/io/export/serialize-headers-footers.ts +36 -40
- package/src/io/export/serialize-main-document.ts +55 -89
- package/src/io/export/serialize-numbering.ts +104 -4
- package/src/io/export/serialize-runtime-revisions.ts +196 -2
- package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
- package/src/io/export/table-properties-xml.ts +318 -0
- package/src/io/normalize/normalize-text.ts +34 -3
- package/src/io/ooxml/parse-comments.ts +6 -0
- package/src/io/ooxml/parse-footnotes.ts +69 -13
- package/src/io/ooxml/parse-headers-footers.ts +54 -11
- package/src/io/ooxml/parse-main-document.ts +112 -42
- package/src/io/ooxml/parse-numbering.ts +341 -26
- package/src/io/ooxml/parse-revisions.ts +118 -4
- package/src/io/ooxml/parse-styles.ts +176 -0
- package/src/io/ooxml/parse-tables.ts +34 -25
- package/src/io/ooxml/revision-boundaries.ts +127 -3
- package/src/io/ooxml/workflow-payload.ts +544 -0
- package/src/model/canonical-document.ts +91 -1
- package/src/model/snapshot.ts +112 -1
- package/src/preservation/store.ts +73 -3
- package/src/review/store/comment-store.ts +19 -1
- package/src/review/store/revision-actions.ts +29 -0
- package/src/review/store/revision-store.ts +12 -1
- package/src/review/store/revision-types.ts +11 -0
- package/src/runtime/context-analytics.ts +824 -0
- package/src/runtime/document-locations.ts +521 -0
- package/src/runtime/document-navigation.ts +14 -1
- package/src/runtime/document-outline.ts +440 -0
- package/src/runtime/document-runtime.ts +941 -45
- package/src/runtime/event-refresh-hints.ts +137 -0
- package/src/runtime/numbering-prefix.ts +67 -39
- package/src/runtime/page-layout-estimation.ts +100 -7
- package/src/runtime/resolved-numbering-geometry.ts +293 -0
- package/src/runtime/session-capabilities.ts +2 -2
- package/src/runtime/suggestions-snapshot.ts +137 -0
- package/src/runtime/surface-projection.ts +223 -27
- package/src/runtime/table-style-resolver.ts +409 -0
- package/src/runtime/view-state.ts +17 -1
- package/src/runtime/workflow-markup.ts +54 -14
- package/src/ui/WordReviewEditor.tsx +1269 -87
- package/src/ui/editor-command-bag.ts +7 -0
- package/src/ui/editor-runtime-boundary.ts +111 -10
- package/src/ui/editor-shell-view.tsx +17 -15
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-tool-context.ts +19 -0
- package/src/ui/headless/selection-tool-resolver.ts +752 -0
- package/src/ui/headless/selection-tool-types.ts +129 -0
- package/src/ui/headless/selection-toolbar-model.ts +10 -33
- package/src/ui/runtime-shortcut-dispatch.ts +365 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
- package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
- package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
- package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
- package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
- package/src/ui-tailwind/theme/editor-theme.css +58 -40
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
- package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
- package/src/validation/compatibility-engine.ts +246 -2
- package/src/validation/docx-comment-proof.ts +24 -11
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockNode,
|
|
3
|
+
InlineNode,
|
|
4
|
+
} from "../model/canonical-document.ts";
|
|
5
|
+
import type {
|
|
6
|
+
DocumentHeadingSnapshot,
|
|
7
|
+
DocumentLocationSnapshot,
|
|
8
|
+
DocumentNavigationSnapshot,
|
|
9
|
+
DocumentOutlineHeadingSnapshot,
|
|
10
|
+
DocumentOutlineSnapshot,
|
|
11
|
+
DocumentSectionSnapshot,
|
|
12
|
+
EditorAnchorProjection,
|
|
13
|
+
EditorStoryTarget,
|
|
14
|
+
PageLayoutSnapshot,
|
|
15
|
+
TocEntrySnapshot,
|
|
16
|
+
TocSnapshot,
|
|
17
|
+
} from "../api/public-types";
|
|
18
|
+
import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
|
|
19
|
+
import { createSelectionSnapshot } from "../core/state/editor-state.ts";
|
|
20
|
+
import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
|
|
21
|
+
import { parseTocLevelRange } from "../io/ooxml/parse-fields.ts";
|
|
22
|
+
import { buildPageLayoutSnapshot, buildResolvedSections } from "./document-layout.ts";
|
|
23
|
+
import { createDocumentNavigationSnapshot } from "./document-navigation.ts";
|
|
24
|
+
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
25
|
+
|
|
26
|
+
function getAnchorOffset(anchor: EditorAnchorProjection): number | undefined {
|
|
27
|
+
switch (anchor.kind) {
|
|
28
|
+
case "range":
|
|
29
|
+
return anchor.from;
|
|
30
|
+
case "node":
|
|
31
|
+
return anchor.at;
|
|
32
|
+
case "detached":
|
|
33
|
+
return anchor.lastKnownRange.from;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createPublicRangeAnchor(from: number, to: number): EditorAnchorProjection {
|
|
38
|
+
return {
|
|
39
|
+
kind: "range",
|
|
40
|
+
from,
|
|
41
|
+
to,
|
|
42
|
+
assoc: { start: -1, end: 1 },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveHeadingPath(
|
|
47
|
+
headings: readonly DocumentHeadingSnapshot[],
|
|
48
|
+
offset: number | undefined,
|
|
49
|
+
): { headingId?: string; headingPath: string[]; parentHeadingIds: string[] } {
|
|
50
|
+
if (offset === undefined) {
|
|
51
|
+
return { headingPath: [], parentHeadingIds: [] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const stack: DocumentHeadingSnapshot[] = [];
|
|
55
|
+
let active: DocumentHeadingSnapshot | undefined;
|
|
56
|
+
|
|
57
|
+
for (const heading of headings) {
|
|
58
|
+
if (heading.offset > offset) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
while (stack.length > 0 && stack[stack.length - 1]!.level >= heading.level) {
|
|
62
|
+
stack.pop();
|
|
63
|
+
}
|
|
64
|
+
stack.push(heading);
|
|
65
|
+
active = heading;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!active) {
|
|
69
|
+
return { headingPath: [], parentHeadingIds: [] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
headingId: active.headingId,
|
|
74
|
+
headingPath: stack.map((heading) => heading.text),
|
|
75
|
+
parentHeadingIds: stack.slice(0, -1).map((heading) => heading.headingId),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createDocumentOutlineSnapshot(input: {
|
|
80
|
+
navigation: DocumentNavigationSnapshot;
|
|
81
|
+
activeStory: EditorStoryTarget;
|
|
82
|
+
selectionHead: number;
|
|
83
|
+
}): DocumentOutlineSnapshot {
|
|
84
|
+
const activeHeading =
|
|
85
|
+
input.activeStory.kind === "main"
|
|
86
|
+
? resolveHeadingPath(input.navigation.headings, input.selectionHead).headingId
|
|
87
|
+
: undefined;
|
|
88
|
+
|
|
89
|
+
const outlineHeadings: DocumentOutlineHeadingSnapshot[] = [];
|
|
90
|
+
const stack: Array<{ headingId: string; level: number }> = [];
|
|
91
|
+
|
|
92
|
+
for (const heading of input.navigation.headings) {
|
|
93
|
+
while (stack.length > 0 && stack[stack.length - 1]!.level >= heading.level) {
|
|
94
|
+
stack.pop();
|
|
95
|
+
}
|
|
96
|
+
outlineHeadings.push({
|
|
97
|
+
...heading,
|
|
98
|
+
parentHeadingIds: stack.map((entry) => entry.headingId),
|
|
99
|
+
});
|
|
100
|
+
stack.push({
|
|
101
|
+
headingId: heading.headingId,
|
|
102
|
+
level: heading.level,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
...(activeHeading ? { activeHeadingId: activeHeading } : {}),
|
|
108
|
+
headings: outlineHeadings,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getSectionHeadingPath(
|
|
113
|
+
headings: readonly DocumentHeadingSnapshot[],
|
|
114
|
+
sectionStart: number,
|
|
115
|
+
sectionEnd: number,
|
|
116
|
+
): { headingPath: string[]; primaryHeadingId?: string; primaryHeadingText?: string } {
|
|
117
|
+
const firstHeading = headings.find(
|
|
118
|
+
(heading) => heading.offset >= sectionStart && heading.offset < sectionEnd,
|
|
119
|
+
);
|
|
120
|
+
if (!firstHeading) {
|
|
121
|
+
return { headingPath: [] };
|
|
122
|
+
}
|
|
123
|
+
const path = resolveHeadingPath(headings, firstHeading.offset);
|
|
124
|
+
return {
|
|
125
|
+
headingPath: path.headingPath,
|
|
126
|
+
primaryHeadingId: firstHeading.headingId,
|
|
127
|
+
primaryHeadingText: firstHeading.text,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createSectionStoryTargets(layout: PageLayoutSnapshot): EditorStoryTarget[] {
|
|
132
|
+
const targets: EditorStoryTarget[] = [MAIN_STORY_TARGET];
|
|
133
|
+
for (const header of layout.headerVariants) {
|
|
134
|
+
targets.push({
|
|
135
|
+
kind: "header",
|
|
136
|
+
relationshipId: header.relationshipId,
|
|
137
|
+
variant: header.variant,
|
|
138
|
+
sectionIndex: layout.sectionIndex,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
for (const footer of layout.footerVariants) {
|
|
142
|
+
targets.push({
|
|
143
|
+
kind: "footer",
|
|
144
|
+
relationshipId: footer.relationshipId,
|
|
145
|
+
variant: footer.variant,
|
|
146
|
+
sectionIndex: layout.sectionIndex,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return targets;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createDocumentSectionSnapshots(
|
|
153
|
+
document: CanonicalDocumentEnvelope,
|
|
154
|
+
navigation?: DocumentNavigationSnapshot,
|
|
155
|
+
): DocumentSectionSnapshot[] {
|
|
156
|
+
const resolvedNavigation =
|
|
157
|
+
navigation ?? createDocumentNavigationSnapshot(document, 0, MAIN_STORY_TARGET);
|
|
158
|
+
const sections = buildResolvedSections(document);
|
|
159
|
+
|
|
160
|
+
return sections.map((section) => {
|
|
161
|
+
const pageIndexes = resolvedNavigation.pages
|
|
162
|
+
.filter((page) => page.sectionIndex === section.index)
|
|
163
|
+
.map((page) => page.pageIndex);
|
|
164
|
+
const pageRange =
|
|
165
|
+
pageIndexes.length > 0
|
|
166
|
+
? { start: pageIndexes[0]!, end: pageIndexes[pageIndexes.length - 1]! }
|
|
167
|
+
: { start: 0, end: 0 };
|
|
168
|
+
const layout = buildPageLayoutSnapshot(
|
|
169
|
+
section.index,
|
|
170
|
+
section.properties ?? document.subParts?.finalSectionProperties,
|
|
171
|
+
document.subParts,
|
|
172
|
+
);
|
|
173
|
+
const headingInfo = getSectionHeadingPath(
|
|
174
|
+
resolvedNavigation.headings,
|
|
175
|
+
section.start,
|
|
176
|
+
section.end,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
sectionId: `section-${section.index}`,
|
|
181
|
+
sectionIndex: section.index,
|
|
182
|
+
pageRange,
|
|
183
|
+
headingPath: headingInfo.headingPath,
|
|
184
|
+
...(headingInfo.primaryHeadingId
|
|
185
|
+
? {
|
|
186
|
+
primaryHeadingId: headingInfo.primaryHeadingId,
|
|
187
|
+
primaryHeadingText: headingInfo.primaryHeadingText,
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
190
|
+
layout,
|
|
191
|
+
anchor: createPublicRangeAnchor(section.start, section.end),
|
|
192
|
+
storyTargets: createSectionStoryTargets(layout),
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function findDocumentSectionSnapshot(
|
|
198
|
+
sections: readonly DocumentSectionSnapshot[],
|
|
199
|
+
input: { sectionIndex?: number; headingId?: string; bookmarkName?: string },
|
|
200
|
+
locations?: readonly DocumentLocationSnapshot[],
|
|
201
|
+
): DocumentSectionSnapshot | null {
|
|
202
|
+
if (typeof input.sectionIndex === "number") {
|
|
203
|
+
return sections.find((section) => section.sectionIndex === input.sectionIndex) ?? null;
|
|
204
|
+
}
|
|
205
|
+
if (input.headingId) {
|
|
206
|
+
return sections.find((section) => section.primaryHeadingId === input.headingId) ?? null;
|
|
207
|
+
}
|
|
208
|
+
if (input.bookmarkName && locations) {
|
|
209
|
+
const location = locations.find((entry) => entry.bookmarkName === input.bookmarkName);
|
|
210
|
+
if (location?.sectionIndex !== undefined) {
|
|
211
|
+
return sections.find((section) => section.sectionIndex === location.sectionIndex) ?? null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function createTocSnapshot(
|
|
218
|
+
document: CanonicalDocumentEnvelope,
|
|
219
|
+
navigation?: DocumentNavigationSnapshot,
|
|
220
|
+
): TocSnapshot | null {
|
|
221
|
+
const registry = document.fieldRegistry;
|
|
222
|
+
const tocStructure = registry?.tocStructure;
|
|
223
|
+
const fallbackFields = document.fieldRegistry?.supported ?? [];
|
|
224
|
+
const tocFieldIndex = tocStructure
|
|
225
|
+
? fallbackFields.find((entry) => entry.fieldFamily === "TOC")?.fieldIndex
|
|
226
|
+
: undefined;
|
|
227
|
+
const fallbackTocField = !tocStructure ? findFirstTocField(document.content.children) : undefined;
|
|
228
|
+
|
|
229
|
+
if (!tocStructure && !fallbackTocField) {
|
|
230
|
+
const hasTocField =
|
|
231
|
+
fallbackFields.some((entry) => entry.fieldFamily === "TOC") ||
|
|
232
|
+
document.fieldRegistry?.preserveOnly.some((entry) => entry.fieldFamily === "TOC");
|
|
233
|
+
return hasTocField ? { status: "missing", entries: [] } : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const resolvedNavigation =
|
|
237
|
+
navigation ?? createDocumentNavigationSnapshot(document, 0, MAIN_STORY_TARGET);
|
|
238
|
+
const headingRefs = buildHeadingRefs(document, resolvedNavigation);
|
|
239
|
+
const derivedEntries: Array<{
|
|
240
|
+
level: number;
|
|
241
|
+
text: string;
|
|
242
|
+
bookmarkName?: string;
|
|
243
|
+
}> = tocStructure
|
|
244
|
+
? tocStructure.entries.map((entry) => {
|
|
245
|
+
const heading = resolveHeadingForTocEntry(entry, headingRefs);
|
|
246
|
+
return {
|
|
247
|
+
level: entry.level,
|
|
248
|
+
text: heading?.text ?? entry.text,
|
|
249
|
+
...(entry.bookmarkName ? { bookmarkName: entry.bookmarkName } : {}),
|
|
250
|
+
};
|
|
251
|
+
})
|
|
252
|
+
: resolvedNavigation.headings
|
|
253
|
+
.filter((heading) => {
|
|
254
|
+
const levelRange = parseTocLevelRange(fallbackTocField!.instruction);
|
|
255
|
+
return heading.level >= levelRange.from && heading.level <= levelRange.to;
|
|
256
|
+
})
|
|
257
|
+
.map((heading) => ({
|
|
258
|
+
level: heading.level,
|
|
259
|
+
text: heading.text,
|
|
260
|
+
bookmarkName: findBookmarkNameForOffset(document, heading.offset),
|
|
261
|
+
}));
|
|
262
|
+
const entries: TocEntrySnapshot[] = derivedEntries.map((entry, index) => {
|
|
263
|
+
const heading = tocStructure
|
|
264
|
+
? resolveHeadingForTocEntry(tocStructure.entries[index]!, headingRefs)
|
|
265
|
+
: resolvedNavigation.headings[index];
|
|
266
|
+
const bookmarkName = headingRefs.find((ref) => ref.heading.headingId === heading?.headingId)?.bookmarkName;
|
|
267
|
+
return {
|
|
268
|
+
tocEntryId: `toc-entry-${index}`,
|
|
269
|
+
level: entry.level,
|
|
270
|
+
text: entry.text,
|
|
271
|
+
...(heading
|
|
272
|
+
? {
|
|
273
|
+
pageIndex: heading.pageIndex,
|
|
274
|
+
anchor: createPublicRangeAnchor(heading.offset, heading.offset),
|
|
275
|
+
headingId: heading.headingId,
|
|
276
|
+
...(bookmarkName ? { bookmarkName } : {}),
|
|
277
|
+
}
|
|
278
|
+
: {}),
|
|
279
|
+
...(entry.bookmarkName ? { bookmarkName: entry.bookmarkName } : {}),
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
status: tocStructure?.status ?? (fallbackTocField?.refreshStatus === "current" ? "current" : "stale"),
|
|
285
|
+
...(typeof tocFieldIndex === "number" ? { sourceFieldIndex: tocFieldIndex } : {}),
|
|
286
|
+
instruction: tocStructure?.instruction ?? fallbackTocField?.instruction,
|
|
287
|
+
entries,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function buildHeadingRefs(
|
|
292
|
+
document: CanonicalDocumentEnvelope,
|
|
293
|
+
navigation: DocumentNavigationSnapshot,
|
|
294
|
+
): Array<{
|
|
295
|
+
heading: DocumentHeadingSnapshot;
|
|
296
|
+
paragraphIndex?: number;
|
|
297
|
+
bookmarkName?: string;
|
|
298
|
+
}> {
|
|
299
|
+
const surface = createEditorSurfaceSnapshot(
|
|
300
|
+
document,
|
|
301
|
+
createSelectionSnapshot(0, 0),
|
|
302
|
+
MAIN_STORY_TARGET,
|
|
303
|
+
);
|
|
304
|
+
const paragraphIndexByOffset = new Map<number, number>();
|
|
305
|
+
for (const block of surface.blocks) {
|
|
306
|
+
if (block.kind !== "paragraph") {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const match = /^paragraph-(\d+)$/.exec(block.blockId);
|
|
310
|
+
if (!match) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
paragraphIndexByOffset.set(block.from, parseInt(match[1]!, 10));
|
|
314
|
+
}
|
|
315
|
+
return navigation.headings.map((heading) => ({
|
|
316
|
+
heading,
|
|
317
|
+
paragraphIndex: paragraphIndexByOffset.get(heading.offset),
|
|
318
|
+
...(findBookmarkNameForOffset(document, heading.offset)
|
|
319
|
+
? { bookmarkName: findBookmarkNameForOffset(document, heading.offset) }
|
|
320
|
+
: {}),
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function resolveHeadingForTocEntry(
|
|
325
|
+
entry: { paragraphIndex: number; bookmarkName?: string },
|
|
326
|
+
headingRefs: ReadonlyArray<{
|
|
327
|
+
heading: DocumentHeadingSnapshot;
|
|
328
|
+
paragraphIndex?: number;
|
|
329
|
+
bookmarkName?: string;
|
|
330
|
+
}>,
|
|
331
|
+
): DocumentHeadingSnapshot | undefined {
|
|
332
|
+
if (entry.bookmarkName) {
|
|
333
|
+
const bookmarkMatch = headingRefs.find((ref) => ref.bookmarkName === entry.bookmarkName);
|
|
334
|
+
if (bookmarkMatch) {
|
|
335
|
+
return bookmarkMatch.heading;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const paragraphMatch = headingRefs.find((ref) => ref.paragraphIndex === entry.paragraphIndex);
|
|
339
|
+
return paragraphMatch?.heading;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function findFirstTocField(blocks: readonly BlockNode[]): Extract<InlineNode, { type: "field" }> | undefined {
|
|
343
|
+
for (const block of blocks) {
|
|
344
|
+
if (block.type === "paragraph") {
|
|
345
|
+
const field = findTocFieldInline(block.children);
|
|
346
|
+
if (field) {
|
|
347
|
+
return field;
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (block.type === "table") {
|
|
352
|
+
for (const row of block.rows) {
|
|
353
|
+
for (const cell of row.cells) {
|
|
354
|
+
const field = findFirstTocField(cell.children);
|
|
355
|
+
if (field) {
|
|
356
|
+
return field;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
363
|
+
const field = findFirstTocField(block.children);
|
|
364
|
+
if (field) {
|
|
365
|
+
return field;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function findTocFieldInline(
|
|
373
|
+
nodes: readonly InlineNode[],
|
|
374
|
+
): Extract<InlineNode, { type: "field" }> | undefined {
|
|
375
|
+
for (const node of nodes) {
|
|
376
|
+
if (node.type === "field" && node.fieldFamily === "TOC") {
|
|
377
|
+
return node;
|
|
378
|
+
}
|
|
379
|
+
if (node.type === "field" || node.type === "hyperlink") {
|
|
380
|
+
const childField = findTocFieldInline(node.children);
|
|
381
|
+
if (childField) {
|
|
382
|
+
return childField;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function findBookmarkNameForOffset(
|
|
390
|
+
document: CanonicalDocumentEnvelope,
|
|
391
|
+
offset: number | undefined,
|
|
392
|
+
): string | undefined {
|
|
393
|
+
if (offset === undefined) {
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let runningOffset = 0;
|
|
398
|
+
for (const block of document.content.children) {
|
|
399
|
+
if (block.type !== "paragraph") {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const paragraphStart = runningOffset;
|
|
403
|
+
for (const child of block.children) {
|
|
404
|
+
if (child.type === "text") {
|
|
405
|
+
runningOffset += child.text.length;
|
|
406
|
+
} else if (child.type === "tab" || child.type === "hard_break") {
|
|
407
|
+
runningOffset += 1;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const paragraphEnd = runningOffset;
|
|
411
|
+
if (offset >= paragraphStart && offset <= paragraphEnd) {
|
|
412
|
+
const bookmarkStart = block.children.find(
|
|
413
|
+
(child): child is Extract<typeof child, { type: "bookmark_start" }> =>
|
|
414
|
+
child.type === "bookmark_start" && Boolean(child.name),
|
|
415
|
+
);
|
|
416
|
+
return bookmarkStart?.name;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function createSectionLocations(
|
|
424
|
+
document: CanonicalDocumentEnvelope,
|
|
425
|
+
sections: readonly DocumentSectionSnapshot[],
|
|
426
|
+
): DocumentLocationSnapshot[] {
|
|
427
|
+
return sections.map((section) => ({
|
|
428
|
+
locationId: `location:${section.sectionId}`,
|
|
429
|
+
anchor: section.anchor,
|
|
430
|
+
storyTarget: MAIN_STORY_TARGET,
|
|
431
|
+
sectionIndex: section.sectionIndex,
|
|
432
|
+
pageIndex: section.pageRange.start,
|
|
433
|
+
...(section.primaryHeadingId ? { headingId: section.primaryHeadingId } : {}),
|
|
434
|
+
...(section.headingPath.length > 0 ? { headingPath: section.headingPath } : {}),
|
|
435
|
+
source: { kind: "navigation" },
|
|
436
|
+
...(findBookmarkNameForOffset(document, getAnchorOffset(section.anchor))
|
|
437
|
+
? { bookmarkName: findBookmarkNameForOffset(document, getAnchorOffset(section.anchor)) }
|
|
438
|
+
: {}),
|
|
439
|
+
}));
|
|
440
|
+
}
|