diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index b40532957c5f0..cb3a35d5fffe5 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -383,6 +383,7 @@ export function createBlockWithFallback( blockNode ) { innerBlocks = [], innerHTML, } = blockNode; + const { innerContent } = blockNode; const freeformContentFallbackBlock = getFreeformContentHandlerName(); const unregisteredFallbackBlock = getUnregisteredTypeHandlerName() || freeformContentFallbackBlock; @@ -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 ); } @@ -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. * diff --git a/packages/blocks/src/api/test/parser.js b/packages/blocks/src/api/test/parser.js index 8c0e50feee3ee..f10001cb0c013 100644 --- a/packages/blocks/src/api/test/parser.js +++ b/packages/blocks/src/api/test/parser.js @@ -19,6 +19,7 @@ import { isOfTypes, isValidByType, isValidByEnum, + serializeBlockNode, } from '../parser'; import { registerBlockType, @@ -757,6 +758,111 @@ describe( 'block parser', () => { } ); } ); + describe( 'serializeBlockNode', () => { + it( 'reserializes block nodes', () => { + const expected = ` +
+ +
+ +

A

+ +
+ + +
+ +
+ +
  • B
  • C
+ + +

D

+ +
+ +
+ +
+ `.replace( /\t/g, '' ); + const input = { + blockName: 'core/columns', + attrs: {}, + innerBlocks: [ + { + blockName: 'core/column', + attrs: {}, + innerBlocks: [ + { + blockName: 'core/paragraph', + attrs: {}, + innerBlocks: [], + innerHTML: '

A

', + innerContent: [ '

A

' ], + }, + ], + innerHTML: '
', + innerContent: [ + '
', + null, + '
', + ], + }, + { + blockName: 'core/column', + attrs: {}, + innerBlocks: [ + { + blockName: 'core/group', + attrs: {}, + innerBlocks: [ + { + blockName: 'core/list', + attrs: {}, + innerBlocks: [], + innerHTML: '', + innerContent: [ '' ], + }, + { + blockName: 'core/paragraph', + attrs: {}, + innerBlocks: [], + innerHTML: '

D

', + innerContent: [ '

D

' ], + }, + ], + innerHTML: '
', + innerContent: [ + '
', + null, + '', + null, + '
' ], + }, + ], + innerHTML: '
', + innerContent: [ + '
', + null, + '
', + ], + }, + ], + innerHTML: '
', + innerContent: [ + '
', + null, + '', + null, + '
', + ], + }; + 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 );