From b76d9f0311019c7d866ce1d9db92140e483887bd Mon Sep 17 00:00:00 2001
From: Ella <4710635+ellatrix@users.noreply.github.com>
Date: Mon, 25 Nov 2024 16:12:44 +0100
Subject: [PATCH] Rich text: preserve comments (#62128)
Co-authored-by: ellatrix
Co-authored-by: lysyjan
Co-authored-by: youknowriad
Co-authored-by: RolfKyburz
Co-authored-by: strarsis
Co-authored-by: danielbachhuber
---
.../src/components/rich-text/content.scss | 19 +++++--
packages/rich-text/src/create.js | 28 ++++++++++
.../src/test/__snapshots__/to-dom.js.snap | 30 +++++++++++
packages/rich-text/src/test/helpers/index.js | 52 +++++++++++++++++++
packages/rich-text/src/to-dom.js | 12 +++--
packages/rich-text/src/to-html-string.js | 9 ++++
packages/rich-text/src/to-tree.js | 15 +++++-
7 files changed, 157 insertions(+), 8 deletions(-)
diff --git a/packages/block-editor/src/components/rich-text/content.scss b/packages/block-editor/src/components/rich-text/content.scss
index 28dcd931a7f5d..67e14b0882d7e 100644
--- a/packages/block-editor/src/components/rich-text/content.scss
+++ b/packages/block-editor/src/components/rich-text/content.scss
@@ -13,10 +13,6 @@
&:focus {
// Removes outline added by the browser.
outline: none;
-
- [data-rich-text-format-boundary] {
- border-radius: $radius-small;
- }
}
}
@@ -34,3 +30,18 @@ figcaption.block-editor-rich-text__editable [data-rich-text-placeholder]::before
background: rgb(255, 255, 0);
}
}
+
+[data-rich-text-comment],
+[data-rich-text-format-boundary] {
+ border-radius: $radius-small;
+}
+
+[data-rich-text-comment] {
+ background-color: currentColor;
+
+ span {
+ filter: invert(100%);
+ color: currentColor;
+ padding: 0 2px;
+ }
+}
diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js
index 898bdfa73b330..d683836d9e2fa 100644
--- a/packages/rich-text/src/create.js
+++ b/packages/rich-text/src/create.js
@@ -469,6 +469,34 @@ function createFromElement( { element, range, isEditableTree } ) {
continue;
}
+ if (
+ node.nodeType === node.COMMENT_NODE ||
+ ( node.nodeType === node.ELEMENT_NODE &&
+ node.tagName === 'SPAN' &&
+ node.hasAttribute( 'data-rich-text-comment' ) )
+ ) {
+ const value = {
+ formats: [ , ],
+ replacements: [
+ {
+ type: '#comment',
+ attributes: {
+ 'data-rich-text-comment':
+ node.nodeType === node.COMMENT_NODE
+ ? node.nodeValue
+ : node.getAttribute(
+ 'data-rich-text-comment'
+ ),
+ },
+ },
+ ],
+ text: OBJECT_REPLACEMENT_CHARACTER,
+ };
+ accumulateSelection( accumulator, node, range, value );
+ mergePair( accumulator, value );
+ continue;
+ }
+
if ( node.nodeType !== node.ELEMENT_NODE ) {
continue;
}
diff --git a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap
index 0daf48aa9a1c3..0e2ac57bea370 100644
--- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap
+++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap
@@ -272,6 +272,21 @@ exports[`recordToDom should not error with overlapping formats (2) 1`] = `
+
+
+
+ comment
+
+
+
+
🍒
@@ -289,6 +304,21 @@ exports[`recordToDom should preserve emoji in formatting 1`] = `
+
+
+
+ /funky
+
+
+
+
test test
diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js
index f246ab956db3a..7658ede7e3773 100644
--- a/packages/rich-text/src/test/helpers/index.js
+++ b/packages/rich-text/src/test/helpers/index.js
@@ -551,6 +551,58 @@ export const spec = [
text: '\ufffc',
},
},
+ {
+ description: 'should preserve comments',
+ html: '',
+ createRange: ( element ) => ( {
+ startOffset: 0,
+ startContainer: element,
+ endOffset: 1,
+ endContainer: element,
+ } ),
+ startPath: [ 0, 0 ],
+ endPath: [ 2, 0 ],
+ record: {
+ start: 0,
+ end: 1,
+ formats: [ , ],
+ replacements: [
+ {
+ attributes: {
+ 'data-rich-text-comment': 'comment',
+ },
+ type: '#comment',
+ },
+ ],
+ text: '\ufffc',
+ },
+ },
+ {
+ description: 'should preserve funky comments',
+ html: '/funky>',
+ createRange: ( element ) => ( {
+ startOffset: 0,
+ startContainer: element,
+ endOffset: 1,
+ endContainer: element,
+ } ),
+ startPath: [ 0, 0 ],
+ endPath: [ 2, 0 ],
+ record: {
+ start: 0,
+ end: 1,
+ formats: [ , ],
+ replacements: [
+ {
+ attributes: {
+ 'data-rich-text-comment': '/funky',
+ },
+ type: '#comment',
+ },
+ ],
+ text: '\ufffc',
+ },
+ },
];
export const specWithRegistration = [
diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js
index e7288e4ba1633..ac8308c7274b5 100644
--- a/packages/rich-text/src/to-dom.js
+++ b/packages/rich-text/src/to-dom.js
@@ -68,10 +68,16 @@ function append( element, child ) {
const { type, attributes } = child;
if ( type ) {
- child = element.ownerDocument.createElement( type );
+ if ( type === '#comment' ) {
+ child = element.ownerDocument.createComment(
+ attributes[ 'data-rich-text-comment' ]
+ );
+ } else {
+ child = element.ownerDocument.createElement( type );
- for ( const key in attributes ) {
- child.setAttribute( key, attributes[ key ] );
+ for ( const key in attributes ) {
+ child.setAttribute( key, attributes[ key ] );
+ }
}
}
diff --git a/packages/rich-text/src/to-html-string.js b/packages/rich-text/src/to-html-string.js
index f770dfdefc128..a4c12b4c47f00 100644
--- a/packages/rich-text/src/to-html-string.js
+++ b/packages/rich-text/src/to-html-string.js
@@ -88,6 +88,15 @@ function remove( object ) {
}
function createElementHTML( { type, attributes, object, children } ) {
+ if ( type === '#comment' ) {
+ // We can't restore the original comment delimiters, because once parsed
+ // into DOM nodes, we don't have the information. But in the future we
+ // could allow comment handlers to specify custom delimiters, for
+ // example `{comment-content}>` for Bits, where `comment-content`
+ // would be `/{bit-name}` or `__{translatable-string}` (TBD).
+ return ``;
+ }
+
let attributeString = '';
for ( const key in attributes ) {
diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js
index 46671c951bc09..0e3caad4f70c8 100644
--- a/packages/rich-text/src/to-tree.js
+++ b/packages/rich-text/src/to-tree.js
@@ -229,7 +229,20 @@ export function toTree( {
const { type, attributes, innerHTML } = replacement;
const formatType = getFormatType( type );
- if ( ! isEditableTree && type === 'script' ) {
+ if ( isEditableTree && type === '#comment' ) {
+ pointer = append( getParent( pointer ), {
+ type: 'span',
+ attributes: {
+ contenteditable: 'false',
+ 'data-rich-text-comment':
+ attributes[ 'data-rich-text-comment' ],
+ },
+ } );
+ append(
+ append( pointer, { type: 'span' } ),
+ attributes[ 'data-rich-text-comment' ].trim()
+ );
+ } else if ( ! isEditableTree && type === 'script' ) {
pointer = append(
getParent( pointer ),
fromFormat( {