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`] = ` `; +exports[`recordToDom should handle SVG 1`] = ` + + + + + + + + +`; + exports[`recordToDom should handle br 1`] = ` @@ -215,6 +230,22 @@ exports[`recordToDom should handle selection before br 1`] = ` `; +exports[`recordToDom should handle xlink 1`] = ` + + + + + + + + +`; + exports[`recordToDom should ignore manually added object replacement character 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`] = ` `; -exports[`recordToDom should handle SVG 1`] = ` - - - - - - - - -`; - exports[`recordToDom should handle br 1`] = ` @@ -230,22 +215,6 @@ exports[`recordToDom should handle selection before br 1`] = ` `; -exports[`recordToDom should handle xlink 1`] = ` - - - - - - - - -`; - exports[`recordToDom should ignore manually added object replacement character 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( + `0` + ); + 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, }; }