@wordpress/blocks
Version:
Block API for WordPress.
215 lines (183 loc) • 5.81 kB
JavaScript
/**
* WordPress dependencies
*/
import { isPhrasingContent, getPhrasingContentSchema } from '@wordpress/dom';
/**
* Internal dependencies
*/
import { hasBlockSupport } from '..';
import { getRawTransforms } from './get-raw-transforms';
export function getBlockContentSchemaFromTransforms( transforms, context ) {
const phrasingContentSchema = getPhrasingContentSchema( context );
const schemaArgs = { phrasingContentSchema, isPaste: context === 'paste' };
const schemas = transforms.map( ( { isMatch, blockName, schema } ) => {
const hasAnchorSupport = hasBlockSupport( blockName, 'anchor' );
schema = typeof schema === 'function' ? schema( schemaArgs ) : schema;
// If the block does not has anchor support and the transform does not
// provides an isMatch we can return the schema right away.
if ( ! hasAnchorSupport && ! isMatch ) {
return schema;
}
if ( ! schema ) {
return {};
}
return Object.fromEntries(
Object.entries( schema ).map( ( [ key, value ] ) => {
let attributes = value.attributes || [];
// If the block supports the "anchor" functionality, it needs to keep its ID attribute.
if ( hasAnchorSupport ) {
attributes = [ ...attributes, 'id' ];
}
return [
key,
{
...value,
attributes,
isMatch: isMatch ? isMatch : undefined,
},
];
} )
);
} );
function mergeTagNameSchemaProperties( objValue, srcValue, key ) {
switch ( key ) {
case 'children': {
if ( objValue === '*' || srcValue === '*' ) {
return '*';
}
return { ...objValue, ...srcValue };
}
case 'attributes':
case 'require': {
return [ ...( objValue || [] ), ...( srcValue || [] ) ];
}
case 'isMatch': {
// If one of the values being merge is undefined (matches everything),
// the result of the merge will be undefined.
if ( ! objValue || ! srcValue ) {
return undefined;
}
// When merging two isMatch functions, the result is a new function
// that returns if one of the source functions returns true.
return ( ...args ) => {
return objValue( ...args ) || srcValue( ...args );
};
}
}
}
// A tagName schema is an object with children, attributes, require, and
// isMatch properties.
function mergeTagNameSchemas( a, b ) {
for ( const key in b ) {
a[ key ] = a[ key ]
? mergeTagNameSchemaProperties( a[ key ], b[ key ], key )
: { ...b[ key ] };
}
return a;
}
// A schema is an object with tagName schemas by tag name.
function mergeSchemas( a, b ) {
for ( const key in b ) {
a[ key ] = a[ key ]
? mergeTagNameSchemas( a[ key ], b[ key ] )
: { ...b[ key ] };
}
return a;
}
return schemas.reduce( mergeSchemas, {} );
}
/**
* Gets the block content schema, which is extracted and merged from all
* registered blocks with raw transforms.
*
* @param {string} context Set to "paste" when in paste context, where the
* schema is more strict.
*
* @return {Object} A complete block content schema.
*/
export function getBlockContentSchema( context ) {
return getBlockContentSchemaFromTransforms( getRawTransforms(), context );
}
/**
* Checks whether HTML can be considered plain text. That is, it does not contain
* any elements that are not line breaks, or it only contains a single non-semantic
* wrapper element (span) with no semantic child elements.
*
* @param {string} HTML The HTML to check.
*
* @return {boolean} Whether the HTML can be considered plain text.
*/
export function isPlain( HTML ) {
if ( ! /<(?!br[ />])/i.test( HTML ) ) {
return true;
}
const doc = document.implementation.createHTMLDocument( '' );
doc.body.innerHTML = HTML;
if ( doc.body.children.length !== 1 ) {
return false;
}
const wrapper = doc.body.children.item( 0 );
const descendants = wrapper.getElementsByTagName( '*' );
for ( let i = 0; i < descendants.length; i++ ) {
if ( descendants.item( i ).tagName !== 'BR' ) {
return false;
}
}
if ( wrapper.tagName !== 'SPAN' ) {
return false;
}
return true;
}
/**
* Given node filters, deeply filters and mutates a NodeList.
*
* @param {NodeList} nodeList The nodeList to filter.
* @param {Array} filters An array of functions that can mutate with the provided node.
* @param {Document} doc The document of the nodeList.
* @param {Object} schema The schema to use.
*/
export function deepFilterNodeList( nodeList, filters, doc, schema ) {
Array.from( nodeList ).forEach( ( node ) => {
deepFilterNodeList( node.childNodes, filters, doc, schema );
filters.forEach( ( item ) => {
// Make sure the node is still attached to the document.
if ( ! doc.contains( node ) ) {
return;
}
item( node, doc, schema );
} );
} );
}
/**
* Given node filters, deeply filters HTML tags.
* Filters from the deepest nodes to the top.
*
* @param {string} HTML The HTML to filter.
* @param {Array} filters An array of functions that can mutate with the provided node.
* @param {Object} schema The schema to use.
*
* @return {string} The filtered HTML.
*/
export function deepFilterHTML( HTML, filters = [], schema ) {
const doc = document.implementation.createHTMLDocument( '' );
doc.body.innerHTML = HTML;
deepFilterNodeList( doc.body.childNodes, filters, doc, schema );
return doc.body.innerHTML;
}
/**
* Gets a sibling within text-level context.
*
* @param {Element} node The subject node.
* @param {string} which "next" or "previous".
*/
export function getSibling( node, which ) {
const sibling = node[ `${ which }Sibling` ];
if ( sibling && isPhrasingContent( sibling ) ) {
return sibling;
}
const { parentNode } = node;
if ( ! parentNode || ! isPhrasingContent( parentNode ) ) {
return;
}
return getSibling( parentNode, which );
}