docx-diff-editor 1.0.46 → 1.0.48

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.d.mts CHANGED
@@ -29,6 +29,10 @@ type DocxContent = File | ProseMirrorJSON | string;
29
29
  interface DiffSegment {
30
30
  type: 'equal' | 'insert' | 'delete';
31
31
  text: string;
32
+ /** Position in docA where this segment starts (for equal/delete segments) */
33
+ posA?: number;
34
+ /** Position in docB where this segment starts (for equal/insert segments) */
35
+ posB?: number;
32
36
  }
33
37
  /**
34
38
  * A format change on unchanged text
@@ -54,6 +58,8 @@ interface DiffResult {
54
58
  textB: string;
55
59
  /** Human-readable summary */
56
60
  summary: string[];
61
+ /** Text spans from docB with marks (for mark preservation during merge) */
62
+ spansB?: TextSpan[];
57
63
  }
58
64
  /**
59
65
  * Type of structural change
@@ -365,6 +371,15 @@ interface DocxDiffEditorRef {
365
371
  /** Parse HTML string to ProseMirror JSON (uses hidden SuperDoc instance) */
366
372
  parseHtml(html: string): Promise<ProseMirrorJSON>;
367
373
  }
374
+ /**
375
+ * Text span with position and marks (used in diffing)
376
+ */
377
+ interface TextSpan {
378
+ text: string;
379
+ from: number;
380
+ to: number;
381
+ marks: ProseMirrorMark[];
382
+ }
368
383
 
369
384
  /**
370
385
  * DocxDiffEditor Component
@@ -454,6 +469,10 @@ declare function parseDocxFile(file: File, SuperDoc: SuperDocConstructor): Promi
454
469
  /**
455
470
  * Diff two ProseMirror JSON documents at the character level.
456
471
  * Detects both text changes and formatting changes.
472
+ *
473
+ * Now also tracks positions in both documents for mark preservation:
474
+ * - posA: position in docA (for equal/delete segments)
475
+ * - posB: position in docB (for equal/insert segments)
457
476
  */
458
477
  declare function diffDocuments(docA: ProseMirrorJSON, docB: ProseMirrorJSON): DiffResult;
459
478
 
@@ -494,6 +513,11 @@ declare function createTrackInsertMark(author?: TrackChangeAuthor, id?: string):
494
513
  declare function createTrackDeleteMark(author?: TrackChangeAuthor, id?: string): ProseMirrorMark;
495
514
  /**
496
515
  * Create a trackFormat mark.
516
+ *
517
+ * Note: SuperDoc's parseFormatList requires all marks in before/after arrays
518
+ * to have both `type` and `attrs` properties. Marks without `attrs` get filtered out,
519
+ * causing empty values in track change bubbles. We normalize marks here to ensure
520
+ * all have at least an empty `attrs` object.
497
521
  */
498
522
  declare function createTrackFormatMark(before: ProseMirrorMark[], after: ProseMirrorMark[], author?: TrackChangeAuthor): ProseMirrorMark;
499
523
 
package/dist/index.d.ts CHANGED
@@ -29,6 +29,10 @@ type DocxContent = File | ProseMirrorJSON | string;
29
29
  interface DiffSegment {
30
30
  type: 'equal' | 'insert' | 'delete';
31
31
  text: string;
32
+ /** Position in docA where this segment starts (for equal/delete segments) */
33
+ posA?: number;
34
+ /** Position in docB where this segment starts (for equal/insert segments) */
35
+ posB?: number;
32
36
  }
33
37
  /**
34
38
  * A format change on unchanged text
@@ -54,6 +58,8 @@ interface DiffResult {
54
58
  textB: string;
55
59
  /** Human-readable summary */
56
60
  summary: string[];
61
+ /** Text spans from docB with marks (for mark preservation during merge) */
62
+ spansB?: TextSpan[];
57
63
  }
58
64
  /**
59
65
  * Type of structural change
@@ -365,6 +371,15 @@ interface DocxDiffEditorRef {
365
371
  /** Parse HTML string to ProseMirror JSON (uses hidden SuperDoc instance) */
366
372
  parseHtml(html: string): Promise<ProseMirrorJSON>;
367
373
  }
374
+ /**
375
+ * Text span with position and marks (used in diffing)
376
+ */
377
+ interface TextSpan {
378
+ text: string;
379
+ from: number;
380
+ to: number;
381
+ marks: ProseMirrorMark[];
382
+ }
368
383
 
369
384
  /**
370
385
  * DocxDiffEditor Component
@@ -454,6 +469,10 @@ declare function parseDocxFile(file: File, SuperDoc: SuperDocConstructor): Promi
454
469
  /**
455
470
  * Diff two ProseMirror JSON documents at the character level.
456
471
  * Detects both text changes and formatting changes.
472
+ *
473
+ * Now also tracks positions in both documents for mark preservation:
474
+ * - posA: position in docA (for equal/delete segments)
475
+ * - posB: position in docB (for equal/insert segments)
457
476
  */
