docx-diff-editor 1.0.46 → 1.0.47

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
 
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
 
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
 
@@ -1516,6 +1524,69 @@ function alignListItems(listA, listB, listPathA, listPathB) {
1516
1524
  function cloneNode(node) {
1517
1525
  return JSON.parse(JSON.stringify(node));
1518
1526
  }
1527
+ function getMarkSpansForRange(spansB, start, end) {
1528
+ const result = [];
1529
+ for (const span of spansB) {
1530
+ if (span.to > start && span.from < end) {
1531
+ const overlapStart = Math.max(span.from, start);
1532
+ const overlapEnd = Math.min(span.to, end);
1533
+ result.push({
1534
+ relStart: overlapStart - start,
1535
+ relEnd: overlapEnd - start,
1536
+ marks: span.marks || []
1537
+ });
1538
+ }
1539
+ }
1540
+ return result;
1541
+ }
1542
+ function createInsertedTextNodes(text, posB, spansB, author, replacementId) {
1543
+ const result = [];
1544
+ const trackMark = createTrackInsertMark(author, replacementId);
1545
+ if (posB === void 0 || spansB.length === 0) {
1546
+ return [{
1547
+ type: "text",
1548
+ text,
1549
+ marks: [trackMark]
1550
+ }];
1551
+ }
1552
+ const markSpans = getMarkSpansForRange(spansB, posB, posB + text.length);
1553
+ if (markSpans.length === 0) {
1554
+ return [{
1555
+ type: "text",
1556
+ text,
1557
+ marks: [trackMark]
1558
+ }];
1559
+ }
1560
+ markSpans.sort((a, b) => a.relStart - b.relStart);
1561
+ let processedUpTo = 0;
1562
+ for (const span of markSpans) {
1563
+ if (span.relStart > processedUpTo) {
1564
+ result.push({
1565
+ type: "text",
1566
+ text: text.substring(processedUpTo, span.relStart),
1567
+ marks: [trackMark]
1568
+ });
1569
+ }
1570
+ if (span.relEnd > span.relStart) {
1571
+ const spanText = text.substring(span.relStart, span.relEnd);
1572
+ const marks = [...span.marks, trackMark];
1573
+ result.push({
1574
+ type: "text",
1575
+ text: spanText,
1576
+ marks
1577
+ });
1578
+ processedUpTo = span.relEnd;
1579
+ }
1580
+ }
1581
+ if (processedUpTo < text.length) {
1582
+ result.push({
1583
+ type: "text",
1584
+ text: text.substring(processedUpTo),
1585
+ marks: [trackMark]
1586
+ });
1587
+ }
1588
+ return result;
1589
+ }
1519
1590
  function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1520
1591
  const merged = cloneNode(docA);
1521
1592
  const charStates = [];
@@ -1550,17 +1621,22 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1550
1621
  insertions.push({
1551
1622
  afterOffset: docAOffset,
1552
1623
  text: nextSegment.text,
1553
- replacementId
1624
+ replacementId,
1625
+ posB: nextSegment.posB
1626
+ // Capture docB position for mark lookup
1554
1627
  });
1555
1628
  segIdx++;
1556
1629
  }
1557
1630
  } else if (segment.type === "insert") {
1558
1631
  insertions.push({
1559
1632
  afterOffset: docAOffset,
1560
- text: segment.text
1633
+ text: segment.text,
1634
+ posB: segment.posB
1635
+ // Capture docB position for mark lookup
1561
1636
  });
1562
1637
  }
1563
1638
  }
1639
+ const spansB = diffResult.spansB || [];
1564
1640
  function transformNode(node, nodeOffset, path) {
1565
1641
  if (node.type === "text" && node.text) {
1566
1642
  const text = node.text;
@@ -1571,11 +1647,14 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1571
1647
  const charState = charStates[charOffset] || { type: "equal" };
1572
1648
  const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
1573
1649
  for (const ins of insertionsHere) {
1574
- result.push({
1575
- type: "text",
1576
- text: ins.text,
1577
- marks: [...node.marks || [], createTrackInsertMark(author, ins.replacementId)]
1578
- });
1650
+ const insertedNodes = createInsertedTextNodes(
1651
+ ins.text,
1652
+ ins.posB,
1653
+ spansB,
1654
+ author,
1655
+ ins.replacementId
1656
+ );
1657
+ result.push(...insertedNodes);
1579
1658
  }
1580
1659
  const currentFormatChange = getFormatChangeAt(nodeOffset + i);
1581
1660
  let j = i + 1;
@@ -1611,11 +1690,14 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1611
1690
  const endOffset = nodeOffset + text.length;
1612
1691
  const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
1613
1692
  for (const ins of endInsertions) {
1614
- result.push({
1615
- type: "text",
1616
- text: ins.text,
1617
- marks: [...node.marks || [], createTrackInsertMark(author, ins.replacementId)]
1618
- });
1693
+ const insertedNodes = createInsertedTextNodes(
1694
+ ins.text,
1695
+ ins.posB,
1696
+ spansB,
1697
+ author,
1698
+ ins.replacementId
1699
+ );
1700
+ result.push(...insertedNodes);
1619
1701
  }
1620
1702
  insertions = insertions.filter(
1621
1703
  (ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
@@ -1650,18 +1732,19 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1650
1732
  }
1651
1733
  if (insertions.length > 0) {
1652
1734
  for (const ins of insertions) {
1735
+ const insertedNodes = createInsertedTextNodes(
1736
+ ins.text,
1737
+ ins.posB,
1738
+ spansB,
1739
+ author,
1740
+ ins.replacementId
1741
+ );
1653
1742
  const insertNode = {
1654
1743
  type: "paragraph",
1655
1744
  content: [
1656
1745
  {
1657
1746
  type: "run",
1658
- content: [
1659
- {
1660
- type: "text",
1661
- text: ins.text,
1662
- marks: [createTrackInsertMark(author, ins.replacementId)]
1663
- }
1664
- ]
1747
+ content: insertedNodes
1665
1748
  }
1666
1749
  ]
1667
1750
  };