Skip to content

Commit

Permalink
Separate Paste Handler (#11539)
Browse files Browse the repository at this point in the history
* Separate paste handler

* Add test for WP raw HTML conversion

* Add docs
  • Loading branch information
ellatrix committed Nov 7, 2018
1 parent a61278d commit 489eb79
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 74 deletions.
2 changes: 1 addition & 1 deletion packages/blocks/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export {
getBlockAttributes,
parseWithAttributeSchema,
} from './parser';
export { default as rawHandler, getPhrasingContentSchema } from './raw-handling';
export { pasteHandler, rawHandler, getPhrasingContentSchema } from './raw-handling';
export {
default as serialize,
getBlockContent,
Expand Down
128 changes: 94 additions & 34 deletions packages/blocks/src/api/raw-handling/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,51 @@ function getRawTransformations() {
} );
}

/**
* Converts HTML directly to blocks. Looks for a matching transform for each
* top-level tag. The HTML should be filtered to not have any text between
* top-level tags and formatted in a way that blocks can handle the HTML.
*
* @param {Object} $1 Named parameters.
* @param {string} $1.html HTML to convert.
* @param {Array} $1.rawTransforms Transforms that can be used.
*
* @return {Array} An array of blocks.
*/
function htmlToBlocks( { html, rawTransforms } ) {
const doc = document.implementation.createHTMLDocument( '' );

doc.body.innerHTML = html;

return Array.from( doc.body.children ).map( ( node ) => {
const rawTransform = findTransform( rawTransforms, ( { isMatch } ) => isMatch( node ) );

if ( ! rawTransform ) {
console.warn(
'A block registered a raw transformation schema for `' + node.nodeName + '` but did not match it. ' +
'Make sure there is a `selector` or `isMatch` property that can match the schema.\n' +
'Sanitized HTML: `' + node.outerHTML + '`'
);

return;
}

const { transform, blockName } = rawTransform;

if ( transform ) {
return transform( node );
}

return createBlock(
blockName,
getBlockAttributes(
getBlockType( blockName ),
node.outerHTML
)
);
} );
}

/**
* Converts an HTML string to known blocks. Strips everything else.
*
Expand All @@ -79,7 +124,7 @@ function getRawTransformations() {
*
* @return {Array|string} A list of blocks or a string, depending on `handlerMode`.
*/
export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO', tagName, canUserUseUnfilteredHTML = false } ) {
export function pasteHandler( { HTML = '', plainText = '', mode = 'AUTO', tagName, canUserUseUnfilteredHTML = false } ) {
// First of all, strip any meta tags.
HTML = HTML.replace( /<meta[^>]+>/, '' );

Expand Down Expand Up @@ -137,9 +182,9 @@ export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO',
return filterInlineHTML( HTML );
}

const rawTransformations = getRawTransformations();
const rawTransforms = getRawTransformations();
const phrasingContentSchema = getPhrasingContentSchema();
const blockContentSchema = getBlockContentSchema( rawTransformations );
const blockContentSchema = getBlockContentSchema( rawTransforms );

const blocks = compact( flatMap( pieces, ( piece ) => {
// Already a block from shortcode.
Expand Down Expand Up @@ -176,37 +221,7 @@ export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO',
// Allows us to ask for this information when we get a report.
console.log( 'Processed HTML piece:\n\n', piece );

const doc = document.implementation.createHTMLDocument( '' );

doc.body.innerHTML = piece;

return Array.from( doc.body.children ).map( ( node ) => {
const rawTransformation = findTransform( rawTransformations, ( { isMatch } ) => isMatch( node ) );

if ( ! rawTransformation ) {
console.warn(
'A block registered a raw transformation schema for `' + node.nodeName + '` but did not match it. ' +
'Make sure there is a `selector` or `isMatch` property that can match the schema.\n' +
'Sanitized HTML: `' + node.outerHTML + '`'
);

return;
}

const { transform, blockName } = rawTransformation;

if ( transform ) {
return transform( node );
}

return createBlock(
blockName,
getBlockAttributes(
getBlockType( blockName ),
node.outerHTML
)
);
} );
return htmlToBlocks( { html: piece, rawTransforms } );
} ) );

// If we're allowed to return inline content and there is only one block
Expand All @@ -225,3 +240,48 @@ export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO',

return blocks;
}

/**
* Converts an HTML string to known blocks.
*
* @param {string} $1.HTML The HTML to convert.
*
* @return {Array} A list of blocks.
*/
export function rawHandler( { HTML = '' } ) {
// If we detect block delimiters, parse entirely as blocks.
if ( HTML.indexOf( '<!-- wp:' ) !== -1 ) {
return parseWithGrammar( HTML );
}

// An array of HTML strings and block objects. The blocks replace matched
// shortcodes.
const pieces = shortcodeConverter( HTML );
const rawTransforms = getRawTransformations();
const blockContentSchema = getBlockContentSchema( rawTransforms );

return compact( flatMap( pieces, ( piece ) => {
// Already a block from shortcode.
if ( typeof piece !== 'string' ) {
return piece;
}

// These filters are essential for some blocks to be able to transform
// from raw HTML. These filters move around some content or add
// additional tags, they do not remove any content.
const filters = [
// Needed to create more and nextpage blocks.
specialCommentConverter,
// Needed to create media blocks.
figureContentReducer,
// Needed to create the quote block, which cannot handle text
// without wrapper paragraphs.
blockquoteNormaliser,
];

piece = deepFilterHTML( piece, filters, blockContentSchema );
piece = normaliseBlocks( piece );

return htmlToBlocks( { html: piece, rawTransforms } );
} ) );
}
4 changes: 2 additions & 2 deletions packages/editor/src/components/block-drop-zone/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
withFilters,
} from '@wordpress/components';
import {
rawHandler,
pasteHandler,
getBlockTransforms,
findTransform,
} from '@wordpress/blocks';
Expand Down Expand Up @@ -70,7 +70,7 @@ class BlockDropZone extends Component {
}

