From 041d9d31fa26d5142a513ab936f68f6a5dfcb231 Mon Sep 17 00:00:00 2001
From: Mitchell Austin
Date: Thu, 13 Aug 2020 10:26:02 -0700
Subject: [PATCH 1/6] Fix support for SVG in RichText
---
packages/rich-text/src/to-dom.js | 39 ++++++++++++++++++++++++--------
1 file changed, 30 insertions(+), 9 deletions(-)
diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js
index e7288e4ba1633..1e52bf004cdad 100644
--- a/packages/rich-text/src/to-dom.js
+++ b/packages/rich-text/src/to-dom.js
@@ -8,6 +8,9 @@ import { isRangeEqual } from './is-range-equal';
/** @typedef {import('./types').RichTextValue} RichTextValue */
+const NS_SVG = 'http://www.w3.org/2000/svg';
+const NS_XLINK = 'http://www.w3.org/1999/xlink';
+
/**
* Creates a path as an array of indices from the given root node to the given
* node.
@@ -64,20 +67,38 @@ function append( element, child ) {
if ( typeof child === 'string' ) {
child = element.ownerDocument.createTextNode( child );
}
-
- const { type, attributes } = child;
-
- if ( type ) {
- child = element.ownerDocument.createElement( type );
-
- for ( const key in attributes ) {
- child.setAttribute( key, attributes[ key ] );
- }
+ if ( 'type' in child ) {
+ child = contextuallyCreate( element, child );
}
return element.appendChild( child );
}
+function contextuallyCreate( element, child ) {
+ const { ownerDocument: doc, namespaceURI } = element;
+ const { type, attributes } = child;
+ const [ childNode, addAttribute ] =
+ type === 'svg' || namespaceURI === NS_SVG
+ ? [
+ doc.createElementNS( NS_SVG, type ),
+ ( node, key, value ) => {
+ if ( key === 'xlink:href' ) {
+ node.setAttributeNS( NS_XLINK, key, value );
+ } else {
+ node.setAttribute( key, value );
+ }
+ },
+ ]
+ : [
+ doc.createElement( type ),
+ ( node, key, value ) => node.setAttribute( key, value ),
+ ];
+ for ( const key in attributes ) {
+ addAttribute( childNode, key, attributes[ key ] );
+ }
+ return childNode;
+}
+
function appendText( node, text ) {
node.appendData( text );
}
From 241a03bb572ec5bc572788295a4d205e94895e59 Mon Sep 17 00:00:00 2001
From: Mitchell Austin
Date: Fri, 14 Aug 2020 09:08:09 -0700
Subject: [PATCH 2/6] add tests for SVG support in RichText
---
packages/rich-text/src/test/to-dom.js | 29 +++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/packages/rich-text/src/test/to-dom.js b/packages/rich-text/src/test/to-dom.js
index 4f9b2df86cad3..035f819b725d3 100644
--- a/packages/rich-text/src/test/to-dom.js
+++ b/packages/rich-text/src/test/to-dom.js
@@ -98,3 +98,32 @@ describe( 'applyValue', () => {
} );
} );
} );
+
+describe( 'toDom-SVG', () => {
+ let body;
+ beforeAll( () => {
+ const svg = { type: 'svg' };
+ const use = { type: 'use', attributes: { 'xlink:href': '#logo' } };
+ body = toDom( {
+ value: {
+ start: 0,
+ end: 1,
+ formats: [ [ svg ] ],
+ replacements: [ use ],
+ text: '\ufffc',
+ },
+ } ).body;
+ } );
+
+ it( 'should create nodes with svg namespace', () => {
+ const target = body.firstElementChild;
+ expect( target.namespaceURI ).toEqual( 'http://www.w3.org/2000/svg' );
+ } );
+
+ it( 'should create attribute xlink:href with xlink namespace', () => {
+ const target = body
+ .querySelector( 'use' )
+ .getAttributeNode( 'xlink:href' );
+ expect( target.namespaceURI ).toEqual( 'http://www.w3.org/1999/xlink' );
+ } );
+} );
From b0fd354fe6c0a9d3195bf4976b3456a4870ef0a0 Mon Sep 17 00:00:00 2001
From: Mitchell Austin
Date: Tue, 5 Sep 2023 07:42:16 -0700
Subject: [PATCH 3/6] Inline contextual creation
---
packages/rich-text/src/to-dom.js | 46 +++++++++++++-------------------
1 file changed, 19 insertions(+), 27 deletions(-)
diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js
index 1e52bf004cdad..c7ae1b853d576 100644
--- a/packages/rich-text/src/to-dom.js
+++ b/packages/rich-text/src/to-dom.js
@@ -67,36 +67,28 @@ function append( element, child ) {
if ( typeof child === 'string' ) {
child = element.ownerDocument.createTextNode( child );
}
- if ( 'type' in child ) {
- child = contextuallyCreate( element, child );
- }
-
- return element.appendChild( child );
-}
-function contextuallyCreate( element, child ) {
- const { ownerDocument: doc, namespaceURI } = element;
const { type, attributes } = child;
- const [ childNode, addAttribute ] =
- type === 'svg' || namespaceURI === NS_SVG
- ? [
- doc.createElementNS( NS_SVG, type ),
- ( node, key, value ) => {
- if ( key === 'xlink:href' ) {
- node.setAttributeNS( NS_XLINK, key, value );
- } else {
- node.setAttribute( key, value );
- }
- },
- ]
- : [
- doc.createElement( type ),
- ( node, key, value ) => node.setAttribute( key, value ),
- ];
- for ( const key in attributes ) {
- addAttribute( childNode, key, attributes[ key ] );
+
+ if ( type ) {
+ let addAttribute;
+ if ( type === 'svg' || element.namespaceURI === NS_SVG ) {
+ child = element.ownerDocument.createElementNS( NS_SVG, type );
+ addAttribute = ( key, value ) => {
+ if ( key === 'xlink:href' )
+ child.setAttributeNS( NS_XLINK, key, value );
+ else child.setAttribute( key, value );
+ };
+ } else {
+ child = element.ownerDocument.createElement( type );
+ addAttribute = child.setAttribute.bind( child );
+ }
+ for ( const key in attributes ) {
+ addAttribute( key, attributes[ key ] );
+ }
}
- return childNode;
+
+ return element.appendChild( child );
}
function appendText( node, text ) {
From 2c49098fb88572cb3064d6dd9f63196b29498350 Mon Sep 17 00:00:00 2001
From: Mitchell Austin
Date: Mon, 11 Sep 2023 09:04:02 -0700
Subject: [PATCH 4/6] Disallow editing of SVG
---
packages/rich-text/src/to-dom.js | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js
index c7ae1b853d576..7d99d79265fe6 100644
--- a/packages/rich-text/src/to-dom.js
+++ b/packages/rich-text/src/to-dom.js
@@ -72,8 +72,14 @@ function append( element, child ) {
if ( type ) {
let addAttribute;
- if ( type === 'svg' || element.namespaceURI === NS_SVG ) {
+ const isSVGTag = type === 'svg';
+ const isSVGContext = element.namespaceURI === NS_SVG;
+ if ( isSVGTag || isSVGContext ) {
child = element.ownerDocument.createElementNS( NS_SVG, type );
+ // Disables editing at top-level SVG elements.
+ if ( isSVGTag && ! isSVGContext ) {
+ child.setAttribute( 'contentEditable', false );
+ }
addAttribute = ( key, value ) => {
if ( key === 'xlink:href' )
child.setAttributeNS( NS_XLINK, key, value );
From cb320386ef43e846e86d5d952d8133bad4cccd92 Mon Sep 17 00:00:00 2001
From: Mitchell Austin
Date: Mon, 11 Sep 2023 09:11:41 -0700
Subject: [PATCH 5/6] Revise unit test approach per feedback
---
.../src/test/__snapshots__/to-dom.js.snap | 31 +++++++++++++
packages/rich-text/src/test/helpers/index.js | 46 +++++++++++++++++++
packages/rich-text/src/test/to-dom.js | 38 ++++-----------
3 files changed, 86 insertions(+), 29 deletions(-)
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 5e2fbcac00de6..ada62d3628beb 100644
--- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap
+++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap
@@ -150,6 +150,21 @@ exports[`recordToDom should filter format boundary attributes 1`] = `
+
+
+
@@ -215,6 +230,22 @@ exports[`recordToDom should handle selection before br 1`] = `
+
+
+
test
diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js
index cff9daa3e24ec..5740d72c3cc1e 100644
--- a/packages/rich-text/src/test/helpers/index.js
+++ b/packages/rich-text/src/test/helpers/index.js
@@ -11,6 +11,9 @@ const em = { type: 'em' };
const strong = { type: 'strong' };
const img = { type: 'img', attributes: { src: '' } };
const a = { type: 'a', attributes: { href: '#' } };
+const svg = { type: 'svg' };
+const path = { type: 'path', attributes: { d: 'M0,0 v2 h4' } };
+const use = { type: 'use', attributes: { 'xlink:href': '#a', href: '#a' } };
export const spec = [
{
@@ -570,6 +573,49 @@ export const spec = [
text: '\ufffc',
},
},
+ {
+ description: 'should handle SVG',
+ html: '',
+ NS_URI: 'http://www.w3.org/2000/svg',
+ selectTarget: ( body ) => body.querySelector( 'path' ),
+ createRange: ( element ) => ( {
+ startOffset: 0,
+ startContainer: element,
+ endOffset: 0,
+ endContainer: element,
+ } ),
+ startPath: [ 0, 0, 0 ],
+ endPath: [ 0, 0, 0 ],
+ record: {
+ start: 0,
+ end: 0,
+ formats: [ [ svg ] ],
+ replacements: [ path ],
+ text: '\ufffc',
+ },
+ },
+ {
+ description: 'should handle xlink',
+ html: '',
+ NS_URI: 'http://www.w3.org/1999/xlink',
+ selectTarget: ( body ) =>
+ body.querySelector( 'use' ).getAttributeNode( 'xlink:href' ),
+ createRange: ( element ) => ( {
+ startOffset: 0,
+ startContainer: element,
+ endOffset: 0,
+ endContainer: element,
+ } ),
+ startPath: [ 0, 0, 0 ],
+ endPath: [ 0, 0, 0 ],
+ record: {
+ start: 0,
+ end: 0,
+ formats: [ [ svg ] ],
+ replacements: [ use ],
+ text: '\ufffc',
+ },
+ },
];
export const specWithRegistration = [
diff --git a/packages/rich-text/src/test/to-dom.js b/packages/rich-text/src/test/to-dom.js
index 035f819b725d3..5f313cfd4001b 100644
--- a/packages/rich-text/src/test/to-dom.js
+++ b/packages/rich-text/src/test/to-dom.js
@@ -21,6 +21,15 @@ describe( 'recordToDom', () => {
expect( selection ).toEqual( { startPath, endPath } );
} );
} );
+
+ spec.filter( ( props ) => 'NS_URI' in props ).forEach(
+ ( { description, NS_URI, record, selectTarget } ) => {
+ it( `${ description } with correct namespace`, () => {
+ const { body } = toDom( { value: record } );
+ expect( selectTarget( body ).namespaceURI ).toEqual( NS_URI );
+ } );
+ }
+ );
} );
describe( 'applyValue', () => {
@@ -98,32 +107,3 @@ describe( 'applyValue', () => {
} );
} );
} );
-
-describe( 'toDom-SVG', () => {
- let body;
- beforeAll( () => {
- const svg = { type: 'svg' };
- const use = { type: 'use', attributes: { 'xlink:href': '#logo' } };
- body = toDom( {
- value: {
- start: 0,
- end: 1,
- formats: [ [ svg ] ],
- replacements: [ use ],
- text: '\ufffc',
- },
- } ).body;
- } );
-
- it( 'should create nodes with svg namespace', () => {
- const target = body.firstElementChild;
- expect( target.namespaceURI ).toEqual( 'http://www.w3.org/2000/svg' );
- } );
-
- it( 'should create attribute xlink:href with xlink namespace', () => {
- const target = body
- .querySelector( 'use' )
- .getAttributeNode( 'xlink:href' );
- expect( target.namespaceURI ).toEqual( 'http://www.w3.org/1999/xlink' );
- } );
-} );
From 3bb9d5737480cf031413ab84cb4d8f3a76445fa1 Mon Sep 17 00:00:00 2001
From: Mitchell Austin
Date: Sat, 16 Sep 2023 14:38:56 -0700
Subject: [PATCH 6/6] Support namespace in formats and not arbitrarily in rich
text.
---
.../rich-text/src/register-format-type.js | 6 +++
.../src/test/__snapshots__/to-dom.js.snap | 31 ------------
packages/rich-text/src/test/helpers/index.js | 46 ------------------
packages/rich-text/src/test/to-dom.js | 48 +++++++++++++++----
packages/rich-text/src/to-dom.js | 26 ++--------
packages/rich-text/src/to-tree.js | 1 +
6 files changed, 51 insertions(+), 107 deletions(-)
diff --git a/packages/rich-text/src/register-format-type.js b/packages/rich-text/src/register-format-type.js
index b2dd048d79e6f..11c31b4a03c9a 100644
--- a/packages/rich-text/src/register-format-type.js
+++ b/packages/rich-text/src/register-format-type.js
@@ -13,6 +13,7 @@ import { store as richTextStore } from './store';
* unique across all registered formats.
* @property {string} tagName The HTML tag this format will wrap the
* selection with.
+ * @property {string} [namespace] The namespace of the `tagName`.
* @property {boolean} interactive Whether format makes content interactive or not.
* @property {string | null} [className] A class to match the format.
* @property {string} title Name of the format.
@@ -60,6 +61,11 @@ export function registerFormatType( name, settings ) {
return;
}
+ if ( 'namespace' in settings && typeof settings.namespace !== 'string' ) {
+ window.console.error( 'Format namespaces must be a string.' );
+ return;
+ }
+
if (
( typeof settings.className !== 'string' ||
settings.className === '' ) &&
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 ada62d3628beb..5e2fbcac00de6 100644
--- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap
+++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap
@@ -150,21 +150,6 @@ exports[`recordToDom should filter format boundary attributes 1`] = `
-
-
-
@@ -230,22 +215,6 @@ exports[`recordToDom should handle selection before br 1`] = `
-
-
-
test
diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js
index 5740d72c3cc1e..cff9daa3e24ec 100644
--- a/packages/rich-text/src/test/helpers/index.js
+++ b/packages/rich-text/src/test/helpers/index.js
@@ -11,9 +11,6 @@ const em = { type: 'em' };
const strong = { type: 'strong' };
const img = { type: 'img', attributes: { src: '' } };
const a = { type: 'a', attributes: { href: '#' } };
-const svg = { type: 'svg' };
-const path = { type: 'path', attributes: { d: 'M0,0 v2 h4' } };
-const use = { type: 'use', attributes: { 'xlink:href': '#a', href: '#a' } };
export const spec = [
{
@@ -573,49 +570,6 @@ export const spec = [
text: '\ufffc',
},
},
- {
- description: 'should handle SVG',
- html: '',
- NS_URI: 'http://www.w3.org/2000/svg',
- selectTarget: ( body ) => body.querySelector( 'path' ),
- createRange: ( element ) => ( {
- startOffset: 0,
- startContainer: element,
- endOffset: 0,
- endContainer: element,
- } ),
- startPath: [ 0, 0, 0 ],
- endPath: [ 0, 0, 0 ],
- record: {
- start: 0,
- end: 0,
- formats: [ [ svg ] ],
- replacements: [ path ],
- text: '\ufffc',
- },
- },
- {
- description: 'should handle xlink',
- html: '',
- NS_URI: 'http://www.w3.org/1999/xlink',
- selectTarget: ( body ) =>
- body.querySelector( 'use' ).getAttributeNode( 'xlink:href' ),
- createRange: ( element ) => ( {
- startOffset: 0,
- startContainer: element,
- endOffset: 0,
- endContainer: element,
- } ),
- startPath: [ 0, 0, 0 ],
- endPath: [ 0, 0, 0 ],
- record: {
- start: 0,
- end: 0,
- formats: [ [ svg ] ],
- replacements: [ use ],
- text: '\ufffc',
- },
- },
];
export const specWithRegistration = [
diff --git a/packages/rich-text/src/test/to-dom.js b/packages/rich-text/src/test/to-dom.js
index 5f313cfd4001b..30c487aade16c 100644
--- a/packages/rich-text/src/test/to-dom.js
+++ b/packages/rich-text/src/test/to-dom.js
@@ -4,6 +4,9 @@
import { toDom, applyValue } from '../to-dom';
import { createElement } from '../create-element';
import { spec } from './helpers';
+import { OBJECT_REPLACEMENT_CHARACTER } from '../special-characters';
+import { registerFormatType } from '../register-format-type';
+import { unregisterFormatType } from '../unregister-format-type';
describe( 'recordToDom', () => {
beforeAll( () => {
@@ -22,14 +25,43 @@ describe( 'recordToDom', () => {
} );
} );
- spec.filter( ( props ) => 'NS_URI' in props ).forEach(
- ( { description, NS_URI, record, selectTarget } ) => {
- it( `${ description } with correct namespace`, () => {
- const { body } = toDom( { value: record } );
- expect( selectTarget( body ).namespaceURI ).toEqual( NS_URI );
- } );
- }
- );
+ it( 'should use the namespace specfied by the format', () => {
+ const formatName = 'my-plugin/nom';
+ const namespace = 'http://www.w3.org/1998/Math/MathML';
+
+ registerFormatType( formatName, {
+ namespace,
+ title: 'Math',
+ tagName: 'math',
+ className: 'nom-math',
+ contentEditable: false,
+ edit() {},
+ } );
+
+ const { body } = toDom( {
+ value: {
+ formats: [ , ],
+ replacements: [
+ {
+ type: 'my-plugin/nom',
+ tagName: 'math',
+ attributes: {},
+ unregisteredAttributes: {},
+ innerHTML: '0',
+ },
+ ],
+ text: OBJECT_REPLACEMENT_CHARACTER,
+ },
+ } );
+
+ unregisterFormatType( formatName );
+
+ const subject = body.firstElementChild;
+ expect( subject.outerHTML ).toBe(
+ ``
+ );
+ expect( subject.namespaceURI ).toBe( namespace );
+ } );
} );
describe( 'applyValue', () => {
diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js
index 7d99d79265fe6..2b8edcb485f65 100644
--- a/packages/rich-text/src/to-dom.js
+++ b/packages/rich-text/src/to-dom.js
@@ -8,9 +8,6 @@ import { isRangeEqual } from './is-range-equal';
/** @typedef {import('./types').RichTextValue} RichTextValue */
-const NS_SVG = 'http://www.w3.org/2000/svg';
-const NS_XLINK = 'http://www.w3.org/1999/xlink';
-
/**
* Creates a path as an array of indices from the given root node to the given
* node.
@@ -71,26 +68,11 @@ function append( element, child ) {
const { type, attributes } = child;
if ( type ) {
- let addAttribute;
- const isSVGTag = type === 'svg';
- const isSVGContext = element.namespaceURI === NS_SVG;
- if ( isSVGTag || isSVGContext ) {
- child = element.ownerDocument.createElementNS( NS_SVG, type );
- // Disables editing at top-level SVG elements.
- if ( isSVGTag && ! isSVGContext ) {
- child.setAttribute( 'contentEditable', false );
- }
- addAttribute = ( key, value ) => {
- if ( key === 'xlink:href' )
- child.setAttributeNS( NS_XLINK, key, value );
- else child.setAttribute( key, value );
- };
- } else {
- child = element.ownerDocument.createElement( type );
- addAttribute = child.setAttribute.bind( child );
- }
+ const namespaceURI = child.namespace || element.namespaceURI;
+ child = element.ownerDocument.createElementNS( namespaceURI, type );
+
for ( const key in attributes ) {
- addAttribute( key, attributes[ key ] );
+ child.setAttribute( key, attributes[ key ] );
}
}
diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js
index c380570db561d..0f2abef757a46 100644
--- a/packages/rich-text/src/to-tree.js
+++ b/packages/rich-text/src/to-tree.js
@@ -107,6 +107,7 @@ function fromFormat( {
type: tagName || formatType.tagName,
object: formatType.object,
attributes: restoreOnAttributes( elementAttributes, isEditableTree ),
+ namespace: formatType.namespace,
};
}