UNPKG

@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
'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