Skip to content

Commit

Permalink
Element / Block API: Add RawHTML component, drop support for HTML str…
Browse files Browse the repository at this point in the history
…ing block save (#4786)

* Element: Add support for RawHTML component

Explicit support by element static string renderer.

* Element: Render dangerous wrapper if props passed

* Blocks: Update blocks to return RawHTML

* Block API: Drop support for raw HTML save return

* Block API: Document WPBlockType type definition

* Block API: Add hook for deprecated raw HTML support

* Serializer: Remove save return from more block test

Unused anyways, misleading to imply it is.

* Serializer: Simplify serialize support to function
  • Loading branch information
aduth authored Feb 6, 2018
1 parent 0b36845 commit 69830a1
Show file tree
Hide file tree
Showing 17 changed files with 335 additions and 72 deletions.
8 changes: 4 additions & 4 deletions blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ editor interface where blocks are implemented.

- `title: string` - A human-readable
[localized](https://codex.wordpress.org/I18n_for_WordPress_Developers#Handling_JavaScript_files)
label for the block. Shown in the block picker.
label for the block. Shown in the block inserter.
- `icon: string | WPElement | Function` - Slug of the
[Dashicon](https://developer.wordpress.org/resource/dashicons/#awards)
to be shown in the control's button, or an element (or function returning an
Expand All @@ -250,9 +250,9 @@ editor interface where blocks are implemented.
editor. A block can update its own state in response to events using the
`setAttributes` function, passing an object of properties to be applied as a
partial update.
- `save( { attributes: Object } ): WPElement | String` - Returns an element
describing the markup of a block to be saved in the published content. This
function is called before save and when switching to an editor's HTML view.
- `save( { attributes: Object } ): WPElement` - Returns an element describing
the markup of a block to be saved in the published content. This function is
called before save and when switching to an editor's HTML view.
- `keywords` - An optional array of keywords used to filter the block list.

### `wp.blocks.getBlockType( name: string )`
Expand Down
29 changes: 27 additions & 2 deletions blocks/api/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,34 @@ import { applyFilters } from '@wordpress/hooks';
import { getCategories } from './categories';

/**
* Block settings keyed by block name.
* Defined behavior of a block type.
*
* @type {Object}
* @typedef {WPBlockType}
*
* @property {string} name Block's namespaced name.
* @property {string} title Human-readable label for a block.
* Shown in the block inserter.
* @property {string} category Category classification of block,
* impacting where block is shown in
* inserter results.
* @property {(string|WPElement)} icon Slug of the Dashicon to be shown
* as the icon for the block in the
* inserter, or element.
* @property {?string[]} keywords Additional keywords to produce
* block as inserter search result.
* @property {?Object} attributes Block attributes.
* @property {Function} save Serialize behavior of a block,
* returning an element describing
* structure of the block's post
* content markup.
* @property {WPComponent} edit Component rendering element to be
* interacted with in an editor.
*/

/**
* Block type definitions keyed by block name.
*
* @type {Object.<string,WPBlockType>}
*/
const blocks = {};

Expand Down
70 changes: 38 additions & 32 deletions blocks/api/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
*/
import { isEmpty, reduce, isObject, castArray, compact, startsWith } from 'lodash';
import { html as beautifyHtml } from 'js-beautify';
import isEqualShallow from 'is-equal-shallow';

/**
* WordPress dependencies
*/
import { Component, createElement, renderToString, cloneElement, Children } from '@wordpress/element';
import { applyFilters } from '@wordpress/hooks';
import { Component, cloneElement, renderToString } from '@wordpress/element';
import { hasFilter, applyFilters } from '@wordpress/hooks';

/**
* Internal dependencies
Expand Down Expand Up @@ -37,33 +38,46 @@ export function getBlockDefaultClassname( blockName ) {
* @return {Object|string} Save content.
*/
export function getSaveElement( blockType, attributes ) {
const { save } = blockType;

let saveElement;
let { save } = blockType;

// Component classes are unsupported for save since serialization must
// occur synchronously. For improved interoperability with higher-order
// components which often return component class, emulate basic support.
if ( save.prototype instanceof Component ) {
saveElement = createElement( save, { attributes } );
} else {
saveElement = save( { attributes } );

// Special-case function render implementation to allow raw HTML return
if ( 'string' === typeof saveElement ) {
return saveElement;
}
const instance = new save( { attributes } );
save = instance.render.bind( instance );
}

const addExtraContainerProps = ( element ) => {
if ( ! element || ! isObject( element ) ) {
return element;
let element = save( { attributes } );

if ( isObject( element ) && hasFilter( 'blocks.getSaveContent.extraProps' ) ) {
/**
* Filters the props applied to the block save result element.
*
* @param {Object} props Props applied to save element.
* @param {WPBlockType} blockType Block type definition.
* @param {Object} attributes Block attributes.
*/
const props = applyFilters(
'blocks.getSaveContent.extraProps',
{ ...element.props },
blockType,
attributes
);

if ( ! isEqualShallow( props, element.props ) ) {
element = cloneElement( element, props );
}
}

// Applying the filters adding extra props
const props = applyFilters( 'blocks.getSaveContent.extraProps', { ...element.props }, blockType, attributes );

return cloneElement( element, props );
};

return Children.map( saveElement, addExtraContainerProps );
/**
* Filters the save result of a block during serialization.
*
* @param {WPElement} element Block save result.
* @param {WPBlockType} blockType Block type definition.
* @param {Object} attributes Block attributes.
*/
return applyFilters( 'blocks.getSaveElement', element, blockType, attributes );
}

/**
Expand All @@ -76,15 +90,7 @@ export function getSaveElement( blockType, attributes ) {
* @return {string} Save content.
*/
export function getSaveContent( blockType, attributes ) {
const saveElement = getSaveElement( blockType, attributes );

// Special-case function render implementation to allow raw HTML return
if ( 'string' === typeof saveElement ) {
return saveElement;
}

// Otherwise, infer as element
return renderToString( saveElement );
return renderToString( getSaveElement( blockType, attributes ) );
}

/**
Expand Down
20 changes: 4 additions & 16 deletions blocks/api/test/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,6 @@ describe( 'block serializer', () => {

describe( 'getSaveContent()', () => {
describe( 'function save', () => {
it( 'should return string verbatim', () => {
const saved = getSaveContent(
{
save: ( { attributes } ) => attributes.fruit,
name: 'core/fruit',
},
{ fruit: 'Bananas' }
);

expect( saved ).toBe( 'Bananas' );
} );

it( 'should return element as string if save returns element', () => {
const saved = getSaveContent(
{
Expand Down Expand Up @@ -129,7 +117,7 @@ describe( 'block serializer', () => {
{ fruit: 'Bananas' }
);

expect( saved ).toBe( '<div>Bananas</div>' );
expect( saved ).toBe( '<div class="wp-block-fruit">Bananas</div>' );
} );
} );
} );
Expand Down Expand Up @@ -269,7 +257,7 @@ describe( 'block serializer', () => {
},
},

save: ( { attributes } ) => attributes.customText,
save: () => null,
} );
} );

Expand Down Expand Up @@ -411,11 +399,11 @@ describe( 'block serializer', () => {
const block = {
name: 'core/chicken',
attributes: {
content: '<p>chicken </p>',
content: 'chicken',
},
isValid: true,
};
expect( getBlockContent( block ) ).toBe( '<p>chicken </p>' );
expect( getBlockContent( block ) ).toBe( 'chicken' );
} );
} );
} );
62 changes: 62 additions & 0 deletions blocks/hooks/deprecated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* External dependencies
*/
import { includes } from 'lodash';

/**
* WordPress dependencies
*/
import { Component, RawHTML } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';

/**
* Wrapper component for RawHTML, logging a warning about unsupported raw
* markup return values from a block's `save` implementation.
*/
export class RawHTMLWithWarning extends Component {
constructor() {
super( ...arguments );

// Disable reason: We're intentionally logging a console warning
// advising the developer to upgrade usage.

// eslint-disable-next-line no-console
console.warn(
'Deprecated: Returning raw HTML from block `save` is not supported. ' +
'Use `wp.element.RawHTML` component instead.\n\n' +
'See: https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/#save'
);
}

render() {
const { children } = this.props;

return <RawHTML>{ children }</RawHTML>;
}
}

/**
* Override save element for a block, providing support for deprecated HTML
* return value, logging a warning advising the developer to use the preferred
* RawHTML component instead.
*
* @param {WPElement} element Original block save return.
*
* @return {WPElement} Dangerously shimmed block save.
*/
export function shimRawHTML( element ) {
// Still support string return from save, but in the same way any component
// could render a string, it should be escaped. Therefore, only shim usage
// which had included some HTML expected to be unescaped.
if ( typeof element === 'string' && ( includes( element, '<' ) || /^\[.+\]$/.test( element ) ) ) {
element = <RawHTMLWithWarning children={ element } />;
}

return element;
}

addFilter(
'blocks.getSaveElement',
'core/deprecated/shim-dangerous-html',
shimRawHTML
);
1 change: 1 addition & 0 deletions blocks/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
*/
import './anchor';
import './custom-class-name';
import './deprecated';
import './generated-class-name';
import './matchers';
71 changes: 71 additions & 0 deletions blocks/hooks/test/deprecated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* External dependencies
*/
import { shallow } from 'enzyme';

/**
* WordPress dependencies
*/
import { createElement } from '@wordpress/element';

/**
* Internal dependencies
*/
import {
RawHTMLWithWarning,
shimRawHTML,
} from '../deprecated';

describe( 'deprecated', () => {
describe( 'RawHTMLWithWarning', () => {
it( 'warns on mount', () => {
shallow( <RawHTMLWithWarning /> );

expect( console ).toHaveWarned();
} );

it( 'renders RawHTML', () => {
const wrapper = shallow(
<RawHTMLWithWarning>
Scary!
</RawHTMLWithWarning>
);

expect( console ).toHaveWarned();
expect( wrapper.name() ).toBe( 'RawHTML' );
expect( wrapper.find( 'RawHTML' ).prop( 'children' ) ).toBe( 'Scary!' );
} );
} );

describe( 'shimRawHTML()', () => {
it( 'should do nothing to elements', () => {
const original = createElement( 'div' );
const result = shimRawHTML( original );

expect( result ).toBe( original );
} );

it( 'should do nothing to non-HTML strings', () => {
const original = 'Not so scary';
const result = shimRawHTML( original );

expect( result ).toBe( original );
} );

it( 'replace HTML strings with RawHTMLWithWarning', () => {
const original = '<p>So scary!</p>';
const result = shimRawHTML( original );

expect( result.type ).toBe( RawHTMLWithWarning );
expect( result.props.children ).toBe( original );
} );

it( 'replace shortcode strings with RawHTMLWithWarning', () => {
const original = '[myshortcode]Hello[/myshortcode]';
const result = shimRawHTML( original );

expect( result.type ).toBe( RawHTMLWithWarning );
expect( result.props.children ).toBe( original );
} );
} );
} );
8 changes: 7 additions & 1 deletion blocks/library/freeform/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* WordPress dependencies
*/
import { RawHTML } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
Expand All @@ -27,10 +28,15 @@ export const settings = {
},
},

supports: {
className: false,
},

edit: OldEditor,

save( { attributes } ) {
const { content } = attributes;
return content;

return <RawHTML>{ content }</RawHTML>;
},
};
2 changes: 1 addition & 1 deletion blocks/library/freeform/test/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Array [
style="display:none"
/>,
<div
class="wp-block-freeform blocks-rich-text__tinymce"
class="blocks-rich-text__tinymce"
/>,
]
`;
Loading

0 comments on commit 69830a1

Please sign in to comment.