Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Want to handle whitespace in pasted HTML according to HTML spec #2718

Closed
wants to merge 11 commits into from
5 changes: 5 additions & 0 deletions .changeset/flat-games-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-core': major
---

Resolve the issue of whitespace in pasted HTML not being handled according to the HTML specification.
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,72 @@ describe('when deserializing all plugins', () => {
).toEqual(output.children);
});
});

describe('when stripWhitespace is true', () => {
// https://github.com/udecode/plate/issues/2713#issuecomment-1780118687
const html = `<p>\n Hello world\n </p>\n\n <p>\n one two \n three\n</p>\n\n<pre>\nhello one two\nthree\nfour\n</pre>\n\n<div style="white-space: pre">\nhello one two\nthree\nfour\n</div>\n\n<div style="white-space: pre-line">\nhello one two\nthree\nfour\n</div>`;
const element = getHtmlDocument(html).body;

const expectedOutput = [
{
text: 'Hello world',
},
{
text: 'one two three',
},
{
text: 'hello one two\nthree\nfour',
},
{
text: '\nhello one two\nthree\nfour',
},
{
text: '\nhello one two\nthree\nfour',
},
];

it('should strip Whitespace by style', () => {
const convertedDocumentFragment = deserializeHtml(createPlateEditor(), {
element,
stripWhitespace: true,
});

expect(convertedDocumentFragment).toEqual(expectedOutput);
});

it('should strip Whitespace Normal start', () => {
const convertedDocumentFragment = deserializeHtml(createPlateEditor(), {
element: getHtmlDocument('<p><strong>Hello </strong> world</p>').body,
stripWhitespace: true,
});

expect(convertedDocumentFragment).toEqual([
{ text: 'Hello ' },
{ text: 'world' },
]);
});

it('should strip Whitespace Normal end', () => {
const convertedDocumentFragment = deserializeHtml(createPlateEditor(), {
element: getHtmlDocument('<p><strong>Hello </strong>world </p>').body,
stripWhitespace: true,
});

expect(convertedDocumentFragment).toEqual([
{ text: 'Hello ' },
{ text: 'world' },
]);
});

it('should strip Whitespace by <pre>', () => {
const convertedDocumentFragment = deserializeHtml(createPlateEditor(), {
element: getHtmlDocument('<pre>\nhello one two\nthree\nfour\n</pre>')
.body,
stripWhitespace: true,
});

expect(convertedDocumentFragment).toEqual([
{ text: 'hello one two\nthree\nfour' },
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PlateEditor } from '../../../types/PlateEditor';
import { normalizeDescendantsToDocumentFragment } from '../../../utils/normalizeDescendantsToDocumentFragment';
import { deserializeHtmlElement } from './deserializeHtmlElement';
import { htmlStringToDOMNode } from './htmlStringToDOMNode';
import { stripWhitespace as stripWhitespaceFunction } from './stripWhitespace';

/**
* Deserialize HTML element to a valid document fragment.
Expand All @@ -20,7 +21,12 @@ export const deserializeHtml = <V extends Value>(
): EDescendant<V>[] => {
// for serializer
if (typeof element === 'string') {
element = htmlStringToDOMNode(element, stripWhitespace);
element = htmlStringToDOMNode(element);
}

if (stripWhitespace) {
// TODO FIXME
stripWhitespaceFunction(element);
}

const fragment = deserializeHtmlElement(editor, element) as EDescendant<V>[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ jsx;
*/
export const htmlBodyToFragment = <V extends Value>(
editor: PlateEditor<V>,
element: HTMLElement
element: HTMLElement,
stripWhitespace = true
): EDescendant<V>[] | undefined => {
if (element.nodeName === 'BODY') {
return jsx(
'fragment',
{},
deserializeHtmlNodeChildren(editor, element)
deserializeHtmlNodeChildren(editor, element, stripWhitespace)
) as EDescendant<V>[];
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { pipeDeserializeHtmlElement } from './pipeDeserializeHtmlElement';
*/
export const htmlElementToElement = <V extends Value>(
editor: PlateEditor<V>,
element: HTMLElement
element: HTMLElement,
stripWhitespace = true
) => {
const deserialized = pipeDeserializeHtmlElement(editor, element);

Expand All @@ -19,7 +20,11 @@ export const htmlElementToElement = <V extends Value>(

let descendants =
node.children ??
(deserializeHtmlNodeChildren(editor, element) as TDescendant[]);
(deserializeHtmlNodeChildren(
editor,
element,
stripWhitespace
) as TDescendant[]);
if (descendants.length === 0 || withoutChildren) {
descendants = [{ text: '' }];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
/**
* Convert HTML string into HTML element.
*/
export const htmlStringToDOMNode = (
rawHtml: string,
stripWhitespace = true
) => {
export const htmlStringToDOMNode = (rawHtml: string) => {
const node = document.createElement('body');
node.innerHTML = rawHtml;

if (stripWhitespace) {
node.innerHTML = node.innerHTML.replaceAll(/(\r\n|[\t\n\r])/g, '');
}

return node;
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('htmlTextNodeToString', () => {
describe('when text node with no characters except \n', () => {
it('should be null', () => {
const input = document.createTextNode('\n\n\n\n\n');
const output = null;
const output = '\n\n\n\n\n';

expect(htmlTextNodeToString(input)).toEqual(output);
});
Expand All @@ -31,7 +31,7 @@ describe('htmlTextNodeToString', () => {
describe('when text node with text and \n characters', () => {
it('should strip \n characters from start and end', () => {
12joan marked this conversation as resolved.
Show resolved Hide resolved
const input = document.createTextNode('\n\n\ntest\n\ntest\n\n');
const output = 'test\n\ntest';
const output = '\n\n\ntest\n\ntest\n\n';

expect(htmlTextNodeToString(input)).toEqual(output);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isHtmlText } from './isHtmlText';

export const htmlTextNodeToString = (node: HTMLElement | ChildNode) => {
if (isHtmlText(node)) {
const trimmedText = node.textContent?.replace(/^\n+|\n+$/g, '') ?? '';
const trimmedText = node.textContent ?? '';
return trimmedText.length > 0 ? trimmedText : null;
}
};
Loading