docx-diff-editor 1.0.60 → 1.0.62

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.mjs CHANGED
@@ -822,17 +822,6 @@ async function parseHtmlToJson(html, SuperDoc) {
822
822
  }
823
823
  }, TIMEOUTS.CLEANUP_DELAY);
824
824
  };
825
- const createMockPasteEvent = (htmlContent) => {
826
- const dataTransfer = new DataTransfer();
827
- dataTransfer.setData("text/html", htmlContent);
828
- dataTransfer.setData("text/plain", "");
829
- const event = new ClipboardEvent("paste", {
830
- bubbles: true,
831
- cancelable: true,
832
- clipboardData: dataTransfer
833
- });
834
- return event;
835
- };
836
825
  const tryPasteApproach = (sd, onSuccess, onFail) => {
837
826
  try {
838
827
  const editor = sd?.activeEditor;
@@ -986,6 +975,16 @@ function syncNumberingToParent(childEditor, parentEditor) {
986
975
  console.warn("[syncNumberingToParent] Failed to sync numbering definitions:", err);
987
976
  }
988
977
  }
978
+ function createMockPasteEvent(htmlContent) {
979
+ const dataTransfer = new DataTransfer();
980
+ dataTransfer.setData("text/html", htmlContent);
981
+ dataTransfer.setData("text/plain", "");
982
+ return new ClipboardEvent("paste", {
983
+ bubbles: true,
984
+ cancelable: true,
985
+ clipboardData: dataTransfer
986
+ });
987
+ }
989
988
  async function parseHtmlWithLinkedEditor(html, mainEditor) {
990
989
  const container = document.createElement("div");
991
990
  container.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:800px;height:600px;visibility:hidden;";
@@ -1007,17 +1006,26 @@ async function parseHtmlWithLinkedEditor(html, mainEditor) {
1007
1006
  }
1008
1007
  }, TIMEOUTS.CLEANUP_DELAY);
1009
1008
  };
