@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 CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.13",
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.13",
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
- "pnpm": {
135
- "onlyBuiltDependencies": [
136
- "esbuild",
137
- "sharp"
138
- ],
139
- "overrides": {
140
- "react": "19.2.4",
141
- "react-dom": "19.2.4"
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
  }
@@ -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
- buildEntries.push({
157
- block: cloneBlock(targetBlocks[match.targetIndex]),
158
- tracked: false,
159
- blockType: targetBlocks[match.targetIndex].type,
160
- beforeText: getBlockDisplayText(baseBlocks[match.baseIndex]),
161
- afterText: getBlockDisplayText(targetBlocks[match.targetIndex]),
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";
@@ -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, "&amp;")
68
+ .replace(/"/g, "&quot;")
69
+ .replace(/</g, "&lt;")
70
+ .replace(/>/g, "&gt;")
71
+ .replace(/'/g, "&apos;");
72
+ }