@beyondwork/docx-react-component 1.0.14 → 1.0.16
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 +43 -24
- package/src/api/public-types.ts +15 -0
- package/src/compare/diff-engine.ts +84 -7
- package/src/compare/index.ts +25 -0
- package/src/compare/snapshot.ts +31 -0
- package/src/core/commands/formatting-commands.ts +225 -0
- package/src/formats/xlsx/io/serialize-shared-strings.ts +72 -0
- package/src/formats/xlsx/io/serialize-sheet.ts +333 -0
- package/src/formats/xlsx/io/serialize-styles.ts +98 -0
- package/src/formats/xlsx/io/serialize-workbook.ts +429 -0
- package/src/formats/xlsx/runtime/cell-commands.ts +567 -0
- package/src/formats/xlsx/runtime/sheet-commands.ts +206 -0
- package/src/formats/xlsx/runtime/workbook-runtime.ts +177 -0
- package/src/formats/xlsx/runtime/workbook-transaction.ts +822 -0
- package/src/io/ooxml/parse-main-document.ts +6 -6
- package/src/io/ooxml/parse-revisions.ts +18 -24
- package/src/legal/bookmarks.ts +35 -0
- package/src/legal/index.ts +32 -0
- package/src/legal/signature-blocks.ts +259 -0
- package/src/runtime/document-runtime.ts +43 -0
- package/src/runtime/numbering-prefix.ts +195 -0
- package/src/runtime/surface-projection.ts +292 -9
- package/src/ui/WordReviewEditor.tsx +107 -4
- package/src/ui-tailwind/editor-surface/pm-schema.ts +148 -13
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +15 -29
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.16",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
|
+
"packageManager": "pnpm@10.30.3",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"sideEffects": [
|
|
8
9
|
"**/*.css"
|
|
@@ -33,10 +34,18 @@
|
|
|
33
34
|
"types": "./src/api/public-types.ts",
|
|
34
35
|
"import": "./src/api/public-types.ts"
|
|
35
36
|
},
|
|
37
|
+
"./compare": {
|
|
38
|
+
"types": "./src/compare/index.ts",
|
|
39
|
+
"import": "./src/compare/index.ts"
|
|
40
|
+
},
|
|
36
41
|
"./io/docx-session": {
|
|
37
42
|
"types": "./src/io/docx-session.ts",
|
|
38
43
|
"import": "./src/io/docx-session.ts"
|
|
39
44
|
},
|
|
45
|
+
"./legal": {
|
|
46
|
+
"types": "./src/legal/index.ts",
|
|
47
|
+
"import": "./src/legal/index.ts"
|
|
48
|
+
},
|
|
40
49
|
"./runtime/document-runtime": {
|
|
41
50
|
"types": "./src/runtime/document-runtime.ts",
|
|
42
51
|
"import": "./src/runtime/document-runtime.ts"
|
|
@@ -45,6 +54,29 @@
|
|
|
45
54
|
"./package.json": "./package.json"
|
|
46
55
|
},
|
|
47
56
|
"types": "./src/index.ts",
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"test": "bash scripts/run-workspace-tests.sh",
|
|
60
|
+
"test:repo": "node scripts/run-repo-tests.mjs core",
|
|
61
|
+
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
62
|
+
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
63
|
+
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
64
|
+
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
65
|
+
"lint": "pnpm run lint:no-authored-js && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
66
|
+
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
67
|
+
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
68
|
+
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
69
|
+
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
70
|
+
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
71
|
+
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
72
|
+
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
73
|
+
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
74
|
+
"wave:status": "bash scripts/wave-status.sh",
|
|
75
|
+
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
76
|
+
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
77
|
+
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
78
|
+
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
79
|
+
},
|
|
48
80
|
"keywords": [
|
|
49
81
|
"docx",
|
|
50
82
|
"word",
|
|
@@ -90,7 +122,7 @@
|
|
|
90
122
|
"tailwindcss": "^4.2.2"
|
|
91
123
|
},
|
|
92
124
|
"devDependencies": {
|
|
93
|
-
"@chllming/wave-orchestration": "^0.9.
|
|
125
|
+
"@chllming/wave-orchestration": "^0.9.15",
|
|
94
126
|
"@types/react": "19.2.14",
|
|
95
127
|
"@types/react-dom": "19.2.3",
|
|
96
128
|
"@typescript/native-preview": "7.0.0-dev.20260409.1",
|
|
@@ -107,27 +139,14 @@
|
|
|
107
139
|
"tsup": "^8.3.0",
|
|
108
140
|
"tsx": "^4.21.0"
|
|
109
141
|
},
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
120
|
-
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
121
|
-
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
122
|
-
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
123
|
-
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
124
|
-
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
125
|
-
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
126
|
-
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
127
|
-
"wave:status": "bash scripts/wave-status.sh",
|
|
128
|
-
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
129
|
-
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
130
|
-
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
131
|
-
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
142
|
+
"pnpm": {
|
|
143
|
+
"onlyBuiltDependencies": [
|
|
144
|
+
"esbuild",
|
|
145
|
+
"sharp"
|
|
146
|
+
],
|
|
147
|
+
"overrides": {
|
|
148
|
+
"react": "19.2.4",
|
|
149
|
+
"react-dom": "19.2.4"
|
|
150
|
+
}
|
|
132
151
|
}
|
|
133
152
|
}
|
package/src/api/public-types.ts
CHANGED
|
@@ -125,6 +125,13 @@ export interface TrackedChangesSnapshot {
|
|
|
125
125
|
|
|
126
126
|
export type FormattingAlignment = "left" | "center" | "right" | "justify";
|
|
127
127
|
|
|
128
|
+
export interface FormattingBreadcrumbItem {
|
|
129
|
+
kind: "table" | "table_row" | "table_cell" | "paragraph" | "sdt_block" | "opaque_block";
|
|
130
|
+
label: string;
|
|
131
|
+
from: number;
|
|
132
|
+
to: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
export interface FormattingStateSnapshot {
|
|
129
136
|
bold: boolean;
|
|
130
137
|
italic: boolean;
|
|
@@ -137,6 +144,7 @@ export interface FormattingStateSnapshot {
|
|
|
137
144
|
textColor?: string;
|
|
138
145
|
highlightColor?: string | null;
|
|
139
146
|
alignment?: FormattingAlignment;
|
|
147
|
+
breadcrumb: FormattingBreadcrumbItem[];
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
export interface SearchOptions {
|
|
@@ -571,17 +579,23 @@ export interface WordReviewEditorRef {
|
|
|
571
579
|
reopenComment(commentId: string): void;
|
|
572
580
|
addCommentReply(commentId: string, body: string): void;
|
|
573
581
|
editCommentBody(commentId: string, body: string): void;
|
|
582
|
+
deleteComment(commentId: string): void;
|
|
574
583
|
acceptChange(changeId: string): void;
|
|
575
584
|
rejectChange(changeId: string): void;
|
|
576
585
|
acceptAllChanges(): void;
|
|
577
586
|
rejectAllChanges(): void;
|
|
578
587
|
exportDocx(options?: ExportDocxOptions): Promise<ExportResult>;
|
|
579
588
|
getSnapshot(): PersistedEditorSnapshot;
|
|
589
|
+
getRenderSnapshot(): RuntimeRenderSnapshot;
|
|
580
590
|
getCompatibilityReport(): CompatibilityReport;
|
|
581
591
|
getWarnings(): EditorWarning[];
|
|
592
|
+
getCommentSidebarSnapshot(): CommentSidebarSnapshot;
|
|
593
|
+
getTrackedChangesSnapshot(): TrackedChangesSnapshot;
|
|
582
594
|
getComments(): CommentSidebarSnapshot;
|
|
583
595
|
getTrackedChanges(): TrackedChangesSnapshot;
|
|
596
|
+
isDirty(): boolean;
|
|
584
597
|
getFormattingState(): FormattingStateSnapshot;
|
|
598
|
+
replaceText(text: string, target?: EditorAnchorProjection): void;
|
|
585
599
|
toggleBold(): void;
|
|
586
600
|
toggleItalic(): void;
|
|
587
601
|
toggleUnderline(): void;
|
|
@@ -610,6 +624,7 @@ export interface WordReviewEditorRef {
|
|
|
610
624
|
setCellBackground(color: string): void;
|
|
611
625
|
search(query: string, options?: SearchOptions): SearchResultSnapshot[];
|
|
612
626
|
clearSearch(): void;
|
|
627
|
+
setSelection(selection: SelectionSnapshot | null): void;
|
|
613
628
|
scrollToRevision(revisionId: string): void;
|
|
614
629
|
scrollToComment(commentId: string): void;
|
|
615
630
|
}
|
|
@@ -20,6 +20,7 @@ export interface CompareVersionRef {
|
|
|
20
20
|
export type CompareChangeKind =
|
|
21
21
|
| "paragraph-insertion"
|
|
22
22
|
| "paragraph-deletion"
|
|
23
|
+
| "paragraph-modification"
|
|
23
24
|
| "structural-insertion"
|
|
24
25
|
| "structural-deletion";
|
|
25
26
|
|
|
@@ -96,6 +97,22 @@ export function compareDocumentSnapshots(
|
|
|
96
97
|
const insertBlock = nextTargetIndex < match.targetIndex ? targetBlocks[nextTargetIndex] : undefined;
|
|
97
98
|
|
|
98
99
|
if (deleteBlock && insertBlock) {
|
|
100
|
+
if (deleteBlock.type === "paragraph" && insertBlock.type === "paragraph") {
|
|
101
|
+
const modificationId = `change-${nextChangeNumber}`;
|
|
102
|
+
nextChangeNumber += 1;
|
|
103
|
+
addParagraphModificationEntry(
|
|
104
|
+
buildEntries,
|
|
105
|
+
changes,
|
|
106
|
+
modificationId,
|
|
107
|
+
deleteBlock,
|
|
108
|
+
insertBlock,
|
|
109
|
+
nextTargetIndex,
|
|
110
|
+
);
|
|
111
|
+
nextBaseIndex += 1;
|
|
112
|
+
nextTargetIndex += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
const deletionId = `change-${nextChangeNumber}`;
|
|
100
117
|
nextChangeNumber += 1;
|
|
101
118
|
addTrackedOrStructuralEntry(
|
|
@@ -153,13 +170,34 @@ export function compareDocumentSnapshots(
|
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
if (match.baseIndex < baseBlocks.length && match.targetIndex < targetBlocks.length) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
const baseText = getBlockDisplayText(baseBlocks[match.baseIndex]);
|
|
174
|
+
const targetText = getBlockDisplayText(targetBlocks[match.targetIndex]);
|
|
175
|
+
const isModified =
|
|
176
|
+
baseBlocks[match.baseIndex].type === "paragraph" &&
|
|
177
|
+
targetBlocks[match.targetIndex].type === "paragraph" &&
|
|
178
|
+
baseText !== targetText;
|
|
179
|
+
|
|
180
|
+
if (isModified) {
|
|
181
|
+
const modChangeId = `change-${nextChangeNumber}`;
|
|
182
|
+
nextChangeNumber += 1;
|
|
183
|
+
addParagraphModificationEntry(
|
|
184
|
+
buildEntries,
|
|
185
|
+
changes,
|
|
186
|
+
modChangeId,
|
|
187
|
+
baseBlocks[match.baseIndex] as ParagraphNode,
|
|
188
|
+
targetBlocks[match.targetIndex] as ParagraphNode,
|
|
189
|
+
match.targetIndex,
|
|
190
|
+
);
|
|
191
|
+
} else {
|
|
192
|
+
buildEntries.push({
|
|
193
|
+
block: cloneBlock(targetBlocks[match.targetIndex]),
|
|
194
|
+
tracked: false,
|
|
195
|
+
blockType: targetBlocks[match.targetIndex].type,
|
|
196
|
+
beforeText: baseText,
|
|
197
|
+
afterText: targetText,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
163
201
|
nextBaseIndex = match.baseIndex + 1;
|
|
164
202
|
nextTargetIndex = match.targetIndex + 1;
|
|
165
203
|
}
|
|
@@ -222,6 +260,45 @@ function addTrackedOrStructuralEntry(
|
|
|
222
260
|
});
|
|
223
261
|
}
|
|
224
262
|
|
|
263
|
+
function addParagraphModificationEntry(
|
|
264
|
+
buildEntries: BuildEntry[],
|
|
265
|
+
changes: CompareChange[],
|
|
266
|
+
changeId: string,
|
|
267
|
+
baseParagraph: ParagraphNode,
|
|
268
|
+
targetParagraph: ParagraphNode,
|
|
269
|
+
targetIndex: number,
|
|
270
|
+
): void {
|
|
271
|
+
const beforeText = getBlockDisplayText(baseParagraph);
|
|
272
|
+
const afterText = getBlockDisplayText(targetParagraph);
|
|
273
|
+
|
|
274
|
+
buildEntries.push({
|
|
275
|
+
changeId: `${changeId}-before`,
|
|
276
|
+
block: cloneBlock(baseParagraph),
|
|
277
|
+
trackChange: "deletion",
|
|
278
|
+
tracked: true,
|
|
279
|
+
blockType: "paragraph",
|
|
280
|
+
beforeText,
|
|
281
|
+
});
|
|
282
|
+
buildEntries.push({
|
|
283
|
+
changeId: `${changeId}-after`,
|
|
284
|
+
block: cloneBlock(targetParagraph),
|
|
285
|
+
trackChange: "insertion",
|
|
286
|
+
tracked: true,
|
|
287
|
+
blockType: "paragraph",
|
|
288
|
+
afterText,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
changes.push({
|
|
292
|
+
changeId,
|
|
293
|
+
kind: "paragraph-modification",
|
|
294
|
+
tracked: true,
|
|
295
|
+
blockType: "paragraph",
|
|
296
|
+
beforeText,
|
|
297
|
+
afterText,
|
|
298
|
+
targetIndex,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
225
302
|
function buildComparedDocument(
|
|
226
303
|
base: CanonicalDocument,
|
|
227
304
|
target: CanonicalDocument,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export {
|
|
2
|
+
compareDocumentSnapshots,
|
|
3
|
+
type CompareChange,
|
|
4
|
+
type CompareChangeKind,
|
|
5
|
+
type CompareDocumentVersionsOptions,
|
|
6
|
+
type CompareVersionRef,
|
|
7
|
+
type VersionCompareResult,
|
|
8
|
+
} from "./diff-engine.ts";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
exportComparedDocumentRedlines,
|
|
12
|
+
type ExportComparedDocumentOptions,
|
|
13
|
+
type ExportComparedDocumentResult,
|
|
14
|
+
} from "./export-redlines.ts";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
buildVersionAuditTrail,
|
|
18
|
+
createDocumentVersionSnapshot,
|
|
19
|
+
createDocumentVersionSnapshotId,
|
|
20
|
+
lockDocumentVersionSnapshot,
|
|
21
|
+
saveDocumentVersionSnapshot,
|
|
22
|
+
type CreateDocumentVersionSnapshotOptions,
|
|
23
|
+
type DocumentVersionSnapshot,
|
|
24
|
+
type VersionAuditEntry,
|
|
25
|
+
} from "./snapshot.ts";
|
package/src/compare/snapshot.ts
CHANGED
|
@@ -53,6 +53,37 @@ export function saveDocumentVersionSnapshot(
|
|
|
53
53
|
return next.sort(compareDocumentVersionSnapshots);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export interface VersionAuditEntry {
|
|
57
|
+
versionId: string;
|
|
58
|
+
name: string;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
documentSignature: string;
|
|
61
|
+
action: "created" | "compared" | "locked";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildVersionAuditTrail(
|
|
65
|
+
snapshots: readonly DocumentVersionSnapshot[],
|
|
66
|
+
): VersionAuditEntry[] {
|
|
67
|
+
return snapshots.map((snapshot) => ({
|
|
68
|
+
versionId: snapshot.versionId,
|
|
69
|
+
name: snapshot.name,
|
|
70
|
+
createdAt: snapshot.createdAt,
|
|
71
|
+
documentSignature: snapshot.documentSignature,
|
|
72
|
+
action: "created",
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function lockDocumentVersionSnapshot(
|
|
77
|
+
snapshot: DocumentVersionSnapshot,
|
|
78
|
+
): DocumentVersionSnapshot {
|
|
79
|
+
return {
|
|
80
|
+
...snapshot,
|
|
81
|
+
name: snapshot.name.startsWith("[locked] ")
|
|
82
|
+
? snapshot.name
|
|
83
|
+
: `[locked] ${snapshot.name}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
56
87
|
function compareDocumentVersionSnapshots(
|
|
57
88
|
left: DocumentVersionSnapshot,
|
|
58
89
|
right: DocumentVersionSnapshot,
|
|
@@ -21,12 +21,14 @@ import type { Command } from "prosemirror-state";
|
|
|
21
21
|
import type { MarkType, Schema } from "prosemirror-model";
|
|
22
22
|
|
|
23
23
|
import type {
|
|
24
|
+
FormattingBreadcrumbItem,
|
|
24
25
|
FormattingAlignment,
|
|
25
26
|
FormattingStateSnapshot,
|
|
26
27
|
PersistedEditorSnapshot,
|
|
27
28
|
RuntimeRenderSnapshot,
|
|
28
29
|
SurfaceBlockSnapshot,
|
|
29
30
|
SurfaceInlineSegment,
|
|
31
|
+
SurfaceTableCellSnapshot,
|
|
30
32
|
} from "../../api/public-types";
|
|
31
33
|
import type {
|
|
32
34
|
BlockNode,
|
|
@@ -209,6 +211,7 @@ const DEFAULT_FORMATTING_STATE: FormattingStateSnapshot = {
|
|
|
209
211
|
strikethrough: false,
|
|
210
212
|
superscript: false,
|
|
211
213
|
subscript: false,
|
|
214
|
+
breadcrumb: [],
|
|
212
215
|
};
|
|
213
216
|
|
|
214
217
|
const INDENT_STEP_TWIPS = 720;
|
|
@@ -268,9 +271,231 @@ export function getFormattingStateFromRenderSnapshot(
|
|
|
268
271
|
alignment: getConsistentValue(paragraphs, (paragraph) =>
|
|
269
272
|
toPublicAlignment(paragraph.alignment),
|
|
270
273
|
),
|
|
274
|
+
breadcrumb: getSelectionBreadcrumb(surface.blocks, snapshot.selection),
|
|
271
275
|
};
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
function getSelectionBreadcrumb(
|
|
279
|
+
blocks: SurfaceBlockSnapshot[],
|
|
280
|
+
selection: RuntimeRenderSnapshot["selection"],
|
|
281
|
+
): FormattingBreadcrumbItem[] {
|
|
282
|
+
const paths = collectSelectionBreadcrumbPaths(blocks, selection);
|
|
283
|
+
if (paths.length === 0) {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let commonLength = paths[0]?.length ?? 0;
|
|
288
|
+
for (let pathIndex = 1; pathIndex < paths.length; pathIndex += 1) {
|
|
289
|
+
const currentPath = paths[pathIndex] ?? [];
|
|
290
|
+
commonLength = Math.min(commonLength, currentPath.length);
|
|
291
|
+
|
|
292
|
+
let compareIndex = 0;
|
|
293
|
+
while (
|
|
294
|
+
compareIndex < commonLength &&
|
|
295
|
+
breadcrumbItemsEqual(paths[0]![compareIndex]!, currentPath[compareIndex]!)
|
|
296
|
+
) {
|
|
297
|
+
compareIndex += 1;
|
|
298
|
+
}
|
|
299
|
+
commonLength = compareIndex;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return paths[0]!.slice(0, commonLength);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function collectSelectionBreadcrumbPaths(
|
|
306
|
+
blocks: SurfaceBlockSnapshot[],
|
|
307
|
+
selection: RuntimeRenderSnapshot["selection"],
|
|
308
|
+
path: FormattingBreadcrumbItem[] = [],
|
|
309
|
+
output: FormattingBreadcrumbItem[][] = [],
|
|
310
|
+
): FormattingBreadcrumbItem[][] {
|
|
311
|
+
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex += 1) {
|
|
312
|
+
const block = blocks[blockIndex];
|
|
313
|
+
if (!block) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (
|
|
318
|
+
!selectionTouchesRange(
|
|
319
|
+
selection.anchor,
|
|
320
|
+
selection.head,
|
|
321
|
+
block.from,
|
|
322
|
+
block.to,
|
|
323
|
+
)
|
|
324
|
+
) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (block.kind === "paragraph") {
|
|
329
|
+
output.push([
|
|
330
|
+
...path,
|
|
331
|
+
{
|
|
332
|
+
kind: "paragraph" as const,
|
|
333
|
+
label: block.styleId ? `Paragraph (${block.styleId})` : "Paragraph",
|
|
334
|
+
from: block.from,
|
|
335
|
+
to: block.to,
|
|
336
|
+
},
|
|
337
|
+
]);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (block.kind === "opaque_block") {
|
|
342
|
+
output.push([
|
|
343
|
+
...path,
|
|
344
|
+
{
|
|
345
|
+
kind: "opaque_block" as const,
|
|
346
|
+
label: block.label,
|
|
347
|
+
from: block.from,
|
|
348
|
+
to: block.to,
|
|
349
|
+
},
|
|
350
|
+
]);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (block.kind === "sdt_block") {
|
|
355
|
+
collectSelectionBreadcrumbPaths(
|
|
356
|
+
block.children,
|
|
357
|
+
selection,
|
|
358
|
+
[
|
|
359
|
+
...path,
|
|
360
|
+
{
|
|
361
|
+
kind: "sdt_block" as const,
|
|
362
|
+
label: block.alias ?? block.tag ?? block.sdtType ?? "Content control",
|
|
363
|
+
from: block.from,
|
|
364
|
+
to: block.to,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
output,
|
|
368
|
+
);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (block.kind === "table") {
|
|
373
|
+
const tablePath = [
|
|
374
|
+
...path,
|
|
375
|
+
{
|
|
376
|
+
kind: "table" as const,
|
|
377
|
+
label: block.styleId ? `Table (${block.styleId})` : "Table",
|
|
378
|
+
from: block.from,
|
|
379
|
+
to: block.to,
|
|
380
|
+
},
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex += 1) {
|
|
384
|
+
const row = block.rows[rowIndex];
|
|
385
|
+
if (!row) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const rowRange = getTableRowRange(block, row);
|
|
389
|
+
if (
|
|
390
|
+
!selectionTouchesRange(
|
|
391
|
+
selection.anchor,
|
|
392
|
+
selection.head,
|
|
393
|
+
rowRange.from,
|
|
394
|
+
rowRange.to,
|
|
395
|
+
)
|
|
396
|
+
) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const rowPath = [
|
|
401
|
+
...tablePath,
|
|
402
|
+
{
|
|
403
|
+
kind: "table_row" as const,
|
|
404
|
+
label: `Row ${rowIndex + 1}`,
|
|
405
|
+
from: rowRange.from,
|
|
406
|
+
to: rowRange.to,
|
|
407
|
+
},
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex += 1) {
|
|
411
|
+
const cell = row.cells[cellIndex];
|
|
412
|
+
if (!cell) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const cellRange = getTableCellRange(block, cell);
|
|
416
|
+
if (
|
|
417
|
+
!selectionTouchesRange(
|
|
418
|
+
selection.anchor,
|
|
419
|
+
selection.head,
|
|
420
|
+
cellRange.from,
|
|
421
|
+
cellRange.to,
|
|
422
|
+
)
|
|
423
|
+
) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
collectSelectionBreadcrumbPaths(
|
|
428
|
+
cell.content,
|
|
429
|
+
selection,
|
|
430
|
+
[
|
|
431
|
+
...rowPath,
|
|
432
|
+
{
|
|
433
|
+
kind: "table_cell",
|
|
434
|
+
label: `Cell ${cellIndex + 1}`,
|
|
435
|
+
from: cellRange.from,
|
|
436
|
+
to: cellRange.to,
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
output,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return output;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function getTableRowRange(
|
|
450
|
+
table: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
|
|
451
|
+
row: (typeof table.rows)[number],
|
|
452
|
+
): { from: number; to: number } {
|
|
453
|
+
let from = Number.POSITIVE_INFINITY;
|
|
454
|
+
let to = Number.NEGATIVE_INFINITY;
|
|
455
|
+
|
|
456
|
+
for (const cell of row.cells) {
|
|
457
|
+
const range = getTableCellRange(table, cell);
|
|
458
|
+
from = Math.min(from, range.from);
|
|
459
|
+
to = Math.max(to, range.to);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return Number.isFinite(from) && Number.isFinite(to)
|
|
463
|
+
? { from, to }
|
|
464
|
+
: { from: table.from, to: table.to };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function getTableCellRange(
|
|
468
|
+
table: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
|
|
469
|
+
cell: SurfaceTableCellSnapshot,
|
|
470
|
+
): { from: number; to: number } {
|
|
471
|
+
if (cell.content.length === 0) {
|
|
472
|
+
return { from: table.from, to: table.to };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let from = Number.POSITIVE_INFINITY;
|
|
476
|
+
let to = Number.NEGATIVE_INFINITY;
|
|
477
|
+
for (const child of cell.content) {
|
|
478
|
+
from = Math.min(from, child.from);
|
|
479
|
+
to = Math.max(to, child.to);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return Number.isFinite(from) && Number.isFinite(to)
|
|
483
|
+
? { from, to }
|
|
484
|
+
: { from: table.from, to: table.to };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function breadcrumbItemsEqual(
|
|
488
|
+
left: FormattingBreadcrumbItem,
|
|
489
|
+
right: FormattingBreadcrumbItem,
|
|
490
|
+
): boolean {
|
|
491
|
+
return (
|
|
492
|
+
left.kind === right.kind &&
|
|
493
|
+
left.label === right.label &&
|
|
494
|
+
left.from === right.from &&
|
|
495
|
+
left.to === right.to
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
274
499
|
export function applyFormattingOperationToDocument(
|
|
275
500
|
document: CanonicalDocumentEnvelope,
|
|
276
501
|
snapshot: RuntimeRenderSnapshot,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CanonicalWorkbook } from "../model/workbook.ts";
|
|
2
|
+
import { listSheets } from "../model/workbook.ts";
|
|
3
|
+
|
|
4
|
+
const SHARED_STRINGS_NAMESPACE = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
5
|
+
|
|
6
|
+
export interface SharedStringTableSerialization {
|
|
7
|
+
xml: string;
|
|
8
|
+
strings: string[];
|
|
9
|
+
indexByValue: Map<string, number>;
|
|
10
|
+
totalCount: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildSharedStringsTable(
|
|
14
|
+
workbook: CanonicalWorkbook,
|
|
15
|
+
): SharedStringTableSerialization {
|
|
16
|
+
const strings: string[] = [];
|
|
17
|
+
const indexByValue = new Map<string, number>();
|
|
18
|
+
let totalCount = 0;
|
|
19
|
+
|
|
20
|
+
for (const sheet of listSheets(workbook)) {
|
|
21
|
+
for (const cell of sheet.cells.values()) {
|
|
22
|
+
if (cell.kind !== "text") {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
totalCount += 1;
|
|
27
|
+
|
|
28
|
+
if (!indexByValue.has(cell.value)) {
|
|
29
|
+
indexByValue.set(cell.value, strings.length);
|
|
30
|
+
strings.push(cell.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
xml: serializeSharedStringsXml(strings, totalCount),
|
|
37
|
+
strings,
|
|
38
|
+
indexByValue,
|
|
39
|
+
totalCount,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function serializeSharedStringsXml(
|
|
44
|
+
strings: readonly string[],
|
|
45
|
+
totalCount: number = strings.length,
|
|
46
|
+
): string {
|
|
47
|
+
const items = strings.map(serializeSharedStringItem);
|
|
48
|
+
const body = items.length > 0 ? `\n${items.join("\n")}\n` : "";
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
52
|
+
`<sst xmlns="${SHARED_STRINGS_NAMESPACE}" count="${totalCount}" uniqueCount="${strings.length}">${body}</sst>`,
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function serializeSharedStringItem(value: string): string {
|
|
57
|
+
const preserveSpace = requiresPreservedSpace(value) ? ` xml:space="preserve"` : "";
|
|
58
|
+
return ` <si><t${preserveSpace}>${escapeXml(value)}</t></si>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function requiresPreservedSpace(value: string): boolean {
|
|
62
|
+
return /^\s/.test(value) || /\s$/.test(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function escapeXml(value: string): string {
|
|
66
|
+
return value
|
|
67
|
+
.replace(/&/g, "&")
|
|
68
|
+
.replace(/"/g, """)
|
|
69
|
+
.replace(/</g, "<")
|
|
70
|
+
.replace(/>/g, ">")
|
|
71
|
+
.replace(/'/g, "'");
|
|
72
|
+
}
|