Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code Block should not render embeds (or shortcodes) #13996

Merged
merged 12 commits into from May 2, 2019
5 changes: 3 additions & 2 deletions packages/block-library/src/code/edit.js
Expand Up @@ -7,13 +7,14 @@ 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 (
<div className={ className }>
<PlainText
value={ attributes.content }
onChange={ ( content ) => setAttributes( { content } ) }
value={ unescape( attributes.content ) }
onChange={ ( content ) => setAttributes( { content: escape( content ) } ) }
placeholder={ __( 'Write code…' ) }
aria-label={ __( 'Code' ) }
/>
Expand Down
5 changes: 3 additions & 2 deletions packages/block-library/src/code/edit.native.js
Expand Up @@ -12,6 +12,7 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import { PlainText } from '@wordpress/block-editor';
import { escape, unescape } from './utils';

/**
* Block code style
Expand All @@ -26,11 +27,11 @@ export default function CodeEdit( props ) {
return (
<View>
<PlainText
value={ attributes.content }
value={ unescape( attributes.content ) }
style={ [ style, styles.blockCode ] }
multiline={ true }
underlineColorAndroid="transparent"
onChange={ ( content ) => setAttributes( { content } ) }
onChange={ ( content ) => setAttributes( { content: escape( content ) } ) }
placeholder={ __( 'Write code…' ) }
aria-label={ __( 'Code' ) }
isSelected={ props.isSelected }
Expand Down
62 changes: 62 additions & 0 deletions packages/block-library/src/code/test/utils.js
@@ -0,0 +1,62 @@
/**
* Internal dependencies
*/
import { escape, unescape } 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]' );
} );

it( 'should escape the protocol of an isolated url', () => {
const text = escape( 'https://example.com/test/' );
expect( text ).toBe( 'https:&#47;&#47;example.com/test/' );
} );

it( 'should not escape the protocol of a non isolated url', () => {
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/' );
} );
} );
} );
117 changes: 117 additions & 0 deletions packages/block-library/src/code/utils.js
@@ -0,0 +1,117 @@
/**
* External dependencies
*/
import { flow } from 'lodash';

/**
* Escapes ampersands, shortcodes, and links.
*
* @param {string} content The content of a code block.
* @return {string} The given content with some characters escaped.
*/
export function escape( content ) {
return flow(
escapeAmpersands,
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
* shortcode like [embed] becomes &#91;embed]
*
* This function replicates the escaping of HTML tags, where a tag like
* <strong> becomes &lt;strong>.
*
* @param {string} content The content of a code block.
* @return {string} The given content with its opening shortcode characters
* converted into their HTML entity counterpart
* (i.e. [ => &#91;)
*/
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
* becomes https:&#47;&#47;youtube.com/watch?x.
*
* 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 its ampersands converted into
* their HTML entity counterpart (i.e. & => &amp;)
*/
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' );
}