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/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