Skip to content

Commit

Permalink
Backport #6309 to Volto 17 (#6384)
Browse files Browse the repository at this point in the history
  • Loading branch information
dobri1408 authored Oct 9, 2024
1 parent 54b4d67 commit d0c533f
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 95 deletions.
1 change: 1 addition & 0 deletions news/6384.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
improve DiffField.jsx with better support for displaying HTML elements such as images @dobri1408
206 changes: 167 additions & 39 deletions src/components/manage/Diff/DiffField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

import React from 'react';
// import { diffWords as dWords } from 'diff';
import { join, map } from 'lodash';
import PropTypes from 'prop-types';
import { Grid } from 'semantic-ui-react';
Expand All @@ -13,20 +12,128 @@ import { Provider } from 'react-intl-redux';
import { createBrowserHistory } from 'history';
import { ConnectedRouter } from 'connected-react-router';
import { useSelector } from 'react-redux';

import config from '@plone/volto/registry';
import { Api } from '@plone/volto/helpers';
import configureStore from '@plone/volto/store';
import { DefaultView } from '@plone/volto/components/';
import { RenderBlocks } from '@plone/volto/components';
import { serializeNodes } from '@plone/volto-slate/editor/render';

import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';

/**
* Enhanced diff words utility
* @function diffWords
* @param oneStr Field one
* @param twoStr Field two
*/
const isHtmlTag = (str) => {
// Match complete HTML tags, including:
// 1. Opening tags like <div>, <img src="example" />, <svg>...</svg>
// 2. Self-closing tags like <img />, <br />
// 3. Closing tags like </div>
return /^<([a-zA-Z]+[0-9]*)\b[^>]*>|^<\/([a-zA-Z]+[0-9]*)\b[^>]*>$|^<([a-zA-Z]+[0-9]*)\b[^>]*\/>$/.test(
str,
);
};

const splitWords = (str) => {
if (typeof str !== 'string') return str;
if (!str) return [];

const result = [];
let currentWord = '';
let insideTag = false;
let insideSpecialTag = false;
let tagBuffer = '';

// Special tags that should not be split (e.g., <img />, <svg> ... </svg>)
const specialTags = ['img', 'svg'];

for (let i = 0; i < str.length; i++) {
const char = str[i];

// Start of an HTML tag
if (char === '<') {
if (currentWord) {
result.push(currentWord); // Push text before the tag
currentWord = '';
}
insideTag = true;
tagBuffer += char;
}
// End of an HTML tag
else if (char === '>') {
tagBuffer += char;
insideTag = false;

// Check if the tagBuffer contains a special tag
const tagNameMatch = tagBuffer.match(/^<\/?([a-zA-Z]+[0-9]*)\b/);
if (tagNameMatch && specialTags.includes(tagNameMatch[1])) {
insideSpecialTag =
tagNameMatch[0].startsWith('<') && !tagNameMatch[0].startsWith('</');
result.push(tagBuffer); // Push the complete special tag as one unit
tagBuffer = '';
continue;
}

result.push(tagBuffer); // Push the complete tag
tagBuffer = '';
}
// Inside the tag or special tag
else if (insideTag || insideSpecialTag) {
tagBuffer += char;
}
// Space outside of tags - push current word
else if (char === ' ' && !insideTag && !insideSpecialTag) {
if (currentWord) {
result.push(currentWord);
currentWord = '';
}
result.push(' ');
} else if (
char === ',' &&
i < str.length - 1 &&
str[i + 1] !== ' ' &&
!insideTag &&
!insideSpecialTag
) {
if (currentWord) {
result.push(currentWord + char);
currentWord = '';
}
result.push(' ');
}
// Accumulate characters outside of tags
else {
currentWord += char;
}
}

// Push any remaining text
if (currentWord) {
result.push(currentWord);
}
if (tagBuffer) {
result.push(tagBuffer); // Push remaining tagBuffer
}

return result;
};

const formatDiffPart = (part, value, side) => {
if (!isHtmlTag(value)) {
if (part.removed && (side === 'left' || side === 'unified')) {
return `<span class="deletion">${value}</span>`;
} else if (part.removed) return '';
else if (part.added && (side === 'right' || side === 'unified')) {
return `<span class="addition">${value}</span>`;
} else if (part.added) return '';
return value;
} else {
if (side === 'unified' && part.added) return value;
else if (side === 'unified' && part.removed) return '';
if (part.removed && side === 'left') {
return value;
} else if (part.removed) return '';
else if (part.added && side === 'right') {
return value;
} else if (part.added) return '';
return value;
}
};

