Skip to content

Commit

Permalink
Block API: Support nesting (InnerBlocks) in unknown block types (Word…
Browse files Browse the repository at this point in the history
…Press#14443)

* Block API: Support nesting in unknown block types

When the block parser finds a block of unknown type, it wraps it in a
special block (per `getUnregisteredTypeHandlerName`) — by default, this
will be a block of type `core/missing`. This new outer block can then
offer the user the possibility to extract the wrapped content.

This commit updates the parsing logic so that the fallback block is fed
the entire tree of content of the unknown block — if it has child blocks
— and not just its own surface-level content.

For instance, when parsing the following unknown block:

```html
<!-- wp:my/unknown -->
  <p>Begin block that contains a list.</p>
  <!-- wp:list -->
  <ul><li>1</li><li>2</li></ul>
  <!-- /wp:list -->
  <p>End a block that contains a list.</p>
<!-- /wp:my/unknown -->
```

Before, the rescued `originalContent` would have been:

```html
<!-- wp:my/unknown -->
  <p>Begin block that contains a list.</p>
  <p>End block that contains a list.</p>
<!-- /wp:my/unknown -->
```

Now, it would be the entire markup for `my/unknown`.

* Add group block in the test template to make sure it works recursively

Props Tug
  • Loading branch information
mcsf authored Jul 9, 2019
1 parent 403fa51 commit a7cc76c
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 7 deletions.
75 changes: 68 additions & 7 deletions packages/blocks/src/api/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ export function createBlockWithFallback( blockNode ) {
innerBlocks = [],
innerHTML,
} = blockNode;
const { innerContent } = blockNode;
const freeformContentFallbackBlock = getFreeformContentHandlerName();
const unregisteredFallbackBlock = getUnregisteredTypeHandlerName() || freeformContentFallbackBlock;

Expand Down Expand Up @@ -416,17 +417,39 @@ export function createBlockWithFallback( blockNode ) {
let blockType = getBlockType( name );

if ( ! blockType ) {
// Preserve undelimited content for use by the unregistered type handler.
const originalUndelimitedContent = innerHTML;
// Since the constituents of the block node are extracted at the start
// of the present function, construct a new object rather than reuse
// `blockNode`.
const reconstitutedBlockNode = {
attrs: attributes,
blockName: originalName,
innerBlocks,
innerContent,
};

// Preserve undelimited content for use by the unregistered type
// handler. A block node's `innerHTML` isn't enough, as that field only
// carries the block's own HTML and not its nested blocks'.
const originalUndelimitedContent = serializeBlockNode(
reconstitutedBlockNode,
{ isCommentDelimited: false }
);

// Preserve full block content for use by the unregistered type
// handler, block boundaries included.
const originalContent = serializeBlockNode(
reconstitutedBlockNode,
{ isCommentDelimited: true }
);

// If detected as a block which is not registered, preserve comment
// delimiters in content of unregistered type handler.
if ( name ) {
innerHTML = getCommentDelimitedContent( name, attributes, innerHTML );
innerHTML = originalContent;
}

name = unregisteredFallbackBlock;
attributes = { originalName, originalUndelimitedContent };
attributes = { originalName, originalContent, originalUndelimitedContent };
blockType = getBlockType( name );
}

Expand Down Expand Up @@ -457,15 +480,53 @@ export function createBlockWithFallback( blockNode ) {
block.isValid = isValidBlockContent( blockType, block.attributes, innerHTML );
}

// Preserve original content for future use in case the block is parsed as
// invalid, or future serialization attempt results in an error.
block.originalContent = innerHTML;
// Preserve original content for future use in case the block is parsed
// as invalid, or future serialization attempt results in an error.
block.originalContent = block.originalContent || innerHTML;

block = getMigratedBlock( block, attributes );

return block;
}

/**
* Serializes a block node into the native HTML-comment-powered block format.
* CAVEAT: This function is intended for reserializing blocks as parsed by
* valid parsers and skips any validation steps. This is NOT a generic
* serialization function for in-memory blocks. For most purposes, see the
* following functions available in the `@wordpress/blocks` package:
*
* @see serializeBlock
* @see serialize
*
* For more on the format of block nodes as returned by valid parsers:
*
* @see `@wordpress/block-serialization-default-parser` package
* @see `@wordpress/block-serialization-spec-parser` package
*
* @param {Object} blockNode A block node as returned by a valid parser.
* @param {?Object} options Serialization options.
* @param {?boolean} options.isCommentDelimited Whether to output HTML comments around blocks.
*
* @return {string} An HTML string representing a block.
*/
export function serializeBlockNode( blockNode, options = {} ) {
const { isCommentDelimited = true } = options;
const { blockName, attrs = {}, innerBlocks = [], innerContent = [] } = blockNode;

let childIndex = 0;
const content = innerContent.map( ( item ) =>
// `null` denotes a nested block, otherwise we have an HTML fragment
item !== null ?
item :
serializeBlockNode( innerBlocks[ childIndex++ ], options )
).join( '\n' ).replace( /\n+/g, '\n' ).trim();

return isCommentDelimited ?
getCommentDelimitedContent( blockName, attrs, content ) :
content;
}

/**
* Creates a parse implementation for the post content which returns a list of blocks.
*
Expand Down
106 changes: 106 additions & 0 deletions packages/blocks/src/api/test/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
isOfTypes,
isValidByType,
isValidByEnum,
serializeBlockNode,
} from '../parser';
import {
registerBlockType,
Expand Down Expand Up @@ -757,6 +758,111 @@ describe( 'block parser', () => {
} );
} );

describe( 'serializeBlockNode', () => {
it( 'reserializes block nodes', () => {
const expected = `<!-- wp:columns -->
<div class="wp-block-columns has-2-columns">
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:paragraph -->
<p>A</p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:group -->
<div class="wp-block-group"><div class="wp-block-group__inner-container">
<!-- wp:list -->
<ul><li>B</li><li>C</li></ul>
<!-- /wp:list -->
<!-- wp:paragraph -->
<p>D</p>
<!-- /wp:paragraph -->
</div></div>
<!-- /wp:group -->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->`.replace( /\t/g, '' );
const input = {
blockName: 'core/columns',
attrs: {},
innerBlocks: [
{
blockName: 'core/column',
attrs: {},
innerBlocks: [
{
blockName: 'core/paragraph',
attrs: {},
innerBlocks: [],
innerHTML: '<p>A</p>',
innerContent: [ '<p>A</p>' ],
},
],
innerHTML: '<div class="wp-block-column"></div>',
innerContent: [
'<div class="wp-block-column">',
null,
'</div>',
],
},
{
blockName: 'core/column',
attrs: {},
innerBlocks: [
{
blockName: 'core/group',
attrs: {},
innerBlocks: [
{
blockName: 'core/list',
attrs: {},
innerBlocks: [],
innerHTML: '<ul><li>B</li><li>C</li></ul>',
innerContent: [ '<ul><li>B</li><li>C</li></ul>' ],
},
{
blockName: 'core/paragraph',
attrs: {},
innerBlocks: [],
innerHTML: '<p>D</p>',
innerContent: [ '<p>D</p>' ],
},
],
innerHTML: '<div class="wp-block-group"><div class="wp-block-group__inner-container"></div></div>',
innerContent: [
'<div class="wp-block-group"><div class="wp-block-group__inner-container">',
null,
'',
null,
'</div></div>' ],
},
],
innerHTML: '<div class="wp-block-column"></div>',
innerContent: [
'<div class="wp-block-column">',
null,
'</div>',
],
},
],
innerHTML: '<div class="wp-block-columns has-2-columns"></div>',
innerContent: [
'<div class="wp-block-columns has-2-columns">',
null,
'',
null,
'</div>',
],
};
const actual = serializeBlockNode( input );

expect( actual ).toEqual( expected );
} );
} );

describe( 'parse() of @wordpress/block-serialization-spec-parser', () => {
// run the test cases using the PegJS defined parser
testCases( parsePegjs );
Expand Down

0 comments on commit a7cc76c

Please sign in to comment.