1010
- try {
1011
- mainEditor.createChildEditor({
1012
- element: container,
1013
- html,
1014
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1015
- onCreate: ({ editor: localEditor }) => {
1009
+ const pasteAndExtract = (editor) => {
1010
+ try {
1011
+ if (!editor?.view?.pasteHTML) {
1012
+ throw new Error("pasteHTML not available on child editor");
1013
+ }
1014
+ editor.commands?.focus?.();
1015
+ if (editor.commands?.selectAll && editor.commands?.deleteSelection) {
1016
+ editor.commands.selectAll();
1017
+ editor.commands.deleteSelection();
1018
+ }
1019
+ const mockEvent = createMockPasteEvent(html);
1020
+ editor.view.pasteHTML(html, mockEvent);
1021
+ setTimeout(() => {
1016
1022
  if (resolved) return;
1017
1023
  try {
1018
- childEditor = localEditor;
1019
- syncNumberingToParent(localEditor, mainEditor);
1020
- const json = localEditor.getJSON();
1024
+ syncNumberingToParent(editor, mainEditor);
1025
+ const json = editor.getJSON();
1026
+ if (!json?.content?.length) {
1027
+ throw new Error("Paste produced empty document");
1028
+ }
1021
1029
  const normalizedJson = normalizeRunProperties(json);
1022
1030
  resolved = true;
1023
1031
  cleanup();
@@ -1027,6 +1035,23 @@ async function parseHtmlWithLinkedEditor(html, mainEditor) {
1027
1035
  cleanup();
1028
1036
  reject(err);
1029
1037
  }
1038
+ }, 100);
1039
+ } catch (err) {
1040
+ resolved = true;
1041
+ cleanup();
1042
+ reject(err);
1043
+ }
1044
+ };
1045
+ try {
1046
+ mainEditor.createChildEditor({
1047
+ element: container,
1048
+ html: "<p></p>",
1049
+ // Minimal empty document - actual HTML pasted via pasteHTML
1050
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1051
+ onCreate: ({ editor: localEditor }) => {
1052
+ if (resolved) return;
1053
+ childEditor = localEditor;
1054
+ pasteAndExtract(localEditor);
1030
1055
  },
1031
1056
  onError: (error) => {
1032
1057
  if (resolved) return;
@@ -1751,410 +1776,175 @@ function alignListItems(listA, listB, listPathA, listPathB) {
1751
1776
  }
1752
1777
  return alignNodes(itemsA, itemsB);
1753
1778
  }
1754
- function cloneNode(node) {
1755
- return JSON.parse(JSON.stringify(node));
1779
+
1780
+ // src/services/attrComparer.ts
1781
+ var KNOWN_DEFAULTS = {
1782
+ paragraph: {
1783
+ textAlign: "left",
1784
+ indent: 0,
1785
+ lineSpacing: 1
1786
+ },
1787
+ heading: {
1788
+ level: 1,
1789
+ textAlign: "left"
1790
+ },
1791
+ table: {
1792
+ alignment: "left",
1793
+ borderStyle: "single"
1794
+ },
1795
+ tableCell: {
1796
+ verticalAlign: "top",
1797
+ colspan: 1,
1798
+ rowspan: 1
1799
+ },
1800
+ listItem: {
1801
+ indent: 0
1802
+ },
1803
+ image: {
1804
+ width: "auto",
1805
+ height: "auto"
1806
+ }
1807
+ };
1808
+ var IGNORED_ATTRS = /* @__PURE__ */ new Set([
1809
+ "id",
1810
+ "class",
1811
+ "data-id",
1812
+ "data-pm-slice",
1813
+ "__trackAttrChanges"
1814
+ ]);
1815
+ function isPlainObject(value) {
1816
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
1756
1817
  }
1757
- function getMarkSpansForRange(spansB, start, end) {
1758
- const result = [];
1759
- for (const span of spansB) {
1760
- if (span.to > start && span.from < end) {
1761
- const overlapStart = Math.max(span.from, start);
1762
- const overlapEnd = Math.min(span.to, end);
1763
- result.push({
1764
- relStart: overlapStart - start,
1765
- relEnd: overlapEnd - start,
1766
- marks: span.marks || []
1818
+ function deepEqual2(a, b) {
1819
+ if (a === b) return true;
1820
+ if (typeof a !== typeof b) return false;
1821
+ if (a === null || b === null) return a === b;
1822
+ if (Array.isArray(a) && Array.isArray(b)) {
1823
+ if (a.length !== b.length) return false;
1824
+ return a.every((val, i) => deepEqual2(val, b[i]));
1825
+ }
1826
+ if (isPlainObject(a) && isPlainObject(b)) {
1827
+ const keysA = Object.keys(a);
1828
+ const keysB = Object.keys(b);
1829
+ if (keysA.length !== keysB.length) return false;
1830
+ return keysA.every((key) => deepEqual2(a[key], b[key]));
1831
+ }
1832
+ return false;
1833
+ }
1834
+ function normalizeValue(value, key, nodeType) {
1835
+ if (value !== void 0 && value !== null) {
1836
+ return value;
1837
+ }
1838
+ const defaults = KNOWN_DEFAULTS[nodeType];
1839
+ if (defaults && key in defaults) {
1840
+ return defaults[key];
1841
+ }
1842
+ return value;
1843
+ }
1844
+ function compareAttrs(attrsA, attrsB, nodeType = "", prefix = "") {
1845
+ const diffs = [];
1846
+ const a = attrsA || {};
1847
+ const b = attrsB || {};
1848
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
1849
+ for (const key of allKeys) {
1850
+ if (IGNORED_ATTRS.has(key)) continue;
1851
+ const fullKey = prefix ? `${prefix}.${key}` : key;
1852
+ const valueA = normalizeValue(a[key], key, nodeType);
1853
+ const valueB = normalizeValue(b[key], key, nodeType);
1854
+ if (isPlainObject(valueA) && isPlainObject(valueB)) {
1855
+ const nestedDiffs = compareAttrs(
1856
+ valueA,
1857
+ valueB,
1858
+ nodeType,
1859
+ fullKey
1860
+ );
1861
+ diffs.push(...nestedDiffs);
1862
+ continue;
1863
+ }
1864
+ if (!deepEqual2(valueA, valueB)) {
1865
+ diffs.push({
1866
+ key: fullKey,
1867
+ before: valueA,
1868
+ after: valueB
1767
1869
  });
1768
1870
  }
1769
1871
  }
1770
- return result;
1872
+ return diffs;
1771
1873
  }
1772
- function createInsertedTextNodes(text, posB, spansB, author, replacementId) {
1773
- const result = [];
1774
- const trackMark = createTrackInsertMark(author, replacementId);
1775
- if (posB === void 0 || spansB.length === 0) {
1776
- return [{
1777
- type: "text",
1778
- text,
1779
- marks: [trackMark]
1780
- }];
1781
- }
1782
- const markSpans = getMarkSpansForRange(spansB, posB, posB + text.length);
1783
- if (markSpans.length === 0) {
1784
- return [{
1785
- type: "text",
1786
- text,
1787
- marks: [trackMark]
1788
- }];
1874
+ function compareNodeAttrs(nodeA, nodeB) {
1875
+ const nodeType = nodeA.type || nodeB.type || "";
1876
+ return compareAttrs(nodeA.attrs, nodeB.attrs, nodeType);
1877
+ }
1878
+
1879
+ // src/services/tableBlockDiffer.ts
1880
+ function detectColumnChanges(matchedRows, tableA, tableB, tablePathA, tablePathB) {
1881
+ const changes = [];
1882
+ if (matchedRows.length === 0) return changes;
1883
+ const firstMatch = matchedRows[0];
1884
+ const rowIdxA = firstMatch.pathA[firstMatch.pathA.length - 1];
1885
+ const rowIdxB = firstMatch.pathB[firstMatch.pathB.length - 1];
1886
+ const rowA = tableA.content?.[rowIdxA];
1887
+ const rowB = tableB.content?.[rowIdxB];
1888
+ if (!rowA || !rowB) return changes;
1889
+ const cellCountA = rowA.content?.length || 0;
1890
+ const cellCountB = rowB.content?.length || 0;
1891
+ const diff = cellCountB - cellCountA;
1892
+ if (diff === 0) return changes;
1893
+ let consistent = true;
1894
+ for (const match of matchedRows) {
1895
+ const idxA = match.pathA[match.pathA.length - 1];
1896
+ const idxB = match.pathB[match.pathB.length - 1];
1897
+ const rA = tableA.content?.[idxA];
1898
+ const rB = tableB.content?.[idxB];
1899
+ if (!rA || !rB) continue;
1900
+ const countA = rA.content?.length || 0;
1901
+ const countB = rB.content?.length || 0;
1902
+ if (countB - countA !== diff) {
1903
+ consistent = false;
1904
+ break;
1905
+ }
1789
1906
  }
1790
- markSpans.sort((a, b) => a.relStart - b.relStart);
1791
- let processedUpTo = 0;
1792
- for (const span of markSpans) {
1793
- if (span.relStart > processedUpTo) {
1794
- result.push({
1795
- type: "text",
1796
- text: text.substring(processedUpTo, span.relStart),
1797
- marks: [trackMark]
1907
+ if (!consistent) return changes;
1908
+ if (diff > 0) {
1909
+ for (let i = 0; i < diff; i++) {
1910
+ changes.push({
1911
+ id: v4(),
1912
+ type: "columnInsert",
1913
+ nodeType: "tableColumn",
1914
+ path: [...tablePathB],
1915
+ node: { type: "column", position: cellCountA + i }
1798
1916
  });
1799
1917
  }
1800
- if (span.relEnd > span.relStart) {
1801
- const spanText = text.substring(span.relStart, span.relEnd);
1802
- const normalizedSpanMarks = normalizeMarksForRendering(span.marks);
1803
- const marks = [...normalizedSpanMarks, trackMark];
1804
- result.push({
1805
- type: "text",
1806
- text: spanText,
1807
- marks
1918
+ } else {
1919
+ for (let i = 0; i < Math.abs(diff); i++) {
1920
+ changes.push({
1921
+ id: v4(),
1922
+ type: "columnDelete",
1923
+ nodeType: "tableColumn",
1924
+ path: [...tablePathA],
1925
+ node: { type: "column", position: cellCountB + i }
1808
1926
  });
1809
- processedUpTo = span.relEnd;
1810
1927
  }
1811
1928
  }
1812
- if (processedUpTo < text.length) {
1813
- result.push({
1814
- type: "text",
1815
- text: text.substring(processedUpTo),
1816
- marks: [trackMark]
1817
- });
1818
- }
1819
- return result;
1929
+ return changes;
1820
1930
  }
1821
- function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
1822
- const merged = cloneNode(docA);
1823
- const charStates = [];
1824
- let insertions = [];
1825
- const formatChanges = diffResult.formatChanges || [];
1826
- function getFormatChangeAt(pos) {
1827
- for (const fc of formatChanges) {
1828
- if (pos >= fc.from && pos < fc.to) {
1829
- return fc;
1830
- }
1831
- }
1832
- return null;
1833
- }
1834
- let docAOffset = 0;
1835
- const segments = diffResult.segments;
1836
- for (let segIdx = 0; segIdx < segments.length; segIdx++) {
1837
- const segment = segments[segIdx];
1838
- if (segment.type === "equal") {
1839
- for (let i = 0; i < segment.text.length; i++) {
1840
- charStates[docAOffset + i] = { type: "equal" };
1841
- }
1842
- docAOffset += segment.text.length;
1843
- } else if (segment.type === "delete") {
1844
- const nextSegment = segments[segIdx + 1];
1845
- const isReplacement = nextSegment && nextSegment.type === "insert";
1846
- const replacementId = isReplacement ? v4() : void 0;
1847
- for (let i = 0; i < segment.text.length; i++) {
1848
- charStates[docAOffset + i] = { type: "delete", replacementId };
1849
- }
1850
- docAOffset += segment.text.length;
1851
- if (isReplacement && nextSegment) {
1852
- insertions.push({
1853
- afterOffset: docAOffset,
1854
- text: nextSegment.text,
1855
- replacementId,
1856
- posB: nextSegment.posB
1857
- // Capture docB position for mark lookup
1858
- });
1859
- segIdx++;
1860
- }
1861
- } else if (segment.type === "insert") {
1862
- insertions.push({
1863
- afterOffset: docAOffset,
1864
- text: segment.text,
1865
- posB: segment.posB
1866
- // Capture docB position for mark lookup
1867
- });
1868
- }
1869
- }
1870
- const spansB = diffResult.spansB || [];
1871
- function transformNode(node, nodeOffset, path) {
1872
- if (node.type === "text" && node.text) {
1873
- const text = node.text;
1874
- const result = [];
1875
- let i = 0;
1876
- while (i < text.length) {
1877
- const charOffset = nodeOffset + i;
1878
- const charState = charStates[charOffset] || { type: "equal" };
1879
- const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
1880
- for (const ins of insertionsHere) {
1881
- const insertedNodes = createInsertedTextNodes(
1882
- ins.text,
1883
- ins.posB,
1884
- spansB,
1885
- author,
1886
- ins.replacementId
1887
- );
1888
- result.push(...insertedNodes);
1889
- }
1890
- const currentFormatChange = getFormatChangeAt(nodeOffset + i);
1891
- let j = i + 1;
1892
- while (j < text.length) {
1893
- const nextState = charStates[nodeOffset + j] || { type: "equal" };
1894
- if (nextState.type !== charState.type) break;
1895
- if (insertions.some((ins) => ins.afterOffset === nodeOffset + j)) break;
1896
- const nextFormatChange = getFormatChangeAt(nodeOffset + j);
1897
- if (currentFormatChange !== nextFormatChange) break;
1898
- j++;
1899
- }
1900
- const chunk = text.substring(i, j);
1901
- let marks = [...node.marks || []];
1902
- if (charState.type === "delete") {
1903
- marks.push(createTrackDeleteMark(author, charState.replacementId));
1904
- } else if (charState.type === "equal") {
1905
- if (currentFormatChange) {
1906
- const trackFormatMark = createTrackFormatMark(
1907
- currentFormatChange.before,
1908
- currentFormatChange.after,
1909
- author
1910
- );
1911
- const normalizedAfterMarks = normalizeMarksForRendering(currentFormatChange.after);
1912
- marks = [...normalizedAfterMarks, trackFormatMark];
1913
- }
1914
- }
1915
- result.push({
1916
- type: "text",
1917
- text: chunk,
1918
- marks: marks.length > 0 ? marks : void 0
1919
- });
1920
- i = j;
1921
- }
1922
- const endOffset = nodeOffset + text.length;
1923
- const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
1924
- for (const ins of endInsertions) {
1925
- const insertedNodes = createInsertedTextNodes(
1926
- ins.text,
1927
- ins.posB,
1928
- spansB,
1929
- author,
1930
- ins.replacementId
1931
- );
1932
- result.push(...insertedNodes);
1933
- }
1934
- insertions = insertions.filter(
1935
- (ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
1936
- );
1937
- return { nodes: result, consumedLength: text.length };
1938
- }
1939
- if (node.content && Array.isArray(node.content)) {
1940
- const newContent = [];
1941
- let offset = nodeOffset;
1942
- for (const child of node.content) {
1943
- const { nodes, consumedLength } = transformNode(child, offset);
1944
- newContent.push(...nodes);
1945
- offset += consumedLength;
1946
- }
1947
- return {
1948
- nodes: [{ ...node, content: newContent }],
1949
- consumedLength: offset - nodeOffset
1950
- };
1951
- }
1952
- return { nodes: [node], consumedLength: 0 };
1953
- }
1954
- if (merged.content && Array.isArray(merged.content)) {
1955
- const newContent = [];
1956
- let offset = 0;
1957
- for (let i = 0; i < merged.content.length; i++) {
1958
- const child = merged.content[i];
1959
- const { nodes, consumedLength } = transformNode(child, offset);
1960
- newContent.push(...nodes);
1961
- offset += consumedLength;
1962
- }
1963
- merged.content = newContent;
1964
- }
1965
- if (insertions.length > 0) {
1966
- for (const ins of insertions) {
1967
- const insertedNodes = createInsertedTextNodes(
1968
- ins.text,
1969
- ins.posB,
1970
- spansB,
1971
- author,
1972
- ins.replacementId
1973
- );
1974
- const insertNode = {
1975
- type: "paragraph",
1976
- content: [
1977
- {
1978
- type: "run",
1979
- content: insertedNodes
1980
- }
1981
- ]
1982
- };
1983
- if (!merged.content) merged.content = [];
1984
- merged.content.push(insertNode);
1985
- }
1986
- }
1987
- return merged;
1988
- }
1989
-
1990
- // src/services/attrComparer.ts
1991
- var KNOWN_DEFAULTS = {
1992
- paragraph: {
1993
- textAlign: "left",
1994
- indent: 0,
1995
- lineSpacing: 1
1996
- },
1997
- heading: {
1998
- level: 1,
1999
- textAlign: "left"
2000
- },
2001
- table: {
2002
- alignment: "left",
2003
- borderStyle: "single"
2004
- },
2005
- tableCell: {
2006
- verticalAlign: "top",
2007
- colspan: 1,
2008
- rowspan: 1
2009
- },
2010
- listItem: {
2011
- indent: 0
2012
- },
2013
- image: {
2014
- width: "auto",
2015
- height: "auto"
2016
- }
2017
- };
2018
- var IGNORED_ATTRS = /* @__PURE__ */ new Set([
2019
- "id",
2020
- "class",
2021
- "data-id",
2022
- "data-pm-slice",
2023
- "__trackAttrChanges"
2024
- ]);
2025
- function isPlainObject(value) {
2026
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
2027
- }
2028
- function deepEqual2(a, b) {
2029
- if (a === b) return true;
2030
- if (typeof a !== typeof b) return false;
2031
- if (a === null || b === null) return a === b;
2032
- if (Array.isArray(a) && Array.isArray(b)) {
2033
- if (a.length !== b.length) return false;
2034
- return a.every((val, i) => deepEqual2(val, b[i]));
2035
- }
2036
- if (isPlainObject(a) && isPlainObject(b)) {
2037
- const keysA = Object.keys(a);
2038
- const keysB = Object.keys(b);
2039
- if (keysA.length !== keysB.length) return false;
2040
- return keysA.every((key) => deepEqual2(a[key], b[key]));
2041
- }
2042
- return false;
2043
- }
2044
- function normalizeValue(value, key, nodeType) {
2045
- if (value !== void 0 && value !== null) {
2046
- return value;
2047
- }
2048
- const defaults = KNOWN_DEFAULTS[nodeType];
2049
- if (defaults && key in defaults) {
2050
- return defaults[key];
2051
- }
2052
- return value;
2053
- }
2054
- function compareAttrs(attrsA, attrsB, nodeType = "", prefix = "") {
2055
- const diffs = [];
2056
- const a = attrsA || {};
2057
- const b = attrsB || {};
2058
- const allKeys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
2059
- for (const key of allKeys) {
2060
- if (IGNORED_ATTRS.has(key)) continue;
2061
- const fullKey = prefix ? `${prefix}.${key}` : key;
2062
- const valueA = normalizeValue(a[key], key, nodeType);
2063
- const valueB = normalizeValue(b[key], key, nodeType);
2064
- if (isPlainObject(valueA) && isPlainObject(valueB)) {
2065
- const nestedDiffs = compareAttrs(
2066
- valueA,
2067
- valueB,
2068
- nodeType,
2069
- fullKey
2070
- );
2071
- diffs.push(...nestedDiffs);
2072
- continue;
2073
- }
2074
- if (!deepEqual2(valueA, valueB)) {
2075
- diffs.push({
2076
- key: fullKey,
2077
- before: valueA,
2078
- after: valueB
2079
- });
2080
- }
2081
- }
2082
- return diffs;
2083
- }
2084
- function compareNodeAttrs(nodeA, nodeB) {
2085
- const nodeType = nodeA.type || nodeB.type || "";
2086
- return compareAttrs(nodeA.attrs, nodeB.attrs, nodeType);
2087
- }
2088
-
2089
- // src/services/tableBlockDiffer.ts
2090
- function detectColumnChanges(matchedRows, tableA, tableB, tablePathA, tablePathB) {
2091
- const changes = [];
2092
- if (matchedRows.length === 0) return changes;
2093
- const firstMatch = matchedRows[0];
2094
- const rowIdxA = firstMatch.pathA[firstMatch.pathA.length - 1];
2095
- const rowIdxB = firstMatch.pathB[firstMatch.pathB.length - 1];
2096
- const rowA = tableA.content?.[rowIdxA];
2097
- const rowB = tableB.content?.[rowIdxB];
2098
- if (!rowA || !rowB) return changes;
2099
- const cellCountA = rowA.content?.length || 0;
2100
- const cellCountB = rowB.content?.length || 0;
2101
- const diff = cellCountB - cellCountA;
2102
- if (diff === 0) return changes;
2103
- let consistent = true;
2104
- for (const match of matchedRows) {
2105
- const idxA = match.pathA[match.pathA.length - 1];
2106
- const idxB = match.pathB[match.pathB.length - 1];
2107
- const rA = tableA.content?.[idxA];
2108
- const rB = tableB.content?.[idxB];
2109
- if (!rA || !rB) continue;
2110
- const countA = rA.content?.length || 0;
2111
- const countB = rB.content?.length || 0;
2112
- if (countB - countA !== diff) {
2113
- consistent = false;
2114
- break;
2115
- }
2116
- }
2117
- if (!consistent) return changes;
2118
- if (diff > 0) {
2119
- for (let i = 0; i < diff; i++) {
2120
- changes.push({
2121
- id: v4(),
2122
- type: "columnInsert",
2123
- nodeType: "tableColumn",
2124
- path: [...tablePathB],
2125
- node: { type: "column", position: cellCountA + i }
2126
- });
2127
- }
2128
- } else {
2129
- for (let i = 0; i < Math.abs(diff); i++) {
2130
- changes.push({
2131
- id: v4(),
2132
- type: "columnDelete",
2133
- nodeType: "tableColumn",
2134
- path: [...tablePathA],
2135
- node: { type: "column", position: cellCountB + i }
2136
- });
2137
- }
2138
- }
2139
- return changes;
2140
- }
2141
- function diffTables(tableA, tableB, tablePathA, tablePathB) {
2142
- const result = {
2143
- rowChanges: [],
2144
- columnChanges: [],
2145
- cellMatches: [],
2146
- tableAttrChanges: null,
2147
- cellAttrChanges: []
2148
- };
2149
- const tableAttrDiffs = compareNodeAttrs(tableA, tableB);
2150
- if (tableAttrDiffs.length > 0) {
2151
- result.tableAttrChanges = {
2152
- id: v4(),
2153
- nodeType: "table",
2154
- pathA: tablePathA,
2155
- pathB: tablePathB,
2156
- changes: tableAttrDiffs
2157
- };
1931
+ function diffTables(tableA, tableB, tablePathA, tablePathB) {
1932
+ const result = {
1933
+ rowChanges: [],
1934
+ columnChanges: [],
1935
+ cellMatches: [],
1936
+ tableAttrChanges: null,
1937
+ cellAttrChanges: []
1938
+ };
1939
+ const tableAttrDiffs = compareNodeAttrs(tableA, tableB);
1940
+ if (tableAttrDiffs.length > 0) {
1941
+ result.tableAttrChanges = {
1942
+ id: v4(),
1943
+ nodeType: "table",
1944
+ pathA: tablePathA,
1945
+ pathB: tablePathB,
1946
+ changes: tableAttrDiffs
1947
+ };
2158
1948
  }
2159
1949
  const rowAlignment = alignTableRows(tableA, tableB, tablePathA, tablePathB);
2160
1950
  for (const inserted of rowAlignment.insertions) {
@@ -2213,290 +2003,777 @@ function diffTables(tableA, tableB, tablePathA, tablePathB) {
2213
2003
  }
2214
2004
  }
2215
2005
  }
2216
- return result;
2217
- }
2218
- function isTable(node) {
2219
- return node?.type === "table";
2220
- }
2221
- function isTableRow(node) {
2222
- return node?.type === "tableRow";
2223
- }
2224
- function getRowLocation(tablePath, rowIndex, tableIndex) {
2225
- return `Table ${tableIndex + 1}, Row ${rowIndex + 1}`;
2226
- }
2227
- function getRowPreview(row, maxLength = 50) {
2228
- const cells = [];
2229
- for (const cell of row.content || []) {
2230
- const cellText = extractCellText(cell);
2231
- if (cellText) {
2232
- cells.push(cellText);
2006
+ return result;
2007
+ }
2008
+ function isTable(node) {
2009
+ return node?.type === "table";
2010
+ }
2011
+ function isTableRow(node) {
2012
+ return node?.type === "tableRow";
2013
+ }
2014
+ function getRowLocation(tablePath, rowIndex, tableIndex) {
2015
+ return `Table ${tableIndex + 1}, Row ${rowIndex + 1}`;
2016
+ }
2017
+ function getRowPreview(row, maxLength = 50) {
2018
+ const cells = [];
2019
+ for (const cell of row.content || []) {
2020
+ const cellText = extractCellText(cell);
2021
+ if (cellText) {
2022
+ cells.push(cellText);
2023
+ }
2024
+ }
2025
+ const preview = cells.join(" | ");
2026
+ if (preview.length > maxLength) {
2027
+ return preview.substring(0, maxLength - 3) + "...";
2028
+ }
2029
+ return preview;
2030
+ }
2031
+ function extractCellText(cell) {
2032
+ if (!cell.content) return "";
2033
+ const texts = [];
2034
+ for (const child of cell.content) {
2035
+ if (child.type === "text") {
2036
+ texts.push(child.text || "");
2037
+ } else if (child.type === "paragraph" && child.content) {
2038
+ for (const pChild of child.content) {
2039
+ if (pChild.type === "text") {
2040
+ texts.push(pChild.text || "");
2041
+ }
2042
+ }
2043
+ }
2044
+ }
2045
+ return texts.join("").trim();
2046
+ }
2047
+ function isList(node) {
2048
+ return node?.type === "bulletList" || node?.type === "orderedList";
2049
+ }
2050
+ function isListItem(node) {
2051
+ return node?.type === "listItem";
2052
+ }
2053
+ function extractListItemText(item) {
2054
+ const texts = [];
2055
+ function extract(node) {
2056
+ if (!node) return;
2057
+ if (node.type === "text") {
2058
+ texts.push(node.text || "");
2059
+ }
2060
+ if (node.content && Array.isArray(node.content)) {
2061
+ for (const child of node.content) {
2062
+ if (!isList(child)) {
2063
+ extract(child);
2064
+ }
2065
+ }
2066
+ }
2067
+ }
2068
+ extract(item);
2069
+ return texts.join("").trim();
2070
+ }
2071
+ function findNestedLists(item) {
2072
+ const lists = [];
2073
+ if (!item.content) return lists;
2074
+ for (const child of item.content) {
2075
+ if (isList(child)) {
2076
+ lists.push(child);
2077
+ }
2078
+ }
2079
+ return lists;
2080
+ }
2081
+ function diffLists(listA, listB, listPathA, listPathB, depth = 0) {
2082
+ const result = {
2083
+ itemChanges: [],
2084
+ itemMatches: [],
2085
+ nestedChanges: []
2086
+ };
2087
+ const alignment = alignListItems(listA, listB, listPathA, listPathB);
2088
+ for (const inserted of alignment.insertions) {
2089
+ result.itemChanges.push({
2090
+ id: v4(),
2091
+ type: "listItemInsert",
2092
+ nodeType: "listItem",
2093
+ path: inserted.path,
2094
+ node: inserted.node
2095
+ });
2096
+ }
2097
+ for (const deleted of alignment.deletions) {
2098
+ result.itemChanges.push({
2099
+ id: v4(),
2100
+ type: "listItemDelete",
2101
+ nodeType: "listItem",
2102
+ path: deleted.path,
2103
+ node: deleted.node
2104
+ });
2105
+ }
2106
+ result.itemMatches = alignment.matched;
2107
+ for (const match of alignment.matched) {
2108
+ const itemIdxA = match.pathA[match.pathA.length - 1];
2109
+ const itemIdxB = match.pathB[match.pathB.length - 1];
2110
+ const itemA = listA.content?.[itemIdxA];
2111
+ const itemB = listB.content?.[itemIdxB];
2112
+ if (!itemA || !itemB) continue;
2113
+ const nestedA = findNestedLists(itemA);
2114
+ const nestedB = findNestedLists(itemB);
2115
+ const maxNested = Math.max(nestedA.length, nestedB.length);
2116
+ for (let i = 0; i < maxNested; i++) {
2117
+ const nA = nestedA[i];
2118
+ const nB = nestedB[i];
2119
+ if (nA && nB) {
2120
+ const nestedResult = diffLists(
2121
+ nA,
2122
+ nB,
2123
+ [...match.pathA, i],
2124
+ [...match.pathB, i],
2125
+ depth + 1
2126
+ );
2127
+ result.nestedChanges.push(nestedResult);
2128
+ } else if (!nA && nB) {
2129
+ result.itemChanges.push({
2130
+ id: v4(),
2131
+ type: "listItemInsert",
2132
+ nodeType: "nestedList",
2133
+ path: [...match.pathB, i],
2134
+ node: nB
2135
+ });
2136
+ } else if (nA && !nB) {
2137
+ result.itemChanges.push({
2138
+ id: v4(),
2139
+ type: "listItemDelete",
2140
+ nodeType: "nestedList",
2141
+ path: [...match.pathA, i],
2142
+ node: nA
2143
+ });
2144
+ }
2145
+ }
2146
+ }
2147
+ return result;
2148
+ }
2149
+ function getListItemLocation(listPath, itemIndex, listIndex, depth = 0) {
2150
+ const depthStr = depth > 0 ? ` (nested, level ${depth + 1})` : "";
2151
+ return `List ${listIndex + 1}, Item ${itemIndex + 1}${depthStr}`;
2152
+ }
2153
+ function getListItemPreview(item, maxLength = 50) {
2154
+ const text = extractListItemText(item);
2155
+ if (text.length > maxLength) {
2156
+ return text.substring(0, maxLength - 3) + "...";
2157
+ }
2158
+ return text || "(empty item)";
2159
+ }
2160
+ function isImage(node) {
2161
+ return node?.type === "image";
2162
+ }
2163
+ function isHorizontalRule(node) {
2164
+ return node?.type === "horizontalRule" || node?.type === "hr";
2165
+ }
2166
+ function isHardBreak(node) {
2167
+ return node?.type === "hardBreak";
2168
+ }
2169
+ function isPageBreak(node) {
2170
+ return node?.type === "pageBreak";
2171
+ }
2172
+ function isEmbedded(node) {
2173
+ const embeddedTypes = [
2174
+ "equation",
2175
+ "math",
2176
+ "embed",
2177
+ "chart",
2178
+ "drawing",
2179
+ "shape"
2180
+ ];
2181
+ return embeddedTypes.includes(node?.type);
2182
+ }
2183
+ function isAtomicNode(node) {
2184
+ return isImage(node) || isHorizontalRule(node) || isHardBreak(node) || isPageBreak(node) || isEmbedded(node);
2185
+ }
2186
+ function getImageIdentifier(node) {
2187
+ if (!isImage(node)) return "";
2188
+ const attrs = node.attrs || {};
2189
+ if (attrs.src) {
2190
+ return `src:${attrs.src}`;
2191
+ }
2192
+ if (attrs.data) {
2193
+ return `data:${simpleHash2(attrs.data)}`;
2194
+ }
2195
+ if (attrs.alt) {
2196
+ return `alt:${attrs.alt}`;
2197
+ }
2198
+ return "unknown";
2199
+ }
2200
+ function simpleHash2(str) {
2201
+ let hash = 5381;
2202
+ const sample = str.substring(0, 1e3);
2203
+ for (let i = 0; i < sample.length; i++) {
2204
+ hash = (hash << 5) + hash ^ sample.charCodeAt(i);
2205
+ }
2206
+ return (hash >>> 0).toString(16);
2207
+ }
2208
+ function findImages(doc, basePath = []) {
2209
+ const images = [];
2210
+ function traverse(node, path) {
2211
+ if (!node) return;
2212
+ if (isImage(node)) {
2213
+ images.push({ node, path: [...path] });
2214
+ }
2215
+ if (node.content && Array.isArray(node.content)) {
2216
+ node.content.forEach((child, i) => {
2217
+ traverse(child, [...path, i]);
2218
+ });
2219
+ }
2220
+ }
2221
+ traverse(doc, basePath);
2222
+ return images;
2223
+ }
2224
+ function diffImages(docA, docB) {
2225
+ const imagesA = findImages(docA);
2226
+ const imagesB = findImages(docB);
2227
+ const inserted = [];
2228
+ const deleted = [];
2229
+ const idsA = /* @__PURE__ */ new Map();
2230
+ const idsB = /* @__PURE__ */ new Map();
2231
+ for (const img of imagesA) {
2232
+ const id = getImageIdentifier(img.node);
2233
+ idsA.set(id, img);
2234
+ }
2235
+ for (const img of imagesB) {
2236
+ const id = getImageIdentifier(img.node);
2237
+ idsB.set(id, img);
2238
+ }
2239
+ for (const [id, img] of idsA) {
2240
+ if (!idsB.has(id)) {
2241
+ deleted.push({
2242
+ id: v4(),
2243
+ type: "imageDelete",
2244
+ nodeType: "image",
2245
+ path: img.path,
2246
+ node: img.node
2247
+ });
2248
+ }
2249
+ }
2250
+ for (const [id, img] of idsB) {
2251
+ if (!idsA.has(id)) {
2252
+ inserted.push({
2253
+ id: v4(),
2254
+ type: "imageInsert",
2255
+ nodeType: "image",
2256
+ path: img.path,
2257
+ node: img.node
2258
+ });
2233
2259
  }
2234
2260
  }
2235
- const preview = cells.join(" | ");
2236
- if (preview.length > maxLength) {
2237
- return preview.substring(0, maxLength - 3) + "...";
2261
+ return { inserted, deleted };
2262
+ }
2263
+ function getImageLocation(path) {
2264
+ if (path.length <= 1) {
2265
+ return `Image at position ${path[0] + 1}`;
2238
2266
  }
2239
- return preview;
2267
+ return `Image (nested at depth ${path.length})`;
2240
2268
  }
2241
- function extractCellText(cell) {
2242
- if (!cell.content) return "";
2243
- const texts = [];
2244
- for (const child of cell.content) {
2245
- if (child.type === "text") {
2246
- texts.push(child.text || "");
2247
- } else if (child.type === "paragraph" && child.content) {
2248
- for (const pChild of child.content) {
2249
- if (pChild.type === "text") {
2250
- texts.push(pChild.text || "");
2251
- }
2252
- }
2269
+ function getImagePreview(node) {
2270
+ if (!isImage(node)) return "";
2271
+ const attrs = node.attrs || {};
2272
+ if (attrs.alt) {
2273
+ return `"${attrs.alt}"`;
2274
+ }
2275
+ if (attrs.src) {
2276
+ const src = attrs.src;
2277
+ const filename = src.split("/").pop()?.split("?")[0];
2278
+ if (filename) {
2279
+ return filename;
2253
2280
  }
2254
2281
  }
2255
- return texts.join("").trim();
2282
+ return "(image)";
2256
2283
  }
2257
- function isList(node) {
2258
- return node?.type === "bulletList" || node?.type === "orderedList";
2284
+
2285
+ // src/services/blockLevelMerger.ts
2286
+ function markAllTextAsInserted(node, sharedId, author) {
2287
+ if (node.type === "text") {
2288
+ const existingMarks = normalizeMarksForRendering(node.marks || []);
2289
+ return {
2290
+ ...node,
2291
+ marks: [...existingMarks, createTrackInsertMark(author, sharedId)]
2292
+ };
2293
+ }
2294
+ if (node.content && Array.isArray(node.content)) {
2295
+ return {
2296
+ ...node,
2297
+ content: node.content.map(
2298
+ (child) => markAllTextAsInserted(child, sharedId, author)
2299
+ )
2300
+ };
2301
+ }
2302
+ return node;
2259
2303
  }
2260
- function isListItem(node) {
2261
- return node?.type === "listItem";
2304
+ function markAllTextAsDeleted(node, sharedId, author) {
2305
+ if (node.type === "text") {
2306
+ const existingMarks = normalizeMarksForRendering(node.marks || []);
2307
+ return {
2308
+ ...node,
2309
+ marks: [...existingMarks, createTrackDeleteMark(author, sharedId)]
2310
+ };
2311
+ }
2312
+ if (node.content && Array.isArray(node.content)) {
2313
+ return {
2314
+ ...node,
2315
+ content: node.content.map(
2316
+ (child) => markAllTextAsDeleted(child, sharedId, author)
2317
+ )
2318
+ };
2319
+ }
2320
+ return node;
2262
2321
  }
2263
- function extractListItemText(item) {
2322
+ function cloneNode(node) {
2323
+ return JSON.parse(JSON.stringify(node));
2324
+ }
2325
+ function extractTextPreview(node, maxLength = 50) {
2264
2326
  const texts = [];
2265
- function extract(node) {
2266
- if (!node) return;
2267
- if (node.type === "text") {
2268
- texts.push(node.text || "");
2327
+ function extract(n) {
2328
+ if (n.type === "text") {
2329
+ texts.push(n.text || "");
2269
2330
  }
2270
- if (node.content && Array.isArray(node.content)) {
2271
- for (const child of node.content) {
2272
- if (!isList(child)) {
2273
- extract(child);
2274
- }
2331
+ if (n.content) {
2332
+ for (const child of n.content) {
2333
+ extract(child);
2275
2334
  }
2276
2335
  }
2277
2336
  }
2278
- extract(item);
2279
- return texts.join("").trim();
2280
- }
2281
- function findNestedLists(item) {
2282
- const lists = [];
2283
- if (!item.content) return lists;
2284
- for (const child of item.content) {
2285
- if (isList(child)) {
2286
- lists.push(child);
2287
- }
2337
+ extract(node);
2338
+ const text = texts.join("").trim();
2339
+ if (text.length > maxLength) {
2340
+ return text.substring(0, maxLength - 3) + "...";
2288
2341
  }
2289
- return lists;
2342
+ return text || "(empty)";
2290
2343
  }
2291
- function diffLists(listA, listB, listPathA, listPathB, depth = 0) {
2292
- const result = {
2293
- itemChanges: [],
2294
- itemMatches: [],
2295
- nestedChanges: []
2296
- };
2297
- const alignment = alignListItems(listA, listB, listPathA, listPathB);
2344
+ function processStructuralChanges(docA, docB, author = DEFAULT_AUTHOR) {
2345
+ const changes = [];
2346
+ const infos = [];
2347
+ const alignment = alignDocuments(docA, docB);
2348
+ let tableIndex = 0;
2349
+ let listIndex = 0;
2350
+ let paragraphIndex = 0;
2298
2351
  for (const inserted of alignment.insertions) {
2299
- result.itemChanges.push({
2300
- id: v4(),
2301
- type: "listItemInsert",
2302
- nodeType: "listItem",
2352
+ const node = inserted.node;
2353
+ const sharedId = v4();
2354
+ const date = (/* @__PURE__ */ new Date()).toISOString();
2355
+ let type = "paragraphInsert";
2356
+ let location = "";
2357
+ let preview = "";
2358
+ if (isTable(node)) {
2359
+ type = "rowInsert";
2360
+ location = `New table at position ${inserted.path[0] + 1}`;
2361
+ preview = `Table with ${node.content?.length || 0} rows`;
2362
+ tableIndex++;
2363
+ } else if (isList(node)) {
2364
+ type = "listItemInsert";
2365
+ location = `New list at position ${inserted.path[0] + 1}`;
2366
+ preview = `List with ${node.content?.length || 0} items`;
2367
+ listIndex++;
2368
+ } else {
2369
+ type = "paragraphInsert";
2370
+ paragraphIndex++;
2371
+ location = `Paragraph ${paragraphIndex}`;
2372
+ preview = extractTextPreview(node);
2373
+ }
2374
+ changes.push({
2375
+ id: sharedId,
2376
+ type,
2377
+ nodeType: node.type,
2303
2378
  path: inserted.path,
2304
- node: inserted.node
2379
+ node: markAllTextAsInserted(cloneNode(node), sharedId, author)
2305
2380
  });
2306
- }
2307
- for (const deleted of alignment.deletions) {
2308
- result.itemChanges.push({
2309
- id: v4(),
2310
- type: "listItemDelete",
2311
- nodeType: "listItem",
2312
- path: deleted.path,
2313
- node: deleted.node
2381
+ infos.push({
2382
+ id: sharedId,
2383
+ type,
2384
+ nodeType: node.type,
2385
+ location,
2386
+ preview,
2387
+ author,
2388
+ date
2314
2389
  });
2315
2390
  }
2316
- result.itemMatches = alignment.matched;
2317
- for (const match of alignment.matched) {
2318
- const itemIdxA = match.pathA[match.pathA.length - 1];
2319
- const itemIdxB = match.pathB[match.pathB.length - 1];
2320
- const itemA = listA.content?.[itemIdxA];
2321
- const itemB = listB.content?.[itemIdxB];
2322
- if (!itemA || !itemB) continue;
2323
- const nestedA = findNestedLists(itemA);
2324
- const nestedB = findNestedLists(itemB);
2325
- const maxNested = Math.max(nestedA.length, nestedB.length);
2326
- for (let i = 0; i < maxNested; i++) {
2327
- const nA = nestedA[i];
2328
- const nB = nestedB[i];
2329
- if (nA && nB) {
2330
- const nestedResult = diffLists(
2331
- nA,
2332
- nB,
2333
- [...match.pathA, i],
2334
- [...match.pathB, i],
2335
- depth + 1
2336
- );
2337
- result.nestedChanges.push(nestedResult);
2338
- } else if (!nA && nB) {
2339
- result.itemChanges.push({
2340
- id: v4(),
2341
- type: "listItemInsert",
2342
- nodeType: "nestedList",
2343
- path: [...match.pathB, i],
2344
- node: nB
2391
+ for (const deleted of alignment.deletions) {
2392
+ const node = deleted.node;
2393
+ const sharedId = v4();
2394
+ const date = (/* @__PURE__ */ new Date()).toISOString();
2395
+ let type = "paragraphDelete";
2396
+ let location = "";
2397
+ let preview = "";
2398
+ if (isTable(node)) {
2399
+ type = "rowDelete";
2400
+ location = `Deleted table at position ${deleted.path[0] + 1}`;
2401
+ preview = `Table with ${node.content?.length || 0} rows`;
2402
+ } else if (isList(node)) {
2403
+ type = "listItemDelete";
2404
+ location = `Deleted list at position ${deleted.path[0] + 1}`;
2405
+ preview = `List with ${node.content?.length || 0} items`;
2406
+ } else {
2407
+ type = "paragraphDelete";
2408
+ location = `Deleted paragraph`;
2409
+ preview = extractTextPreview(node);
2410
+ }
2411
+ changes.push({
2412
+ id: sharedId,
2413
+ type,
2414
+ nodeType: node.type,
2415
+ path: deleted.path,
2416
+ node: markAllTextAsDeleted(cloneNode(node), sharedId, author)
2417
+ });
2418
+ infos.push({
2419
+ id: sharedId,
2420
+ type,
2421
+ nodeType: node.type,
2422
+ location,
2423
+ preview,
2424
+ author,
2425
+ date
2426
+ });
2427
+ }
2428
+ for (const match of alignment.matched) {
2429
+ const nodeA = docA.content?.[match.pathA[0]];
2430
+ const nodeB = docB.content?.[match.pathB[0]];
2431
+ if (!nodeA || !nodeB) continue;
2432
+ if (isTable(nodeA) && isTable(nodeB)) {
2433
+ tableIndex++;
2434
+ const tableResult = diffTables(nodeA, nodeB, match.pathA, match.pathB);
2435
+ for (const rowChange of tableResult.rowChanges) {
2436
+ const sharedId = rowChange.id;
2437
+ const date = (/* @__PURE__ */ new Date()).toISOString();
2438
+ const rowIndex = rowChange.path[rowChange.path.length - 1];
2439
+ const isInsert = rowChange.type === "rowInsert";
2440
+ const location = getRowLocation(rowChange.path, rowIndex, tableIndex - 1);
2441
+ const preview = getRowPreview(rowChange.node);
2442
+ const markedNode = isInsert ? markAllTextAsInserted(cloneNode(rowChange.node), sharedId, author) : markAllTextAsDeleted(cloneNode(rowChange.node), sharedId, author);
2443
+ changes.push({
2444
+ ...rowChange,
2445
+ node: markedNode
2345
2446
  });
2346
- } else if (nA && !nB) {
2347
- result.itemChanges.push({
2348
- id: v4(),
2349
- type: "listItemDelete",
2350
- nodeType: "nestedList",
2351
- path: [...match.pathA, i],
2352
- node: nA
2447
+ infos.push({
2448
+ id: sharedId,
2449
+ type: rowChange.type,
2450
+ nodeType: "tableRow",
2451
+ location,
2452
+ preview,
2453
+ author,
2454
+ date
2455
+ });
2456
+ }
2457
+ }
2458
+ if (isList(nodeA) && isList(nodeB)) {
2459
+ listIndex++;
2460
+ const listResult = diffLists(nodeA, nodeB, match.pathA, match.pathB);
2461
+ for (const itemChange of listResult.itemChanges) {
2462
+ const sharedId = itemChange.id;
2463
+ const date = (/* @__PURE__ */ new Date()).toISOString();
2464
+ const itemIndex = itemChange.path[itemChange.path.length - 1];
2465
+ const isInsert = itemChange.type === "listItemInsert";
2466
+ const location = getListItemLocation(itemChange.path, itemIndex, listIndex - 1);
2467
+ const preview = getListItemPreview(itemChange.node);
2468
+ const markedNode = isInsert ? markAllTextAsInserted(cloneNode(itemChange.node), sharedId, author) : markAllTextAsDeleted(cloneNode(itemChange.node), sharedId, author);
2469
+ changes.push({
2470
+ ...itemChange,
2471
+ node: markedNode
2472
+ });
2473
+ infos.push({
2474
+ id: sharedId,
2475
+ type: itemChange.type,
2476
+ nodeType: "listItem",
2477
+ location,
2478
+ preview,
2479
+ author,
2480
+ date
2353
2481
  });
2354
2482
  }
2355
2483
  }
2356
2484
  }
2357
- return result;
2358
- }
2359
- function getListItemLocation(listPath, itemIndex, listIndex, depth = 0) {
2360
- const depthStr = depth > 0 ? ` (nested, level ${depth + 1})` : "";
2361
- return `List ${listIndex + 1}, Item ${itemIndex + 1}${depthStr}`;
2362
- }
2363
- function getListItemPreview(item, maxLength = 50) {
2364
- const text = extractListItemText(item);
2365
- if (text.length > maxLength) {
2366
- return text.substring(0, maxLength - 3) + "...";
2485
+ const imageChanges = diffImages(docA, docB);
2486
+ for (const imgInsert of imageChanges.inserted) {
2487
+ const sharedId = imgInsert.id;
2488
+ const date = (/* @__PURE__ */ new Date()).toISOString();
2489
+ infos.push({
2490
+ id: sharedId,
2491
+ type: "imageInsert",
2492
+ nodeType: "image",
2493
+ location: getImageLocation(imgInsert.path),
2494
+ preview: getImagePreview(imgInsert.node),
2495
+ author,
2496
+ date
2497
+ });
2498
+ changes.push(imgInsert);
2367
2499
  }
2368
- return text || "(empty item)";
2369
- }
2370
- function isImage(node) {
2371
- return node?.type === "image";
2372
- }
2373
- function isHorizontalRule(node) {
2374
- return node?.type === "horizontalRule" || node?.type === "hr";
2375
- }
2376
- function isHardBreak(node) {
2377
- return node?.type === "hardBreak";
2378
- }
2379
- function isPageBreak(node) {
2380
- return node?.type === "pageBreak";
2500
+ for (const imgDelete of imageChanges.deleted) {
2501
+ const sharedId = imgDelete.id;
2502
+ const date = (/* @__PURE__ */ new Date()).toISOString();
2503
+ infos.push({
2504
+ id: sharedId,
2505
+ type: "imageDelete",
2506
+ nodeType: "image",
2507
+ location: getImageLocation(imgDelete.path),
2508
+ preview: getImagePreview(imgDelete.node),
2509
+ author,
2510
+ date
2511
+ });
2512
+ changes.push(imgDelete);
2513
+ }
2514
+ return { changes, infos };
2381
2515
  }
2382
- function isEmbedded(node) {
2383
- const embeddedTypes = [
2384
- "equation",
2385
- "math",
2386
- "embed",
2387
- "chart",
2388
- "drawing",
2389
- "shape"
2390
- ];
2391
- return embeddedTypes.includes(node?.type);
2516
+ function generateStructuralChangeSummary(infos) {
2517
+ const summary = [];
2518
+ const rowInserts = infos.filter((i) => i.type === "rowInsert").length;
2519
+ const rowDeletes = infos.filter((i) => i.type === "rowDelete").length;
2520
+ const paragraphInserts = infos.filter((i) => i.type === "paragraphInsert").length;
2521
+ const paragraphDeletes = infos.filter((i) => i.type === "paragraphDelete").length;
2522
+ const listItemInserts = infos.filter((i) => i.type === "listItemInsert").length;
2523
+ const listItemDeletes = infos.filter((i) => i.type === "listItemDelete").length;
2524
+ const imageInserts = infos.filter((i) => i.type === "imageInsert").length;
2525
+ const imageDeletes = infos.filter((i) => i.type === "imageDelete").length;
2526
+ if (rowInserts > 0) summary.push(`${rowInserts} row(s) inserted`);
2527
+ if (rowDeletes > 0) summary.push(`${rowDeletes} row(s) deleted`);
2528
+ if (paragraphInserts > 0) summary.push(`${paragraphInserts} paragraph(s) inserted`);
2529
+ if (paragraphDeletes > 0) summary.push(`${paragraphDeletes} paragraph(s) deleted`);
2530
+ if (listItemInserts > 0) summary.push(`${listItemInserts} list item(s) inserted`);
2531
+ if (listItemDeletes > 0) summary.push(`${listItemDeletes} list item(s) deleted`);
2532
+ if (imageInserts > 0) summary.push(`${imageInserts} image(s) inserted`);
2533
+ if (imageDeletes > 0) summary.push(`${imageDeletes} image(s) deleted`);
2534
+ return summary;
2392
2535
  }
2393
- function isAtomicNode(node) {
2394
- return isImage(node) || isHorizontalRule(node) || isHardBreak(node) || isPageBreak(node) || isEmbedded(node);
2536
+ function cloneNode2(node) {
2537
+ return JSON.parse(JSON.stringify(node));
2395
2538
  }
2396
- function getImageIdentifier(node) {
2397
- if (!isImage(node)) return "";
2398
- const attrs = node.attrs || {};
2399
- if (attrs.src) {
2400
- return `src:${attrs.src}`;
2401
- }
2402
- if (attrs.data) {
2403
- return `data:${simpleHash2(attrs.data)}`;
2404
- }
2405
- if (attrs.alt) {
2406
- return `alt:${attrs.alt}`;
2539
+ function getMarkSpansForRange(spansB, start, end) {
2540
+ const result = [];
2541
+ for (const span of spansB) {
2542
+ if (span.to > start && span.from < end) {
2543
+ const overlapStart = Math.max(span.from, start);
2544
+ const overlapEnd = Math.min(span.to, end);
2545
+ result.push({
2546
+ relStart: overlapStart - start,
2547
+ relEnd: overlapEnd - start,
2548
+ marks: span.marks || []
2549
+ });
2550
+ }
2407
2551
  }
2408
- return "unknown";
2552
+ return result;
2409
2553
  }
2410
- function simpleHash2(str) {
2411
- let hash = 5381;
2412
- const sample = str.substring(0, 1e3);
2413
- for (let i = 0; i < sample.length; i++) {
2414
- hash = (hash << 5) + hash ^ sample.charCodeAt(i);
2554
+ function createInsertedTextNodes(text, posB, spansB, author, replacementId) {
2555
+ const result = [];
2556
+ const trackMark = createTrackInsertMark(author, replacementId);
2557
+ if (posB === void 0 || spansB.length === 0) {
2558
+ return [{
2559
+ type: "text",
2560
+ text,
2561
+ marks: [trackMark]
2562
+ }];
2415
2563
  }
2416
- return (hash >>> 0).toString(16);
2417
- }
2418
- function findImages(doc, basePath = []) {
2419
- const images = [];
2420
- function traverse(node, path) {
2421
- if (!node) return;
2422
- if (isImage(node)) {
2423
- images.push({ node, path: [...path] });
2564
+ const markSpans = getMarkSpansForRange(spansB, posB, posB + text.length);
2565
+ if (markSpans.length === 0) {
2566
+ return [{
2567
+ type: "text",
2568
+ text,
2569
+ marks: [trackMark]
2570
+ }];
2571
+ }
2572
+ markSpans.sort((a, b) => a.relStart - b.relStart);
2573
+ let processedUpTo = 0;
2574
+ for (const span of markSpans) {
2575
+ if (span.relStart > processedUpTo) {
2576
+ result.push({
2577
+ type: "text",
2578
+ text: text.substring(processedUpTo, span.relStart),
2579
+ marks: [trackMark]
2580
+ });
2424
2581
  }
2425
- if (node.content && Array.isArray(node.content)) {
2426
- node.content.forEach((child, i) => {
2427
- traverse(child, [...path, i]);
2582
+ if (span.relEnd > span.relStart) {
2583
+ const spanText = text.substring(span.relStart, span.relEnd);
2584
+ const normalizedSpanMarks = normalizeMarksForRendering(span.marks);
2585
+ const marks = [...normalizedSpanMarks, trackMark];
2586
+ result.push({
2587
+ type: "text",
2588
+ text: spanText,
2589
+ marks
2428
2590
  });
2591
+ processedUpTo = span.relEnd;
2429
2592
  }
2430
2593
  }
2431
- traverse(doc, basePath);
2432
- return images;
2433
- }
2434
- function diffImages(docA, docB) {
2435
- const imagesA = findImages(docA);
2436
- const imagesB = findImages(docB);
2437
- const inserted = [];
2438
- const deleted = [];
2439
- const idsA = /* @__PURE__ */ new Map();
2440
- const idsB = /* @__PURE__ */ new Map();
2441
- for (const img of imagesA) {
2442
- const id = getImageIdentifier(img.node);
2443
- idsA.set(id, img);
2444
- }
2445
- for (const img of imagesB) {
2446
- const id = getImageIdentifier(img.node);
2447
- idsB.set(id, img);
2594
+ if (processedUpTo < text.length) {
2595
+ result.push({
2596
+ type: "text",
2597
+ text: text.substring(processedUpTo),
2598
+ marks: [trackMark]
2599
+ });
2448
2600
  }
2449
- for (const [id, img] of idsA) {
2450
- if (!idsB.has(id)) {
2451
- deleted.push({
2452
- id: v4(),
2453
- type: "imageDelete",
2454
- nodeType: "image",
2455
- path: img.path,
2456
- node: img.node
2457
- });
2601
+ return result;
2602
+ }
2603
+ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
2604
+ const merged = cloneNode2(docA);
2605
+ const charStates = [];
2606
+ let insertions = [];
2607
+ const formatChanges = diffResult.formatChanges || [];
2608
+ function getFormatChangeAt(pos) {
2609
+ for (const fc of formatChanges) {
2610
+ if (pos >= fc.from && pos < fc.to) {
2611
+ return fc;
2612
+ }
2458
2613
  }
2614
+ return null;
2459
2615
  }
2460
- for (const [id, img] of idsB) {
2461
- if (!idsA.has(id)) {
2462
- inserted.push({
2463
- id: v4(),
2464
- type: "imageInsert",
2465
- nodeType: "image",
2466
- path: img.path,
2467
- node: img.node
2616
+ let docAOffset = 0;
2617
+ const segments = diffResult.segments;
2618
+ for (let segIdx = 0; segIdx < segments.length; segIdx++) {
2619
+ const segment = segments[segIdx];
2620
+ if (segment.type === "equal") {
2621
+ for (let i = 0; i < segment.text.length; i++) {
2622
+ charStates[docAOffset + i] = { type: "equal" };
2623
+ }
2624
+ docAOffset += segment.text.length;
2625
+ } else if (segment.type === "delete") {
2626
+ const nextSegment = segments[segIdx + 1];
2627
+ const isReplacement = nextSegment && nextSegment.type === "insert";
2628
+ const replacementId = isReplacement ? v4() : void 0;
2629
+ for (let i = 0; i < segment.text.length; i++) {
2630
+ charStates[docAOffset + i] = { type: "delete", replacementId };
2631
+ }
2632
+ docAOffset += segment.text.length;
2633
+ if (isReplacement && nextSegment) {
2634
+ insertions.push({
2635
+ afterOffset: docAOffset,
2636
+ text: nextSegment.text,
2637
+ replacementId,
2638
+ posB: nextSegment.posB
2639
+ // Capture docB position for mark lookup
2640
+ });
2641
+ segIdx++;
2642
+ }
2643
+ } else if (segment.type === "insert") {
2644
+ insertions.push({
2645
+ afterOffset: docAOffset,
2646
+ text: segment.text,
2647
+ posB: segment.posB
2648
+ // Capture docB position for mark lookup
2468
2649
  });
2469
2650
  }
2470
2651
  }
2471
- return { inserted, deleted };
2472
- }
2473
- function getImageLocation(path) {
2474
- if (path.length <= 1) {
2475
- return `Image at position ${path[0] + 1}`;
2652
+ const spansB = diffResult.spansB || [];
2653
+ function transformNode(node, nodeOffset, path) {
2654
+ if (node.type === "text" && node.text) {
2655
+ const text = node.text;
2656
+ const result = [];
2657
+ let i = 0;
2658
+ while (i < text.length) {
2659
+ const charOffset = nodeOffset + i;
2660
+ const charState = charStates[charOffset] || { type: "equal" };
2661
+ const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
2662
+ for (const ins of insertionsHere) {
2663
+ const insertedNodes = createInsertedTextNodes(
2664
+ ins.text,
2665
+ ins.posB,
2666
+ spansB,
2667
+ author,
2668
+ ins.replacementId
2669
+ );
2670
+ result.push(...insertedNodes);
2671
+ }
2672
+ const currentFormatChange = getFormatChangeAt(nodeOffset + i);
2673
+ let j = i + 1;
2674
+ while (j < text.length) {
2675
+ const nextState = charStates[nodeOffset + j] || { type: "equal" };
2676
+ if (nextState.type !== charState.type) break;
2677
+ if (insertions.some((ins) => ins.afterOffset === nodeOffset + j)) break;
2678
+ const nextFormatChange = getFormatChangeAt(nodeOffset + j);
2679
+ if (currentFormatChange !== nextFormatChange) break;
2680
+ j++;
2681
+ }
2682
+ const chunk = text.substring(i, j);
2683
+ let marks = [...node.marks || []];
2684
+ if (charState.type === "delete") {
2685
+ marks.push(createTrackDeleteMark(author, charState.replacementId));
2686
+ } else if (charState.type === "equal") {
2687
+ if (currentFormatChange) {
2688
+ const trackFormatMark = createTrackFormatMark(
2689
+ currentFormatChange.before,
2690
+ currentFormatChange.after,
2691
+ author
2692
+ );
2693
+ const normalizedAfterMarks = normalizeMarksForRendering(currentFormatChange.after);
2694
+ marks = [...normalizedAfterMarks, trackFormatMark];
2695
+ }
2696
+ }
2697
+ result.push({
2698
+ type: "text",
2699
+ text: chunk,
2700
+ marks: marks.length > 0 ? marks : void 0
2701
+ });
2702
+ i = j;
2703
+ }
2704
+ const endOffset = nodeOffset + text.length;
2705
+ const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
2706
+ for (const ins of endInsertions) {
2707
+ const insertedNodes = createInsertedTextNodes(
2708
+ ins.text,
2709
+ ins.posB,
2710
+ spansB,
2711
+ author,
2712
+ ins.replacementId
2713
+ );
2714
+ result.push(...insertedNodes);
2715
+ }
2716
+ insertions = insertions.filter(
2717
+ (ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
2718
+ );
2719
+ return { nodes: result, consumedLength: text.length };
2720
+ }
2721
+ if (node.content && Array.isArray(node.content)) {
2722
+ const newContent = [];
2723
+ let offset = nodeOffset;
2724
+ for (const child of node.content) {
2725
+ const { nodes, consumedLength } = transformNode(child, offset);
2726
+ newContent.push(...nodes);
2727
+ offset += consumedLength;
2728
+ }
2729
+ return {
2730
+ nodes: [{ ...node, content: newContent }],
2731
+ consumedLength: offset - nodeOffset
2732
+ };
2733
+ }
2734
+ return { nodes: [node], consumedLength: 0 };
2476
2735
  }
2477
- return `Image (nested at depth ${path.length})`;
2478
- }
2479
- function getImagePreview(node) {
2480
- if (!isImage(node)) return "";
2481
- const attrs = node.attrs || {};
2482
- if (attrs.alt) {
2483
- return `"${attrs.alt}"`;
2736
+ if (merged.content && Array.isArray(merged.content)) {
2737
+ const newContent = [];
2738
+ let offset = 0;
2739
+ for (let i = 0; i < merged.content.length; i++) {
2740
+ const child = merged.content[i];
2741
+ const { nodes, consumedLength } = transformNode(child, offset);
2742
+ newContent.push(...nodes);
2743
+ offset += consumedLength;
2744
+ }
2745
+ merged.content = newContent;
2484
2746
  }
2485
- if (attrs.src) {
2486
- const src = attrs.src;
2487
- const filename = src.split("/").pop()?.split("?")[0];
2488
- if (filename) {
2489
- return filename;
2747
+ if (insertions.length > 0) {
2748
+ for (const ins of insertions) {
2749
+ const insertedNodes = createInsertedTextNodes(
2750
+ ins.text,
2751
+ ins.posB,
2752
+ spansB,
2753
+ author,
2754
+ ins.replacementId
2755
+ );
2756
+ const insertNode = {
2757
+ type: "paragraph",
2758
+ content: [
2759
+ {
2760
+ type: "run",
2761
+ content: insertedNodes
2762
+ }
2763
+ ]
2764
+ };
2765
+ if (!merged.content) merged.content = [];
2766
+ merged.content.push(insertNode);
2490
2767
  }
2491
2768
  }
2492
- return "(image)";
2769
+ return merged;
2493
2770
  }
2494
2771
 
2495
2772
  // src/services/structuralMerger.ts
2496
- function cloneNode2(node) {
2773
+ function cloneNode3(node) {
2497
2774
  return JSON.parse(JSON.stringify(node));
2498
2775
  }
2499
- function markAllTextAsInserted(node, sharedId, author) {
2776
+ function markAllTextAsInserted2(node, sharedId, author) {
2500
2777
  if (node.type === "text") {
2501
2778
  const existingMarks = normalizeMarksForRendering(node.marks || []);
2502
2779
  return {
@@ -2508,13 +2785,13 @@ function markAllTextAsInserted(node, sharedId, author) {
2508
2785
  return {
2509
2786
  ...node,
2510
2787
  content: node.content.map(
2511
- (child) => markAllTextAsInserted(child, sharedId, author)
2788
+ (child) => markAllTextAsInserted2(child, sharedId, author)
2512
2789
  )
2513
2790
  };
2514
2791
  }
2515
2792
  return { ...node };
2516
2793
  }
2517
- function markAllTextAsDeleted(node, sharedId, author) {
2794
+ function markAllTextAsDeleted2(node, sharedId, author) {
2518
2795
  if (node.type === "text") {
2519
2796
  const existingMarks = normalizeMarksForRendering(node.marks || []);
2520
2797
  return {
@@ -2526,13 +2803,13 @@ function markAllTextAsDeleted(node, sharedId, author) {
2526
2803
  return {
2527
2804
  ...node,
2528
2805
  content: node.content.map(
2529
- (child) => markAllTextAsDeleted(child, sharedId, author)
2806
+ (child) => markAllTextAsDeleted2(child, sharedId, author)
2530
2807
  )
2531
2808
  };
2532
2809
  }
2533
2810
  return { ...node };
2534
2811
  }
2535
- function extractTextPreview(node, maxLength = 50) {
2812
+ function extractTextPreview2(node, maxLength = 50) {
2536
2813
  const texts = [];
2537
2814
  function extract(n) {
2538
2815
  if (n.type === "text") {
@@ -2718,7 +2995,7 @@ function mergeMatchedBlock(nodeA, nodeB, blockIndex, author) {
2718
2995
  diff,
2719
2996
  author
2720
2997
  );
2721
- const mergedNode = merged.content?.[0] || cloneNode2(nodeB);
2998
+ const mergedNode = merged.content?.[0] || cloneNode3(nodeB);
2722
2999
  return { mergedNode, infos, changes };
2723
3000
  }
2724
3001
  function mergeMatchedTable(tableA, tableB, tableIndex, author) {
@@ -2747,13 +3024,13 @@ function mergeMatchedTable(tableA, tableB, tableIndex, author) {
2747
3024
  if (deletedIndices.has(checkIdx) && !processedDeletions.has(checkIdx)) {
2748
3025
  const deletedRow = rowsA[checkIdx];
2749
3026
  const changeId = v4();
2750
- mergedRows.push(markAllTextAsDeleted(cloneNode2(deletedRow), changeId, author));
3027
+ mergedRows.push(markAllTextAsDeleted2(cloneNode3(deletedRow), changeId, author));
2751
3028
  tableInfos.push({
2752
3029
  id: changeId,
2753
3030
  type: "rowDelete",
2754
3031
  nodeType: "tableRow",
2755
3032
  location: `Table ${tableIndex}, Row ${checkIdx + 1}`,
2756
- preview: extractTextPreview(deletedRow),
3033
+ preview: extractTextPreview2(deletedRow),
2757
3034
  author,
2758
3035
  date: (/* @__PURE__ */ new Date()).toISOString()
2759
3036
  });
@@ -2765,13 +3042,13 @@ function mergeMatchedTable(tableA, tableB, tableIndex, author) {
2765
3042
  changeCount += changes;
2766
3043
  } else {
2767
3044
  const changeId = v4();
2768
- mergedRows.push(markAllTextAsInserted(cloneNode2(rowB), changeId, author));
3045
+ mergedRows.push(markAllTextAsInserted2(cloneNode3(rowB), changeId, author));
2769
3046
  tableInfos.push({
2770
3047
  id: changeId,
2771
3048
  type: "rowInsert",
2772
3049
  nodeType: "tableRow",
2773
3050
  location: `Table ${tableIndex}, Row ${idxB + 1}`,
2774
- preview: extractTextPreview(rowB),
3051
+ preview: extractTextPreview2(rowB),
2775
3052
  author,
2776
3053
  date: (/* @__PURE__ */ new Date()).toISOString()
2777
3054
  });
@@ -2781,13 +3058,13 @@ function mergeMatchedTable(tableA, tableB, tableIndex, author) {
2781
3058
  const idxA = deletion.path[deletion.path.length - 1];
2782
3059
  if (!processedDeletions.has(idxA)) {
2783
3060
  const changeId = v4();
2784
- mergedRows.push(markAllTextAsDeleted(cloneNode2(deletion.node), changeId, author));
3061
+ mergedRows.push(markAllTextAsDeleted2(cloneNode3(deletion.node), changeId, author));
2785
3062
  tableInfos.push({
2786
3063
  id: changeId,
2787
3064
  type: "rowDelete",
2788
3065
  nodeType: "tableRow",
2789
3066
  location: `Table ${tableIndex}, Row ${idxA + 1}`,
2790
- preview: extractTextPreview(deletion.node),
3067
+ preview: extractTextPreview2(deletion.node),
2791
3068
  author,
2792
3069
  date: (/* @__PURE__ */ new Date()).toISOString()
2793
3070
  });
@@ -2825,13 +3102,13 @@ function mergeMatchedList(listA, listB, listIndex, author) {
2825
3102
  if (deletedIndices.has(checkIdx) && !processedDeletions.has(checkIdx)) {
2826
3103
  const deletedItem = itemsA[checkIdx];
2827
3104
  const changeId = v4();
2828
- mergedItems.push(markAllTextAsDeleted(cloneNode2(deletedItem), changeId, author));
3105
+ mergedItems.push(markAllTextAsDeleted2(cloneNode3(deletedItem), changeId, author));
2829
3106
  listInfos.push({
2830
3107
  id: changeId,
2831
3108
  type: "listItemDelete",
2832
3109
  nodeType: "listItem",
2833
3110
  location: `List ${listIndex}, Item ${checkIdx + 1}`,
2834
- preview: extractTextPreview(deletedItem),
3111
+ preview: extractTextPreview2(deletedItem),
2835
3112
  author,
2836
3113
  date: (/* @__PURE__ */ new Date()).toISOString()
2837
3114
  });
@@ -2843,13 +3120,13 @@ function mergeMatchedList(listA, listB, listIndex, author) {
2843
3120
  changeCount += changes;
2844
3121
  } else {
2845
3122
  const changeId = v4();
2846
- mergedItems.push(markAllTextAsInserted(cloneNode2(itemB), changeId, author));
3123
+ mergedItems.push(markAllTextAsInserted2(cloneNode3(itemB), changeId, author));
2847
3124
  listInfos.push({
2848
3125
  id: changeId,
2849
3126
  type: "listItemInsert",
2850
3127
  nodeType: "listItem",
2851
3128
  location: `List ${listIndex}, Item ${idxB + 1}`,
2852
- preview: extractTextPreview(itemB),
3129
+ preview: extractTextPreview2(itemB),
2853
3130
  author,
2854
3131
  date: (/* @__PURE__ */ new Date()).toISOString()
2855
3132
  });
@@ -2859,13 +3136,13 @@ function mergeMatchedList(listA, listB, listIndex, author) {
2859
3136
  const idxA = deletion.path[deletion.path.length - 1];
2860
3137
  if (!processedDeletions.has(idxA)) {
2861
3138
  const changeId = v4();
2862
- mergedItems.push(markAllTextAsDeleted(cloneNode2(deletion.node), changeId, author));
3139
+ mergedItems.push(markAllTextAsDeleted2(cloneNode3(deletion.node), changeId, author));
2863
3140
  listInfos.push({
2864
3141
  id: changeId,
2865
3142
  type: "listItemDelete",
2866
3143
  nodeType: "listItem",
2867
3144
  location: `List ${listIndex}, Item ${idxA + 1}`,
2868
- preview: extractTextPreview(deletion.node),
3145
+ preview: extractTextPreview2(deletion.node),
2869
3146
  author,
2870
3147
  date: (/* @__PURE__ */ new Date()).toISOString()
2871
3148
  });
@@ -2879,14 +3156,14 @@ function mergeMatchedList(listA, listB, listIndex, author) {
2879
3156
  }
2880
3157
  function createInsertedBlock(node, blockIndex, author) {
2881
3158
  const changeId = v4();
2882
- const markedNode = markAllTextAsInserted(cloneNode2(node), changeId, author);
3159
+ const markedNode = markAllTextAsInserted2(cloneNode3(node), changeId, author);
2883
3160
  const nodeDesc = getNodeTypeDescription(node);
2884
3161
  const info = {
2885
3162
  id: changeId,
2886
3163
  type: isTable(node) ? "rowInsert" : isList(node) ? "listItemInsert" : isImage(node) ? "imageInsert" : "paragraphInsert",
2887
3164
  nodeType: node.type || "unknown",
2888
3165
  location: `${nodeDesc} inserted at position ${blockIndex}`,
2889
- preview: extractTextPreview(node),
3166
+ preview: extractTextPreview2(node),
2890
3167
  author,
2891
3168
  date: (/* @__PURE__ */ new Date()).toISOString()
2892
3169
  };
@@ -2894,14 +3171,14 @@ function createInsertedBlock(node, blockIndex, author) {
2894
3171
  }
2895
3172
  function createDeletedBlock(node, blockIndex, author) {
2896
3173
  const changeId = v4();
2897
- const markedNode = markAllTextAsDeleted(cloneNode2(node), changeId, author);
3174
+ const markedNode = markAllTextAsDeleted2(cloneNode3(node), changeId, author);
2898
3175
  const nodeDesc = getNodeTypeDescription(node);
2899
3176
  const info = {
2900
3177
  id: changeId,
2901
3178
  type: isTable(node) ? "rowDelete" : isList(node) ? "listItemDelete" : isImage(node) ? "imageDelete" : "paragraphDelete",
2902
3179
  nodeType: node.type || "unknown",
2903
3180
  location: `${nodeDesc} deleted from position ${blockIndex}`,
2904
- preview: extractTextPreview(node),
3181
+ preview: extractTextPreview2(node),
2905
3182
  author,
2906
3183
  date: (/* @__PURE__ */ new Date()).toISOString()
2907
3184
  };
@@ -2960,8 +3237,9 @@ var DocxDiffEditor = forwardRef(
2960
3237
  const mountedRef = useRef(true);
2961
3238
  const initRef = useRef(false);
2962
3239
  const readyRef = useRef(false);
3240
+ const rollbackJsonRef = useRef(null);
2963
3241
  const [isLoading, setIsLoading] = useState(true);
2964
- const [error, setError] = useState(null);
3242
+ const [fatalError, setFatalError] = useState(null);
2965
3243
  const [sourceJson, setSourceJson] = useState(null);
2966
3244
  const [mergedJson, setMergedJson] = useState(null);
2967
3245
  const [diffResult, setDiffResult] = useState(null);
@@ -3018,11 +3296,31 @@ var DocxDiffEditor = forwardRef(
3018
3296
  sd.setTrackedChangesPreferences({ mode: "off", enabled: false });
3019
3297
  }
3020
3298
  }, []);
3021
- const handleError = useCallback(
3022
- (err) => {
3023
- const error2 = err instanceof Error ? err : new Error(err);
3024
- setError(error2.message);
3025
- onError?.(error2);
3299
+ const handleFatalError = useCallback(
3300
+ (err, operation) => {
3301
+ const error = err instanceof Error ? err : new Error(err);
3302
+ setFatalError(error.message);
3303
+ onError?.({
3304
+ error,
3305
+ type: "fatal",
3306
+ operation,
3307
+ recoverable: false,
3308
+ message: error.message
3309
+ });
3310
+ },
3311
+ [onError]
3312
+ );
3313
+ const handleOperationError = useCallback(
3314
+ (err, operation, phase) => {
3315
+ const error = err instanceof Error ? err : new Error(err);
3316
+ onError?.({
3317
+ error,
3318
+ type: "operation",
3319
+ operation,
3320
+ recoverable: true,
3321
+ message: error.message,
3322
+ phase
3323
+ });
3026
3324
  },
3027
3325
  [onError]
3028
3326
  );
@@ -3180,7 +3478,7 @@ var DocxDiffEditor = forwardRef(
3180
3478
  return;
3181
3479
  }
3182
3480
  setIsLoading(true);
3183
- setError(null);
3481
+ setFatalError(null);
3184
3482
  destroySuperdoc();
3185
3483
  try {
3186
3484
  const { SuperDoc } = await import('superdoc');
@@ -3203,17 +3501,19 @@ var DocxDiffEditor = forwardRef(
3203
3501
  if (sd?.activeEditor && isProseMirrorJSON(initialSource)) {
3204
3502
  setEditorContent(sd.activeEditor, initialSource);
3205
3503
  setSourceJson(initialSource);
3504
+ rollbackJsonRef.current = initialSource;
3206
3505
  onSourceLoaded?.(initialSource);
3207
3506
  }
3208
3507
  } else {
3209
3508
  setSourceJson(json);
3509
+ rollbackJsonRef.current = json;
3210
3510
  onSourceLoaded?.(json);
3211
3511
  }
3212
3512
  setIsLoading(false);
3213
3513
  onReady?.();
3214
3514
  } catch (err) {
3215
3515
  console.error("Failed to initialize SuperDoc:", err);
3216
- handleError(err instanceof Error ? err : new Error("Failed to load editor"));
3516
+ handleFatalError(err instanceof Error ? err : new Error("Failed to load editor"), "init");
3217
3517
  setIsLoading(false);
3218
3518
  initRef.current = false;
3219
3519
  }
@@ -3227,7 +3527,7 @@ var DocxDiffEditor = forwardRef(
3227
3527
  destroySuperdoc,
3228
3528
  createSuperdoc,
3229
3529
  setEditorContent,
3230
- handleError
3530
+ handleFatalError
3231
3531
  ]);
3232
3532
  useEffect(() => {
3233
3533
  mountedRef.current = true;
@@ -3262,42 +3562,98 @@ var DocxDiffEditor = forwardRef(
3262
3562
  * Accepts File (DOCX), HTML string, or ProseMirror JSON.
3263
3563
  * Note: This destroys and recreates the SuperDoc instance.
3264
3564
  * For JSON content updates, prefer updateContent() to preserve the existing template.
3565
+ *
3566
+ * On failure, attempts to preserve the previous editor state.
3567
+ * Returns void on success, or SetSourceError on recoverable failure.
3265
3568
  */
3266
3569
  async setSource(content) {
3267
3570
  if (!SuperDocRef.current) {
3268
- throw new Error("Editor not initialized");
3571
+ const error = new Error("Editor not initialized");
3572
+ handleOperationError(error, "setSource");
3573
+ return { success: false, error, message: error.message };
3269
3574
  }
3575
+ const currentRollback = superdocRef.current?.activeEditor ? superdocRef.current.activeEditor.getJSON() : rollbackJsonRef.current;
3270
3576
  setIsLoading(true);
3271
- setError(null);
3577
+ setFatalError(null);
3272
3578
  try {
3273
3579
  const contentType = detectContentType(content);
3274
- let json;
3275
- destroySuperdoc();
3276
- if (contentType === "file") {
3277
- const result = await createSuperdoc({ document: content });
3278
- json = result.json;
3279
- } else if (contentType === "html") {
3280
- const result = await createSuperdoc({ html: content });
3281
- json = result.json;
3282
- } else {
3283
- const result = await createSuperdoc(templateDocx ? { document: templateDocx } : {});
3284
- if (result.superdoc?.activeEditor && isProseMirrorJSON(content)) {
3285
- setEditorContent(result.superdoc.activeEditor, content);
3286
- json = content;
3580
+ let validatedJson = null;
3581
+ try {
3582
+ if (contentType === "file") {
3583
+ validatedJson = await parseDocxFile(content, SuperDocRef.current);
3584
+ } else if (contentType === "html") {
3585
+ validatedJson = await parseHtmlToJson(content, SuperDocRef.current);
3287
3586
  } else {
3587
+ if (!isProseMirrorJSON(content)) {
3588
+ throw new Error("Invalid ProseMirror JSON structure");
3589
+ }
3590
+ validatedJson = content;
3591
+ }
3592
+ } catch (parseErr) {
3593
+ const error = parseErr instanceof Error ? parseErr : new Error("Failed to parse content");
3594
+ handleOperationError(error, "setSource");
3595
+ setIsLoading(false);
3596
+ return { success: false, error, message: error.message };
3597
+ }
3598
+ destroySuperdoc();
3599
+ let json;
3600
+ try {
3601
+ if (contentType === "file") {
3602
+ const result = await createSuperdoc({ document: content });
3603
+ json = result.json;
3604
+ } else if (contentType === "html") {
3605
+ const result = await createSuperdoc({ html: content });
3288
3606
  json = result.json;
3607
+ } else {
3608
+ const result = await createSuperdoc(templateDocx ? { document: templateDocx } : {});
3609
+ if (result.superdoc?.activeEditor && validatedJson) {
3610
+ setEditorContent(result.superdoc.activeEditor, validatedJson);
3611
+ json = validatedJson;
3612
+ } else {
3613
+ json = result.json;
3614
+ }
3615
+ }
3616
+ } catch (createErr) {
3617
+ console.warn("[DocxDiffEditor] Failed to create new editor, attempting recovery:", createErr);
3618
+ if (currentRollback) {
3619
+ try {
3620
+ const recoveryResult = await createSuperdoc(templateDocx ? { document: templateDocx } : {});
3621
+ if (recoveryResult.superdoc?.activeEditor) {
3622
+ setEditorContent(recoveryResult.superdoc.activeEditor, currentRollback);
3623
+ setSourceJson(currentRollback);
3624
+ setEditingMode(superdocRef.current);
3625
+ }
3626
+ const error = createErr instanceof Error ? createErr : new Error("Failed to set source");
3627
+ handleOperationError(error, "setSource");
3628
+ setIsLoading(false);
3629
+ return { success: false, error, message: error.message };
3630
+ } catch (recoveryErr) {
3631
+ console.error("[DocxDiffEditor] Recovery failed:", recoveryErr);
3632
+ const error = createErr instanceof Error ? createErr : new Error("Failed to set source");
3633
+ handleFatalError(error, "setSource");
3634
+ setIsLoading(false);
3635
+ return { success: false, error, message: error.message };
3636
+ }
3637
+ } else {
3638
+ const error = createErr instanceof Error ? createErr : new Error("Failed to set source");
3639
+ handleFatalError(error, "setSource");
3640
+ setIsLoading(false);
3641
+ return { success: false, error, message: error.message };
3289
3642
  }
3290
3643
  }
3291
3644
  setSourceJson(json);
3292
3645
  setMergedJson(null);
3293
3646
  setDiffResult(null);
3294
3647
  setEditingMode(superdocRef.current);
3648
+ rollbackJsonRef.current = json;
3295
3649
  onSourceLoaded?.(json);
3650
+ setIsLoading(false);
3651
+ return;
3296
3652
  } catch (err) {
3297
- handleError(err instanceof Error ? err : new Error("Failed to set source"));
3298
- throw err;
3299
- } finally {
3653
+ const error = err instanceof Error ? err : new Error("Failed to set source");
3654
+ handleFatalError(error, "setSource");
3300
3655
  setIsLoading(false);
3656
+ return { success: false, error, message: error.message };
3301
3657
  }
3302
3658
  },
3303
3659
  /**
@@ -3309,21 +3665,27 @@ var DocxDiffEditor = forwardRef(
3309
3665
  *
3310
3666
  * To compare against the original source document, call setSource() again
3311
3667
  * before compareWith().
3668
+ *
3669
+ * Returns a union type - check `result.success` to determine outcome:
3670
+ * - `success: true` - Comparison succeeded
3671
+ * - `success: false` - Comparison failed, editor unchanged
3312
3672
  */
3313
3673
  async compareWith(content) {
3314
3674
  if (!SuperDocRef.current) {
3315
- throw new Error("Editor not initialized");
3675
+ const error = new Error("Editor not initialized");
3676
+ handleOperationError(error, "compareWith", "parsing");
3677
+ return { success: false, error, message: error.message, phase: "parsing" };
3316
3678
  }
3317
3679
  if (!superdocRef.current?.activeEditor) {
3318
- throw new Error("Editor not ready. Ensure a document is loaded first.");
3680
+ const error = new Error("Editor not ready. Ensure a document is loaded first.");
3681
+ handleOperationError(error, "compareWith", "parsing");
3682
+ return { success: false, error, message: error.message, phase: "parsing" };
3319
3683
  }
3684
+ const rollbackJson = superdocRef.current.activeEditor.getJSON();
3320
3685
  setIsLoading(true);
3686
+ let newJson;
3687
+ const contentType = detectContentType(content);
3321
3688
  try {
3322
- const currentEditorJson = superdocRef.current.activeEditor.getJSON();
3323
- const cleanBaseline = acceptAllChangesInJson(currentEditorJson) || { type: "doc", content: [] };
3324
- setSourceJson(cleanBaseline);
3325
- const contentType = detectContentType(content);
3326
- let newJson;
3327
3689
  if (contentType === "file") {
3328
3690
  newJson = await parseDocxFile(content, SuperDocRef.current);
3329
3691
  } else if (contentType === "html") {
@@ -3378,91 +3740,122 @@ var DocxDiffEditor = forwardRef(
3378
3740
  }
3379
3741
  newJson = content;
3380
3742
  }
3743
+ } catch (parseErr) {
3744
+ const error = parseErr instanceof Error ? parseErr : new Error("Failed to parse content");
3745
+ handleOperationError(error, "compareWith", "parsing");
3746
+ setIsLoading(false);
3747
+ return { success: false, error, message: error.message, phase: "parsing" };
3748
+ }
3749
+ let normalizedMerged;
3750
+ let normalizedNewJson;
3751
+ let structInfos;
3752
+ let diff;
3753
+ let cleanBaseline;
3754
+ try {
3755
+ const currentEditorJson = superdocRef.current.activeEditor.getJSON();
3756
+ cleanBaseline = acceptAllChangesInJson(currentEditorJson) || { type: "doc", content: [] };
3381
3757
  const cleanNewJson = acceptAllChangesInJson(newJson) || { type: "doc", content: [] };
3382
- const normalizedNewJson = normalizeRunProperties(cleanNewJson);
3758
+ normalizedNewJson = normalizeRunProperties(cleanNewJson);
3383
3759
  const structuralResult = mergeWithStructuralAwareness(
3384
3760
  cleanBaseline,
3385
3761
  normalizedNewJson,
3386
3762
  author
3387
3763
  );
3388
3764
  const merged = structuralResult.mergedDoc;
3389
- const structInfos = structuralResult.structuralInfos;
3390
- const normalizedMerged = normalizeRunProperties(merged);
3391
- setMergedJson(normalizedMerged);
3392
- const diff = diffDocuments(cleanBaseline, cleanNewJson);
3393
- setDiffResult(diff);
3394
- let usedFallback = false;
3395
- if (superdocRef.current?.activeEditor) {
3396
- try {
3397
- setEditorContent(superdocRef.current.activeEditor, normalizedMerged);
3398
- enableReviewMode(superdocRef.current);
3399
- const sd = superdocRef.current;
3400
- if (sd.commentsStore?.processLoadedDocxComments) {
3401
- setTimeout(() => {
3402
- try {
3403
- sd.commentsStore.processLoadedDocxComments({
3404
- superdoc: sd,
3405
- editor: sd.activeEditor,
3406
- comments: [],
3407
- // Empty array - we just want to trigger createCommentForTrackChanges
3408
- documentId: sd.activeEditor?.options?.documentId || "primary"
3409
- });
3410
- } catch (err) {
3411
- console.warn("[DocxDiffEditor] Failed to process track changes for bubbles:", err);
3412
- }
3413
- }, 50);
3414
- }
3415
- } catch (contentErr) {
3416
- console.warn(
3417
- "[DocxDiffEditor] Failed to apply merged content with track changes. Falling back to direct content update without track bubbles.",
3418
- contentErr
3419
- );
3420
- usedFallback = true;
3765
+ structInfos = structuralResult.structuralInfos;
3766
+ normalizedMerged = normalizeRunProperties(merged);
3767
+ diff = diffDocuments(cleanBaseline, cleanNewJson);
3768
+ } catch (mergeErr) {
3769
+ const error = mergeErr instanceof Error ? mergeErr : new Error("Failed to merge documents");
3770
+ handleOperationError(error, "compareWith", "merging");
3771
+ setIsLoading(false);
3772
+ return { success: false, error, message: error.message, phase: "merging" };
3773
+ }
3774
+ let usedFallback = false;
3775
+ try {
3776
+ setEditorContent(superdocRef.current.activeEditor, normalizedMerged);
3777
+ enableReviewMode(superdocRef.current);
3778
+ const sd = superdocRef.current;
3779
+ if (sd.commentsStore?.processLoadedDocxComments) {
3780
+ setTimeout(() => {
3421
3781
  try {
3422
- setEditorContent(superdocRef.current.activeEditor, normalizedNewJson);
3423
- setEditingMode(superdocRef.current);
3424
- } catch (fallbackErr) {
3425
- console.error("[DocxDiffEditor] Fallback content update also failed:", fallbackErr);
3426
- throw contentErr;
3782
+ sd.commentsStore.processLoadedDocxComments({
3783
+ superdoc: sd,
3784
+ editor: sd.activeEditor,
3785
+ comments: [],
3786
+ documentId: sd.activeEditor?.options?.documentId || "primary"
3787
+ });
3788
+ } catch (err) {
3789
+ console.warn("[DocxDiffEditor] Failed to process track changes for bubbles:", err);
3427
3790
  }
3428
- }
3429
- }
3430
- if (!usedFallback) {
3431
- setStructuralChanges(structInfos);
3432
- setIsPaneDismissed(false);
3433
- } else {
3434
- setStructuralChanges([]);
3791
+ }, 50);
3435
3792
  }
3436
- const insertions = diff.segments.filter((s) => s.type === "insert").length;
3437
- const deletions = diff.segments.filter((s) => s.type === "delete").length;
3438
- const formatChanges = diff.formatChanges?.length || 0;
3439
- const structuralChangeCount = usedFallback ? 0 : structInfos.length;
3440
- const combinedSummary = [...structuralResult.summary];
3441
- if (diff.summary.length > 0 && structuralResult.summary.length === 0) {
3442
- combinedSummary.push(...diff.summary);
3793
+ } catch (contentErr) {
3794
+ console.warn(
3795
+ "[DocxDiffEditor] Failed to apply merged content with track changes. Falling back to direct content update without track bubbles.",
3796
+ contentErr
3797
+ );
3798
+ usedFallback = true;
3799
+ try {
3800
+ setEditorContent(superdocRef.current.activeEditor, normalizedNewJson);
3801
+ setEditingMode(superdocRef.current);
3802
+ } catch (fallbackErr) {
3803
+ console.warn("[DocxDiffEditor] Fallback content update failed, attempting rollback:", fallbackErr);
3804
+ try {
3805
+ setEditorContent(superdocRef.current.activeEditor, rollbackJson);
3806
+ setEditingMode(superdocRef.current);
3807
+ const error = contentErr instanceof Error ? contentErr : new Error("Failed to apply comparison");
3808
+ handleOperationError(error, "compareWith", "applying");
3809
+ setIsLoading(false);
3810
+ return { success: false, error, message: error.message, phase: "applying" };
3811
+ } catch (rollbackErr) {
3812
+ console.error("[DocxDiffEditor] All recovery attempts failed:", rollbackErr);
3813
+ const error = contentErr instanceof Error ? contentErr : new Error("Failed to apply comparison");
3814
+ handleFatalError(error, "compareWith");
3815
+ setIsLoading(false);
3816
+ return { success: false, error, message: error.message, phase: "applying" };
3817
+ }
3443
3818
  }
3444
- if (usedFallback) {
3445
- combinedSummary.push("Note: Track change visualization unavailable for this content");
3819
+ }
3820
+ setSourceJson(cleanBaseline);
3821
+ setMergedJson(normalizedMerged);
3822
+ setDiffResult(diff);
3823
+ rollbackJsonRef.current = usedFallback ? normalizedNewJson : normalizedMerged;
3824
+ if (!usedFallback) {
3825
+ setStructuralChanges(structInfos);
3826
+ setIsPaneDismissed(false);
3827
+ } else {
3828
+ setStructuralChanges([]);
3829
+ }
3830
+ const insertions = diff.segments.filter((s) => s.type === "insert").length;
3831
+ const deletions = diff.segments.filter((s) => s.type === "delete").length;
3832
+ const formatChanges = diff.formatChanges?.length || 0;
3833
+ const structuralChangeCount = usedFallback ? 0 : structInfos.length;
3834
+ const combinedSummary = [...diff.summary || []];
3835
+ if (structInfos.length > 0 && !usedFallback) {
3836
+ const structSummaries = generateStructuralChangeSummary(structInfos);
3837
+ if (structSummaries.length > 0) {
3838
+ combinedSummary.unshift(...structSummaries);
3446
3839
  }
3447
- const result = {
3448
- totalChanges: insertions + deletions + formatChanges + structuralChangeCount,
3449
- insertions,
3450
- deletions,
3451
- formatChanges,
3452
- structuralChanges: structuralChangeCount,
3453
- summary: combinedSummary,
3454
- mergedJson: usedFallback ? normalizedNewJson : merged,
3455
- structuralChangeInfos: usedFallback ? [] : structInfos,
3456
- usedFallback
3457
- };
3458
- onComparisonComplete?.(result);
3459
- return result;
3460
- } catch (err) {
3461
- handleError(err instanceof Error ? err : new Error("Comparison failed"));
3462
- throw err;
3463
- } finally {
3464
- setIsLoading(false);
3465
3840
  }
3841
+ if (usedFallback) {
3842
+ combinedSummary.push("Note: Track change visualization unavailable for this content");
3843
+ }
3844
+ const result = {
3845
+ success: true,
3846
+ totalChanges: insertions + deletions + formatChanges + structuralChangeCount,
3847
+ insertions,
3848
+ deletions,
3849
+ formatChanges,
3850
+ structuralChanges: structuralChangeCount,
3851
+ summary: combinedSummary,
3852
+ mergedJson: usedFallback ? normalizedNewJson : normalizedMerged,
3853
+ structuralChangeInfos: usedFallback ? [] : structInfos,
3854
+ usedFallback
3855
+ };
3856
+ onComparisonComplete?.(result);
3857
+ setIsLoading(false);
3858
+ return result;
3466
3859
  },
3467
3860
  /**
3468
3861
  * Get raw diff segments
@@ -3790,7 +4183,8 @@ var DocxDiffEditor = forwardRef(
3790
4183
  setEditingMode,
3791
4184
  onSourceLoaded,
3792
4185
  onComparisonComplete,
3793
- handleError
4186
+ handleFatalError,
4187
+ handleOperationError
3794
4188
  ]
3795
4189
  );
3796
4190
  return /* @__PURE__ */ jsxs("div", { className: `dde-container ${className}`.trim(), children: [
@@ -3798,7 +4192,7 @@ var DocxDiffEditor = forwardRef(
3798
4192
  /* @__PURE__ */ jsx("div", { className: "dde-loading__spinner" }),
3799
4193
  /* @__PURE__ */ jsx("p", { className: "dde-loading__text", children: "Loading document..." })
3800
4194
  ] }),
3801
- error && /* @__PURE__ */ jsxs("div", { className: "dde-error", children: [
4195
+ fatalError && /* @__PURE__ */ jsxs("div", { className: "dde-error", children: [
3802
4196
  /* @__PURE__ */ jsx("div", { className: "dde-error__icon", children: /* @__PURE__ */ jsx(
3803
4197
  "svg",
3804
4198
  {
@@ -3818,7 +4212,7 @@ var DocxDiffEditor = forwardRef(
3818
4212
  }
3819
4213
  ) }),
3820
4214
  /* @__PURE__ */ jsx("p", { className: "dde-error__title", children: "Failed to load document" }),
3821
- /* @__PURE__ */ jsx("p", { className: "dde-error__message", children: error })
4215
+ /* @__PURE__ */ jsx("p", { className: "dde-error__message", children: fatalError })
3822
4216
  ] }),
3823
4217
  showToolbar && /* @__PURE__ */ jsx(
3824
4218
  "div",
@@ -3855,256 +4249,6 @@ var DocxDiffEditor_default = DocxDiffEditor;
3855
4249
 
3856
4250
  // src/services/index.ts
3857
4251
  init_nodeFingerprint();
3858
- function markAllTextAsInserted2(node, sharedId, author) {
3859
- if (node.type === "text") {
3860
- const existingMarks = normalizeMarksForRendering(node.marks || []);
3861
- return {
3862
- ...node,
3863
- marks: [...existingMarks, createTrackInsertMark(author, sharedId)]
3864
- };
3865
- }
3866
- if (node.content && Array.isArray(node.content)) {
3867
- return {
3868
- ...node,
3869
- content: node.content.map(
3870
- (child) => markAllTextAsInserted2(child, sharedId, author)
3871
- )
3872
- };
3873
- }
3874
- return node;
3875
- }
3876
- function markAllTextAsDeleted2(node, sharedId, author) {
3877
- if (node.type === "text") {
3878
- const existingMarks = normalizeMarksForRendering(node.marks || []);
3879
- return {
3880
- ...node,
3881
- marks: [...existingMarks, createTrackDeleteMark(author, sharedId)]
3882
- };
3883
- }
3884
- if (node.content && Array.isArray(node.content)) {
3885
- return {
3886
- ...node,
3887
- content: node.content.map(
3888
- (child) => markAllTextAsDeleted2(child, sharedId, author)
3889
- )
3890
- };
3891
- }
3892
- return node;
3893
- }
3894
- function cloneNode3(node) {
3895
- return JSON.parse(JSON.stringify(node));
3896
- }
3897
- function extractTextPreview2(node, maxLength = 50) {
3898
- const texts = [];
3899
- function extract(n) {
3900
- if (n.type === "text") {
3901
- texts.push(n.text || "");
3902
- }
3903
- if (n.content) {
3904
- for (const child of n.content) {
3905
- extract(child);
3906
- }
3907
- }
3908
- }
3909
- extract(node);
3910
- const text = texts.join("").trim();
3911
- if (text.length > maxLength) {
3912
- return text.substring(0, maxLength - 3) + "...";
3913
- }
3914
- return text || "(empty)";
3915
- }
3916
- function processStructuralChanges(docA, docB, author = DEFAULT_AUTHOR) {
3917
- const changes = [];
3918
- const infos = [];
3919
- const alignment = alignDocuments(docA, docB);
3920
- let tableIndex = 0;
3921
- let listIndex = 0;
3922
- let paragraphIndex = 0;
3923
- for (const inserted of alignment.insertions) {
3924
- const node = inserted.node;
3925
- const sharedId = v4();
3926
- const date = (/* @__PURE__ */ new Date()).toISOString();
3927
- let type = "paragraphInsert";
3928
- let location = "";
3929
- let preview = "";
3930
- if (isTable(node)) {
3931
- type = "rowInsert";
3932
- location = `New table at position ${inserted.path[0] + 1}`;
3933
- preview = `Table with ${node.content?.length || 0} rows`;
3934
- tableIndex++;
3935
- } else if (isList(node)) {
3936
- type = "listItemInsert";
3937
- location = `New list at position ${inserted.path[0] + 1}`;
3938
- preview = `List with ${node.content?.length || 0} items`;
3939
- listIndex++;
3940
- } else {
3941
- type = "paragraphInsert";
3942
- paragraphIndex++;
3943
- location = `Paragraph ${paragraphIndex}`;
3944
- preview = extractTextPreview2(node);
3945
- }
3946
- changes.push({
3947
- id: sharedId,
3948
- type,
3949
- nodeType: node.type,
3950
- path: inserted.path,
3951
- node: markAllTextAsInserted2(cloneNode3(node), sharedId, author)
3952
- });
3953
- infos.push({
3954
- id: sharedId,
3955
- type,
3956
- nodeType: node.type,
3957
- location,
3958
- preview,
3959
- author,
3960
- date
3961
- });
3962
- }
3963
- for (const deleted of alignment.deletions) {
3964
- const node = deleted.node;
3965
- const sharedId = v4();
3966
- const date = (/* @__PURE__ */ new Date()).toISOString();
3967
- let type = "paragraphDelete";
3968
- let location = "";
3969
- let preview = "";
3970
- if (isTable(node)) {
3971
- type = "rowDelete";
3972
- location = `Deleted table at position ${deleted.path[0] + 1}`;
3973
- preview = `Table with ${node.content?.length || 0} rows`;
3974
- } else if (isList(node)) {
3975
- type = "listItemDelete";
3976
- location = `Deleted list at position ${deleted.path[0] + 1}`;
3977
- preview = `List with ${node.content?.length || 0} items`;
3978
- } else {
3979
- type = "paragraphDelete";
3980
- location = `Deleted paragraph`;
3981
- preview = extractTextPreview2(node);
3982
- }
3983
- changes.push({
3984
- id: sharedId,
3985
- type,
3986
- nodeType: node.type,
3987
- path: deleted.path,
3988
- node: markAllTextAsDeleted2(cloneNode3(node), sharedId, author)
3989
- });
3990
- infos.push({
3991
- id: sharedId,
3992
- type,
3993
- nodeType: node.type,
3994
- location,
3995
- preview,
3996
- author,
3997
- date
3998
- });
3999
- }
4000
- for (const match of alignment.matched) {
4001
- const nodeA = docA.content?.[match.pathA[0]];
4002
- const nodeB = docB.content?.[match.pathB[0]];
4003
- if (!nodeA || !nodeB) continue;
4004
- if (isTable(nodeA) && isTable(nodeB)) {
4005
- tableIndex++;
4006
- const tableResult = diffTables(nodeA, nodeB, match.pathA, match.pathB);
4007
- for (const rowChange of tableResult.rowChanges) {
4008
- const sharedId = rowChange.id;
4009
- const date = (/* @__PURE__ */ new Date()).toISOString();
4010
- const rowIndex = rowChange.path[rowChange.path.length - 1];
4011
- const isInsert = rowChange.type === "rowInsert";
4012
- const location = getRowLocation(rowChange.path, rowIndex, tableIndex - 1);
4013
- const preview = getRowPreview(rowChange.node);
4014
- const markedNode = isInsert ? markAllTextAsInserted2(cloneNode3(rowChange.node), sharedId, author) : markAllTextAsDeleted2(cloneNode3(rowChange.node), sharedId, author);
4015
- changes.push({
4016
- ...rowChange,
4017
- node: markedNode
4018
- });
4019
- infos.push({
4020
- id: sharedId,
4021
- type: rowChange.type,
4022
- nodeType: "tableRow",
4023
- location,
4024
- preview,
4025
- author,
4026
- date
4027
- });
4028
- }
4029
- }
4030
- if (isList(nodeA) && isList(nodeB)) {
4031
- listIndex++;
4032
- const listResult = diffLists(nodeA, nodeB, match.pathA, match.pathB);
4033
- for (const itemChange of listResult.itemChanges) {
4034
- const sharedId = itemChange.id;
4035
- const date = (/* @__PURE__ */ new Date()).toISOString();
4036
- const itemIndex = itemChange.path[itemChange.path.length - 1];
4037
- const isInsert = itemChange.type === "listItemInsert";
4038
- const location = getListItemLocation(itemChange.path, itemIndex, listIndex - 1);
4039
- const preview = getListItemPreview(itemChange.node);
4040
- const markedNode = isInsert ? markAllTextAsInserted2(cloneNode3(itemChange.node), sharedId, author) : markAllTextAsDeleted2(cloneNode3(itemChange.node), sharedId, author);
4041
- changes.push({
4042
- ...itemChange,
4043
- node: markedNode
4044
- });
4045
- infos.push({
4046
- id: sharedId,
4047
- type: itemChange.type,
4048
- nodeType: "listItem",
4049
- location,
4050
- preview,
4051
- author,
4052
- date
4053
- });
4054
- }
4055
- }
4056
- }
4057
- const imageChanges = diffImages(docA, docB);
4058
- for (const imgInsert of imageChanges.inserted) {
4059
- const sharedId = imgInsert.id;
4060
- const date = (/* @__PURE__ */ new Date()).toISOString();
4061
- infos.push({
4062
- id: sharedId,
4063
- type: "imageInsert",
4064
- nodeType: "image",
4065
- location: getImageLocation(imgInsert.path),
4066
- preview: getImagePreview(imgInsert.node),
4067
- author,
4068
- date
4069
- });
4070
- changes.push(imgInsert);
4071
- }
4072
- for (const imgDelete of imageChanges.deleted) {
4073
- const sharedId = imgDelete.id;
4074
- const date = (/* @__PURE__ */ new Date()).toISOString();
4075
- infos.push({
4076
- id: sharedId,
4077
- type: "imageDelete",
4078
- nodeType: "image",
4079
- location: getImageLocation(imgDelete.path),
4080
- preview: getImagePreview(imgDelete.node),
4081
- author,
4082
- date
4083
- });
4084
- changes.push(imgDelete);
4085
- }
4086
- return { changes, infos };
4087
- }
4088
- function generateStructuralChangeSummary(infos) {
4089
- const summary = [];
4090
- const rowInserts = infos.filter((i) => i.type === "rowInsert").length;
4091
- const rowDeletes = infos.filter((i) => i.type === "rowDelete").length;
4092
- const paragraphInserts = infos.filter((i) => i.type === "paragraphInsert").length;
4093
- const paragraphDeletes = infos.filter((i) => i.type === "paragraphDelete").length;
4094
- const listItemInserts = infos.filter((i) => i.type === "listItemInsert").length;
4095
- const listItemDeletes = infos.filter((i) => i.type === "listItemDelete").length;
4096
- const imageInserts = infos.filter((i) => i.type === "imageInsert").length;
4097
- const imageDeletes = infos.filter((i) => i.type === "imageDelete").length;
4098
- if (rowInserts > 0) summary.push(`${rowInserts} row(s) inserted`);
4099
- if (rowDeletes > 0) summary.push(`${rowDeletes} row(s) deleted`);
4100
- if (paragraphInserts > 0) summary.push(`${paragraphInserts} paragraph(s) inserted`);
4101
- if (paragraphDeletes > 0) summary.push(`${paragraphDeletes} paragraph(s) deleted`);
4102
- if (listItemInserts > 0) summary.push(`${listItemInserts} list item(s) inserted`);
4103
- if (listItemDeletes > 0) summary.push(`${listItemDeletes} list item(s) deleted`);
4104
- if (imageInserts > 0) summary.push(`${imageInserts} image(s) inserted`);
4105
- if (imageDeletes > 0) summary.push(`${imageDeletes} image(s) deleted`);
4106
- return summary;
4107
- }
4108
4252
 
4109
4253
  // src/blankTemplate.ts
4110
4254
  var BLANK_DOCX_BASE64 = `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`;