458
477
  declare function diffDocuments(docA: ProseMirrorJSON, docB: ProseMirrorJSON): DiffResult;
459
478
 
@@ -494,6 +513,11 @@ declare function createTrackInsertMark(author?: TrackChangeAuthor, id?: string):
494
513
  declare function createTrackDeleteMark(author?: TrackChangeAuthor, id?: string): ProseMirrorMark;
495
514
  /**
496
515
  * Create a trackFormat mark.
516
+ *
517
+ * Note: SuperDoc's parseFormatList requires all marks in before/after arrays
518
+ * to have both `type` and `attrs` properties. Marks without `attrs` get filtered out,
519
+ * causing empty values in track change bubbles. We normalize marks here to ensure
520
+ * all have at least an empty `attrs` object.
497
521
  */
498
522
  declare function createTrackFormatMark(before: ProseMirrorMark[], after: ProseMirrorMark[], author?: TrackChangeAuthor): ProseMirrorMark;
499
523
 
package/dist/index.js CHANGED
@@ -1002,14 +1002,20 @@ function diffDocuments(docA, docB) {
1002
1002
  const segments = [];
1003
1003
  let insertCount = 0;
1004
1004
  let deleteCount = 0;
1005
+ let posA = 0;
1006
+ let posB = 0;
1005
1007
  for (const [op, text] of diffs) {
1006
1008
  if (op === DIFF_EQUAL) {
1007
- segments.push({ type: "equal", text });
1009
+ segments.push({ type: "equal", text, posA, posB });
1010
+ posA += text.length;
1011
+ posB += text.length;
1008
1012
  } else if (op === DIFF_INSERT) {
1009
- segments.push({ type: "insert", text });
1013
+ segments.push({ type: "insert", text, posB });
1014
+ posB += text.length;
1010
1015
  insertCount++;
1011
1016
  } else if (op === DIFF_DELETE) {
1012
- segments.push({ type: "delete", text });
1017
+ segments.push({ type: "delete", text, posA });
1018
+ posA += text.length;
1013
1019
  deleteCount++;
1014
1020
  }
1015
1021
  }
@@ -1034,7 +1040,9 @@ function diffDocuments(docA, docB) {
1034
1040
  formatChanges,
1035
1041
  textA,
1036
1042
  textB,
1037
- summary
1043
+ summary,
1044
+ spansB
1045
+ // Include docB spans for mark preservation during merge
1038
1046
  };
1039
1047
  }
1040
1048
 
@@ -1295,6 +1303,34 @@ function groupReplacements(changes) {
1295
1303
  }
1296
1304
  return result;
1297
1305
  }
