docx-diff-editor 1.0.0
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/LICENSE +191 -0
- package/README.md +218 -0
- package/dist/index.d.mts +318 -0
- package/dist/index.d.ts +318 -0
- package/dist/index.js +1194 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1168 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +384 -0
- package/package.json +73 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var DiffMatchPatch = require('diff-match-patch');
|
|
7
|
+
var uuid = require('uuid');
|
|
8
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var DiffMatchPatch__default = /*#__PURE__*/_interopDefault(DiffMatchPatch);
|
|
13
|
+
|
|
14
|
+
// src/DocxDiffEditor.tsx
|
|
15
|
+
|
|
16
|
+
// src/blankTemplate.ts
|
|
17
|
+
var BLANK_DOCX_BASE64 = `UEsDBBQAAAAIAAAAAACHTuJAXQAAAABgAAAACwAIAF9yZWxzLy5yZWxzIKIEACigAAAAAAAAAK2O
|
|
18
|
+
wQrCMBBE7wv+Q9i7TetBRKT2IoIHr7Jsm0KzCdkV9e9N8QNc5jDMvGGql9eb8KCIOgQNs6IA4VgG
|
|
19
|
+
E1yr4dQcZksQmNAZ9MGRhpchrKrJpCYfE+dD7GOPBSuc4g5TyvMlELeEPuJ0EJjzpgnRY8pjbGnG
|
|
20
|
+
9oYt0XlRLCj+D+FvK/HJ9e1j0NTUU0i/rYCtOnW7kCnGagD/n1BLAQI/AxQAAAAIAAAAAACHTuJA
|
|
21
|
+
XQAAAABgAAAACwAYAAAAAAAAAAAArYEAAAAAX3JlbHMvLnJlbHNVVAUABx4AAABQSWECAAAAAFBL
|
|
22
|
+
AQIfAxQAAAAIAAAAAABU+/yzrQEAAKYFAAARABgAAAAAAAEAAACkgZMAAABkb2NQcm9wcy9jb3Jl
|
|
23
|
+
LnhtbFVUBQAHHgAAAFBLAQIfAxQAAAAIAAAAAABJ8bJvAwEAABcDAAAQABgAAAAAAAEAAACkgZMC
|
|
24
|
+
AABkb2NQcm9wcy9hcHAueG1sVVQFAAceAAAAUEsBAh8DFAAAAAgAAAAAANeufwOOAQAA2AMAABEA
|
|
25
|
+
GAAAAAAAAQAAAKSByAMAAHdvcmQvZG9jdW1lbnQueG1sVVQFAAceAAAAUEsBAh8DFAAAAAgAAAAA
|
|
26
|
+
ADlvn/FGBAAALAIAAB8AGAAAAAAAAQAAAKSBiQUAAHdvcmQvX3JlbHMvZG9jdW1lbnQueG1sLnJl
|
|
27
|
+
bHNVVAUABx4AAABQSwECHwMUAAAACAAAAAAATbh/e0oBAACtAgAAEQAYAAAAAAABAAAApIEWBgAA
|
|
28
|
+
d29yZC9zdHlsZXMueG1sVVQFAAceAAAAUEsBAh8DFAAAAAgAAAAAAMxe3mTsAAAA/AEAABMAGAAA
|
|
29
|
+
AAAAAQAAAKSBkwcAAFtDb250ZW50X1R5cGVzXS54bWxVVAUABx4AAABQSwUGAAAAAAcABwBdAQAA
|
|
30
|
+
uggAAAAA`;
|
|
31
|
+
function base64ToBlob(base64, mimeType) {
|
|
32
|
+
const byteCharacters = atob(base64.replace(/\s/g, ""));
|
|
33
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
34
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
35
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
36
|
+
}
|
|
37
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
38
|
+
return new Blob([byteArray], { type: mimeType });
|
|
39
|
+
}
|
|
40
|
+
function base64ToFile(base64, filename, mimeType) {
|
|
41
|
+
const blob = base64ToBlob(base64, mimeType);
|
|
42
|
+
return new File([blob], filename, { type: mimeType });
|
|
43
|
+
}
|
|
44
|
+
function getBlankTemplateFile() {
|
|
45
|
+
return base64ToFile(
|
|
46
|
+
BLANK_DOCX_BASE64,
|
|
47
|
+
"blank-template.docx",
|
|
48
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
function getBlankTemplateBlob() {
|
|
52
|
+
return base64ToBlob(
|
|
53
|
+
BLANK_DOCX_BASE64,
|
|
54
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
function isValidDocxFile(file) {
|
|
58
|
+
const validTypes = [
|
|
59
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
60
|
+
"application/msword"
|
|
61
|
+
];
|
|
62
|
+
return validTypes.includes(file.type) || file.name.endsWith(".docx");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/constants.ts
|
|
66
|
+
var DEFAULT_AUTHOR = {
|
|
67
|
+
name: "DocxDiff Editor",
|
|
68
|
+
email: "editor@docxdiff.local"
|
|
69
|
+
};
|
|
70
|
+
var DEFAULT_SUPERDOC_USER = {
|
|
71
|
+
name: "DocxDiff User",
|
|
72
|
+
email: "user@docxdiff.local"
|
|
73
|
+
};
|
|
74
|
+
var TRACK_CHANGE_PERMISSIONS = [
|
|
75
|
+
"RESOLVE_OWN",
|
|
76
|
+
"RESOLVE_OTHER",
|
|
77
|
+
"REJECT_OWN",
|
|
78
|
+
"REJECT_OTHER"
|
|
79
|
+
];
|
|
80
|
+
var CSS_PREFIX = "dde";
|
|
81
|
+
var TIMEOUTS = {
|
|
82
|
+
/** Timeout for document parsing (ms) */
|
|
83
|
+
PARSE_TIMEOUT: 3e4,
|
|
84
|
+
/** Small delay for React settling (ms) */
|
|
85
|
+
INIT_DELAY: 100,
|
|
86
|
+
/** Cleanup delay (ms) */
|
|
87
|
+
CLEANUP_DELAY: 100
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/services/contentResolver.ts
|
|
91
|
+
function detectContentType(content) {
|
|
92
|
+
if (content instanceof File) {
|
|
93
|
+
return "file";
|
|
94
|
+
}
|
|
95
|
+
if (typeof content === "string") {
|
|
96
|
+
return "html";
|
|
97
|
+
}
|
|
98
|
+
return "json";
|
|
99
|
+
}
|
|
100
|
+
function isProseMirrorJSON(content) {
|
|
101
|
+
if (!content || typeof content !== "object") return false;
|
|
102
|
+
const obj = content;
|
|
103
|
+
return typeof obj.type === "string" && (obj.type === "doc" || Array.isArray(obj.content));
|
|
104
|
+
}
|
|
105
|
+
async function parseDocxFile(file, SuperDoc) {
|
|
106
|
+
const container = document.createElement("div");
|
|
107
|
+
container.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:800px;height:600px;visibility:hidden;";
|
|
108
|
+
document.body.appendChild(container);
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
let superdoc = null;
|
|
111
|
+
let resolved = false;
|
|
112
|
+
const cleanup = () => {
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
if (superdoc) {
|
|
115
|
+
try {
|
|
116
|
+
const sd = superdoc;
|
|
117
|
+
superdoc = null;
|
|
118
|
+
sd.destroy?.();
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (container.parentNode) {
|
|
123
|
+
container.parentNode.removeChild(container);
|
|
124
|
+
}
|
|
125
|
+
}, TIMEOUTS.CLEANUP_DELAY);
|
|
126
|
+
};
|
|
127
|
+
setTimeout(async () => {
|
|
128
|
+
if (resolved) return;
|
|
129
|
+
try {
|
|
130
|
+
superdoc = new SuperDoc({
|
|
131
|
+
selector: container,
|
|
132
|
+
document: file,
|
|
133
|
+
documentMode: "viewing",
|
|
134
|
+
rulers: false,
|
|
135
|
+
user: { name: "Parser", email: "parser@local" },
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
|
+
onReady: ({ superdoc: sd }) => {
|
|
138
|
+
if (resolved) return;
|
|
139
|
+
try {
|
|
140
|
+
const editor = sd?.activeEditor;
|
|
141
|
+
if (!editor) {
|
|
142
|
+
throw new Error("No active editor found");
|
|
143
|
+
}
|
|
144
|
+
const json = editor.getJSON();
|
|
145
|
+
resolved = true;
|
|
146
|
+
cleanup();
|
|
147
|
+
resolve(json);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
resolved = true;
|
|
150
|
+
cleanup();
|
|
151
|
+
reject(err);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
onException: ({ error: err }) => {
|
|
155
|
+
if (resolved) return;
|
|
156
|
+
resolved = true;
|
|
157
|
+
cleanup();
|
|
158
|
+
reject(err);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
if (!resolved) {
|
|
163
|
+
resolved = true;
|
|
164
|
+
cleanup();
|
|
165
|
+
reject(new Error("Document parsing timed out"));
|
|
166
|
+
}
|
|
167
|
+
}, TIMEOUTS.PARSE_TIMEOUT);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
cleanup();
|
|
170
|
+
reject(err);
|
|
171
|
+
}
|
|
172
|
+
}, 50);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async function parseHtmlContent(html, SuperDoc, templateDocx) {
|
|
176
|
+
const template = templateDocx || getBlankTemplateFile();
|
|
177
|
+
const container = document.createElement("div");
|
|
178
|
+
container.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:800px;height:600px;visibility:hidden;";
|
|
179
|
+
document.body.appendChild(container);
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
let superdoc = null;
|
|
182
|
+
let resolved = false;
|
|
183
|
+
const cleanup = () => {
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
if (superdoc) {
|
|
186
|
+
try {
|
|
187
|
+
const sd = superdoc;
|
|
188
|
+
superdoc = null;
|
|
189
|
+
sd.destroy?.();
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (container.parentNode) {
|
|
194
|
+
container.parentNode.removeChild(container);
|
|
195
|
+
}
|
|
196
|
+
}, TIMEOUTS.CLEANUP_DELAY);
|
|
197
|
+
};
|
|
198
|
+
setTimeout(async () => {
|
|
199
|
+
if (resolved) return;
|
|
200
|
+
try {
|
|
201
|
+
superdoc = new SuperDoc({
|
|
202
|
+
selector: container,
|
|
203
|
+
document: template,
|
|
204
|
+
html,
|
|
205
|
+
// SuperDoc's HTML initialization option
|
|
206
|
+
documentMode: "viewing",
|
|
207
|
+
rulers: false,
|
|
208
|
+
user: { name: "Parser", email: "parser@local" },
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
|
+
onReady: ({ superdoc: sd }) => {
|
|
211
|
+
if (resolved) return;
|
|
212
|
+
try {
|
|
213
|
+
const editor = sd?.activeEditor;
|
|
214
|
+
if (!editor) {
|
|
215
|
+
throw new Error("No active editor found");
|
|
216
|
+
}
|
|
217
|
+
const json = editor.getJSON();
|
|
218
|
+
resolved = true;
|
|
219
|
+
cleanup();
|
|
220
|
+
resolve(json);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
resolved = true;
|
|
223
|
+
cleanup();
|
|
224
|
+
reject(err);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
onException: ({ error: err }) => {
|
|
228
|
+
if (resolved) return;
|
|
229
|
+
resolved = true;
|
|
230
|
+
cleanup();
|
|
231
|
+
reject(err);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
if (!resolved) {
|
|
236
|
+
resolved = true;
|
|
237
|
+
cleanup();
|
|
238
|
+
reject(new Error("HTML parsing timed out"));
|
|
239
|
+
}
|
|
240
|
+
}, TIMEOUTS.PARSE_TIMEOUT);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
cleanup();
|
|
243
|
+
reject(err);
|
|
244
|
+
}
|
|
245
|
+
}, 50);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
async function resolveContent(content, SuperDoc, templateDocx) {
|
|
249
|
+
const type = detectContentType(content);
|
|
250
|
+
switch (type) {
|
|
251
|
+
case "file":
|
|
252
|
+
return {
|
|
253
|
+
json: await parseDocxFile(content, SuperDoc),
|
|
254
|
+
type: "file"
|
|
255
|
+
};
|
|
256
|
+
case "html":
|
|
257
|
+
return {
|
|
258
|
+
json: await parseHtmlContent(content, SuperDoc, templateDocx),
|
|
259
|
+
type: "html"
|
|
260
|
+
};
|
|
261
|
+
case "json":
|
|
262
|
+
if (!isProseMirrorJSON(content)) {
|
|
263
|
+
throw new Error("Invalid ProseMirror JSON structure");
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
json: content,
|
|
267
|
+
type: "json"
|
|
268
|
+
};
|
|
269
|
+
default:
|
|
270
|
+
throw new Error(`Unknown content type: ${type}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
var dmp = new DiffMatchPatch__default.default();
|
|
274
|
+
var DIFF_DELETE = -1;
|
|
275
|
+
var DIFF_INSERT = 1;
|
|
276
|
+
var DIFF_EQUAL = 0;
|
|
277
|
+
function extractTextSpans(node, offset = 0) {
|
|
278
|
+
const spans = [];
|
|
279
|
+
if (!node) return spans;
|
|
280
|
+
if (node.type === "text" && node.text) {
|
|
281
|
+
spans.push({
|
|
282
|
+
text: node.text,
|
|
283
|
+
from: offset,
|
|
284
|
+
to: offset + node.text.length,
|
|
285
|
+
marks: node.marks || []
|
|
286
|
+
});
|
|
287
|
+
return spans;
|
|
288
|
+
}
|
|
289
|
+
if (node.content && Array.isArray(node.content)) {
|
|
290
|
+
let currentOffset = offset;
|
|
291
|
+
for (const child of node.content) {
|
|
292
|
+
const childSpans = extractTextSpans(child, currentOffset);
|
|
293
|
+
spans.push(...childSpans);
|
|
294
|
+
for (const span of childSpans) {
|
|
295
|
+
currentOffset = Math.max(currentOffset, span.to);
|
|
296
|
+
}
|
|
297
|
+
if (childSpans.length === 0 && child.type === "text" && child.text) {
|
|
298
|
+
currentOffset += child.text.length;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return spans;
|
|
303
|
+
}
|
|
304
|
+
function extractTextContent(node) {
|
|
305
|
+
if (!node) return "";
|
|
306
|
+
if (node.type === "text" && node.text) {
|
|
307
|
+
return node.text;
|
|
308
|
+
}
|
|
309
|
+
if (node.content && Array.isArray(node.content)) {
|
|
310
|
+
return node.content.map(extractTextContent).join("");
|
|
311
|
+
}
|
|
312
|
+
return "";
|
|
313
|
+
}
|
|
314
|
+
function deepEqual(a, b) {
|
|
315
|
+
if (a === b) return true;
|
|
316
|
+
if (typeof a !== typeof b) return false;
|
|
317
|
+
if (typeof a !== "object" || a === null || b === null) return false;
|
|
318
|
+
const objA = a;
|
|
319
|
+
const objB = b;
|
|
320
|
+
const keysA = Object.keys(objA);
|
|
321
|
+
const keysB = Object.keys(objB);
|
|
322
|
+
if (keysA.length !== keysB.length) return false;
|
|
323
|
+
for (const key of keysA) {
|
|
324
|
+
if (!keysB.includes(key)) return false;
|
|
325
|
+
if (!deepEqual(objA[key], objB[key])) return false;
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
function marksEqual(marksA, marksB) {
|
|
330
|
+
if (marksA.length !== marksB.length) return false;
|
|
331
|
+
const sortedA = [...marksA].sort((a, b) => (a.type || "").localeCompare(b.type || ""));
|
|
332
|
+
const sortedB = [...marksB].sort((a, b) => (a.type || "").localeCompare(b.type || ""));
|
|
333
|
+
return deepEqual(sortedA, sortedB);
|
|
334
|
+
}
|
|
335
|
+
function getMarksAtPosition(spans, pos) {
|
|
336
|
+
for (const span of spans) {
|
|
337
|
+
if (pos >= span.from && pos < span.to) {
|
|
338
|
+
return span.marks;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
function detectFormatChanges(spansA, spansB, segments) {
|
|
344
|
+
const formatChanges = [];
|
|
345
|
+
let posA = 0;
|
|
346
|
+
let posB = 0;
|
|
347
|
+
for (const segment of segments) {
|
|
348
|
+
if (segment.type === "equal") {
|
|
349
|
+
let i = 0;
|
|
350
|
+
while (i < segment.text.length) {
|
|
351
|
+
const marksA = getMarksAtPosition(spansA, posA + i);
|
|
352
|
+
const marksB = getMarksAtPosition(spansB, posB + i);
|
|
353
|
+
if (!marksEqual(marksA, marksB)) {
|
|
354
|
+
const startI = i;
|
|
355
|
+
const startMarksA = marksA;
|
|
356
|
+
const startMarksB = marksB;
|
|
357
|
+
while (i < segment.text.length) {
|
|
358
|
+
const currentMarksA = getMarksAtPosition(spansA, posA + i);
|
|
359
|
+
const currentMarksB = getMarksAtPosition(spansB, posB + i);
|
|
360
|
+
if (marksEqual(currentMarksA, startMarksA) && marksEqual(currentMarksB, startMarksB)) {
|
|
361
|
+
i++;
|
|
362
|
+
} else {
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
formatChanges.push({
|
|
367
|
+
from: posA + startI,
|
|
368
|
+
to: posA + i,
|
|
369
|
+
text: segment.text.substring(startI, i),
|
|
370
|
+
before: startMarksA,
|
|
371
|
+
after: startMarksB
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
i++;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
posA += segment.text.length;
|
|
378
|
+
posB += segment.text.length;
|
|
379
|
+
} else if (segment.type === "delete") {
|
|
380
|
+
posA += segment.text.length;
|
|
381
|
+
} else if (segment.type === "insert") {
|
|
382
|
+
posB += segment.text.length;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return formatChanges;
|
|
386
|
+
}
|
|
387
|
+
function diffDocuments(docA, docB) {
|
|
388
|
+
const textA = extractTextContent(docA);
|
|
389
|
+
const textB = extractTextContent(docB);
|
|
390
|
+
const diffs = dmp.diff_main(textA, textB);
|
|
391
|
+
dmp.diff_cleanupSemantic(diffs);
|
|
392
|
+
const segments = [];
|
|
393
|
+
let insertCount = 0;
|
|
394
|
+
let deleteCount = 0;
|
|
395
|
+
for (const [op, text] of diffs) {
|
|
396
|
+
if (op === DIFF_EQUAL) {
|
|
397
|
+
segments.push({ type: "equal", text });
|
|
398
|
+
} else if (op === DIFF_INSERT) {
|
|
399
|
+
segments.push({ type: "insert", text });
|
|
400
|
+
insertCount++;
|
|
401
|
+
} else if (op === DIFF_DELETE) {
|
|
402
|
+
segments.push({ type: "delete", text });
|
|
403
|
+
deleteCount++;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const spansA = extractTextSpans(docA);
|
|
407
|
+
const spansB = extractTextSpans(docB);
|
|
408
|
+
const formatChanges = detectFormatChanges(spansA, spansB, segments);
|
|
409
|
+
const summary = [];
|
|
410
|
+
if (insertCount > 0) {
|
|
411
|
+
summary.push(`${insertCount} insertion(s)`);
|
|
412
|
+
}
|
|
413
|
+
if (deleteCount > 0) {
|
|
414
|
+
summary.push(`${deleteCount} deletion(s)`);
|
|
415
|
+
}
|
|
416
|
+
if (formatChanges.length > 0) {
|
|
417
|
+
summary.push(`${formatChanges.length} format change(s)`);
|
|
418
|
+
}
|
|
419
|
+
if (insertCount === 0 && deleteCount === 0 && formatChanges.length === 0) {
|
|
420
|
+
summary.push("No changes detected");
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
segments,
|
|
424
|
+
formatChanges,
|
|
425
|
+
textA,
|
|
426
|
+
textB,
|
|
427
|
+
summary
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function createTrackInsertMark(author = DEFAULT_AUTHOR) {
|
|
431
|
+
return {
|
|
432
|
+
type: "trackInsert",
|
|
433
|
+
attrs: {
|
|
434
|
+
id: uuid.v4(),
|
|
435
|
+
author: author.name,
|
|
436
|
+
authorEmail: author.email,
|
|
437
|
+
authorImage: "",
|
|
438
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function createTrackDeleteMark(author = DEFAULT_AUTHOR) {
|
|
443
|
+
return {
|
|
444
|
+
type: "trackDelete",
|
|
445
|
+
attrs: {
|
|
446
|
+
id: uuid.v4(),
|
|
447
|
+
author: author.name,
|
|
448
|
+
authorEmail: author.email,
|
|
449
|
+
authorImage: "",
|
|
450
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function createTrackFormatMark(before, after, author = DEFAULT_AUTHOR) {
|
|
455
|
+
return {
|
|
456
|
+
type: "trackFormat",
|
|
457
|
+
attrs: {
|
|
458
|
+
id: uuid.v4(),
|
|
459
|
+
author: author.name,
|
|
460
|
+
authorEmail: author.email,
|
|
461
|
+
authorImage: "",
|
|
462
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
463
|
+
before,
|
|
464
|
+
after
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/services/mergeDocuments.ts
|
|
470
|
+
function cloneNode(node) {
|
|
471
|
+
return JSON.parse(JSON.stringify(node));
|
|
472
|
+
}
|
|
473
|
+
function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
474
|
+
const merged = cloneNode(docA);
|
|
475
|
+
const charStates = [];
|
|
476
|
+
let insertions = [];
|
|
477
|
+
const formatChanges = diffResult.formatChanges || [];
|
|
478
|
+
function getFormatChangeAt(pos) {
|
|
479
|
+
for (const fc of formatChanges) {
|
|
480
|
+
if (pos >= fc.from && pos < fc.to) {
|
|
481
|
+
return fc;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
let docAOffset = 0;
|
|
487
|
+
for (const segment of diffResult.segments) {
|
|
488
|
+
if (segment.type === "equal") {
|
|
489
|
+
for (let i = 0; i < segment.text.length; i++) {
|
|
490
|
+
charStates[docAOffset + i] = { type: "equal" };
|
|
491
|
+
}
|
|
492
|
+
docAOffset += segment.text.length;
|
|
493
|
+
} else if (segment.type === "delete") {
|
|
494
|
+
for (let i = 0; i < segment.text.length; i++) {
|
|
495
|
+
charStates[docAOffset + i] = { type: "delete" };
|
|
496
|
+
}
|
|
497
|
+
docAOffset += segment.text.length;
|
|
498
|
+
} else if (segment.type === "insert") {
|
|
499
|
+
insertions.push({
|
|
500
|
+
afterOffset: docAOffset,
|
|
501
|
+
text: segment.text
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function transformNode(node, nodeOffset, path) {
|
|
506
|
+
if (node.type === "text" && node.text) {
|
|
507
|
+
const text = node.text;
|
|
508
|
+
const result = [];
|
|
509
|
+
let i = 0;
|
|
510
|
+
while (i < text.length) {
|
|
511
|
+
const charOffset = nodeOffset + i;
|
|
512
|
+
const charState = charStates[charOffset] || { type: "equal" };
|
|
513
|
+
const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
|
|
514
|
+
for (const ins of insertionsHere) {
|
|
515
|
+
result.push({
|
|
516
|
+
type: "text",
|
|
517
|
+
text: ins.text,
|
|
518
|
+
marks: [...node.marks || [], createTrackInsertMark(author)]
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
const currentFormatChange = getFormatChangeAt(nodeOffset + i);
|
|
522
|
+
let j = i + 1;
|
|
523
|
+
while (j < text.length) {
|
|
524
|
+
const nextState = charStates[nodeOffset + j] || { type: "equal" };
|
|
525
|
+
if (nextState.type !== charState.type) break;
|
|
526
|
+
if (insertions.some((ins) => ins.afterOffset === nodeOffset + j)) break;
|
|
527
|
+
const nextFormatChange = getFormatChangeAt(nodeOffset + j);
|
|
528
|
+
if (currentFormatChange !== nextFormatChange) break;
|
|
529
|
+
j++;
|
|
530
|
+
}
|
|
531
|
+
const chunk = text.substring(i, j);
|
|
532
|
+
let marks = [...node.marks || []];
|
|
533
|
+
if (charState.type === "delete") {
|
|
534
|
+
marks.push(createTrackDeleteMark(author));
|
|
535
|
+
} else if (charState.type === "equal") {
|
|
536
|
+
if (currentFormatChange) {
|
|
537
|
+
const trackFormatMark = createTrackFormatMark(
|
|
538
|
+
currentFormatChange.before,
|
|
539
|
+
currentFormatChange.after,
|
|
540
|
+
author
|
|
541
|
+
);
|
|
542
|
+
marks = [...currentFormatChange.after, trackFormatMark];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
result.push({
|
|
546
|
+
type: "text",
|
|
547
|
+
text: chunk,
|
|
548
|
+
marks: marks.length > 0 ? marks : void 0
|
|
549
|
+
});
|
|
550
|
+
i = j;
|
|
551
|
+
}
|
|
552
|
+
const endOffset = nodeOffset + text.length;
|
|
553
|
+
const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
|
|
554
|
+
for (const ins of endInsertions) {
|
|
555
|
+
result.push({
|
|
556
|
+
type: "text",
|
|
557
|
+
text: ins.text,
|
|
558
|
+
marks: [...node.marks || [], createTrackInsertMark(author)]
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
insertions = insertions.filter(
|
|
562
|
+
(ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
|
|
563
|
+
);
|
|
564
|
+
return { nodes: result, consumedLength: text.length };
|
|
565
|
+
}
|
|
566
|
+
if (node.content && Array.isArray(node.content)) {
|
|
567
|
+
const newContent = [];
|
|
568
|
+
let offset = nodeOffset;
|
|
569
|
+
for (const child of node.content) {
|
|
570
|
+
const { nodes, consumedLength } = transformNode(child, offset);
|
|
571
|
+
newContent.push(...nodes);
|
|
572
|
+
offset += consumedLength;
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
nodes: [{ ...node, content: newContent }],
|
|
576
|
+
consumedLength: offset - nodeOffset
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
return { nodes: [node], consumedLength: 0 };
|
|
580
|
+
}
|
|
581
|
+
if (merged.content && Array.isArray(merged.content)) {
|
|
582
|
+
const newContent = [];
|
|
583
|
+
let offset = 0;
|
|
584
|
+
for (let i = 0; i < merged.content.length; i++) {
|
|
585
|
+
const child = merged.content[i];
|
|
586
|
+
const { nodes, consumedLength } = transformNode(child, offset);
|
|
587
|
+
newContent.push(...nodes);
|
|
588
|
+
offset += consumedLength;
|
|
589
|
+
}
|
|
590
|
+
merged.content = newContent;
|
|
591
|
+
}
|
|
592
|
+
if (insertions.length > 0) {
|
|
593
|
+
for (const ins of insertions) {
|
|
594
|
+
const insertNode = {
|
|
595
|
+
type: "paragraph",
|
|
596
|
+
content: [
|
|
597
|
+
{
|
|
598
|
+
type: "run",
|
|
599
|
+
content: [
|
|
600
|
+
{
|
|
601
|
+
type: "text",
|
|
602
|
+
text: ins.text,
|
|
603
|
+
marks: [createTrackInsertMark(author)]
|
|
604
|
+
}
|
|
605
|
+
]
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
};
|
|
609
|
+
if (!merged.content) merged.content = [];
|
|
610
|
+
merged.content.push(insertNode);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return merged;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/services/changeContextExtractor.ts
|
|
617
|
+
function extractEnrichedChanges(mergedJson) {
|
|
618
|
+
const changes = [];
|
|
619
|
+
const context = {
|
|
620
|
+
currentSection: null,
|
|
621
|
+
currentParagraphText: "",
|
|
622
|
+
currentNodeType: "unknown"
|
|
623
|
+
};
|
|
624
|
+
traverseDocument(mergedJson, context, changes);
|
|
625
|
+
return groupReplacements(changes);
|
|
626
|
+
}
|
|
627
|
+
function traverseDocument(node, context, changes) {
|
|
628
|
+
if (!node) return;
|
|
629
|
+
if (node.type === "heading") {
|
|
630
|
+
context.currentSection = extractAllText(node);
|
|
631
|
+
context.headingLevel = node.attrs?.level || 1;
|
|
632
|
+
context.currentNodeType = "heading";
|
|
633
|
+
context.currentParagraphText = context.currentSection;
|
|
634
|
+
} else if (node.type === "paragraph") {
|
|
635
|
+
context.currentNodeType = "paragraph";
|
|
636
|
+
context.currentParagraphText = extractAllText(node);
|
|
637
|
+
} else if (node.type === "listItem") {
|
|
638
|
+
context.currentNodeType = "listItem";
|
|
639
|
+
context.currentParagraphText = extractAllText(node);
|
|
640
|
+
} else if (node.type === "tableCell") {
|
|
641
|
+
context.currentNodeType = "tableCell";
|
|
642
|
+
context.currentParagraphText = extractAllText(node);
|
|
643
|
+
}
|
|
644
|
+
if (node.type === "text" && node.marks) {
|
|
645
|
+
const trackMark = findTrackChangeMark(node.marks);
|
|
646
|
+
if (trackMark) {
|
|
647
|
+
const change = createEnrichedChange(node, trackMark, context);
|
|
648
|
+
if (change) changes.push(change);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (node.content && Array.isArray(node.content)) {
|
|
652
|
+
for (const child of node.content) {
|
|
653
|
+
traverseDocument(child, context, changes);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function extractAllText(node) {
|
|
658
|
+
if (!node) return "";
|
|
659
|
+
if (node.type === "text") {
|
|
660
|
+
return node.text || "";
|
|
661
|
+
}
|
|
662
|
+
if (node.content && Array.isArray(node.content)) {
|
|
663
|
+
return node.content.map(extractAllText).join("");
|
|
664
|
+
}
|
|
665
|
+
return "";
|
|
666
|
+
}
|
|
667
|
+
function findTrackChangeMark(marks) {
|
|
668
|
+
return marks.find(
|
|
669
|
+
(m) => m.type === "trackInsert" || m.type === "trackDelete" || m.type === "trackFormat"
|
|
670
|
+
) || null;
|
|
671
|
+
}
|
|
672
|
+
function createEnrichedChange(node, trackMark, context) {
|
|
673
|
+
const text = node.text || "";
|
|
674
|
+
const location = buildLocation(context);
|
|
675
|
+
const surroundingText = extractSurroundingSentence(text, context.currentParagraphText);
|
|
676
|
+
if (trackMark.type === "trackInsert") {
|
|
677
|
+
return {
|
|
678
|
+
type: "insertion",
|
|
679
|
+
text,
|
|
680
|
+
location,
|
|
681
|
+
surroundingText,
|
|
682
|
+
charCount: text.length
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (trackMark.type === "trackDelete") {
|
|
686
|
+
return {
|
|
687
|
+
type: "deletion",
|
|
688
|
+
text,
|
|
689
|
+
location,
|
|
690
|
+
surroundingText,
|
|
691
|
+
charCount: text.length
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (trackMark.type === "trackFormat") {
|
|
695
|
+
const before = trackMark.attrs?.before || [];
|
|
696
|
+
const after = trackMark.attrs?.after || [];
|
|
697
|
+
return {
|
|
698
|
+
type: "format",
|
|
699
|
+
text,
|
|
700
|
+
location,
|
|
701
|
+
surroundingText,
|
|
702
|
+
formatDetails: {
|
|
703
|
+
added: after.map((m) => m.type).filter((t) => !before.some((b) => b.type === t)),
|
|
704
|
+
removed: before.map((m) => m.type).filter((t) => !after.some((a) => a.type === t))
|
|
705
|
+
},
|
|
706
|
+
charCount: text.length
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
function extractSurroundingSentence(changedText, paragraphText) {
|
|
712
|
+
if (!paragraphText || !changedText) return "";
|
|
713
|
+
const changeIndex = paragraphText.indexOf(changedText);
|
|
714
|
+
if (changeIndex === -1) {
|
|
715
|
+
return truncate(paragraphText, 150);
|
|
716
|
+
}
|
|
717
|
+
const sentenceBreaks = /([.;!?]\s+)/g;
|
|
718
|
+
const sentences = [];
|
|
719
|
+
let lastEnd = 0;
|
|
720
|
+
let match;
|
|
721
|
+
while ((match = sentenceBreaks.exec(paragraphText)) !== null) {
|
|
722
|
+
sentences.push({
|
|
723
|
+
text: paragraphText.slice(lastEnd, match.index + match[0].length).trim(),
|
|
724
|
+
start: lastEnd,
|
|
725
|
+
end: match.index + match[0].length
|
|
726
|
+
});
|
|
727
|
+
lastEnd = match.index + match[0].length;
|
|
728
|
+
}
|
|
729
|
+
if (lastEnd < paragraphText.length) {
|
|
730
|
+
sentences.push({
|
|
731
|
+
text: paragraphText.slice(lastEnd).trim(),
|
|
732
|
+
start: lastEnd,
|
|
733
|
+
end: paragraphText.length
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
const changeEnd = changeIndex + changedText.length;
|
|
737
|
+
for (const sentence of sentences) {
|
|
738
|
+
if (changeIndex >= sentence.start && changeIndex < sentence.end) {
|
|
739
|
+
return truncate(sentence.text, 200);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const windowSize = 100;
|
|
743
|
+
const start = Math.max(0, changeIndex - windowSize);
|
|
744
|
+
const end = Math.min(paragraphText.length, changeEnd + windowSize);
|
|
745
|
+
let result = paragraphText.slice(start, end);
|
|
746
|
+
if (start > 0) result = "..." + result;
|
|
747
|
+
if (end < paragraphText.length) result = result + "...";
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
function truncate(text, maxLen) {
|
|
751
|
+
if (!text) return "";
|
|
752
|
+
const cleaned = text.replace(/\s+/g, " ").trim();
|
|
753
|
+
if (cleaned.length <= maxLen) return cleaned;
|
|
754
|
+
return cleaned.slice(0, maxLen - 3).trim() + "...";
|
|
755
|
+
}
|
|
756
|
+
function buildLocation(context) {
|
|
757
|
+
const nodeType = context.currentNodeType;
|
|
758
|
+
let description;
|
|
759
|
+
if (nodeType === "heading") {
|
|
760
|
+
description = context.headingLevel === 1 ? "document title" : "section heading";
|
|
761
|
+
} else if (context.currentSection) {
|
|
762
|
+
description = `"${truncate(context.currentSection, 50)}" section`;
|
|
763
|
+
} else {
|
|
764
|
+
description = "document body";
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
nodeType,
|
|
768
|
+
headingLevel: context.headingLevel,
|
|
769
|
+
sectionTitle: context.currentSection || void 0,
|
|
770
|
+
description
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function groupReplacements(changes) {
|
|
774
|
+
const result = [];
|
|
775
|
+
let i = 0;
|
|
776
|
+
while (i < changes.length) {
|
|
777
|
+
const current = changes[i];
|
|
778
|
+
const next = changes[i + 1];
|
|
779
|
+
if (current.type === "deletion" && next?.type === "insertion" && current.location.sectionTitle === next.location.sectionTitle) {
|
|
780
|
+
result.push({
|
|
781
|
+
type: "replacement",
|
|
782
|
+
oldText: current.text,
|
|
783
|
+
newText: next.text,
|
|
784
|
+
location: current.location,
|
|
785
|
+
surroundingText: current.surroundingText || next.surroundingText,
|
|
786
|
+
charCount: (current.charCount || 0) + (next.charCount || 0)
|
|
787
|
+
});
|
|
788
|
+
i += 2;
|
|
789
|
+
} else {
|
|
790
|
+
result.push(current);
|
|
791
|
+
i++;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return result;
|
|
795
|
+
}
|
|
796
|
+
var permissionResolver = ({ permission }) => {
|
|
797
|
+
return TRACK_CHANGE_PERMISSIONS.includes(permission) ? true : void 0;
|
|
798
|
+
};
|
|
799
|
+
var DocxDiffEditor = react.forwardRef(
|
|
800
|
+
function DocxDiffEditor2({
|
|
801
|
+
initialSource,
|
|
802
|
+
templateDocx,
|
|
803
|
+
showRulers = false,
|
|
804
|
+
showToolbar = true,
|
|
805
|
+
author = DEFAULT_AUTHOR,
|
|
806
|
+
onReady,
|
|
807
|
+
onSourceLoaded,
|
|
808
|
+
onComparisonComplete,
|
|
809
|
+
onError,
|
|
810
|
+
className = "",
|
|
811
|
+
toolbarClassName = "",
|
|
812
|
+
editorClassName = ""
|
|
813
|
+
}, ref) {
|
|
814
|
+
const containerRef = react.useRef(null);
|
|
815
|
+
const toolbarRef = react.useRef(null);
|
|
816
|
+
const superdocRef = react.useRef(null);
|
|
817
|
+
const SuperDocRef = react.useRef(null);
|
|
818
|
+
const mountedRef = react.useRef(true);
|
|
819
|
+
const initRef = react.useRef(false);
|
|
820
|
+
const readyRef = react.useRef(false);
|
|
821
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
822
|
+
const [error, setError] = react.useState(null);
|
|
823
|
+
const [sourceJson, setSourceJson] = react.useState(null);
|
|
824
|
+
const [mergedJson, setMergedJson] = react.useState(null);
|
|
825
|
+
const [diffResult, setDiffResult] = react.useState(null);
|
|
826
|
+
const instanceId = react.useRef(`dde-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
|
|
827
|
+
const editorId = `dde-editor-${instanceId.current}`;
|
|
828
|
+
const toolbarId = `dde-toolbar-${instanceId.current}`;
|
|
829
|
+
const setEditorContent = react.useCallback((editor, json) => {
|
|
830
|
+
if (editor.commands?.setContent) {
|
|
831
|
+
editor.commands.setContent(json);
|
|
832
|
+
} else if (editor.setContent) {
|
|
833
|
+
editor.setContent(json);
|
|
834
|
+
} else {
|
|
835
|
+
const { state, view } = editor;
|
|
836
|
+
if (state?.doc && view && json.content) {
|
|
837
|
+
const newDoc = state.schema.nodeFromJSON(json);
|
|
838
|
+
const tr = state.tr.replaceWith(0, state.doc.content.size, newDoc.content);
|
|
839
|
+
view.dispatch(tr);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}, []);
|
|
843
|
+
const enableReviewMode = react.useCallback((sd) => {
|
|
844
|
+
if (sd.setTrackedChangesPreferences) {
|
|
845
|
+
sd.setTrackedChangesPreferences({ mode: "review", enabled: true });
|
|
846
|
+
} else if (sd.activeEditor?.commands?.enableTrackChanges) {
|
|
847
|
+
sd.activeEditor.commands.enableTrackChanges();
|
|
848
|
+
}
|
|
849
|
+
}, []);
|
|
850
|
+
const setEditingMode = react.useCallback((sd) => {
|
|
851
|
+
if (sd.setTrackedChangesPreferences) {
|
|
852
|
+
sd.setTrackedChangesPreferences({ mode: "editing", enabled: false });
|
|
853
|
+
}
|
|
854
|
+
}, []);
|
|
855
|
+
const getTemplateFile = react.useCallback(() => {
|
|
856
|
+
return templateDocx || getBlankTemplateFile();
|
|
857
|
+
}, [templateDocx]);
|
|
858
|
+
const handleError = react.useCallback(
|
|
859
|
+
(err) => {
|
|
860
|
+
const error2 = err instanceof Error ? err : new Error(err);
|
|
861
|
+
setError(error2.message);
|
|
862
|
+
onError?.(error2);
|
|
863
|
+
},
|
|
864
|
+
[onError]
|
|
865
|
+
);
|
|
866
|
+
const initialize = react.useCallback(async () => {
|
|
867
|
+
if (initRef.current || !containerRef.current || !mountedRef.current) return;
|
|
868
|
+
if (!showToolbar && !toolbarRef.current) ; else if (showToolbar && !toolbarRef.current) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
initRef.current = true;
|
|
872
|
+
await new Promise((resolve) => setTimeout(resolve, TIMEOUTS.INIT_DELAY));
|
|
873
|
+
if (!mountedRef.current || !containerRef.current) {
|
|
874
|
+
initRef.current = false;
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
setIsLoading(true);
|
|
878
|
+
setError(null);
|
|
879
|
+
if (superdocRef.current) {
|
|
880
|
+
try {
|
|
881
|
+
superdocRef.current.destroy?.();
|
|
882
|
+
} catch {
|
|
883
|
+
}
|
|
884
|
+
superdocRef.current = null;
|
|
885
|
+
}
|
|
886
|
+
containerRef.current.id = editorId;
|
|
887
|
+
if (toolbarRef.current) {
|
|
888
|
+
toolbarRef.current.id = toolbarId;
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
const { SuperDoc } = await import('superdoc');
|
|
892
|
+
await import('superdoc/style.css');
|
|
893
|
+
SuperDocRef.current = SuperDoc;
|
|
894
|
+
let initialDoc;
|
|
895
|
+
let initialContent = null;
|
|
896
|
+
if (initialSource) {
|
|
897
|
+
const contentType = detectContentType(initialSource);
|
|
898
|
+
if (contentType === "file") {
|
|
899
|
+
initialDoc = initialSource;
|
|
900
|
+
} else {
|
|
901
|
+
initialDoc = getTemplateFile();
|
|
902
|
+
try {
|
|
903
|
+
const resolved = await resolveContent(initialSource, SuperDoc, templateDocx);
|
|
904
|
+
initialContent = resolved.json;
|
|
905
|
+
} catch (err) {
|
|
906
|
+
handleError(err instanceof Error ? err : new Error("Failed to resolve initial content"));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
} else {
|
|
910
|
+
initialDoc = getTemplateFile();
|
|
911
|
+
}
|
|
912
|
+
const superdoc = new SuperDoc({
|
|
913
|
+
selector: `#${editorId}`,
|
|
914
|
+
toolbar: showToolbar ? `#${toolbarId}` : void 0,
|
|
915
|
+
document: initialDoc,
|
|
916
|
+
documentMode: "editing",
|
|
917
|
+
role: "editor",
|
|
918
|
+
rulers: showRulers,
|
|
919
|
+
user: DEFAULT_SUPERDOC_USER,
|
|
920
|
+
permissionResolver,
|
|
921
|
+
onReady: ({ superdoc: sd }) => {
|
|
922
|
+
superdocRef.current = sd;
|
|
923
|
+
readyRef.current = true;
|
|
924
|
+
if (initialContent && sd?.activeEditor) {
|
|
925
|
+
try {
|
|
926
|
+
setEditorContent(sd.activeEditor, initialContent);
|
|
927
|
+
setSourceJson(initialContent);
|
|
928
|
+
onSourceLoaded?.(initialContent);
|
|
929
|
+
} catch (err) {
|
|
930
|
+
console.error("Failed to set initial content:", err);
|
|
931
|
+
}
|
|
932
|
+
} else if (sd?.activeEditor) {
|
|
933
|
+
try {
|
|
934
|
+
const json = sd.activeEditor.getJSON();
|
|
935
|
+
setSourceJson(json);
|
|
936
|
+
onSourceLoaded?.(json);
|
|
937
|
+
} catch (err) {
|
|
938
|
+
console.error("Failed to extract JSON:", err);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
setIsLoading(false);
|
|
942
|
+
onReady?.();
|
|
943
|
+
},
|
|
944
|
+
onException: ({ error: err }) => {
|
|
945
|
+
console.error("SuperDoc error:", err);
|
|
946
|
+
handleError(err);
|
|
947
|
+
setIsLoading(false);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
superdocRef.current = superdoc;
|
|
951
|
+
} catch (err) {
|
|
952
|
+
console.error("Failed to initialize SuperDoc:", err);
|
|
953
|
+
handleError(err instanceof Error ? err : new Error("Failed to load editor"));
|
|
954
|
+
setIsLoading(false);
|
|
955
|
+
}
|
|
956
|
+
initRef.current = false;
|
|
957
|
+
}, [
|
|
958
|
+
initialSource,
|
|
959
|
+
showRulers,
|
|
960
|
+
showToolbar,
|
|
961
|
+
templateDocx,
|
|
962
|
+
editorId,
|
|
963
|
+
toolbarId,
|
|
964
|
+
onReady,
|
|
965
|
+
onSourceLoaded,
|
|
966
|
+
getTemplateFile,
|
|
967
|
+
setEditorContent,
|
|
968
|
+
handleError
|
|
969
|
+
]);
|
|
970
|
+
react.useEffect(() => {
|
|
971
|
+
mountedRef.current = true;
|
|
972
|
+
initialize();
|
|
973
|
+
return () => {
|
|
974
|
+
mountedRef.current = false;
|
|
975
|
+
if (superdocRef.current) {
|
|
976
|
+
try {
|
|
977
|
+
superdocRef.current.destroy?.();
|
|
978
|
+
} catch {
|
|
979
|
+
}
|
|
980
|
+
superdocRef.current = null;
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
}, [initialize]);
|
|
984
|
+
react.useImperativeHandle(
|
|
985
|
+
ref,
|
|
986
|
+
() => ({
|
|
987
|
+
/**
|
|
988
|
+
* Set the source/base document
|
|
989
|
+
*/
|
|
990
|
+
async setSource(content) {
|
|
991
|
+
if (!SuperDocRef.current) {
|
|
992
|
+
throw new Error("Editor not initialized");
|
|
993
|
+
}
|
|
994
|
+
setIsLoading(true);
|
|
995
|
+
try {
|
|
996
|
+
const resolved = await resolveContent(content, SuperDocRef.current, templateDocx);
|
|
997
|
+
setSourceJson(resolved.json);
|
|
998
|
+
setMergedJson(null);
|
|
999
|
+
setDiffResult(null);
|
|
1000
|
+
if (superdocRef.current?.activeEditor) {
|
|
1001
|
+
setEditorContent(superdocRef.current.activeEditor, resolved.json);
|
|
1002
|
+
setEditingMode(superdocRef.current);
|
|
1003
|
+
}
|
|
1004
|
+
onSourceLoaded?.(resolved.json);
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
handleError(err instanceof Error ? err : new Error("Failed to set source"));
|
|
1007
|
+
throw err;
|
|
1008
|
+
} finally {
|
|
1009
|
+
setIsLoading(false);
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
/**
|
|
1013
|
+
* Compare source with new content, show track changes
|
|
1014
|
+
*/
|
|
1015
|
+
async compareWith(content) {
|
|
1016
|
+
if (!SuperDocRef.current) {
|
|
1017
|
+
throw new Error("Editor not initialized");
|
|
1018
|
+
}
|
|
1019
|
+
if (!sourceJson) {
|
|
1020
|
+
throw new Error("No source document set. Call setSource() first.");
|
|
1021
|
+
}
|
|
1022
|
+
setIsLoading(true);
|
|
1023
|
+
try {
|
|
1024
|
+
const resolved = await resolveContent(content, SuperDocRef.current, templateDocx);
|
|
1025
|
+
const diff = diffDocuments(sourceJson, resolved.json);
|
|
1026
|
+
setDiffResult(diff);
|
|
1027
|
+
const merged = mergeDocuments(sourceJson, resolved.json, diff, author);
|
|
1028
|
+
setMergedJson(merged);
|
|
1029
|
+
if (superdocRef.current?.activeEditor) {
|
|
1030
|
+
setEditorContent(superdocRef.current.activeEditor, merged);
|
|
1031
|
+
enableReviewMode(superdocRef.current);
|
|
1032
|
+
}
|
|
1033
|
+
const insertions = diff.segments.filter((s) => s.type === "insert").length;
|
|
1034
|
+
const deletions = diff.segments.filter((s) => s.type === "delete").length;
|
|
1035
|
+
const formatChanges = diff.formatChanges?.length || 0;
|
|
1036
|
+
const result = {
|
|
1037
|
+
totalChanges: insertions + deletions + formatChanges,
|
|
1038
|
+
insertions,
|
|
1039
|
+
deletions,
|
|
1040
|
+
formatChanges,
|
|
1041
|
+
summary: diff.summary,
|
|
1042
|
+
mergedJson: merged
|
|
1043
|
+
};
|
|
1044
|
+
onComparisonComplete?.(result);
|
|
1045
|
+
return result;
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
handleError(err instanceof Error ? err : new Error("Comparison failed"));
|
|
1048
|
+
throw err;
|
|
1049
|
+
} finally {
|
|
1050
|
+
setIsLoading(false);
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
/**
|
|
1054
|
+
* Get raw diff segments
|
|
1055
|
+
*/
|
|
1056
|
+
getDiffSegments() {
|
|
1057
|
+
return diffResult?.segments || [];
|
|
1058
|
+
},
|
|
1059
|
+
/**
|
|
1060
|
+
* Get enriched changes with context for LLM processing
|
|
1061
|
+
*/
|
|
1062
|
+
getEnrichedChangesContext() {
|
|
1063
|
+
if (!mergedJson) return [];
|
|
1064
|
+
return extractEnrichedChanges(mergedJson);
|
|
1065
|
+
},
|
|
1066
|
+
/**
|
|
1067
|
+
* Get current document content as JSON
|
|
1068
|
+
*/
|
|
1069
|
+
getContent() {
|
|
1070
|
+
if (superdocRef.current?.activeEditor) {
|
|
1071
|
+
return superdocRef.current.activeEditor.getJSON();
|
|
1072
|
+
}
|
|
1073
|
+
return mergedJson || sourceJson || { type: "doc", content: [] };
|
|
1074
|
+
},
|
|
1075
|
+
/**
|
|
1076
|
+
* Get source document JSON (before comparison)
|
|
1077
|
+
*/
|
|
1078
|
+
getSourceContent() {
|
|
1079
|
+
return sourceJson;
|
|
1080
|
+
},
|
|
1081
|
+
/**
|
|
1082
|
+
* Export current document to DOCX blob
|
|
1083
|
+
*/
|
|
1084
|
+
async exportDocx() {
|
|
1085
|
+
if (!superdocRef.current?.activeEditor) {
|
|
1086
|
+
throw new Error("Editor not ready");
|
|
1087
|
+
}
|
|
1088
|
+
const blob = await superdocRef.current.activeEditor.exportDocx({
|
|
1089
|
+
isFinalDoc: false
|
|
1090
|
+
});
|
|
1091
|
+
if (!blob) {
|
|
1092
|
+
throw new Error("Export returned no data");
|
|
1093
|
+
}
|
|
1094
|
+
return blob;
|
|
1095
|
+
},
|
|
1096
|
+
/**
|
|
1097
|
+
* Reset to source state (clear comparison)
|
|
1098
|
+
*/
|
|
1099
|
+
resetComparison() {
|
|
1100
|
+
if (sourceJson && superdocRef.current?.activeEditor) {
|
|
1101
|
+
setEditorContent(superdocRef.current.activeEditor, sourceJson);
|
|
1102
|
+
setEditingMode(superdocRef.current);
|
|
1103
|
+
setMergedJson(null);
|
|
1104
|
+
setDiffResult(null);
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
/**
|
|
1108
|
+
* Check if editor is ready
|
|
1109
|
+
*/
|
|
1110
|
+
isReady() {
|
|
1111
|
+
return readyRef.current;
|
|
1112
|
+
}
|
|
1113
|
+
}),
|
|
1114
|
+
[
|
|
1115
|
+
sourceJson,
|
|
1116
|
+
mergedJson,
|
|
1117
|
+
diffResult,
|
|
1118
|
+
templateDocx,
|
|
1119
|
+
author,
|
|
1120
|
+
setEditorContent,
|
|
1121
|
+
enableReviewMode,
|
|
1122
|
+
setEditingMode,
|
|
1123
|
+
onSourceLoaded,
|
|
1124
|
+
onComparisonComplete,
|
|
1125
|
+
handleError
|
|
1126
|
+
]
|
|
1127
|
+
);
|
|
1128
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `dde-container ${className}`.trim(), children: [
|
|
1129
|
+
isLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dde-loading", children: [
|
|
1130
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "dde-loading__spinner" }),
|
|
1131
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "dde-loading__text", children: "Loading document..." })
|
|
1132
|
+
] }),
|
|
1133
|
+
error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dde-error", children: [
|
|
1134
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "dde-error__icon", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1135
|
+
"svg",
|
|
1136
|
+
{
|
|
1137
|
+
className: "dde-error__svg",
|
|
1138
|
+
fill: "none",
|
|
1139
|
+
stroke: "currentColor",
|
|
1140
|
+
viewBox: "0 0 24 24",
|
|
1141
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1142
|
+
"path",
|
|
1143
|
+
{
|
|
1144
|
+
strokeLinecap: "round",
|
|
1145
|
+
strokeLinejoin: "round",
|
|
1146
|
+
strokeWidth: "2",
|
|
1147
|
+
d: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
1148
|
+
}
|
|
1149
|
+
)
|
|
1150
|
+
}
|
|
1151
|
+
) }),
|
|
1152
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "dde-error__title", children: "Failed to load document" }),
|
|
1153
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "dde-error__message", children: error })
|
|
1154
|
+
] }),
|
|
1155
|
+
showToolbar && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1156
|
+
"div",
|
|
1157
|
+
{
|
|
1158
|
+
ref: toolbarRef,
|
|
1159
|
+
className: `dde-toolbar ${toolbarClassName}`.trim()
|
|
1160
|
+
}
|
|
1161
|
+
),
|
|
1162
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1163
|
+
"div",
|
|
1164
|
+
{
|
|
1165
|
+
ref: containerRef,
|
|
1166
|
+
className: `dde-editor ${editorClassName}`.trim()
|
|
1167
|
+
}
|
|
1168
|
+
)
|
|
1169
|
+
] });
|
|
1170
|
+
}
|
|
1171
|
+
);
|
|
1172
|
+
var DocxDiffEditor_default = DocxDiffEditor;
|
|
1173
|
+
|
|
1174
|
+
exports.CSS_PREFIX = CSS_PREFIX;
|
|
1175
|
+
exports.DEFAULT_AUTHOR = DEFAULT_AUTHOR;
|
|
1176
|
+
exports.DEFAULT_SUPERDOC_USER = DEFAULT_SUPERDOC_USER;
|
|
1177
|
+
exports.DocxDiffEditor = DocxDiffEditor;
|
|
1178
|
+
exports.createTrackDeleteMark = createTrackDeleteMark;
|
|
1179
|
+
exports.createTrackFormatMark = createTrackFormatMark;
|
|
1180
|
+
exports.createTrackInsertMark = createTrackInsertMark;
|
|
1181
|
+
exports.default = DocxDiffEditor_default;
|
|
1182
|
+
exports.detectContentType = detectContentType;
|
|
1183
|
+
exports.diffDocuments = diffDocuments;
|
|
1184
|
+
exports.extractEnrichedChanges = extractEnrichedChanges;
|
|
1185
|
+
exports.getBlankTemplateBlob = getBlankTemplateBlob;
|
|
1186
|
+
exports.getBlankTemplateFile = getBlankTemplateFile;
|
|
1187
|
+
exports.isProseMirrorJSON = isProseMirrorJSON;
|
|
1188
|
+
exports.isValidDocxFile = isValidDocxFile;
|
|
1189
|
+
exports.mergeDocuments = mergeDocuments;
|
|
1190
|
+
exports.parseDocxFile = parseDocxFile;
|
|
1191
|
+
exports.parseHtmlContent = parseHtmlContent;
|
|
1192
|
+
exports.resolveContent = resolveContent;
|
|
1193
|
+
//# sourceMappingURL=index.js.map
|
|
1194
|
+
//# sourceMappingURL=index.js.map
|