@beyondwork/docx-react-component 1.0.13 → 1.0.15
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 +32 -35
- package/src/api/public-types.ts +6 -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/selection/review-anchors.ts +89 -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 +13 -0
- package/src/runtime/numbering-prefix.ts +195 -0
- package/src/runtime/session-capabilities.ts +22 -1
- package/src/runtime/surface-projection.ts +287 -8
- package/src/ui/WordReviewEditor.tsx +120 -10
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +8 -2
- 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/src/ui-tailwind/tw-review-workspace.tsx +3 -0
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.15",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
|
-
"packageManager": "pnpm@10.30.3",
|
|
7
6
|
"type": "module",
|
|
8
7
|
"sideEffects": [
|
|
9
8
|
"**/*.css"
|
|
@@ -34,10 +33,18 @@
|
|
|
34
33
|
"types": "./src/api/public-types.ts",
|
|
35
34
|
"import": "./src/api/public-types.ts"
|
|
36
35
|
},
|
|
36
|
+
"./compare": {
|
|
37
|
+
"types": "./src/compare/index.ts",
|
|
38
|
+
"import": "./src/compare/index.ts"
|
|
39
|
+
},
|
|
37
40
|
"./io/docx-session": {
|
|
38
41
|
"types": "./src/io/docx-session.ts",
|
|
39
42
|
"import": "./src/io/docx-session.ts"
|
|
40
43
|
},
|
|
44
|
+
"./legal": {
|
|
45
|
+
"types": "./src/legal/index.ts",
|
|
46
|
+
"import": "./src/legal/index.ts"
|
|
47
|
+
},
|
|
41
48
|
"./runtime/document-runtime": {
|
|
42
49
|
"types": "./src/runtime/document-runtime.ts",
|
|
43
50
|
"import": "./src/runtime/document-runtime.ts"
|
|
@@ -46,29 +53,6 @@
|
|
|
46
53
|
"./package.json": "./package.json"
|
|
47
54
|
},
|
|
48
55
|
"types": "./src/index.ts",
|
|
49
|
-
"scripts": {
|
|
50
|
-
"build": "tsup",
|
|
51
|
-
"test": "bash scripts/run-workspace-tests.sh",
|
|
52
|
-
"test:repo": "node scripts/run-repo-tests.mjs core",
|
|
53
|
-
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
54
|
-
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
55
|
-
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
56
|
-
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
57
|
-
"lint": "pnpm run lint:no-authored-js && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
58
|
-
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
59
|
-
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
60
|
-
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
61
|
-
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
62
|
-
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
63
|
-
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
64
|
-
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
65
|
-
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
66
|
-
"wave:status": "bash scripts/wave-status.sh",
|
|
67
|
-
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
68
|
-
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
69
|
-
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
70
|
-
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
71
|
-
},
|
|
72
56
|
"keywords": [
|
|
73
57
|
"docx",
|
|
74
58
|
"word",
|
|
@@ -114,7 +98,7 @@
|
|
|
114
98
|
"tailwindcss": "^4.2.2"
|
|
115
99
|
},
|
|
116
100
|
"devDependencies": {
|
|
117
|
-
"@chllming/wave-orchestration": "^0.9.
|
|
101
|
+
"@chllming/wave-orchestration": "^0.9.15",
|
|
118
102
|
"@types/react": "19.2.14",
|
|
119
103
|
"@types/react-dom": "19.2.3",
|
|
120
104
|
"@typescript/native-preview": "7.0.0-dev.20260409.1",
|
|
@@ -131,14 +115,27 @@
|
|
|
131
115
|
"tsup": "^8.3.0",
|
|
132
116
|
"tsx": "^4.21.0"
|
|
133
117
|
},
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
118
|
+
"scripts": {
|
|
119
|
+
"build": "tsup",
|
|
120
|
+
"test": "bash scripts/run-workspace-tests.sh",
|
|
121
|
+
"test:repo": "node scripts/run-repo-tests.mjs core",
|
|
122
|
+
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
123
|
+
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
124
|
+
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
125
|
+
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
126
|
+
"lint": "pnpm run lint:no-authored-js && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
127
|
+
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
128
|
+
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
129
|
+
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
130
|
+
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
131
|
+
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
132
|
+
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
133
|
+
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
134
|
+
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
135
|
+
"wave:status": "bash scripts/wave-status.sh",
|
|
136
|
+
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
137
|
+
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
138
|
+
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
139
|
+
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
143
140
|
}
|
|
144
141
|
}
|
package/src/api/public-types.ts
CHANGED
|
@@ -571,16 +571,21 @@ export interface WordReviewEditorRef {
|
|
|
571
571
|
reopenComment(commentId: string): void;
|
|
572
572
|
addCommentReply(commentId: string, body: string): void;
|
|
573
573
|
editCommentBody(commentId: string, body: string): void;
|
|
574
|
+
deleteComment(commentId: string): void;
|
|
574
575
|
acceptChange(changeId: string): void;
|
|
575
576
|
rejectChange(changeId: string): void;
|
|
576
577
|
acceptAllChanges(): void;
|
|
577
578
|
rejectAllChanges(): void;
|
|
578
579
|
exportDocx(options?: ExportDocxOptions): Promise<ExportResult>;
|
|
579
580
|
getSnapshot(): PersistedEditorSnapshot;
|
|
581
|
+
getRenderSnapshot(): RuntimeRenderSnapshot;
|
|
580
582
|
getCompatibilityReport(): CompatibilityReport;
|
|
581
583
|
getWarnings(): EditorWarning[];
|
|
584
|
+
getCommentSidebarSnapshot(): CommentSidebarSnapshot;
|
|
585
|
+
getTrackedChangesSnapshot(): TrackedChangesSnapshot;
|
|
582
586
|
getComments(): CommentSidebarSnapshot;
|
|
583
587
|
getTrackedChanges(): TrackedChangesSnapshot;
|
|
588
|
+
isDirty(): boolean;
|
|
584
589
|
getFormattingState(): FormattingStateSnapshot;
|
|
585
590
|
toggleBold(): void;
|
|
586
591
|
toggleItalic(): void;
|
|
@@ -610,6 +615,7 @@ export interface WordReviewEditorRef {
|
|
|
610
615
|
setCellBackground(color: string): void;
|
|
611
616
|
search(query: string, options?: SearchOptions): SearchResultSnapshot[];
|
|
612
617
|
clearSearch(): void;
|
|
618
|
+
setSelection(selection: SelectionSnapshot | null): void;
|
|
613
619
|
scrollToRevision(revisionId: string): void;
|
|
614
620
|
scrollToComment(commentId: string): void;
|
|
615
621
|
}
|
|
@@ -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,
|
|
@@ -76,6 +76,16 @@ export function rangeStaysWithinSingleParagraph(
|
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const surfaceBlocks = readSurfaceBlocks(content);
|
|
80
|
+
if (surfaceBlocks) {
|
|
81
|
+
return surfaceBlocks.some(
|
|
82
|
+
(block) =>
|
|
83
|
+
block.kind === "paragraph" &&
|
|
84
|
+
normalized.from >= block.from &&
|
|
85
|
+
normalized.to <= block.to,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
const story = parseTextStory(content);
|
|
80
90
|
const upperBound = Math.min(normalized.to, story.units.length);
|
|
81
91
|
|
|
@@ -92,3 +102,82 @@ export function rangeStaysWithinSingleParagraph(
|
|
|
92
102
|
|
|
93
103
|
return true;
|
|
94
104
|
}
|
|
105
|
+
|
|
106
|
+
export function canCreateDocxCommentAnchor(
|
|
107
|
+
content: unknown,
|
|
108
|
+
anchor: ReviewAnchor,
|
|
109
|
+
): boolean {
|
|
110
|
+
if (anchor.kind !== "range") {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const normalized = normalizeRange(anchor.range);
|
|
115
|
+
if (normalized.from === normalized.to) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return rangeStaysWithinSingleParagraph(content, normalized);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readSurfaceBlocks(
|
|
123
|
+
content: unknown,
|
|
124
|
+
): Array<{ kind: string; from: number; to: number }> | undefined {
|
|
125
|
+
if (!content || typeof content !== "object" || !("blocks" in content)) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const blocks = (content as { blocks?: unknown }).blocks;
|
|
130
|
+
if (!Array.isArray(blocks)) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const normalized = flattenSurfaceBlocks(blocks);
|
|
135
|
+
|
|
136
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function flattenSurfaceBlocks(
|
|
140
|
+
blocks: unknown[],
|
|
141
|
+
): Array<{ kind: string; from: number; to: number }> {
|
|
142
|
+
const flattened: Array<{ kind: string; from: number; to: number }> = [];
|
|
143
|
+
|
|
144
|
+
for (const block of blocks) {
|
|
145
|
+
if (
|
|
146
|
+
!block ||
|
|
147
|
+
typeof block !== "object" ||
|
|
148
|
+
typeof (block as { kind?: unknown }).kind !== "string" ||
|
|
149
|
+
typeof (block as { from?: unknown }).from !== "number" ||
|
|
150
|
+
typeof (block as { to?: unknown }).to !== "number"
|
|
151
|
+
) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
flattened.push({
|
|
156
|
+
kind: (block as { kind: string }).kind,
|
|
157
|
+
from: (block as { from: number }).from,
|
|
158
|
+
to: (block as { to: number }).to,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
(block as { kind: string }).kind === "table" &&
|
|
163
|
+
Array.isArray((block as { rows?: unknown }).rows)
|
|
164
|
+
) {
|
|
165
|
+
for (const row of (block as { rows: Array<{ cells?: unknown[] }> }).rows) {
|
|
166
|
+
for (const cell of row.cells ?? []) {
|
|
167
|
+
if (cell && typeof cell === "object" && Array.isArray((cell as { content?: unknown[] }).content)) {
|
|
168
|
+
flattened.push(...flattenSurfaceBlocks((cell as { content: unknown[] }).content));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
(block as { kind: string }).kind === "sdt_block" &&
|
|
176
|
+
Array.isArray((block as { children?: unknown[] }).children)
|
|
177
|
+
) {
|
|
178
|
+
flattened.push(...flattenSurfaceBlocks((block as { children: unknown[] }).children));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return flattened;
|
|
183
|
+
}
|
|
@@ -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
|
+
}
|