From 08522f8519b2ec2c54b752f89d703f18594ed4ba Mon Sep 17 00:00:00 2001 From: Zihua Li <635902+luin@users.noreply.github.com> Date: Tue, 27 Aug 2024 21:47:19 +0800 Subject: [PATCH 1/3] Convert non-breaking spaces to regular spaces --- packages/quill/src/modules/clipboard.ts | 14 +++++------ .../quill/test/unit/modules/clipboard.spec.ts | 24 +++++++++++++------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/quill/src/modules/clipboard.ts b/packages/quill/src/modules/clipboard.ts index e4c3f755b5..8e35efd5c4 100644 --- a/packages/quill/src/modules/clipboard.ts +++ b/packages/quill/src/modules/clipboard.ts @@ -624,6 +624,9 @@ function matchTable( return delta; } +const NBSP = '\u00a0'; +const SPACE_EXCLUDE_NBSP = `[^\\S${NBSP}]`; + function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) { // @ts-expect-error let text = node.data; @@ -639,12 +642,8 @@ function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) { ) { return delta; } - const replacer = (collapse: unknown, match: string) => { - const replaced = match.replace(/[^\u00a0]/g, ''); // \u00a0 is nbsp; - return replaced.length < 1 && collapse ? ' ' : replaced; - }; text = text.replace(/\r\n/g, ' ').replace(/\n/g, ' '); - text = text.replace(/\s\s+/g, replacer.bind(replacer, true)); // collapse whitespace + text = text.replace(new RegExp(`${SPACE_EXCLUDE_NBSP}{2,}`, 'g'), ' '); // collapse whitespace if ( (node.previousSibling == null && node.parentElement != null && @@ -652,7 +651,7 @@ function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) { (node.previousSibling instanceof Element && isLine(node.previousSibling, scroll)) ) { - text = text.replace(/^\s+/, replacer.bind(replacer, false)); + text = text.replace(new RegExp(`^${SPACE_EXCLUDE_NBSP}+`), ''); } if ( (node.nextSibling == null && @@ -660,8 +659,9 @@ function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) { isLine(node.parentElement, scroll)) || (node.nextSibling instanceof Element && isLine(node.nextSibling, scroll)) ) { - text = text.replace(/\s+$/, replacer.bind(replacer, false)); + text = text.replace(new RegExp(`${SPACE_EXCLUDE_NBSP}+$`), ''); } + text = text.replaceAll(NBSP, ' '); } return delta.insert(text); } diff --git a/packages/quill/test/unit/modules/clipboard.spec.ts b/packages/quill/test/unit/modules/clipboard.spec.ts index 0ba7c159ed..69142690a2 100644 --- a/packages/quill/test/unit/modules/clipboard.spec.ts +++ b/packages/quill/test/unit/modules/clipboard.spec.ts @@ -244,6 +244,12 @@ describe('Clipboard', () => { expect(delta).toEqual(new Delta().insert('0\n1 2 3 4\n5 6 7 8')); }); + test('multiple whitespaces', () => { + const html = '
0 1 2
'; const delta = createClipboard().convert({ html }); @@ -256,19 +262,23 @@ describe('Clipboard', () => { const html = '0 1 2'; const delta = createClipboard().convert({ html }); expect(delta).toEqual( - new Delta() - .insert('0\u00a0') - .insert('1', { bold: true }) - .insert('\u00a02'), + new Delta().insert('0 ').insert('1', { bold: true }).insert(' 2'), ); }); test('consecutive intentional whitespace', () => { const html = ' 1 '; const delta = createClipboard().convert({ html }); - expect(delta).toEqual( - new Delta().insert('\u00a0\u00a01\u00a0\u00a0', { bold: true }), - ); + expect(delta).toEqual(new Delta().insert(' 1 ', { bold: true })); + }); + + test('intentional whitespace at line start/end', () => { + expect( + createClipboard().convert({ html: '0
2
' }), + ).toEqual(new Delta().insert('0 \n 2')); + expect( + createClipboard().convert({ html: '0
2
' }), + ).toEqual(new Delta().insert('0 \n 2')); }); test('newlines between inline elements', () => { From 0a40b93386c12b57dcc3d8392d69c41e34512cb8 Mon Sep 17 00:00:00 2001 From: Zihua Li <635902+luin@users.noreply.github.com> Date: Tue, 27 Aug 2024 22:01:50 +0800 Subject: [PATCH 2/3] Convert regular spaces to non-breaking spaces when exporting to HTML --- packages/quill/src/core/editor.ts | 3 ++- packages/quill/test/unit/core/editor.spec.ts | 28 ++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/quill/src/core/editor.ts b/packages/quill/src/core/editor.ts index a19485840d..ed93366ed5 100644 --- a/packages/quill/src/core/editor.ts +++ b/packages/quill/src/core/editor.ts @@ -370,7 +370,8 @@ function convertHTML( return blot.html(index, length); } if (blot instanceof TextBlot) { - return escapeText(blot.value().slice(index, index + length)); + const escapedText = escapeText(blot.value().slice(index, index + length)); + return escapedText.replace(/\s/g, ' '); } if (blot instanceof ParentBlot) { // TODO fix API diff --git a/packages/quill/test/unit/core/editor.spec.ts b/packages/quill/test/unit/core/editor.spec.ts index 0c595332bb..2d47a1dec9 100644 --- a/packages/quill/test/unit/core/editor.spec.ts +++ b/packages/quill/test/unit/core/editor.spec.ts @@ -28,9 +28,11 @@ import { ColorClass } from '../../../src/formats/color.js'; import Quill from '../../../src/core.js'; import { normalizeHTML } from '../__helpers__/utils.js'; -const createEditor = (html: string) => { +const createEditor = (htmlOrContents: string | Delta) => { const container = document.createElement('div'); - container.innerHTML = normalizeHTML(html); + if (typeof htmlOrContents === 'string') { + container.innerHTML = normalizeHTML(htmlOrContents); + } document.body.appendChild(container); const quill = new Quill(container, { registry: createRegistry([ @@ -54,6 +56,9 @@ const createEditor = (html: string) => { SizeClass, ]), }); + if (typeof htmlOrContents !== 'string') { + quill.setContents(htmlOrContents); + } return quill.editor; }; @@ -1246,6 +1251,25 @@ describe('Editor', () => { ); }); + test('collapsible spaces', () => { + expect( + createEditor('123 123 123
').getHTML( + 0, + 11, + ), + ).toEqual('123 123 123'); + + expect(createEditor(new Delta().insert('1 2\n')).getHTML(0, 5)).toEqual( + '1 2', + ); + + expect( + createEditor( + new Delta().insert(' 123', { bold: true }).insert('\n'), + ).getHTML(0, 5), + ).toEqual(' 123'); + }); + test('mixed list', () => { const editor = createEditor( ` From e7a3bfdb670997f3a38591c3742473263d1bced7 Mon Sep 17 00:00:00 2001 From: Zihua Li <635902+luin@users.noreply.github.com> Date: Tue, 27 Aug 2024 22:24:04 +0800 Subject: [PATCH 3/3] Prefer to use String#replaceAll instead of RegExp --- packages/quill/src/core/editor.ts | 2 +- packages/quill/src/modules/clipboard.ts | 2 +- packages/quill/tsconfig.json | 2 +- tsconfig.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/quill/src/core/editor.ts b/packages/quill/src/core/editor.ts index ed93366ed5..b6391a7cd4 100644 --- a/packages/quill/src/core/editor.ts +++ b/packages/quill/src/core/editor.ts @@ -371,7 +371,7 @@ function convertHTML( } if (blot instanceof TextBlot) { const escapedText = escapeText(blot.value().slice(index, index + length)); - return escapedText.replace(/\s/g, ' '); + return escapedText.replaceAll(' ', ' '); } if (blot instanceof ParentBlot) { // TODO fix API diff --git a/packages/quill/src/modules/clipboard.ts b/packages/quill/src/modules/clipboard.ts index 8e35efd5c4..ea3df80403 100644 --- a/packages/quill/src/modules/clipboard.ts +++ b/packages/quill/src/modules/clipboard.ts @@ -629,7 +629,7 @@ const SPACE_EXCLUDE_NBSP = `[^\\S${NBSP}]`; function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) { // @ts-expect-error - let text = node.data; + let text = node.data as string; // Word represents empty line with