/** * WordPress dependencies */ import { renderToString } from '@wordpress/element'; /** * Internal dependencies */ import { convertLegacyBlockNameAndAttributes } from './parser/convert-legacy-block'; import { createBlock } from './factory'; import { getBlockType } from './registration'; import type { Block, BlockAttribute } from '../types'; type TemplateItem = [ string, Record< string, unknown >?, TemplateItem[]? ]; /** * Checks whether a list of blocks matches a template by comparing the block names. * * @param blocks Block list. * @param template Block template. * * @return Whether the list of blocks matches a templates. */ export function doBlocksMatchTemplate( blocks: Block[] = [], template: TemplateItem[] = [] ): boolean { return ( blocks.length === template.length && template.every( ( [ name, , innerBlocksTemplate ], index ) => { const block = blocks[ index ]; return ( name === block.name && doBlocksMatchTemplate( block.innerBlocks, innerBlocksTemplate ) ); } ) ); } const isHTMLAttribute = ( attributeDefinition: BlockAttribute | undefined ): boolean => attributeDefinition?.source === 'html'; const isQueryAttribute = ( attributeDefinition: BlockAttribute | undefined ): boolean => attributeDefinition?.source === 'query'; function normalizeAttributes( schema: Record< string, BlockAttribute >, values: Record< string, unknown > | undefined ): Record< string, unknown > { if ( ! values ) { return {}; } return Object.fromEntries( Object.entries( values ).map( ( [ key, value ] ) => [ key, normalizeAttribute( schema[ key ], value ), ] ) ); } function normalizeAttribute( definition: BlockAttribute | undefined, value: unknown ): unknown { if ( isHTMLAttribute( definition ) && Array.isArray( value ) ) { // Introduce a deprecated call at this point // When we're confident that "children" format should be removed from the templates. return renderToString( value ); } if ( isQueryAttribute( definition ) && value ) { return ( value as Record< string, unknown >[] ).map( ( subValues: Record< string, unknown > ) => { return normalizeAttributes( definition!.query!, subValues ); } ); } return value; } /** * Synchronize a block list with a block template. * * Synchronizing a block list with a block template means that we loop over the blocks * keep the block as is if it matches the block at the same position in the template * (If it has the same name) and if doesn't match, we create a new block based on the template. * Extra blocks not present in the template are removed. * * @param blocks Block list. * @param template Block template. * * @return Updated Block list. */ export function synchronizeBlocksWithTemplate( blocks: Block[] = [], template?: TemplateItem[] ): Block[] { // If no template is provided, return blocks unmodified. if ( ! template ) { return blocks; } return template.map( ( [ name, attributes, innerBlocksTemplate ], index ) => { const block = blocks[ index ]; if ( block && block.name === name ) { const innerBlocks = synchronizeBlocksWithTemplate( block.innerBlocks, innerBlocksTemplate ); return { ...block, innerBlocks }; } // To support old templates that were using the "children" format // for the attributes using "html" strings now, we normalize the template attributes // before creating the blocks. const blockType = getBlockType( name ); const normalizedAttributes = normalizeAttributes( blockType?.attributes ?? {}, attributes ); const [ blockName, blockAttributes ] = convertLegacyBlockNameAndAttributes( name, normalizedAttributes ); return createBlock( blockName!, blockAttributes, synchronizeBlocksWithTemplate( [], innerBlocksTemplate ) ); } ); }