/** * Internal dependencies */ import { DEPRECATED_ENTRY_KEYS } from '../constants'; import { validateBlock } from '../validation'; import { getBlockAttributes } from './get-block-attributes'; import { applyBuiltInValidationFixes } from './apply-built-in-validation-fixes'; import { omit } from '../utils'; import type { Block, BlockDeprecation, BlockType, RawBlock } from '../../types'; /** * Function that takes no arguments and always returns false. * * @return Always returns false. */ function stubFalse(): boolean { return false; } /** * Given a block object, returns a new copy of the block with any applicable * deprecated migrations applied, or the original block if it was both valid * and no eligible migrations exist. * * @param block Parsed and invalid block object. * @param rawBlock Raw block object. * @param blockType Block type. This is normalize not necessary and * can be inferred from the block name, * but it's here for performance reasons. * * @return Migrated block object. */ export function applyBlockDeprecatedVersions( block: Block, rawBlock: RawBlock, blockType: BlockType ): Block { const parsedAttributes = rawBlock.attrs ?? {}; const { deprecated: deprecatedDefinitions } = blockType; // Bail early if there are no registered deprecations to be handled. if ( ! deprecatedDefinitions || ! deprecatedDefinitions.length ) { return block; } // By design, blocks lack any sort of version tracking. Instead, to process // outdated content the system operates a queue out of all the defined // attribute shapes and tries each definition until the input produces a // valid result. This mechanism seeks to avoid polluting the user-space with // machine-specific code. An invalid block is thus a block that could not be // matched successfully with any of the registered deprecation definitions. for ( let i = 0; i < deprecatedDefinitions.length; i++ ) { // A block can opt into a migration even if the block is valid by // defining `isEligible` on its deprecation. If the block is both valid // and does not opt to migrate, skip. const { isEligible = stubFalse } = deprecatedDefinitions[ i ]; if ( block.isValid && ! ( isEligible as Function )( parsedAttributes, block.innerBlocks, { blockNode: rawBlock, block, } ) ) { continue; } // Block type properties which could impact either serialization or // parsing are not considered in the deprecated block type by default, // and must be explicitly provided. const deprecatedBlockType = Object.assign( omit( blockType as unknown as Record< string, unknown >, DEPRECATED_ENTRY_KEYS ), deprecatedDefinitions[ i ] ) as unknown as BlockType; let migratedBlock = { ...block, attributes: getBlockAttributes( deprecatedBlockType, block.originalContent ?? '', parsedAttributes ), }; // Ignore the deprecation if it produces a block which is not valid. let [ isValid ] = validateBlock( migratedBlock, deprecatedBlockType ); // If the migrated block is not valid initially, try the built-in fixes. if ( ! isValid ) { migratedBlock = applyBuiltInValidationFixes( migratedBlock, deprecatedBlockType ); [ isValid ] = validateBlock( migratedBlock, deprecatedBlockType ); } // An invalid block does not imply incorrect HTML but the fact block // source information could be lost on re-serialization. if ( ! isValid ) { continue; } let migratedInnerBlocks = migratedBlock.innerBlocks; let migratedAttributes = migratedBlock.attributes; // A block may provide custom behavior to assign new attributes and/or // inner blocks. const { migrate } = deprecatedBlockType as unknown as BlockDeprecation; if ( migrate ) { let migrated = migrate( migratedAttributes, block.innerBlocks ); if ( ! Array.isArray( migrated ) ) { migrated = [ migrated ] as unknown as [ Record< string, unknown >, Block[], ]; } [ migratedAttributes = parsedAttributes, migratedInnerBlocks = block.innerBlocks, ] = migrated; } block = { ...block, attributes: migratedAttributes, innerBlocks: migratedInnerBlocks, isValid: true, validationIssues: [], }; } return block; }