diff --git a/package-lock.json b/package-lock.json index 48c0805aef9ed..9083d7ee16cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52208,6 +52208,7 @@ "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", + "@wordpress/block-serialization-default-parser": "file:../block-serialization-default-parser", "@wordpress/blocks": "file:../blocks", "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", @@ -67214,6 +67215,7 @@ "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", + "@wordpress/block-serialization-default-parser": "file:../block-serialization-default-parser", "@wordpress/blocks": "file:../blocks", "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 640a5fc319330..1f4fa7588ec6c 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -39,6 +39,7 @@ "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", + "@wordpress/block-serialization-default-parser": "file:../block-serialization-default-parser", "@wordpress/blocks": "file:../blocks", "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 9d3c5f27a7cc9..9cc793362296c 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -20,7 +20,7 @@ import { checkAllowListRecursive, getAllPatternsDependants, getInsertBlockTypeDependants, - getParsedPattern, + getGrammar, } from './utils'; import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; import { STORE_NAME } from './constants'; @@ -300,10 +300,10 @@ export const hasAllowedPatterns = createRegistrySelector( ( select ) => if ( ! inserter ) { return false; } - const { blocks } = getParsedPattern( pattern ); + const grammar = getGrammar( pattern ); return ( - checkAllowListRecursive( blocks, allowedBlockTypes ) && - blocks.every( ( { name: blockName } ) => + checkAllowListRecursive( grammar, allowedBlockTypes ) && + grammar.every( ( { name: blockName } ) => canInsertBlockType( state, blockName, rootClientId ) ) ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 32fa081b390ae..95ef1b1684eeb 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -27,6 +27,7 @@ import { getAllPatternsDependants, getInsertBlockTypeDependants, getParsedPattern, + getGrammar, } from './utils'; import { orderBy } from '../utils/sorting'; import { STORE_NAME } from './constants'; @@ -2376,17 +2377,27 @@ export const __experimentalGetAllowedPatterns = createRegistrySelector( const { getAllPatterns } = unlock( select( STORE_NAME ) ); const patterns = getAllPatterns(); const { allowedBlockTypes } = getSettings( state ); - const parsedPatterns = patterns .filter( ( { inserter = true } ) => !! inserter ) - .map( getParsedPattern ); + .map( ( pattern ) => { + return { + ...pattern, + get blocks() { + return getParsedPattern( pattern ).blocks; + }, + }; + } ); + const availableParsedPatterns = parsedPatterns.filter( - ( { blocks } ) => - checkAllowListRecursive( blocks, allowedBlockTypes ) + ( pattern ) => + checkAllowListRecursive( + getGrammar( pattern ), + allowedBlockTypes + ) ); const patternsAllowed = availableParsedPatterns.filter( - ( { blocks } ) => - blocks.every( ( { name } ) => + ( pattern ) => + getGrammar( pattern ).every( ( { blockName: name } ) => canInsertBlockType( state, name, rootClientId ) ) ); diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index 71c5ac44f45b8..b630912a5163d 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { parse } from '@wordpress/blocks'; +import { parse as grammarParse } from '@wordpress/block-serialization-default-parser'; /** * Internal dependencies @@ -13,6 +14,7 @@ import { STORE_NAME } from './constants'; export const withRootClientIdOptionKey = Symbol( 'withRootClientId' ); const parsedPatternCache = new WeakMap(); +const grammarMapCache = new WeakMap(); function parsePattern( pattern ) { const blocks = parse( pattern.content, { @@ -37,14 +39,24 @@ function parsePattern( pattern ) { export function getParsedPattern( pattern ) { let parsedPattern = parsedPatternCache.get( pattern ); - if ( parsedPattern ) { - return parsedPattern; + if ( ! parsedPattern ) { + parsedPattern = parsePattern( pattern ); + parsedPatternCache.set( pattern, parsedPattern ); } - parsedPattern = parsePattern( pattern ); - parsedPatternCache.set( pattern, parsedPattern ); return parsedPattern; } +export function getGrammar( pattern ) { + let grammarMap = grammarMapCache.get( pattern ); + if ( ! grammarMap ) { + grammarMap = grammarParse( pattern.content ); + // Block names are null only at the top level for whitespace. + grammarMap = grammarMap.filter( ( block ) => block.blockName !== null ); + grammarMapCache.set( pattern, grammarMap ); + } + return grammarMap; +} + export const checkAllowList = ( list, item, defaultResult = null ) => { if ( typeof list === 'boolean' ) { return list; diff --git a/packages/block-editor/tsconfig.json b/packages/block-editor/tsconfig.json index 192b6f7de7e12..37fb61895d252 100644 --- a/packages/block-editor/tsconfig.json +++ b/packages/block-editor/tsconfig.json @@ -8,6 +8,7 @@ "references": [ { "path": "../a11y" }, { "path": "../api-fetch" }, + { "path": "../block-serialization-default-parser" }, { "path": "../blob" }, { "path": "../components" }, { "path": "../compose" }, diff --git a/test/e2e/specs/editor/various/parsing-patterns.spec.js b/test/e2e/specs/editor/various/parsing-patterns.spec.js new file mode 100644 index 0000000000000..d8abe7a46fbc1 --- /dev/null +++ b/test/e2e/specs/editor/various/parsing-patterns.spec.js @@ -0,0 +1,57 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Parsing patterns', () => { + test( 'Considers a pattern with whitespace an allowed pattern', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost(); + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ { name: 'core/button', attributes: { text: 'a' } } ], + } ); + await page.keyboard.press( 'ArrowDown' ); + await page.getByLabel( 'Toggle block inserter' ).click(); + + await page.getByRole( 'tab', { name: 'Patterns' } ).click(); + await page.evaluate( () => { + window.wp.data.dispatch( 'core/block-editor' ).updateSettings( { + __experimentalBlockPatterns: [ + { + name: 'test/whitespace', + title: 'Pattern with top-level whitespace', + description: '', + content: ` +
test
+ + + +
test
+`, + }, + ], + } ); + } ); + await page.fill( + 'role=region[name="Block Library"i] >> role=searchbox[name="Search for blocks and patterns"i]', + 'whitespace' + ); + await page + .locator( 'role=option[name="Pattern with top-level whitespace"i]' ) + .click(); + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/buttons', + innerBlocks: [ + { name: 'core/button', attributes: { text: 'a' } }, + { name: 'core/button', attributes: { text: 'test' } }, + { name: 'core/button', attributes: { text: 'test' } }, + ], + }, + ] ); + } ); +} );