onHTMLDrop( HTML, position ) {
const blocks = rawHandler( { HTML, mode: 'BLOCKS' } );
const blocks = pasteHandler( { HTML, mode: 'BLOCKS' } );

if ( blocks.length ) {
this.props.insertBlocks( blocks, this.getInsertIndex( position ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ const blockToHTML = ( block ) => createBlock( 'core/html', {
} );
const blockToBlocks = ( block ) => rawHandler( {
HTML: block.originalContent,
mode: 'BLOCKS',
} );

export default withDispatch( ( dispatch, { block } ) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,17 @@ import BlockConvertButton from './block-convert-button';

export default compose(
withSelect( ( select, { clientId } ) => {
const { getBlock, canUserUseUnfilteredHTML } = select( 'core/editor' );
const block = getBlock( clientId );
const block = select( 'core/editor' ).getBlock( clientId );

return {
block,
canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(),
shouldRender: ( block && block.name === 'core/html' ),
};
} ),
withDispatch( ( dispatch, { block, canUserUseUnfilteredHTML } ) => ( {
withDispatch( ( dispatch, { block } ) => ( {
onClick: () => dispatch( 'core/editor' ).replaceBlocks(
block.clientId,
rawHandler( {
HTML: getBlockContent( block ),
mode: 'BLOCKS',
canUserUseUnfilteredHTML,
} ),
rawHandler( { HTML: getBlockContent( block ) } ),
),
} ) ),
)( BlockConvertButton );
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,17 @@ import BlockConvertButton from './block-convert-button';

export default compose(
withSelect( ( select, { clientId } ) => {
const { canUserUseUnfilteredHTML, getBlock } = select( 'core/editor' );
const block = getBlock( clientId );
const block = select( 'core/editor' ).getBlock( clientId );

return {
block,
canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(),
shouldRender: ( block && block.name === getFreeformContentHandlerName() ),
};
} ),
withDispatch( ( dispatch, { block, canUserUseUnfilteredHTML } ) => ( {
withDispatch( ( dispatch, { block } ) => ( {
onClick: () => dispatch( 'core/editor' ).replaceBlocks(
block.clientId,
rawHandler( {
HTML: serialize( block ),
mode: 'BLOCKS',
canUserUseUnfilteredHTML,
} )
rawHandler( { HTML: serialize( block ) } )
),
} ) ),
)( BlockConvertButton );
6 changes: 3 additions & 3 deletions packages/editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
import { createBlobURL } from '@wordpress/blob';
import { BACKSPACE, DELETE, ENTER, rawShortcut } from '@wordpress/keycodes';
import { withDispatch, withSelect } from '@wordpress/data';
import { rawHandler, children, getBlockTransforms, findTransform } from '@wordpress/blocks';
import { pasteHandler, children, getBlockTransforms, findTransform } from '@wordpress/blocks';
import { withInstanceId, withSafeTimeout, compose } from '@wordpress/compose';
import { isURL } from '@wordpress/url';
import {
Expand Down Expand Up @@ -306,7 +306,7 @@ export class RichText extends Component {
// Note: a pasted file may have the URL as plain text.
if ( item && ! html ) {
const file = item.getAsFile ? item.getAsFile() : item;
const content = rawHandler( {
const content = pasteHandler( {
HTML: `<img src="${ createBlobURL( file ) }">`,
mode: 'BLOCKS',
tagName: this.props.tagName,
Expand Down Expand Up @@ -358,7 +358,7 @@ export class RichText extends Component {
mode = 'AUTO';
}

const content = rawHandler( {
const content = pasteHandler( {
HTML: html,
plainText,
mode,
Expand Down
35 changes: 35 additions & 0 deletions test/integration/__snapshots__/blocks-raw-handling.spec.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Blocks raw handling rawHandler should convert HTML post to blocks with minimal content changes 1`] = `
"<!-- wp:heading -->
<h2>Howdy</h2>
<!-- /wp:heading -->
<!-- wp:image -->
<figure class=\\"wp-block-image\\"><img src=\\"https://w.org\\" alt=\\"\\"/></figure>
<!-- /wp:image -->
<!-- wp:paragraph -->
<p>This is a paragraph.</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Preserve <span style=\\"color:red\\">me</span>!</p>
<!-- /wp:paragraph -->
<!-- wp:heading {\\"level\\":3} -->
<h3>More tag</h3>
<!-- /wp:heading -->
<!-- wp:more -->
<!--more-->
<!-- /wp:more -->
<!-- wp:heading {\\"level\\":3} -->
<h3>Shortcode</h3>
<!-- /wp:heading -->
<!-- wp:gallery {\\"columns\\":3,\\"linkTo\\":\\"attachment\\"} -->
<ul class=\\"wp-block-gallery columns-3 is-cropped\\"><li class=\\"blocks-gallery-item\\"><figure><img data-id=\\"1\\" class=\\"wp-image-1\\"/></figure></li></ul>
<!-- /wp:gallery -->"
`;
Loading

0 comments on commit 489eb79

Please sign in to comment.