@crashbytes/react-version-compare
Version:
A React component for comparing strings and arrays with precise word-level and item-level highlighting of differences.
376 lines (368 loc) • 12.6 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
var richTextTypes = require('@contentful/rich-text-types');
var Diff = require('diff');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var Diff__namespace = /*#__PURE__*/_interopNamespaceDefault(Diff);
// Exported type guard for Contentful documents
function isContentfulDocument(value) {
return value && typeof value === 'object' && value.nodeType === richTextTypes.BLOCKS.DOCUMENT && Array.isArray(value.content);
}
// Extract plain text from Contentful document
function extractPlainText(document) {
const extractFromNode = node => {
if (node.nodeType === 'text') {
return node.value;
}
if ('content' in node && node.content) {
return node.content.map(child => extractFromNode(child)).join('');
}
return '';
};
return extractFromNode(document);
}
// Extract structured content for structure diff
function extractStructuredContent(document) {
const result = [];
const extractFromNode = node => {
if (node.nodeType === 'text') return;
if ('content' in node && node.content) {
const textContent = node.content.map(child => child.nodeType === 'text' ? child.value : '').join('');
let displayType = node.nodeType;
let headingLevel;
switch (node.nodeType) {
case richTextTypes.BLOCKS.HEADING_1:
displayType = 'Heading';
headingLevel = 1;
break;
case richTextTypes.BLOCKS.HEADING_2:
displayType = 'Heading';
headingLevel = 2;
break;
case richTextTypes.BLOCKS.HEADING_3:
displayType = 'Heading';
headingLevel = 3;
break;
case richTextTypes.BLOCKS.HEADING_4:
displayType = 'Heading';
headingLevel = 4;
break;
case richTextTypes.BLOCKS.HEADING_5:
displayType = 'Heading';
headingLevel = 5;
break;
case richTextTypes.BLOCKS.HEADING_6:
displayType = 'Heading';
headingLevel = 6;
break;
case richTextTypes.BLOCKS.PARAGRAPH:
displayType = 'Text';
break;
case richTextTypes.BLOCKS.UL_LIST:
displayType = 'List';
break;
case richTextTypes.BLOCKS.OL_LIST:
displayType = 'Numbered List';
break;
case richTextTypes.BLOCKS.LIST_ITEM:
displayType = 'List Item';
break;
case richTextTypes.BLOCKS.QUOTE:
displayType = 'Quote';
break;
case richTextTypes.BLOCKS.TABLE:
displayType = 'Table';
break;
default:
displayType = node.nodeType.charAt(0).toUpperCase() + node.nodeType.slice(1);
}
if (textContent.trim()) {
result.push({
type: displayType,
content: textContent.trim(),
level: headingLevel
});
}
node.content.forEach(child => {
if (child.nodeType !== 'text') extractFromNode(child);
});
}
};
if (document.content) document.content.forEach(node => extractFromNode(node));
return result;
}
// Main Contentful diff renderer
async function renderContentfulDiff(origDoc, modDoc, compareMode, caseSensitive, renderStringDiff) {
// Dynamically import diff for Vite/ESM compatibility
const DiffModule = await import('diff');
const Diff = DiffModule.default ?? DiffModule;
const {
diffWords,
diffArrays
} = Diff;
if (compareMode === 'structure') {
const origStructure = extractStructuredContent(origDoc);
const modStructure = extractStructuredContent(modDoc);
const diff = diffArrays(origStructure, modStructure, {
comparator: (a, b) => a.type === b.type && a.content === b.content && a.level === b.level
});
const originalParts = [];
const modifiedParts = [];
let origIdx = 0;
let modIdx = 0;
diff.forEach(part => {
if (part.added) {
part.value.forEach((modItem, i) => {
originalParts.push(jsxRuntime.jsx("div", {
className: "diff-blank-line"
}, `blank-orig-${modIdx + i}`));
modifiedParts.push(jsxRuntime.jsxs("div", {
className: "diff-added-line",
children: [jsxRuntime.jsx("span", {
className: "diff-structure-type",
children: modItem.type
}), modItem.level && jsxRuntime.jsxs("span", {
className: "diff-structure-level",
children: [" H", modItem.level]
}), jsxRuntime.jsxs("span", {
className: "diff-structure-content",
children: [": ", modItem.content]
})]
}, `added-mod-${modIdx + i}`));
});
modIdx += part.count || 0;
} else if (part.removed) {
part.value.forEach((origItem, i) => {
originalParts.push(jsxRuntime.jsxs("div", {
className: "diff-removed-line",
children: [jsxRuntime.jsx("span", {
className: "diff-structure-type",
children: origItem.type
}), origItem.level && jsxRuntime.jsxs("span", {
className: "diff-structure-level",
children: [" H", origItem.level]
}), jsxRuntime.jsxs("span", {
className: "diff-structure-content",
children: [": ", origItem.content]
})]
}, `removed-orig-${origIdx + i}`));
modifiedParts.push(jsxRuntime.jsx("div", {
className: "diff-blank-line"
}, `blank-mod-${origIdx + i}`));
});
origIdx += part.count || 0;
} else {
part.value.forEach((item, i) => {
originalParts.push(jsxRuntime.jsxs("div", {
className: "diff-unchanged-line",
children: [jsxRuntime.jsx("span", {
className: "diff-structure-type",
children: item.type
}), item.level && jsxRuntime.jsxs("span", {
className: "diff-structure-level",
children: [" H", item.level]
}), jsxRuntime.jsxs("span", {
className: "diff-structure-content",
children: [": ", item.content]
})]
}, `unchanged-orig-${origIdx + i}`));
modifiedParts.push(jsxRuntime.jsxs("div", {
className: "diff-unchanged-line",
children: [jsxRuntime.jsx("span", {
className: "diff-structure-type",
children: item.type
}), item.level && jsxRuntime.jsxs("span", {
className: "diff-structure-level",
children: [" H", item.level]
}), jsxRuntime.jsxs("span", {
className: "diff-structure-content",
children: [": ", item.content]
})]
}, `unchanged-mod-${modIdx + i}`));
});
origIdx += part.count || 0;
modIdx += part.count || 0;
}
});
return {
originalParts,
modifiedParts
};
} else {
// Text-based comparison of Contentful documents
const origText = extractPlainText(origDoc);
const modText = extractPlainText(modDoc);
return renderStringDiff(origText, modText);
}
}
function renderStringDiff(orig, mod) {
const difference = Diff__namespace.diffWords(orig, mod);
const originalParts = [];
const modifiedParts = [];
for (const part of difference) {
if (part.removed) {
originalParts.push(jsxRuntime.jsx("span", {
className: "diff-removed",
children: part.value
}, originalParts.length));
} else if (part.added) {
modifiedParts.push(jsxRuntime.jsx("span", {
className: "diff-added",
children: part.value
}, modifiedParts.length));
} else {
originalParts.push(jsxRuntime.jsx("span", {
className: "diff-unchanged",
children: part.value
}, originalParts.length));
modifiedParts.push(jsxRuntime.jsx("span", {
className: "diff-unchanged",
children: part.value
}, modifiedParts.length));
}
}
return {
originalParts,
modifiedParts
};
}
function renderArrayDiff(original, modified) {
const maxLength = Math.max(original.length, modified.length);
const originalParts = [];
const modifiedParts = [];
for (let i = 0; i < maxLength; i++) {
const orig = original[i] ?? '';
const mod = modified[i] ?? '';
if (orig === mod) {
originalParts.push(jsxRuntime.jsx("div", {
className: "diff-unchanged-line",
children: orig
}, `orig-${i}`));
modifiedParts.push(jsxRuntime.jsx("div", {
className: "diff-unchanged-line",
children: mod
}, `mod-${i}`));
} else {
originalParts.push(jsxRuntime.jsx("div", {
className: orig ? "diff-removed-line" : "diff-blank-line",
children: orig
}, `orig-${i}`));
modifiedParts.push(jsxRuntime.jsx("div", {
className: mod ? "diff-added-line" : "diff-blank-line",
children: mod
}, `mod-${i}`));
}
}
return {
originalParts,
modifiedParts
};
}
const Compare = ({
original,
modified,
className = '',
viewMode = 'side-by-side',
caseSensitive = true,
compareMode = 'text'
}) => {
const isStringComparison = typeof original === 'string' && typeof modified === 'string';
const isArrayComparison = Array.isArray(original) && Array.isArray(modified);
const isContentfulComparison = isContentfulDocument(original) && isContentfulDocument(modified);
const [contentfulParts, setContentfulParts] = react.useState(null);
react.useEffect(() => {
let cancelled = false;
if (isContentfulComparison) {
setContentfulParts(null); // reset while loading
renderContentfulDiff(original, modified, compareMode, caseSensitive, renderStringDiff).then(result => {
if (!cancelled) setContentfulParts(result);
});
}
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [original, modified, compareMode, caseSensitive, isContentfulComparison]);
let originalParts = [],
modifiedParts = [];
if (isStringComparison) {
({
originalParts,
modifiedParts
} = renderStringDiff(original, modified));
} else if (isArrayComparison) {
({
originalParts,
modifiedParts
} = renderArrayDiff(original, modified));
} else if (isContentfulComparison) {
if (contentfulParts) {
originalParts = contentfulParts.originalParts;
modifiedParts = contentfulParts.modifiedParts;
} else {
originalParts = [jsxRuntime.jsx("div", {
children: "Loading..."
}, "loading")];
modifiedParts = [jsxRuntime.jsx("div", {
children: "Loading..."
}, "loading")];
}
}
if (!isStringComparison && !isArrayComparison && !isContentfulComparison) {
return jsxRuntime.jsx("div", {
className: `compare-error ${className}`,
children: "Error: Invalid input for comparison."
});
}
if (viewMode === 'inline') {
return jsxRuntime.jsx("div", {
className: `compare-inline ${className}`,
children: jsxRuntime.jsxs("div", {
className: "compare-content",
children: [originalParts, modifiedParts]
})
});
}
return jsxRuntime.jsxs("div", {
className: `compare-side-by-side ${className}`,
children: [jsxRuntime.jsxs("div", {
className: "compare-panel",
children: [jsxRuntime.jsx("div", {
className: "compare-header original-header",
children: "Original"
}), jsxRuntime.jsx("div", {
className: "compare-content original-content",
children: originalParts
})]
}), jsxRuntime.jsxs("div", {
className: "compare-panel",
children: [jsxRuntime.jsx("div", {
className: "compare-header modified-header",
children: "Modified"
}), jsxRuntime.jsx("div", {
className: "compare-content modified-content",
children: modifiedParts
})]
})]
});
};
exports.Compare = Compare;
exports.default = Compare;
//# sourceMappingURL=index.js.map