/**
* Diff field component.
Expand All @@ -36,6 +143,7 @@ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
* @param {Object} schema Field schema
* @returns {string} Markup of the component.
*/

const DiffField = ({
one,
two,
Expand All @@ -51,7 +159,10 @@ const DiffField = ({
timeStyle: 'short',
};
const diffWords = (oneStr, twoStr) => {
return diffLib.diffWords(String(oneStr), String(twoStr));
return diffLib.diffArrays(
splitWords(String(oneStr)),
splitWords(String(twoStr)),
);
};

let parts, oneArray, twoArray;
Expand All @@ -78,14 +189,14 @@ const DiffField = ({
ReactDOMServer.renderToStaticMarkup(
<Provider store={store}>
<ConnectedRouter history={history}>
<DefaultView content={contentOne} />
<RenderBlocks content={contentOne} />
</ConnectedRouter>
</Provider>,
),
ReactDOMServer.renderToStaticMarkup(
<Provider store={store}>
<ConnectedRouter history={history}>
<DefaultView content={contentTwo} />
<RenderBlocks content={contentTwo} />
</ConnectedRouter>
</Provider>,
),
Expand Down Expand Up @@ -116,7 +227,30 @@ const DiffField = ({
}
case 'textarea':
default:
parts = diffWords(one, two);
const Widget = config.widgets?.views?.widget?.[schema.widget];

if (Widget) {
const api = new Api();
const history = createBrowserHistory();
const store = configureStore(window.__data, history, api);
parts = diffWords(
ReactDOMServer.renderToStaticMarkup(
<Provider store={store}>
<ConnectedRouter history={history}>
<Widget value={one} />
</ConnectedRouter>
</Provider>,
),
ReactDOMServer.renderToStaticMarkup(
<Provider store={store}>
<ConnectedRouter history={history}>
<Widget value={two} />
</ConnectedRouter>
</Provider>,
),
);
} else parts = diffWords(one, two);

break;
}
} else if (schema.type === 'object') {
Expand All @@ -128,6 +262,7 @@ const DiffField = ({
} else {
parts = diffWords(one?.title || one, two?.title || two);
}

return (
<Grid data-testid="DiffField">
<Grid.Row>
Expand All @@ -140,14 +275,12 @@ const DiffField = ({
<span
dangerouslySetInnerHTML={{
__html: join(
map(
parts,
(part) =>
(part.removed &&
`<span class="deletion">${part.value}</span>`) ||
(!part.added && `<span>${part.value}</span>`) ||
'',
),
map(parts, (part) => {
let combined = (part.value || []).reduce((acc, value) => {
return acc + formatDiffPart(part, value, 'left');
}, '');
return combined;
}),
'',
),
}}
Expand All @@ -157,14 +290,12 @@ const DiffField = ({
<span
dangerouslySetInnerHTML={{
__html: join(
map(
parts,
(part) =>
(part.added &&
`<span class="addition">${part.value}</span>`) ||
(!part.removed && `<span>${part.value}</span>`) ||
'',
),
map(parts, (part) => {
let combined = (part.value || []).reduce((acc, value) => {
return acc + formatDiffPart(part, value, 'right');
}, '');
return combined;
}),
'',
),
}}
Expand All @@ -178,15 +309,12 @@ const DiffField = ({
<span
dangerouslySetInnerHTML={{
__html: join(
map(
parts,
(part) =>
(part.removed &&
`<span class="deletion">${part.value}</span>`) ||
(part.added &&
`<span class="addition">${part.value}</span>`) ||
(!part.added && `<span>${part.value}</span>`),
),
map(parts, (part) => {
let combined = (part.value || []).reduce((acc, value) => {
return acc + formatDiffPart(part, value, 'unified');
}, '');
return combined;
}),
'',
),
}}
Expand Down
21 changes: 9 additions & 12 deletions src/components/manage/Diff/__snapshots__/Diff.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -232,38 +232,35 @@ exports[`Diff renders a diff component 1`] = `
class="top aligned six wide column"
>
<span>
<span>
My
</span>
My
<span
class="deletion"
>
old
</span>
<span>
title
<span
class="deletion"
>
title
</span>
</span>
</div>
<div
class="top aligned six wide column"
>
<span>
<span>
My
</span>
My
<span
class="addition"
>
new
</span>
<span>
title
</span>
<span
class="addition"
>
,
title,
</span>
</span>
</div>
Expand Down
Loading

0 comments on commit d0c533f

Please sign in to comment.