@wordpress/blocks
Version:
Block API for WordPress.
280 lines (258 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;
}, []);
}
//# sourceMappingURL=index.js.map