1306
+ function ensureValidCssColor(color) {
1307
+ if (typeof color !== "string" || !color) {
1308
+ return void 0;
1309
+ }
1310
+ if (/^[0-9a-fA-F]{6}$/.test(color)) {
1311
+ return `#${color}`;
1312
+ }
1313
+ if (/^[0-9a-fA-F]{3}$/.test(color)) {
1314
+ return `#${color}`;
1315
+ }
1316
+ return color;
1317
+ }
1318
+ function normalizeMark(mark) {
1319
+ const attrs = { ...mark.attrs || {} };
1320
+ if (attrs.color !== void 0) {
1321
+ attrs.color = ensureValidCssColor(attrs.color);
1322
+ }
1323
+ return {
1324
+ type: mark.type,
1325
+ attrs
1326
+ };
1327
+ }
1328
+ function normalizeMarks(marks) {
1329
+ return marks.map(normalizeMark);
1330
+ }
1331
+ function normalizeMarksForRendering(marks) {
1332
+ return normalizeMarks(marks);
1333
+ }
1298
1334
  function createTrackInsertMark(author = DEFAULT_AUTHOR, id) {
1299
1335
  return {
1300
1336
  type: "trackInsert",
@@ -1320,6 +1356,8 @@ function createTrackDeleteMark(author = DEFAULT_AUTHOR, id) {
1320
1356
  };
1321
1357
  }
1322
1358
  function createTrackFormatMark(before, after, author = DEFAULT_AUTHOR) {
1359
+ const normalizedBefore = normalizeMarks(before);
1360
+ const normalizedAfter = normalizeMarks(after);
1323
1361
  return {
1324
1362
  type: "trackFormat",
1325
1363
  attrs: {
@@ -1328,8 +1366,8 @@ function createTrackFormatMark(before, after, author = DEFAULT_AUTHOR) {
1328
1366
  authorEmail: author.email,
1329
1367
  authorImage: "",
1330
1368
  date: (/* @__PURE__ */ new Date()).toISOString(),
1331
- before,
1332
- after
1369
+ before: normalizedBefore,
1370
+ after: normalizedAfter
1333
1371
  }
1334
1372
  };
1335
1373
  }
@@ -1516,6 +1554,70 @@ function alignListItems(listA, listB, listPathA, listPathB) {
1516
1554
  function cloneNode(node) {
1517
1555
  return JSON.parse(JSON.stringify(node));
1518
1556
  }
1557
+ function getMarkSpansForRange(spansB, start, end) {
1558
+ const result = [];
1559
+ for (const span of spansB) {
1560
+ if (span.to > start && span.from < end) {
1561
+ const overlapStart = Math.max(span.from, start);
1562
+ const overlapEnd = Math.min(span.to, end);
1563
+ result.push({
1564
+ relStart: overlapStart - start,
1565
+ relEnd: overlapEnd - start,
1566
+ marks: span.marks || []
1567
+ });
1568
+ }
1569
+ }
1570
+ return result;
1571
+ }
1572
+ function createInsertedTextNodes(text, posB, spansB, author, replacementId) {
1573
+ const result = [];
1574
+ const trackMark = createTrackInsertMark(author, replacementId);
1575
+ if (posB === void 0 || spansB.length === 0) {
1576
+ return [{
1577
+ type: "text",
1578
+ text,
1579
+ marks: [trackMark]
1580
+ }];
1581
+ }
1582
+ const markSpans = getMarkSpansForRange(spansB, posB, posB + text.length);
1583
+ if (markSpans.length === 0) {
1584
+ return [{
1585
+ type: "text",
1586
+ text,
1587
+ marks: [trackMark]
1588
+ }];
1589
+ }
1590
+ markSpans.sort((a, b) => a.relStart - b.relStart);
1591
+ let processedUpTo = 0;
1592
+ for (const span of markSpans) {
1593
+ if (span.relStart > processedUpTo) {
1594
+ result.push({
1595
+ type: "text",
1596
+ text: text.substring(processedUpTo, span.relStart),
1597
+ marks: [trackMark]
1598
+ });
1599
+ }
1600
+ if (span.relEnd > span.relStart) {
1601
+ const spanText = text.substring(span.relStart, span.relEnd);
1602
+ const normalizedSpanMarks = normalizeMarksForRendering(span.marks);
1603
+ const marks = [...normalizedSpanMarks, trackMark];
1604
+ result.push({
1605
+ type: "text",
1606
+ text: spanText,
1607
+ marks
1608
+ });
1609
+ processedUpTo = span.relEnd;
1610
+ }
1611
+ }
1612
+ if (processedUpTo < text.length) {
1613
+ result.push({
1614
+ type: "text",
1615
+ text: text.substring(processedUpTo),
1616
+ marks: [trackMark]
1617
+ });
1618
+ }
1619
+ return result;
1620
+ }
1519
1621
  function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1520
1622
  const merged = cloneNode(docA);
1521
1623
  const charStates = [];
@@ -1550,17 +1652,22 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1550
1652
  insertions.push({
1551
1653
  afterOffset: docAOffset,
1552
1654
  text: nextSegment.text,
1553
- replacementId
1655
+ replacementId,
1656
+ posB: nextSegment.posB
1657
+ // Capture docB position for mark lookup
1554
1658
  });
1555
1659
  segIdx++;
1556
1660
  }
1557
1661
  } else if (segment.type === "insert") {
1558
1662
  insertions.push({
1559
1663
  afterOffset: docAOffset,
1560
- text: segment.text
1664
+ text: segment.text,
1665
+ posB: segment.posB
1666
+ // Capture docB position for mark lookup
1561
1667
  });
1562
1668
  }
1563
1669
  }
1670
+ const spansB = diffResult.spansB || [];
1564
1671
  function transformNode(node, nodeOffset, path) {
1565
1672
  if (node.type === "text" && node.text) {
1566
1673
  const text = node.text;
@@ -1571,11 +1678,14 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1571
1678
  const charState = charStates[charOffset] || { type: "equal" };
1572
1679
  const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
1573
1680
  for (const ins of insertionsHere) {
1574
- result.push({
1575
- type: "text",
1576
- text: ins.text,
1577
- marks: [...node.marks || [], createTrackInsertMark(author, ins.replacementId)]
1578
- });
1681
+ const insertedNodes = createInsertedTextNodes(
1682
+ ins.text,
1683
+ ins.posB,
1684
+ spansB,
1685
+ author,
1686
+ ins.replacementId
1687
+ );
1688
+ result.push(...insertedNodes);
1579
1689
  }
1580
1690
  const currentFormatChange = getFormatChangeAt(nodeOffset + i);
1581
1691
  let j = i + 1;
@@ -1598,7 +1708,8 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1598
1708
  currentFormatChange.after,
1599
1709
  author
1600
1710
  );
1601
- marks = [...currentFormatChange.after, trackFormatMark];
1711
+ const normalizedAfterMarks = normalizeMarksForRendering(currentFormatChange.after);
1712
+ marks = [...normalizedAfterMarks, trackFormatMark];
1602
1713
  }
