@wordpress/blocks
Version:
Block API for WordPress.
319 lines (288 loc) • 11.3 kB
JavaScript
/**
* WordPress dependencies
*/
import { parse as grammarParse } from '@wordpress/block-serialization-default-parser';
import { autop } from '@wordpress/autop';
/**
* Internal dependencies
*/
import {
getFreeformContentHandlerName,
getUnregisteredTypeHandlerName,
getBlockType,
} from '../registration';
import { getSaveContent } from '../serializer';
import { validateBlock } from '../validation';
import { createBlock } from '../factory';
import { convertLegacyBlockNameAndAttributes } from './convert-legacy-block';
import { serializeRawBlock } from './serialize-raw-block';
import { getBlockAttributes } from './get-block-attributes';
import { applyBlockDeprecatedVersions } from './apply-block-deprecated-versions';
import { applyBuiltInValidationFixes } from './apply-built-in-validation-fixes';
/**
* The raw structure of a block includes its attributes, inner
* blocks, and inner HTML. It is important to distinguish inner blocks from
* the HTML content of the block as only the latter is relevant for block
* validation and edit operations.
*
* @typedef WPRawBlock
*
* @property {string=} blockName Block name
* @property {Object=} attrs Block raw or comment attributes.
* @property {string} innerHTML HTML content of the block.
* @property {(string|null)[]} innerContent Content without inner blocks.
* @property {WPRawBlock[]} innerBlocks Inner Blocks.
*/
/**
* Fully parsed block object.
*
* @typedef WPBlock
*
* @property {string} name Block name
* @property {Object} attributes Block raw or comment attributes.
* @property {WPBlock[]} innerBlocks Inner Blocks.
* @property {string} originalContent Original content of the block before validation fixes.
* @property {boolean} isValid Whether the block is valid.
* @property {Object[]} validationIssues Validation issues.
* @property {WPRawBlock} [__unstableBlockSource] Un-processed original copy of block if created through parser.
*/
/**
* @typedef {Object} ParseOptions
* @property {boolean?} __unstableSkipMigrationLogs If a block is migrated from a deprecated version, skip logging the migration details.
* @property {boolean?} __unstableSkipAutop Whether to skip autop when processing freeform content.
*/
/**
* Convert legacy blocks to their canonical form. This function is used
* both in the parser level for previous content and to convert such blocks
* used in Custom Post Types templates.
*
* @param {WPRawBlock} rawBlock
*
* @return {WPRawBlock} The block's name and attributes, changed accordingly if a match was found
*/
function convertLegacyBlocks( rawBlock ) {
const [ correctName, correctedAttributes ] =
convertLegacyBlockNameAndAttributes(
rawBlock.blockName,
rawBlock.attrs
);
return {
...rawBlock,
blockName: correctName,
attrs: correctedAttributes,
};
}
/**
* Normalize the raw block by applying the fallback block name if none given,
* sanitize the parsed HTML...
*
* @param {WPRawBlock} rawBlock The raw block object.
* @param {ParseOptions?} options Extra options for handling block parsing.
*
* @return {WPRawBlock} The normalized block object.
*/
export function normalizeRawBlock( rawBlock, options ) {
const fallbackBlockName = getFreeformContentHandlerName();
// If the grammar parsing don't produce any block name, use the freeform block.
const rawBlockName = rawBlock.blockName || getFreeformContentHandlerName();
const rawAttributes = rawBlock.attrs || {};
const rawInnerBlocks = rawBlock.innerBlocks || [];
let rawInnerHTML = rawBlock.innerHTML.trim();
// Fallback content may be upgraded from classic content expecting implicit
// automatic paragraphs, so preserve them. Assumes wpautop is idempotent,
// meaning there are no negative consequences to repeated autop calls.
if (
rawBlockName === fallbackBlockName &&
rawBlockName === 'core/freeform' &&
! options?.__unstableSkipAutop
) {
rawInnerHTML = autop( rawInnerHTML ).trim();
}
return {
...rawBlock,
blockName: rawBlockName,
attrs: rawAttributes,
innerHTML: rawInnerHTML,
innerBlocks: rawInnerBlocks,
};
}
/**
* Uses the "unregistered blockType" to create a block object.
*
* @param {WPRawBlock} rawBlock block.
*
* @return {WPRawBlock} The unregistered block object.
*/
function createMissingBlockType( rawBlock ) {
const unregisteredFallbackBlock =
getUnregisteredTypeHandlerName() || getFreeformContentHandlerName();
// 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 = serializeRawBlock( rawBlock, {
isCommentDelimited: false,
} );
// Preserve full block content for use by the unregistered type
// handler, block boundaries included.
const originalContent = serializeRawBlock( rawBlock, {
isCommentDelimited: true,
} );
return {
blockName: unregisteredFallbackBlock,
attrs: {
originalName: rawBlock.blockName,
originalContent,
originalUndelimitedContent,
},
innerHTML: rawBlock.blockName ? originalContent : rawBlock.innerHTML,
innerBlocks: rawBlock.innerBlocks,
innerContent: rawBlock.innerContent,
};
}
/**
* Validates a block and wraps with validation meta.
*
* The name here is regrettable but `validateBlock` is already taken.
*
* @param {WPBlock} unvalidatedBlock
* @param {import('../registration').WPBlockType} blockType
* @return {WPBlock} validated block, with auto-fixes if initially invalid
*/
function applyBlockValidation( unvalidatedBlock, blockType ) {
// Attempt to validate the block.
const [ isValid ] = validateBlock( unvalidatedBlock, blockType );
if ( isValid ) {
return { ...unvalidatedBlock, isValid, validationIssues: [] };
}
// If the block is invalid, attempt some built-in fixes
// like custom classNames handling.
const fixedBlock = applyBuiltInValidationFixes(
unvalidatedBlock,
blockType
);
// Attempt to validate the block once again after the built-in fixes.
const [ isFixedValid, validationIssues ] = validateBlock(
fixedBlock,
blockType
);
return { ...fixedBlock, isValid: isFixedValid, validationIssues };
}
/**
* Given a raw block returned by grammar parsing, returns a fully parsed block.
*
* @param {WPRawBlock} rawBlock The raw block object.
* @param {ParseOptions} options Extra options for handling block parsing.
*
* @return {WPBlock | undefined} Fully parsed block.
*/
export function parseRawBlock( rawBlock, options ) {
let normalizedBlock = normalizeRawBlock( rawBlock, options );
// During the lifecycle of the project, we renamed some old blocks
// and transformed others to new blocks. To avoid breaking existing content,
// we added this function to properly parse the old content.
normalizedBlock = convertLegacyBlocks( normalizedBlock );
// Try finding the type for known block name.
let blockType = getBlockType( normalizedBlock.blockName );
// If not blockType is found for the specified name, fallback to the "unregisteredBlockType".
if ( ! blockType ) {
normalizedBlock = createMissingBlockType( normalizedBlock );
blockType = getBlockType( normalizedBlock.blockName );
}
// If it's an empty freeform block or there's no blockType (no missing block handler)
// Then, just ignore the block.
// It might be a good idea to throw a warning here.
// TODO: I'm unsure about the unregisteredFallbackBlock check,
// it might ignore some dynamic unregistered third party blocks wrongly.
const isFallbackBlock =
normalizedBlock.blockName === getFreeformContentHandlerName() ||
normalizedBlock.blockName === getUnregisteredTypeHandlerName();
if ( ! blockType || ( ! normalizedBlock.innerHTML && isFallbackBlock ) ) {
return;
}
// Parse inner blocks recursively.
const parsedInnerBlocks = normalizedBlock.innerBlocks
.map( ( innerBlock ) => parseRawBlock( innerBlock, options ) )
// See https://github.com/WordPress/gutenberg/pull/17164.
.filter( ( innerBlock ) => !! innerBlock );
// Get the fully parsed block.
const parsedBlock = createBlock(
normalizedBlock.blockName,
getBlockAttributes(
blockType,
normalizedBlock.innerHTML,
normalizedBlock.attrs
),
parsedInnerBlocks
);
parsedBlock.originalContent = normalizedBlock.innerHTML;
const validatedBlock = applyBlockValidation( parsedBlock, blockType );
const { validationIssues } = validatedBlock;
// Run the block deprecation and migrations.
// This is performed on both invalid and valid blocks because
// migration using the `migrate` functions should run even
// if the output is deemed valid.
const updatedBlock = applyBlockDeprecatedVersions(
validatedBlock,
normalizedBlock,
blockType
);
if ( ! updatedBlock.isValid ) {
// Preserve the original unprocessed version of the block
// that we received (no fixes, no deprecations) so that
// we can save it as close to exactly the same way as
// we loaded it. This is important to avoid corruption
// and data loss caused by block implementations trying
// to process data that isn't fully recognized.
updatedBlock.__unstableBlockSource = rawBlock;
}
if (
! validatedBlock.isValid &&
updatedBlock.isValid &&
! options?.__unstableSkipMigrationLogs
) {
/* eslint-disable no-console */
console.groupCollapsed( 'Updated Block: %s', blockType.name );
console.info(
'Block successfully updated for `%s` (%o).\n\nNew content generated by `save` function:\n\n%s\n\nContent retrieved from post body:\n\n%s',
blockType.name,
blockType,
getSaveContent( blockType, updatedBlock.attributes ),
updatedBlock.originalContent
);
console.groupEnd();
/* eslint-enable no-console */
} else if ( ! validatedBlock.isValid && ! updatedBlock.isValid ) {
validationIssues.forEach( ( { log, args } ) => log( ...args ) );
}
return updatedBlock;
}
/**
* Utilizes an optimized token-driven parser based on the Gutenberg grammar spec
* defined through a parsing expression grammar to take advantage of the regular
* cadence provided by block delimiters -- composed syntactically through HTML
* comments -- which, given a general HTML document as an input, returns a block
* list array representation.
*
* This is a recursive-descent parser that scans linearly once through the input
* document. Instead of directly recursing it utilizes a trampoline mechanism to
* prevent stack overflow. This initial pass is mainly interested in separating
* and isolating the blocks serialized in the document and manifestly not in the
* content within the blocks.
*
* @see
* https://developer.wordpress.org/block-editor/packages/packages-block-serialization-default-parser/
*
* @param {string} content The post content.
* @param {ParseOptions} options Extra options for handling block parsing.
*
* @return {Array} Block list.
*/
export default function parse( content, options ) {
return grammarParse( content ).reduce( ( accumulator, rawBlock ) => {
const block = parseRawBlock( rawBlock, options );
if ( block ) {
accumulator.push( block );
}
return accumulator;
}, [] );
}