/** * WordPress dependencies */ import { regexp, next } from '@wordpress/shortcode'; /** * Internal dependencies */ import { createBlock, getBlockTransforms, findTransform } from '../factory'; import { getBlockType } from '../registration'; import { getBlockAttributes } from '../parser/get-block-attributes'; import { applyBuiltInValidationFixes } from '../parser/apply-built-in-validation-fixes'; import type { Block } from '../../types'; interface ShortcodeTransform { type: string; blockName: string; tag: string | string[]; isMatch?: ( attrs: unknown ) => boolean; transform?: ( ...args: unknown[] ) => Block | Block[]; attributes: Record< string, { shortcode?: ( ...args: unknown[] ) => unknown } >; } const castArray = < T >( maybeArray: T | T[] ): T[] => Array.isArray( maybeArray ) ? maybeArray : [ maybeArray ]; const beforeLineRegexp = /(\n|

|)\s*$/; const afterLineRegexp = /^\s*(\n|<\/p>|)/; function segmentHTMLToShortcodeBlock( HTML: string, lastIndex: number = 0, excludedBlockNames: string[] = [] ): Array< string | Block > { // Get all matches. const transformsFrom = getBlockTransforms( 'from' ) as unknown as ShortcodeTransform[]; const transformation = findTransform( transformsFrom as unknown as Parameters< typeof findTransform >[ 0 ], ( ( transform: unknown ) => { const t = transform as ShortcodeTransform; return ( excludedBlockNames.indexOf( t.blockName ) === -1 && t.type === 'shortcode' && castArray( t.tag ).some( ( tag: string ) => regexp( tag ).test( HTML ) ) ); } ) as Parameters< typeof findTransform >[ 1 ] ) as unknown as ShortcodeTransform | null; if ( ! transformation ) { return [ HTML ]; } const transformTags = castArray( transformation.tag ); const transformTag = transformTags.find( ( tag ) => regexp( tag ).test( HTML ) ); let match: any; const previousIndex = lastIndex; if ( ( match = next( transformTag!, HTML, lastIndex ) ) ) { lastIndex = match.index + match.content.length; const beforeHTML = HTML.substr( 0, match.index ); const afterHTML = HTML.substr( lastIndex ); // If the shortcode content does not contain HTML and the shortcode is // not on a new line (or in paragraph from Markdown converter), // consider the shortcode as inline text, and thus skip conversion for // this segment. if ( ! match.shortcode.content?.includes( '<' ) && ! ( beforeLineRegexp.test( beforeHTML ) && afterLineRegexp.test( afterHTML ) ) ) { return segmentHTMLToShortcodeBlock( HTML, lastIndex ); } // If a transformation's `isMatch` predicate fails for the inbound // shortcode, try again by excluding the current block type. // // This is the only call to `segmentHTMLToShortcodeBlock` that should // ever carry over `excludedBlockNames`. Other calls in the module // should skip that argument as a way to reset the exclusion state, so // that one `isMatch` fail in an HTML fragment doesn't prevent any // valid matches in subsequent fragments. if ( transformation.isMatch && ! transformation.isMatch( match.shortcode.attrs ) ) { return segmentHTMLToShortcodeBlock( HTML, previousIndex, [ ...excludedBlockNames, transformation.blockName!, ] ); } let blocks: Block[] = []; if ( typeof transformation.transform === 'function' ) { // Passing all of `match` as second argument is intentionally broad // but shouldn't be too relied upon. // // See: https://github.com/WordPress/gutenberg/pull/3610#discussion_r152546926 blocks = ( [] as Block[] ).concat( transformation.transform( match.shortcode.attrs, match ) as | Block | Block[] ); // Applying the built-in fixes can enhance the attributes with missing content like "className". blocks = blocks.map( ( block: Block ) => { block.originalContent = match.shortcode.content; return applyBuiltInValidationFixes( block, getBlockType( block.name )! ); } ); } else { const attributes = Object.fromEntries( Object.entries( transformation.attributes ) .filter( ( [ , schema ] ) => schema.shortcode ) // Passing all of `match` as second argument is intentionally broad // but shouldn't be too relied upon. // // See: https://github.com/WordPress/gutenberg/pull/3610#discussion_r152546926 .map( ( [ key, schema ] ) => [ key, schema.shortcode!( match.shortcode.attrs, match ), ] ) ); const blockType = getBlockType( transformation.blockName ); if ( ! blockType ) { return [ HTML ]; } const transformationBlockType = { ...blockType, attributes: transformation.attributes, }; let block = createBlock( transformation.blockName, getBlockAttributes( transformationBlockType as unknown as string, match.shortcode.content, attributes ) ); // Applying the built-in fixes can enhance the attributes with missing content like "className". block.originalContent = match.shortcode.content; block = applyBuiltInValidationFixes( block, transformationBlockType as unknown as Parameters< typeof applyBuiltInValidationFixes >[ 1 ] ); blocks = [ block ]; } return [ ...segmentHTMLToShortcodeBlock( beforeHTML.replace( beforeLineRegexp, '' ) ), ...blocks, ...segmentHTMLToShortcodeBlock( afterHTML.replace( afterLineRegexp, '' ) ), ]; } return [ HTML ]; } export default segmentHTMLToShortcodeBlock;