diff --git a/package-lock.json b/package-lock.json index e489d2de0e3937..7d9582d1c90066 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7288,6 +7288,7 @@ "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/editor": "file:packages/editor", "@wordpress/element": "file:packages/element", + "@wordpress/escape-html": "file:packages/escape-html", "@wordpress/i18n": "file:packages/i18n", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keycodes": "file:packages/keycodes", @@ -22199,7 +22200,7 @@ "dependencies": { "clone-deep": { "version": "0.2.4", - "resolved": "http://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", "dev": true, "requires": { @@ -22233,7 +22234,7 @@ "dependencies": { "kind-of": { "version": "2.0.1", - "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", "dev": true, "requires": { diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 8d4cd8ba268545..185e0396858875 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -37,6 +37,7 @@ "@wordpress/deprecated": "file:../deprecated", "@wordpress/editor": "file:../editor", "@wordpress/element": "file:../element", + "@wordpress/escape-html": "file:../escape-html", "@wordpress/i18n": "file:../i18n", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/keycodes": "file:../keycodes", diff --git a/packages/block-library/src/code/edit.js b/packages/block-library/src/code/edit.js index 2926774f46ff22..5aa4b337bd3f12 100644 --- a/packages/block-library/src/code/edit.js +++ b/packages/block-library/src/code/edit.js @@ -7,14 +7,13 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { PlainText } from '@wordpress/block-editor'; -import { escape, unescape } from './utils'; export default function CodeEdit( { attributes, setAttributes, className } ) { return (
setAttributes( { content: escape( content ) } ) } + value={ attributes.content } + onChange={ ( content ) => setAttributes( { content } ) } placeholder={ __( 'Write codeā€¦' ) } aria-label={ __( 'Code' ) } /> diff --git a/packages/block-library/src/code/save.js b/packages/block-library/src/code/save.js index 21d74669c854b9..ece9eef96dae3d 100644 --- a/packages/block-library/src/code/save.js +++ b/packages/block-library/src/code/save.js @@ -1,3 +1,8 @@ +/** + * Internal dependencies + */ +import { escape } from './utils'; + export default function save( { attributes } ) { - return <pre><code>{ attributes.content }</code></pre>; + return <pre><code>{ escape( attributes.content ) }</code></pre>; } diff --git a/packages/block-library/src/code/test/utils.js b/packages/block-library/src/code/test/utils.js index 4926eef16d5282..d3e9de573cf77f 100644 --- a/packages/block-library/src/code/test/utils.js +++ b/packages/block-library/src/code/test/utils.js @@ -1,15 +1,10 @@ /** * Internal dependencies */ -import { escape, unescape } from '../utils'; +import { escape } from '../utils'; describe( 'core/code', () => { describe( 'escape()', () => { - it( 'should escape ampersands', () => { - const text = escape( '&' ); - expect( text ).toBe( '&amp;' ); - } ); - it( 'should escape opening square brackets', () => { const text = escape( '[shortcode][/shortcode]' ); expect( text ).toBe( '&#91;shortcode]&#91;/shortcode]' ); @@ -24,39 +19,5 @@ describe( 'core/code', () => { const text = escape( 'Text https://example.com/test/' ); expect( text ).toBe( 'Text https://example.com/test/' ); } ); - - it( 'should escape ampersands last', () => { - const text = escape( '[shortcode][/shortcode]' ); - expect( text ).toBe( '&#91;shortcode]&#91;/shortcode]' ); - expect( text ).not.toBe( '&amp;#91;shortcode]&amp;#91;/shortcode]' ); - } ); - } ); - - describe( 'unescape()', () => { - it( 'should unescape escaped ampersands', () => { - const text = unescape( '&amp;' ); - expect( text ).toBe( '&' ); - } ); - - it( 'should unescape escaped opening square brackets', () => { - const text = unescape( '&#91;shortcode]&#91;/shortcode]' ); - expect( text ).toBe( '[shortcode][/shortcode]' ); - } ); - - it( 'should unescape the escaped protocol of an isolated url', () => { - const text = unescape( 'https:&#47;&#47;example.com/test/' ); - expect( text ).toBe( 'https://example.com/test/' ); - } ); - - it( 'should revert the result of escape()', () => { - const ampersand = unescape( escape( '&' ) ); - expect( ampersand ).toBe( '&' ); - - const squareBracket = unescape( escape( '[shortcode][/shortcode]' ) ); - expect( squareBracket ).toBe( '[shortcode][/shortcode]' ); - - const url = unescape( escape( 'https://example.com/test/' ) ); - expect( url ).toBe( 'https://example.com/test/' ); - } ); } ); } ); diff --git a/packages/block-library/src/code/utils.js b/packages/block-library/src/code/utils.js index fb8557a95b18ee..5178e3d0135fca 100644 --- a/packages/block-library/src/code/utils.js +++ b/packages/block-library/src/code/utils.js @@ -3,6 +3,11 @@ */ import { flow } from 'lodash'; +/** + * WordPress dependencies + */ +import { escapeEditableHTML } from '@wordpress/escape-html'; + /** * Escapes ampersands, shortcodes, and links. * @@ -11,49 +16,12 @@ import { flow } from 'lodash'; */ export function escape( content ) { return flow( - escapeAmpersands, + escapeEditableHTML, escapeOpeningSquareBrackets, escapeProtocolInIsolatedUrls )( content || '' ); } -/** - * Unescapes escaped ampersands, shortcodes, and links. - * - * @param {string} content Content with (maybe) escaped ampersands, shortcodes, and links. - * @return {string} The given content with escaped characters unescaped. - */ -export function unescape( content ) { - return flow( - unescapeProtocolInIsolatedUrls, - unescapeOpeningSquareBrackets, - unescapeAmpersands - )( content || '' ); -} - -/** - * Returns the given content with all its ampersand characters converted - * into their HTML entity counterpart (i.e. & => &amp;) - * - * @param {string} content The content of a code block. - * @return {string} The given content with its ampersands converted into - * their HTML entity counterpart (i.e. & => &amp;) - */ -function escapeAmpersands( content ) { - return content.replace( /&/g, '&amp;' ); -} - -/** - * Returns the given content with all &amp; HTML entities converted into &. - * - * @param {string} content The content of a code block. - * @return {string} The given content with all &amp; HTML entities - * converted into &. - */ -function unescapeAmpersands( content ) { - return content.replace( /&amp;/g, '&' ); -} - /** * Returns the given content with all opening shortcode characters converted * into their HTML entity counterpart (i.e. [ => &#91;). For instance, a @@ -71,16 +39,6 @@ function escapeOpeningSquareBrackets( content ) { return content.replace( /\[/g, '&#91;' ); } -/** - * Returns the given content translating all &#91; into [. - * - * @param {string} content The content of a code block. - * @return {string} The given content with all &#91; into [. - */ -function unescapeOpeningSquareBrackets( content ) { - return content.replace( /&#91;/g, '[' ); -} - /** * Converts the first two forward slashes of any isolated URL into their HTML * counterparts (i.e. // => &#47;&#47;). For instance, https://youtube.com/watch?x @@ -98,20 +56,3 @@ function unescapeOpeningSquareBrackets( content ) { function escapeProtocolInIsolatedUrls( content ) { return content.replace( /^(\s*https?:)\/\/([^\s<>"]+\s*)$/m, '$1&#47;&#47;$2' ); } - -/** - * Converts the first two forward slashes of any isolated URL from the HTML entity - * &#73; into /. - * - * An isolated URL is a URL that sits in its own line, surrounded only by spacing - * characters. - * - * See https://github.com/WordPress/wordpress-develop/blob/5.1.1/src/wp-includes/class-wp-embed.php#L403 - * - * @param {string} content The content of a code block. - * @return {string} The given content with the first two forward slashes of any - * isolated URL from the HTML entity &#73; into /. - */ -function unescapeProtocolInIsolatedUrls( content ) { - return content.replace( /^(\s*https?:)&#47;&#47;([^\s<>"]+\s*)$/m, '$1//$2' ); -} diff --git a/packages/escape-html/README.md b/packages/escape-html/README.md index 764a6052d7e79c..6f112276748131 100644 --- a/packages/escape-html/README.md +++ b/packages/escape-html/README.md @@ -64,6 +64,20 @@ _Returns_ - `string`: Escaped attribute value. +<a name="escapeEditableHTML" href="#escapeEditableHTML">#</a> **escapeEditableHTML** + +Returns an escaped Editable HTML element value. This is different from +`escapeHTML`, because for editable HTML, ALL ampersands must be escaped in +order to render the content correctly on the page. + +_Parameters_ + +- _value_ `string`: Element value. + +_Returns_ + +- `string`: Escaped HTML element value. + <a name="escapeHTML" href="#escapeHTML">#</a> **escapeHTML** Returns an escaped HTML element value. diff --git a/packages/escape-html/src/index.js b/packages/escape-html/src/index.js index d31f43f63f70bb..8b72830a74c4b9 100644 --- a/packages/escape-html/src/index.js +++ b/packages/escape-html/src/index.js @@ -96,6 +96,19 @@ export function escapeHTML( value ) { return escapeLessThan( escapeAmpersand( value ) ); } +/** + * Returns an escaped Editable HTML element value. This is different from + * `escapeHTML`, because for editable HTML, ALL ampersands must be escaped in + * order to render the content correctly on the page. + * + * @param {string} value Element value. + * + * @return {string} Escaped HTML element value. + */ +export function escapeEditableHTML( value ) { + return escapeLessThan( value.replace( /&/g, '&amp;' ) ); +} + /** * Returns true if the given attribute name is valid, or false otherwise. * diff --git a/packages/escape-html/src/test/index.js b/packages/escape-html/src/test/index.js index 9558c58c7e23ab..2c564a771f4bdc 100644 --- a/packages/escape-html/src/test/index.js +++ b/packages/escape-html/src/test/index.js @@ -8,6 +8,7 @@ import { escapeAttribute, escapeHTML, isValidAttributeName, + escapeEditableHTML, } from '../'; import __unstableEscapeGreaterThan from '../escape-greater'; @@ -94,3 +95,11 @@ describe( 'isValidAttributeName', () => { expect( result ).toBe( true ); } ); } ); + +describe( 'escapeEditableHTML', () => { + it( 'should escape < and all ampersands', () => { + const result = escapeEditableHTML( '<a href="https://w.org">WP</a> & &lt;strong&gt;' ); + + expect( result ).toBe( '&lt;a href="https://w.org">WP&lt;/a> &amp; &amp;lt;strong&amp;gt;' ); + } ); +} ); diff --git a/packages/rich-text/src/to-html-string.js b/packages/rich-text/src/to-html-string.js index bf6c60fda0442d..06aa0434992028 100644 --- a/packages/rich-text/src/to-html-string.js +++ b/packages/rich-text/src/to-html-string.js @@ -3,7 +3,7 @@ */ import { - escapeHTML, + escapeEditableHTML, escapeAttribute, isValidAttributeName, } from '@wordpress/escape-html'; @@ -106,6 +106,6 @@ function createElementHTML( { type, attributes, object, children } ) { function createChildrenHTML( children = [] ) { return children.map( ( child ) => { - return child.text === undefined ? createElementHTML( child ) : escapeHTML( child.text ); + return child.text === undefined ? createElementHTML( child ) : escapeEditableHTML( child.text ); } ).join( '' ); }