@wordpress/blocks
Version:
Block API for WordPress.
436 lines (386 loc) • 12.6 kB
JavaScript
/**
* WordPress dependencies
*/
import {
Component,
cloneElement,
renderToString,
RawHTML,
} from '@wordpress/element';
import { hasFilter, applyFilters } from '@wordpress/hooks';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { removep } from '@wordpress/autop';
import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
import {
getBlockType,
getFreeformContentHandlerName,
getUnregisteredTypeHandlerName,
} from './registration';
import { serializeRawBlock } from './parser/serialize-raw-block';
import { isUnmodifiedDefaultBlock, normalizeBlockType } from './utils';
/** @typedef {import('./parser').WPBlock} WPBlock */
/**
* @typedef {Object} WPBlockSerializationOptions Serialization Options.
*
* @property {boolean} isInnerBlocks Whether we are serializing inner blocks.
*/
/**
* Returns the block's default classname from its name.
*
* @param {string} blockName The block name.
*
* @return {string} The block's default class.
*/
export function getBlockDefaultClassName( blockName ) {
// Generated HTML classes for blocks follow the `wp-block-{name}` nomenclature.
// Blocks provided by WordPress drop the prefixes 'core/' or 'core-' (historically used in 'core-embed/').
const className =
'wp-block-' + blockName.replace( /\//, '-' ).replace( /^core-/, '' );
return applyFilters(
'blocks.getBlockDefaultClassName',
className,
blockName
);
}
/**
* Returns the block's default menu item classname from its name.
*
* @param {string} blockName The block name.
*
* @return {string} The block's default menu item class.
*/
export function getBlockMenuDefaultClassName( blockName ) {
// Generated HTML classes for blocks follow the `editor-block-list-item-{name}` nomenclature.
// Blocks provided by WordPress drop the prefixes 'core/' or 'core-' (historically used in 'core-embed/').
const className =
'editor-block-list-item-' +
blockName.replace( /\//, '-' ).replace( /^core-/, '' );
return applyFilters(
'blocks.getBlockMenuDefaultClassName',
className,
blockName
);
}
const blockPropsProvider = {};
const innerBlocksPropsProvider = {};
/**
* Call within a save function to get the props for the block wrapper.
*
* @param {Object} props Optional. Props to pass to the element.
*/
export function getBlockProps( props = {} ) {
const { blockType, attributes } = blockPropsProvider;
return getBlockProps.skipFilters
? props
: applyFilters(
'blocks.getSaveContent.extraProps',
{ ...props },
blockType,
attributes
);
}
/**
* Call within a save function to get the props for the inner blocks wrapper.
*
* @param {Object} props Optional. Props to pass to the element.
*/
export function getInnerBlocksProps( props = {} ) {
const { innerBlocks } = innerBlocksPropsProvider;
// Allow a different component to be passed to getSaveElement to handle
// inner blocks, bypassing the default serialisation.
if ( ! Array.isArray( innerBlocks ) ) {
return { ...props, children: innerBlocks };
}
// Value is an array of blocks, so defer to block serializer.
const html = serialize( innerBlocks, { isInnerBlocks: true } );
// Use special-cased raw HTML tag to avoid default escaping.
const children = <RawHTML>{ html }</RawHTML>;
return { ...props, children };
}
/**
* Given a block type containing a save render implementation and attributes, returns the
* enhanced element to be saved or string when raw HTML expected.
*
* @param {string|Object} blockTypeOrName Block type or name.
* @param {Object} attributes Block attributes.
* @param {?Array} innerBlocks Nested blocks.
*
* @return {Object|string} Save element or raw HTML string.
*/
export function getSaveElement(
blockTypeOrName,
attributes,
innerBlocks = []
) {
const blockType = normalizeBlockType( blockTypeOrName );
if ( ! blockType?.save ) {
return null;
}
let { save } = blockType;
// Component classes are unsupported for save since serialization must
// occur synchronously. For improved interoperability with higher-order
// components which often return component class, emulate basic support.
if ( save.prototype instanceof Component ) {
const instance = new save( { attributes } );
save = instance.render.bind( instance );
}
blockPropsProvider.blockType = blockType;
blockPropsProvider.attributes = attributes;
innerBlocksPropsProvider.innerBlocks = innerBlocks;
let element = save( { attributes, innerBlocks } );
if (
element !== null &&
typeof element === 'object' &&
hasFilter( 'blocks.getSaveContent.extraProps' ) &&
! ( blockType.apiVersion > 1 )
) {
/**
* Filters the props applied to the block save result element.
*
* @param {Object} props Props applied to save element.
* @param {WPBlock} blockType Block type definition.
* @param {Object} attributes Block attributes.
*/
const props = applyFilters(
'blocks.getSaveContent.extraProps',
{ ...element.props },
blockType,
attributes
);
if ( ! isShallowEqual( props, element.props ) ) {
element = cloneElement( element, props );
}
}
/**
* Filters the save result of a block during serialization.
*
* @param {Element} element Block save result.
* @param {WPBlock} blockType Block type definition.
* @param {Object} attributes Block attributes.
*/
return applyFilters(
'blocks.getSaveElement',
element,
blockType,
attributes
);
}
/**
* Given a block type containing a save render implementation and attributes, returns the
* static markup to be saved.
*
* @param {string|Object} blockTypeOrName Block type or name.
* @param {Object} attributes Block attributes.
* @param {?Array} innerBlocks Nested blocks.
*
* @return {string} Save content.
*/
export function getSaveContent( blockTypeOrName, attributes, innerBlocks ) {
const blockType = normalizeBlockType( blockTypeOrName );
return renderToString(
getSaveElement( blockType, attributes, innerBlocks )
);
}
/**
* Returns attributes which are to be saved and serialized into the block
* comment delimiter.
*
* When a block exists in memory it contains as its attributes both those
* parsed the block comment delimiter _and_ those which matched from the
* contents of the block.
*
* This function returns only those attributes which are needed to persist and
* which cannot be matched from the block content.
*
* @param {Object<string,*>} blockType Block type.
* @param {Object<string,*>} attributes Attributes from in-memory block data.
*
* @return {Object<string,*>} Subset of attributes for comment serialization.
*/
export function getCommentAttributes( blockType, attributes ) {
return Object.entries( blockType.attributes ?? {} ).reduce(
( accumulator, [ key, attributeSchema ] ) => {
const value = attributes[ key ];
// Ignore undefined values.
if ( undefined === value ) {
return accumulator;
}
// Ignore all attributes but the ones with an "undefined" source
// "undefined" source refers to attributes saved in the block comment.
if ( attributeSchema.source !== undefined ) {
return accumulator;
}
// Ignore all local attributes
if ( attributeSchema.role === 'local' ) {
return accumulator;
}
if ( attributeSchema.__experimentalRole === 'local' ) {
deprecated( '__experimentalRole attribute', {
since: '6.7',
version: '6.8',
alternative: 'role attribute',
hint: `Check the block.json of the ${ blockType?.name } block.`,
} );
return accumulator;
}
// Ignore default value.
if (
'default' in attributeSchema &&
JSON.stringify( attributeSchema.default ) ===
JSON.stringify( value )
) {
return accumulator;
}
// Otherwise, include in comment set.
accumulator[ key ] = value;
return accumulator;
},
{}
);
}
/**
* Given an attributes object, returns a string in the serialized attributes
* format prepared for post content.
*
* @param {Object} attributes Attributes object.
*
* @return {string} Serialized attributes.
*/
export function serializeAttributes( attributes ) {
return (
JSON.stringify( attributes )
// Replace escaped `\` characters with the unicode escape sequence.
.replaceAll( '\\\\', '\\u005c' )
// Don't break HTML comments.
.replaceAll( '--', '\\u002d\\u002d' )
// Don't break non-standard-compliant tools.
.replaceAll( '<', '\\u003c' )
.replaceAll( '>', '\\u003e' )
.replaceAll( '&', '\\u0026' )
// Replace escaped quotes (`\"`) to prevent problems with wp_kses_stripsplashes.
// This simple replacement is safe because `\\` has already been replaced.
// `\"` is not a JSON string quote like `"\\"`.
.replaceAll( '\\"', '\\u0022' )
);
}
/**
* Given a block object, returns the Block's Inner HTML markup.
*
* @param {Object} block Block instance.
*
* @return {string} HTML.
*/
export function getBlockInnerHTML( block ) {
// If block was parsed as invalid or encounters an error while generating
// save content, use original content instead to avoid content loss. If a
// block contains nested content, exempt it from this condition because we
// otherwise have no access to its original content and content loss would
// still occur.
let saveContent = block.originalContent;
if ( block.isValid || block.innerBlocks.length ) {
try {
saveContent = getSaveContent(
block.name,
block.attributes,
block.innerBlocks
);
} catch ( error ) {}
}
return saveContent;
}
/**
* Returns the content of a block, including comment delimiters.
*
* @param {string} rawBlockName Block name.
* @param {Object} attributes Block attributes.
* @param {string} content Block save content.
*
* @return {string} Comment-delimited block content.
*/
export function getCommentDelimitedContent(
rawBlockName,
attributes,
content
) {
const serializedAttributes =
attributes && Object.entries( attributes ).length
? serializeAttributes( attributes ) + ' '
: '';
// Strip core blocks of their namespace prefix.
const blockName = rawBlockName?.startsWith( 'core/' )
? rawBlockName.slice( 5 )
: rawBlockName;
// @todo make the `wp:` prefix potentially configurable.
if ( ! content ) {
return `<!-- wp:${ blockName } ${ serializedAttributes }/-->`;
}
return (
`<!-- wp:${ blockName } ${ serializedAttributes }-->\n` +
content +
`\n<!-- /wp:${ blockName } -->`
);
}
/**
* Returns the content of a block, including comment delimiters, determining
* serialized attributes and content form from the current state of the block.
*
* @param {WPBlock} block Block instance.
* @param {WPBlockSerializationOptions} options Serialization options.
*
* @return {string} Serialized block.
*/
export function serializeBlock( block, { isInnerBlocks = false } = {} ) {
if ( ! block.isValid && block.__unstableBlockSource ) {
return serializeRawBlock( block.__unstableBlockSource );
}
const blockName = block.name;
const saveContent = getBlockInnerHTML( block );
if (
blockName === getUnregisteredTypeHandlerName() ||
( ! isInnerBlocks && blockName === getFreeformContentHandlerName() )
) {
return saveContent;
}
const blockType = getBlockType( blockName );
if ( ! blockType ) {
return saveContent;
}
const saveAttributes = getCommentAttributes( blockType, block.attributes );
return getCommentDelimitedContent( blockName, saveAttributes, saveContent );
}
export function __unstableSerializeAndClean( blocks ) {
// A single unmodified default block is assumed to
// be equivalent to an empty post.
if ( blocks.length === 1 && isUnmodifiedDefaultBlock( blocks[ 0 ] ) ) {
blocks = [];
}
let content = serialize( blocks );
// For compatibility, treat a post consisting of a
// single freeform block as legacy content and apply
// pre-block-editor removep'd content formatting.
if (
blocks.length === 1 &&
blocks[ 0 ].name === getFreeformContentHandlerName() &&
blocks[ 0 ].name === 'core/freeform'
) {
content = removep( content );
}
return content;
}
/**
* Takes a block or set of blocks and returns the serialized post content.
*
* @param {Array} blocks Block(s) to serialize.
* @param {WPBlockSerializationOptions} options Serialization options.
*
* @return {string} The post content.
*/
export default function serialize( blocks, options ) {
const blocksArray = Array.isArray( blocks ) ? blocks : [ blocks ];
return blocksArray
.map( ( block ) => serializeBlock( block, options ) )
.join( '\n\n' );
}