@beyondwork/docx-react-component 1.0.18 → 1.0.19
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 +8 -2
- package/package.json +24 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +374 -4
- package/src/api/session-state.ts +58 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +5 -1
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +329 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +1 -1
- package/src/index.ts +30 -0
- package/src/io/docx-session.ts +260 -39
- package/src/io/export/serialize-main-document.ts +202 -5
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/normalize/normalize-text.ts +63 -25
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-footnotes.ts +212 -20
- package/src/io/ooxml/parse-headers-footers.ts +229 -25
- package/src/io/ooxml/parse-inline-media.ts +16 -0
- package/src/io/ooxml/parse-main-document.ts +411 -6
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/model/canonical-document.ts +133 -3
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +2 -1
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +564 -0
- package/src/runtime/document-runtime.ts +265 -35
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
- package/src/runtime/session-capabilities.ts +2 -0
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +239 -12
- package/src/runtime/table-schema.ts +87 -5
- package/src/runtime/view-state.ts +459 -0
- package/src/ui/WordReviewEditor.tsx +1902 -312
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
- package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
- package/src/ui-tailwind/theme/editor-theme.css +123 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
- package/src/validation/compatibility-engine.ts +92 -20
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +487 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PersistedEditorSnapshot,
|
|
3
|
+
RuntimeRenderSnapshot,
|
|
4
|
+
SurfaceBlockSnapshot,
|
|
5
|
+
} from "../../api/public-types";
|
|
6
|
+
import type {
|
|
7
|
+
BlockNode,
|
|
8
|
+
DocumentRootNode,
|
|
9
|
+
ParagraphNode,
|
|
10
|
+
TableNode,
|
|
11
|
+
} from "../../model/canonical-document.ts";
|
|
12
|
+
|
|
13
|
+
type CanonicalDocumentEnvelope = PersistedEditorSnapshot["canonicalDocument"];
|
|
14
|
+
|
|
15
|
+
export interface StyleMutationResult {
|
|
16
|
+
document: CanonicalDocumentEnvelope;
|
|
17
|
+
selection: RuntimeRenderSnapshot["selection"];
|
|
18
|
+
changed: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function applyParagraphStyleToDocument(
|
|
22
|
+
document: CanonicalDocumentEnvelope,
|
|
23
|
+
snapshot: RuntimeRenderSnapshot,
|
|
24
|
+
styleId: string | null,
|
|
25
|
+
): StyleMutationResult {
|
|
26
|
+
const surface = snapshot.surface;
|
|
27
|
+
if (!surface) {
|
|
28
|
+
return {
|
|
29
|
+
document,
|
|
30
|
+
selection: snapshot.selection,
|
|
31
|
+
changed: false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const nextDocument = structuredClone(document);
|
|
36
|
+
const root = nextDocument.content as DocumentRootNode;
|
|
37
|
+
const normalizedStyleId = normalizeStyleId(styleId);
|
|
38
|
+
if (!isValidParagraphStyleId(document, normalizedStyleId)) {
|
|
39
|
+
return {
|
|
40
|
+
document,
|
|
41
|
+
selection: snapshot.selection,
|
|
42
|
+
changed: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let changed = false;
|
|
46
|
+
|
|
47
|
+
visitParagraphBindings(root.children, surface.blocks, (paragraph, paragraphSurface) => {
|
|
48
|
+
if (
|
|
49
|
+
!selectionTouchesRange(
|
|
50
|
+
snapshot.selection.anchor,
|
|
51
|
+
snapshot.selection.head,
|
|
52
|
+
paragraphSurface.from,
|
|
53
|
+
paragraphSurface.to,
|
|
54
|
+
)
|
|
55
|
+
) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (paragraph.styleId === normalizedStyleId) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (normalizedStyleId) {
|
|
64
|
+
paragraph.styleId = normalizedStyleId;
|
|
65
|
+
} else {
|
|
66
|
+
delete paragraph.styleId;
|
|
67
|
+
}
|
|
68
|
+
changed = true;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
document: changed ? nextDocument : document,
|
|
73
|
+
selection: snapshot.selection,
|
|
74
|
+
changed,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function applyTableStyleToDocument(
|
|
79
|
+
document: CanonicalDocumentEnvelope,
|
|
80
|
+
snapshot: RuntimeRenderSnapshot,
|
|
81
|
+
styleId: string | null,
|
|
82
|
+
): StyleMutationResult {
|
|
83
|
+
const surface = snapshot.surface;
|
|
84
|
+
if (!surface) {
|
|
85
|
+
return {
|
|
86
|
+
document,
|
|
87
|
+
selection: snapshot.selection,
|
|
88
|
+
changed: false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nextDocument = structuredClone(document);
|
|
93
|
+
const root = nextDocument.content as DocumentRootNode;
|
|
94
|
+
const normalizedStyleId = normalizeStyleId(styleId);
|
|
95
|
+
if (!isValidTableStyleId(document, normalizedStyleId)) {
|
|
96
|
+
return {
|
|
97
|
+
document,
|
|
98
|
+
selection: snapshot.selection,
|
|
99
|
+
changed: false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
let changed = false;
|
|
103
|
+
let applied = false;
|
|
104
|
+
|
|
105
|
+
visitTableBindings(root.children, surface.blocks, (table, tableSurface) => {
|
|
106
|
+
if (
|
|
107
|
+
applied ||
|
|
108
|
+
!selectionTouchesRange(
|
|
109
|
+
snapshot.selection.anchor,
|
|
110
|
+
snapshot.selection.head,
|
|
111
|
+
tableSurface.from,
|
|
112
|
+
tableSurface.to,
|
|
113
|
+
)
|
|
114
|
+
) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (table.styleId === normalizedStyleId) {
|
|
119
|
+
applied = true;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (normalizedStyleId) {
|
|
124
|
+
table.styleId = normalizedStyleId;
|
|
125
|
+
} else {
|
|
126
|
+
delete table.styleId;
|
|
127
|
+
}
|
|
128
|
+
changed = true;
|
|
129
|
+
applied = true;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
document: changed ? nextDocument : document,
|
|
134
|
+
selection: snapshot.selection,
|
|
135
|
+
changed,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeStyleId(styleId: string | null): string | undefined {
|
|
140
|
+
if (styleId === null) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const normalized = styleId.trim();
|
|
144
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isValidParagraphStyleId(
|
|
148
|
+
document: CanonicalDocumentEnvelope,
|
|
149
|
+
styleId: string | undefined,
|
|
150
|
+
): boolean {
|
|
151
|
+
return styleId === undefined || Boolean(document.styles.paragraphs[styleId]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isValidTableStyleId(
|
|
155
|
+
document: CanonicalDocumentEnvelope,
|
|
156
|
+
styleId: string | undefined,
|
|
157
|
+
): boolean {
|
|
158
|
+
return styleId === undefined || Boolean(document.styles.tables[styleId]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function visitParagraphBindings(
|
|
162
|
+
blocks: BlockNode[],
|
|
163
|
+
surfaceBlocks: SurfaceBlockSnapshot[],
|
|
164
|
+
visitor: (paragraph: ParagraphNode, surface: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>) => void,
|
|
165
|
+
): void {
|
|
166
|
+
for (let index = 0; index < Math.min(blocks.length, surfaceBlocks.length); index += 1) {
|
|
167
|
+
const block = blocks[index];
|
|
168
|
+
const surface = surfaceBlocks[index];
|
|
169
|
+
if (!block || !surface) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (block.type === "paragraph" && surface.kind === "paragraph") {
|
|
174
|
+
visitor(block, surface);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (block.type === "table" && surface.kind === "table") {
|
|
179
|
+
for (let rowIndex = 0; rowIndex < Math.min(block.rows.length, surface.rows.length); rowIndex += 1) {
|
|
180
|
+
const row = block.rows[rowIndex];
|
|
181
|
+
const surfaceRow = surface.rows[rowIndex];
|
|
182
|
+
if (!row || !surfaceRow) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
for (let cellIndex = 0; cellIndex < Math.min(row.cells.length, surfaceRow.cells.length); cellIndex += 1) {
|
|
186
|
+
const cell = row.cells[cellIndex];
|
|
187
|
+
const surfaceCell = surfaceRow.cells[cellIndex];
|
|
188
|
+
if (!cell || !surfaceCell) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
visitParagraphBindings(cell.children, surfaceCell.content, visitor);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (block.type === "sdt" && surface.kind === "sdt_block") {
|
|
198
|
+
visitParagraphBindings(block.children, surface.children, visitor);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function visitTableBindings(
|
|
204
|
+
blocks: BlockNode[],
|
|
205
|
+
surfaceBlocks: SurfaceBlockSnapshot[],
|
|
206
|
+
visitor: (table: TableNode, surface: Extract<SurfaceBlockSnapshot, { kind: "table" }>) => void,
|
|
207
|
+
): void {
|
|
208
|
+
for (let index = 0; index < Math.min(blocks.length, surfaceBlocks.length); index += 1) {
|
|
209
|
+
const block = blocks[index];
|
|
210
|
+
const surface = surfaceBlocks[index];
|
|
211
|
+
if (!block || !surface) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (block.type === "table" && surface.kind === "table") {
|
|
216
|
+
visitor(block, surface);
|
|
217
|
+
for (let rowIndex = 0; rowIndex < Math.min(block.rows.length, surface.rows.length); rowIndex += 1) {
|
|
218
|
+
const row = block.rows[rowIndex];
|
|
219
|
+
const surfaceRow = surface.rows[rowIndex];
|
|
220
|
+
if (!row || !surfaceRow) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
for (let cellIndex = 0; cellIndex < Math.min(row.cells.length, surfaceRow.cells.length); cellIndex += 1) {
|
|
224
|
+
const cell = row.cells[cellIndex];
|
|
225
|
+
const surfaceCell = surfaceRow.cells[cellIndex];
|
|
226
|
+
if (!cell || !surfaceCell) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
visitTableBindings(cell.children, surfaceCell.content, visitor);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (block.type === "sdt" && surface.kind === "sdt_block") {
|
|
236
|
+
visitTableBindings(block.children, surface.children, visitor);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function selectionTouchesRange(
|
|
242
|
+
anchor: number,
|
|
243
|
+
head: number,
|
|
244
|
+
rangeFrom: number,
|
|
245
|
+
rangeTo: number,
|
|
246
|
+
): boolean {
|
|
247
|
+
const selectionFrom = Math.min(anchor, head);
|
|
248
|
+
const selectionTo = Math.max(anchor, head);
|
|
249
|
+
if (selectionFrom === selectionTo) {
|
|
250
|
+
return selectionFrom >= rangeFrom && selectionFrom <= rangeTo;
|
|
251
|
+
}
|
|
252
|
+
return rangesOverlap(selectionFrom, selectionTo, rangeFrom, rangeTo);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function rangesOverlap(
|
|
256
|
+
leftFrom: number,
|
|
257
|
+
leftTo: number,
|
|
258
|
+
rightFrom: number,
|
|
259
|
+
rightTo: number,
|
|
260
|
+
): boolean {
|
|
261
|
+
return leftFrom < rightTo && leftTo > rightFrom;
|
|
262
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EditorStoryTarget,
|
|
3
|
+
SearchOptions as PublicSearchOptions,
|
|
4
|
+
SecondaryStorySurface,
|
|
5
|
+
SurfaceBlockSnapshot,
|
|
6
|
+
} from "../../api/public-types";
|
|
7
|
+
|
|
8
|
+
export interface SearchTextOptions extends PublicSearchOptions {
|
|
9
|
+
caseSensitive?: boolean;
|
|
10
|
+
regex?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SearchResult {
|
|
14
|
+
from: number;
|
|
15
|
+
to: number;
|
|
16
|
+
text: string;
|
|
17
|
+
index: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SurfaceSearchResult extends SearchResult {
|
|
21
|
+
excerpt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SecondaryStorySearchResult extends SurfaceSearchResult {
|
|
25
|
+
storyTarget: EditorStoryTarget;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ProjectedSurfaceText {
|
|
29
|
+
text: string;
|
|
30
|
+
offsetMap: Array<number | null>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SurfaceProjectionBuilder {
|
|
34
|
+
text: string[];
|
|
35
|
+
offsetMap: Array<number | null>;
|
|
36
|
+
nextRuntimeOffset: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildSearchPattern(
|
|
40
|
+
query: string,
|
|
41
|
+
options: SearchTextOptions = {},
|
|
42
|
+
): RegExp | null {
|
|
43
|
+
if (!query) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const caseSensitive = options.matchCase ?? options.caseSensitive ?? false;
|
|
48
|
+
const regex = options.regex ?? false;
|
|
49
|
+
const wholeWord = options.wholeWord ?? false;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const source = regex
|
|
53
|
+
? query
|
|
54
|
+
: query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
55
|
+
const wrapped = wholeWord ? `\\b${source}\\b` : source;
|
|
56
|
+
return new RegExp(wrapped, caseSensitive ? "g" : "gi");
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function findSearchMatches(
|
|
63
|
+
text: string,
|
|
64
|
+
query: string,
|
|
65
|
+
options: SearchTextOptions = {},
|
|
66
|
+
): SearchResult[] {
|
|
67
|
+
const pattern = buildSearchPattern(query, options);
|
|
68
|
+
if (!pattern) return [];
|
|
69
|
+
|
|
70
|
+
const results: SearchResult[] = [];
|
|
71
|
+
let match: RegExpExecArray | null;
|
|
72
|
+
pattern.lastIndex = 0;
|
|
73
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
74
|
+
results.push({
|
|
75
|
+
from: match.index,
|
|
76
|
+
to: match.index + match[0].length,
|
|
77
|
+
text: match[0],
|
|
78
|
+
index: results.length,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (match[0].length === 0) {
|
|
82
|
+
pattern.lastIndex += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createSearchExcerpt(
|
|
90
|
+
text: string,
|
|
91
|
+
from: number,
|
|
92
|
+
to: number,
|
|
93
|
+
radius = 24,
|
|
94
|
+
): string {
|
|
95
|
+
const safeFrom = Math.max(0, Math.min(from, text.length));
|
|
96
|
+
const safeTo = Math.max(safeFrom, Math.min(to, text.length));
|
|
97
|
+
const start = Math.max(0, safeFrom - radius);
|
|
98
|
+
const end = Math.min(text.length, safeTo + radius);
|
|
99
|
+
const prefix = start > 0 ? "…" : "";
|
|
100
|
+
const suffix = end < text.length ? "…" : "";
|
|
101
|
+
const excerpt = text
|
|
102
|
+
.slice(start, end)
|
|
103
|
+
.replace(/[\uFFF9\uFFFA]/gu, "")
|
|
104
|
+
.replace(/\s{2,}/gu, " ")
|
|
105
|
+
.trim();
|
|
106
|
+
return `${prefix}${excerpt}${suffix}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function searchSurfaceBlocks(
|
|
110
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
111
|
+
query: string,
|
|
112
|
+
options: SearchTextOptions = {},
|
|
113
|
+
): SurfaceSearchResult[] {
|
|
114
|
+
const projection = projectSurfaceText(blocks);
|
|
115
|
+
return findSearchMatches(projection.text, query, options)
|
|
116
|
+
.map((match) => {
|
|
117
|
+
const range = resolveProjectedRuntimeRange(
|
|
118
|
+
projection.offsetMap,
|
|
119
|
+
match.from,
|
|
120
|
+
match.to,
|
|
121
|
+
);
|
|
122
|
+
if (!range) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
...match,
|
|
128
|
+
from: range.from,
|
|
129
|
+
to: range.to,
|
|
130
|
+
excerpt: createSearchExcerpt(projection.text, match.from, match.to),
|
|
131
|
+
};
|
|
132
|
+
})
|
|
133
|
+
.filter((match): match is SurfaceSearchResult => Boolean(match));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function searchSecondaryStories(
|
|
137
|
+
stories: readonly SecondaryStorySurface[],
|
|
138
|
+
query: string,
|
|
139
|
+
options: SearchTextOptions = {},
|
|
140
|
+
): SecondaryStorySearchResult[] {
|
|
141
|
+
const results: SecondaryStorySearchResult[] = [];
|
|
142
|
+
|
|
143
|
+
for (const story of stories) {
|
|
144
|
+
for (const match of searchSurfaceBlocks(story.blocks, query, options)) {
|
|
145
|
+
results.push({
|
|
146
|
+
storyTarget: story.target,
|
|
147
|
+
from: match.from,
|
|
148
|
+
to: match.to,
|
|
149
|
+
text: match.text,
|
|
150
|
+
index: results.length,
|
|
151
|
+
excerpt: match.excerpt,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function projectSurfaceText(
|
|
160
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
161
|
+
): ProjectedSurfaceText {
|
|
162
|
+
const builder: SurfaceProjectionBuilder = {
|
|
163
|
+
text: [],
|
|
164
|
+
offsetMap: [],
|
|
165
|
+
nextRuntimeOffset: 0,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
appendSurfaceBlocks(builder, blocks);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
text: builder.text.join(""),
|
|
172
|
+
offsetMap: builder.offsetMap,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function appendSurfaceBlocks(
|
|
177
|
+
builder: SurfaceProjectionBuilder,
|
|
178
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
179
|
+
): void {
|
|
180
|
+
for (const block of blocks) {
|
|
181
|
+
if (block.kind === "opaque_block") {
|
|
182
|
+
if (block.fragmentId.startsWith("preview:")) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
appendAtomicSpan(builder, block.from, block.to, "\uFFFA");
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (block.kind === "table") {
|
|
190
|
+
appendRuntimeGap(builder, block.from);
|
|
191
|
+
for (const [rowIndex, row] of block.rows.entries()) {
|
|
192
|
+
if (rowIndex > 0) {
|
|
193
|
+
appendSyntheticSeparator(builder, "\n");
|
|
194
|
+
}
|
|
195
|
+
for (const [cellIndex, cell] of row.cells.entries()) {
|
|
196
|
+
if (cellIndex > 0) {
|
|
197
|
+
appendSyntheticSeparator(builder, "\t");
|
|
198
|
+
}
|
|
199
|
+
appendSurfaceBlocks(builder, cell.content);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
appendRuntimeGap(builder, block.to);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (block.kind === "sdt_block") {
|
|
207
|
+
appendRuntimeGap(builder, block.from);
|
|
208
|
+
appendSurfaceBlocks(builder, block.children);
|
|
209
|
+
appendRuntimeGap(builder, block.to);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const segment of block.segments) {
|
|
214
|
+
switch (segment.kind) {
|
|
215
|
+
case "text":
|
|
216
|
+
appendTextSpan(builder, segment.text, segment.from);
|
|
217
|
+
break;
|
|
218
|
+
case "tab":
|
|
219
|
+
appendAtomicSpan(builder, segment.from, segment.to, "\t");
|
|
220
|
+
break;
|
|
221
|
+
case "hard_break":
|
|
222
|
+
appendAtomicSpan(builder, segment.from, segment.to, "\n");
|
|
223
|
+
break;
|
|
224
|
+
case "image":
|
|
225
|
+
appendAtomicSpan(builder, segment.from, segment.to, "\uFFFC");
|
|
226
|
+
break;
|
|
227
|
+
case "opaque_inline":
|
|
228
|
+
appendAtomicSpan(builder, segment.from, segment.to, "\uFFF9");
|
|
229
|
+
break;
|
|
230
|
+
case "note_ref":
|
|
231
|
+
appendAtomicSpan(builder, segment.from, segment.to, "\uFFFA");
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
appendRuntimeGap(builder, block.to);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function appendTextSpan(
|
|
241
|
+
builder: SurfaceProjectionBuilder,
|
|
242
|
+
text: string,
|
|
243
|
+
startOffset: number,
|
|
244
|
+
): void {
|
|
245
|
+
appendRuntimeGap(builder, startOffset);
|
|
246
|
+
|
|
247
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
248
|
+
builder.text.push(text[index]!);
|
|
249
|
+
builder.offsetMap.push(startOffset + index);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
builder.nextRuntimeOffset = Math.max(
|
|
253
|
+
builder.nextRuntimeOffset,
|
|
254
|
+
startOffset + text.length,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function appendAtomicSpan(
|
|
259
|
+
builder: SurfaceProjectionBuilder,
|
|
260
|
+
from: number,
|
|
261
|
+
to: number,
|
|
262
|
+
char: string,
|
|
263
|
+
): void {
|
|
264
|
+
appendRuntimeGap(builder, from);
|
|
265
|
+
const width = Math.max(1, to - from);
|
|
266
|
+
|
|
267
|
+
for (let index = 0; index < width; index += 1) {
|
|
268
|
+
builder.text.push(char);
|
|
269
|
+
builder.offsetMap.push(from + index);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
builder.nextRuntimeOffset = Math.max(builder.nextRuntimeOffset, to);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function appendRuntimeGap(
|
|
276
|
+
builder: SurfaceProjectionBuilder,
|
|
277
|
+
nextFrom: number,
|
|
278
|
+
): void {
|
|
279
|
+
for (
|
|
280
|
+
let offset = builder.nextRuntimeOffset;
|
|
281
|
+
offset < nextFrom;
|
|
282
|
+
offset += 1
|
|
283
|
+
) {
|
|
284
|
+
builder.text.push("\n");
|
|
285
|
+
builder.offsetMap.push(offset);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
builder.nextRuntimeOffset = Math.max(builder.nextRuntimeOffset, nextFrom);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function appendSyntheticSeparator(
|
|
292
|
+
builder: SurfaceProjectionBuilder,
|
|
293
|
+
value: string,
|
|
294
|
+
): void {
|
|
295
|
+
for (const char of value) {
|
|
296
|
+
builder.text.push(char);
|
|
297
|
+
builder.offsetMap.push(null);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function resolveProjectedRuntimeRange(
|
|
302
|
+
offsetMap: ReadonlyArray<number | null>,
|
|
303
|
+
from: number,
|
|
304
|
+
to: number,
|
|
305
|
+
): { from: number; to: number } | null {
|
|
306
|
+
let runtimeFrom: number | undefined;
|
|
307
|
+
for (let index = from; index < to; index += 1) {
|
|
308
|
+
const offset = offsetMap[index];
|
|
309
|
+
if (offset !== null && offset !== undefined) {
|
|
310
|
+
runtimeFrom = offset;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let runtimeTo: number | undefined;
|
|
316
|
+
for (let index = to - 1; index >= from; index -= 1) {
|
|
317
|
+
const offset = offsetMap[index];
|
|
318
|
+
if (offset !== null && offset !== undefined) {
|
|
319
|
+
runtimeTo = offset + 1;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (runtimeFrom === undefined || runtimeTo === undefined) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { from: runtimeFrom, to: runtimeTo };
|
|
329
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { EditorStoryTarget } from "../../api/public-types";
|
|
2
|
+
|
|
1
3
|
export type Position = number;
|
|
2
4
|
export type Assoc = -1 | 1;
|
|
3
5
|
|
|
@@ -211,6 +213,45 @@ export function areAnchorsEqual(
|
|
|
211
213
|
return false;
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
export function storyTargetsEqual(
|
|
217
|
+
left: EditorStoryTarget | undefined,
|
|
218
|
+
right: EditorStoryTarget | undefined,
|
|
219
|
+
): boolean {
|
|
220
|
+
if (left === right) return true;
|
|
221
|
+
if (!left || !right) return left === right;
|
|
222
|
+
if (left.kind !== right.kind) return false;
|
|
223
|
+
if (left.kind === "main") return true;
|
|
224
|
+
if (left.kind === "header" && right.kind === "header") {
|
|
225
|
+
return (
|
|
226
|
+
left.relationshipId === right.relationshipId &&
|
|
227
|
+
left.variant === right.variant &&
|
|
228
|
+
(left.sectionIndex === undefined ||
|
|
229
|
+
right.sectionIndex === undefined
|
|
230
|
+
? true
|
|
231
|
+
: left.sectionIndex === right.sectionIndex)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (left.kind === "footer" && right.kind === "footer") {
|
|
235
|
+
return (
|
|
236
|
+
left.relationshipId === right.relationshipId &&
|
|
237
|
+
left.variant === right.variant &&
|
|
238
|
+
(left.sectionIndex === undefined ||
|
|
239
|
+
right.sectionIndex === undefined
|
|
240
|
+
? true
|
|
241
|
+
: left.sectionIndex === right.sectionIndex)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (left.kind === "footnote" && right.kind === "footnote") {
|
|
245
|
+
return left.noteId === right.noteId;
|
|
246
|
+
}
|
|
247
|
+
if (left.kind === "endnote" && right.kind === "endnote") {
|
|
248
|
+
return left.noteId === right.noteId;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const MAIN_STORY_TARGET: EditorStoryTarget = { kind: "main" };
|
|
254
|
+
|
|
214
255
|
function mapPositionThroughStep(
|
|
215
256
|
position: Position,
|
|
216
257
|
assoc: Assoc,
|
|
@@ -62,7 +62,7 @@ export interface EditorError {
|
|
|
62
62
|
| "internal_invariant";
|
|
63
63
|
message: string;
|
|
64
64
|
isFatal: boolean;
|
|
65
|
-
source: "import" | "runtime" | "validation" | "datastore" | "export";
|
|
65
|
+
source: "import" | "runtime" | "validation" | "datastore" | "host" | "export";
|
|
66
66
|
details?: Record<string, unknown>;
|
|
67
67
|
}
|
|
68
68
|
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export { WordReviewEditor } from "./ui/WordReviewEditor.tsx";
|
|
2
|
+
export {
|
|
3
|
+
createEditorSessionState,
|
|
4
|
+
editorSessionStateFromPersistedSnapshot,
|
|
5
|
+
persistedSnapshotFromEditorSessionState,
|
|
6
|
+
EDITOR_SESSION_STATE_VERSION,
|
|
7
|
+
} from "./api/session-state.ts";
|
|
2
8
|
export type {
|
|
9
|
+
EditorSessionState,
|
|
10
|
+
EditorHostAdapter,
|
|
3
11
|
WordReviewEditorProps,
|
|
4
12
|
WordReviewEditorRef,
|
|
5
13
|
EditorUser,
|
|
@@ -18,12 +26,32 @@ export type {
|
|
|
18
26
|
RuntimeRenderSnapshot,
|
|
19
27
|
SelectionSnapshot,
|
|
20
28
|
EditorAnchorProjection,
|
|
29
|
+
EditorStoryTarget,
|
|
30
|
+
PageLayoutSnapshot,
|
|
31
|
+
WorkspaceMode,
|
|
32
|
+
ZoomLevel,
|
|
33
|
+
SectionBreakType,
|
|
34
|
+
SectionLayoutPatch,
|
|
35
|
+
SectionPageNumberingPatch,
|
|
36
|
+
HeaderFooterLinkPatch,
|
|
37
|
+
EditorViewStateSnapshot,
|
|
38
|
+
ViewMode,
|
|
39
|
+
CaretAffinity,
|
|
40
|
+
ActiveListContext,
|
|
41
|
+
ActiveNoteContext,
|
|
42
|
+
PageRegionHitTest,
|
|
43
|
+
LayoutMeasurement,
|
|
21
44
|
DocumentStats,
|
|
22
45
|
CommentSidebarSnapshot,
|
|
23
46
|
CommentSidebarThreadSnapshot,
|
|
24
47
|
CommentSidebarThreadEntrySnapshot,
|
|
25
48
|
TrackedChangesSnapshot,
|
|
26
49
|
TrackedChangeEntrySnapshot,
|
|
50
|
+
StyleCatalogSnapshot,
|
|
51
|
+
StyleCatalogEntrySnapshot,
|
|
52
|
+
DocumentNavigationSnapshot,
|
|
53
|
+
DocumentPageSnapshot,
|
|
54
|
+
DocumentHeadingSnapshot,
|
|
27
55
|
AddCommentParams,
|
|
28
56
|
ExportDocxOptions,
|
|
29
57
|
ExportResult,
|
|
@@ -31,6 +59,8 @@ export type {
|
|
|
31
59
|
AutosaveState,
|
|
32
60
|
EditorTelemetryEvent,
|
|
33
61
|
LoadResult,
|
|
62
|
+
SaveSessionParams,
|
|
63
|
+
SaveSessionResult,
|
|
34
64
|
SaveSnapshotParams,
|
|
35
65
|
SaveSnapshotResult,
|
|
36
66
|
SaveExportParams,
|