1603
1714
  }
1604
1715
  result.push({
@@ -1611,11 +1722,14 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1611
1722
  const endOffset = nodeOffset + text.length;
1612
1723
  const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
1613
1724
  for (const ins of endInsertions) {
1614
- result.push({
1615
- type: "text",
1616
- text: ins.text,
1617
- marks: [...node.marks || [], createTrackInsertMark(author, ins.replacementId)]
1618
- });
1725
+ const insertedNodes = createInsertedTextNodes(
1726
+ ins.text,
1727
+ ins.posB,
1728
+ spansB,
1729
+ author,
1730
+ ins.replacementId
1731
+ );
1732
+ result.push(...insertedNodes);
1619
1733
  }
1620
1734
  insertions = insertions.filter(
1621
1735
  (ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
@@ -1650,18 +1764,19 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1650
1764
  }
1651
1765
  if (insertions.length > 0) {
1652
1766
  for (const ins of insertions) {
1767
+ const insertedNodes = createInsertedTextNodes(
1768
+ ins.text,
1769
+ ins.posB,
1770
+ spansB,
1771
+ author,
1772
+ ins.replacementId
1773
+ );
1653
1774
  const insertNode = {
1654
1775
  type: "paragraph",
1655
1776
  content: [
1656
1777
  {
1657
1778
  type: "run",
1658
- content: [
1659
- {
1660
- type: "text",
1661
- text: ins.text,
1662
- marks: [createTrackInsertMark(author, ins.replacementId)]
1663
- }
1664
- ]
1779
+ content: insertedNodes
1665
1780
  }
1666
1781
  ]
1667
1782
  };
@@ -2183,9 +2298,10 @@ function cloneNode2(node) {
2183
2298
  }
2184
2299
  function markAllTextAsInserted(node, sharedId, author) {
2185
2300
  if (node.type === "text") {
2301
+ const existingMarks = normalizeMarksForRendering(node.marks || []);
2186
2302
  return {
2187
2303
  ...node,
2188
- marks: [...node.marks || [], createTrackInsertMark(author, sharedId)]
2304
+ marks: [...existingMarks, createTrackInsertMark(author, sharedId)]
2189
2305
  };
2190
2306
  }
2191
2307
  if (node.content && Array.isArray(node.content)) {
@@ -2200,9 +2316,10 @@ function markAllTextAsInserted(node, sharedId, author) {
2200
2316
  }
2201
2317
  function markAllTextAsDeleted(node, sharedId, author) {
2202
2318
  if (node.type === "text") {
2319
+ const existingMarks = normalizeMarksForRendering(node.marks || []);
2203
2320
  return {
2204
2321
  ...node,
2205
- marks: [...node.marks || [], createTrackDeleteMark(author, sharedId)]
2322
+ marks: [...existingMarks, createTrackDeleteMark(author, sharedId)]
2206
2323
  };
2207
2324
  }
2208
2325
  if (node.content && Array.isArray(node.content)) {
@@ -3463,9 +3580,10 @@ var DocxDiffEditor_default = DocxDiffEditor;
3463
3580
  init_nodeFingerprint();
3464
3581
  function markAllTextAsInserted2(node, sharedId, author) {
3465
3582
  if (node.type === "text") {
3583
+ const existingMarks = normalizeMarksForRendering(node.marks || []);
3466
3584
  return {
3467
3585
  ...node,
3468
- marks: [...node.marks || [], createTrackInsertMark(author, sharedId)]
3586
+ marks: [...existingMarks, createTrackInsertMark(author, sharedId)]
3469
3587
  };
3470
3588
  }
3471
3589
  if (node.content && Array.isArray(node.content)) {
@@ -3480,9 +3598,10 @@ function markAllTextAsInserted2(node, sharedId, author) {
3480
3598
  }
3481
3599
  function markAllTextAsDeleted2(node, sharedId, author) {
3482
3600
  if (node.type === "text") {
3601
+ const existingMarks = normalizeMarksForRendering(node.marks || []);
3483
3602
  return {
3484
3603
  ...node,
3485
- marks: [...node.marks || [], createTrackDeleteMark(author, sharedId)]
3604
+ marks: [...existingMarks, createTrackDeleteMark(author, sharedId)]
3486
3605
  };
3487
3606
  }
3488
3607
  if (node.content && Array.isArray(node.content)) {