diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index b38e4ede0bf678..f91fe13518a32e 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -1203,7 +1203,7 @@ Returns an action object used to signal that the blocks have been updated. _Parameters_ - _blocks_ `Array`: Block Array. -- _options_ `?Object`: Optional options. +- _options_ `?WPEditorResetEditorBlocksActionOptions`: Optional options. _Returns_ diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index fdbc957dd802e7..35502610a0e59a 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,5 +1,10 @@ ## Master +### New Features + +- `parse` now accepts an options object. Current options include the ability to disable default validation (`validate`). +- New function: `validate`. Given a block or array of blocks, assigns the `isValid` property of each block corresponding to the validation result. + ### Improvements - Omitting `attributes` or `keywords` settings will now stub default values (an empty object or empty array, respectively). diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 331be71c3a15f4..5074e2da4b5947 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -746,6 +746,16 @@ _Parameters_ - _slug_ `string`: Block category slug. - _category_ `Object`: Object containing the category properties that should be updated. +# **validate** + +Given a block or array of blocks, assigns the `isValid` property of each +block corresponding to the validation result. This mutates the original +array or object. + +_Parameters_ + +- _blocks_ `(WPBlock|Array)`: Block or array of blocks to validate. + # **withBlockContentContext** A Higher Order Component used to inject BlockContent using context to the diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 68e60ed43952b1..0a38e4e9cfa17a 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -20,7 +20,10 @@ export { getSaveElement, getSaveContent, } from './serializer'; -export { isValidBlockContent } from './validation'; +export { + default as validate, + isValidBlockContent, +} from './validation'; export { getCategories, setCategories, diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index 0e67b0219142d1..11c70d07d68a17 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -26,6 +26,24 @@ import { attr, html, text, query, node, children, prop } from './matchers'; import { normalizeBlockType } from './utils'; import { DEPRECATED_ENTRY_KEYS } from './constants'; +/** + * Options for parse. + * + * @typedef {Object} WPBlocksParseOptions + * + * @property {boolean} validate Whether to validate and assign result as + * `isValid` on parsed block results. + */ + +/** + * Default options for parse. + * + * @type {WPBlocksParseOptions} + */ +export const DEFAULT_PARSE_OPTIONS = { + validate: true, +}; + /** * Sources which are guaranteed to return a string value. * @@ -373,11 +391,12 @@ export function getMigratedBlock( block, parsedAttributes ) { /** * Creates a block with fallback to the unknown type handler. * - * @param {Object} blockNode Parsed block node. + * @param {Object} blockNode Parsed block node. + * @param {WPBlocksParseOptions} parseOptions Parser options. * * @return {?Object} An initialized block object (if possible). */ -export function createBlockWithFallback( blockNode ) { +export function createBlockWithFallback( blockNode, parseOptions ) { const { blockName: originalName } = blockNode; let { attrs: attributes, @@ -477,7 +496,7 @@ export function createBlockWithFallback( blockNode ) { // provided there are no changes in attributes. The validation procedure thus compares the // provided source value with the serialized output before there are any modifications to // the block. When both match, the block is marked as valid. - if ( ! isFallbackBlock ) { + if ( ! isFallbackBlock && parseOptions.validate ) { block.isValid = isValidBlockContent( blockType, block.attributes, innerHTML ); } @@ -535,14 +554,22 @@ export function serializeBlockNode( blockNode, options = {} ) { * * @return {Function} An implementation which parses the post content. */ -const createParse = ( parseImplementation ) => - ( content ) => parseImplementation( content ).reduce( ( memo, blockNode ) => { - const block = createBlockWithFallback( blockNode ); - if ( block ) { - memo.push( block ); +function createParse( parseImplementation ) { + return ( content, options = DEFAULT_PARSE_OPTIONS ) => { + if ( options !== DEFAULT_PARSE_OPTIONS ) { + options = { ...DEFAULT_PARSE_OPTIONS, ...options }; } - return memo; - }, [] ); + + return parseImplementation( content ).reduce( ( memo, blockNode ) => { + const block = createBlockWithFallback( blockNode, options ); + if ( block ) { + memo.push( block ); + } + + return memo; + }, [] ); + }; +} /** * Parses the post content with a PegJS grammar and returns a list of blocks. diff --git a/packages/blocks/src/api/test/parser.js b/packages/blocks/src/api/test/parser.js index f10001cb0c0133..2643b381bbebc5 100644 --- a/packages/blocks/src/api/test/parser.js +++ b/packages/blocks/src/api/test/parser.js @@ -8,6 +8,7 @@ import deepFreeze from 'deep-freeze'; * Internal dependencies */ import { + DEFAULT_PARSE_OPTIONS, getBlockAttribute, getBlockAttributes, createBlockWithFallback, @@ -671,7 +672,7 @@ describe( 'block parser', () => { blockName: 'core/test-block', innerHTML: 'Bananas', attrs: { fruit: 'Bananas' }, - } ); + }, DEFAULT_PARSE_OPTIONS ); expect( block.name ).toEqual( 'core/test-block' ); expect( block.attributes ).toEqual( { fruit: 'Bananas' } ); } ); @@ -682,7 +683,7 @@ describe( 'block parser', () => { const block = createBlockWithFallback( { blockName: 'core/test-block', innerHTML: '', - } ); + }, DEFAULT_PARSE_OPTIONS ); expect( block.name ).toEqual( 'core/test-block' ); expect( block.attributes ).toEqual( {} ); } ); @@ -695,7 +696,7 @@ describe( 'block parser', () => { blockName: 'core/test-block', innerHTML: 'Bananas', attrs: { fruit: 'Bananas' }, - } ); + }, DEFAULT_PARSE_OPTIONS ); expect( block.name ).toBe( 'core/unregistered-block' ); expect( block.attributes.content ).toContain( 'wp:test-block' ); } ); @@ -706,7 +707,7 @@ describe( 'block parser', () => { const block = createBlockWithFallback( { innerHTML: 'content', - } ); + }, DEFAULT_PARSE_OPTIONS ); expect( block.name ).toEqual( 'core/freeform-block' ); expect( block.attributes ).toEqual( { content: '

content

' } ); } ); @@ -715,7 +716,7 @@ describe( 'block parser', () => { const block = createBlockWithFallback( { blockName: 'core/test-block', innerHTML: '', - } ); + }, DEFAULT_PARSE_OPTIONS ); expect( block ).toBeUndefined(); } ); @@ -749,7 +750,7 @@ describe( 'block parser', () => { blockName: 'core/test-block', innerHTML: 'Bananas', attrs: { fruit: 'Bananas' }, - } ); + }, DEFAULT_PARSE_OPTIONS ); expect( block.name ).toEqual( 'core/test-block' ); expect( block.attributes ).toEqual( { fruit: 'Big Bananas' } ); expect( block.isValid ).toBe( true ); diff --git a/packages/blocks/src/api/test/validation.js b/packages/blocks/src/api/test/validation.js index fcfb61bff6ae2c..a296c8d0ec12c8 100644 --- a/packages/blocks/src/api/test/validation.js +++ b/packages/blocks/src/api/test/validation.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { +import validate, { isValidCharacterReference, DecodeEntityParser, getTextPiecesSplitOnWhitespace, @@ -662,4 +662,48 @@ describe( 'validation', () => { expect( isValid ).toBe( true ); } ); } ); + + describe( 'validate', () => { + it( 'returns undefined', () => { + registerBlockType( 'core/test-block', defaultBlockSettings ); + + const result = validate( [ + { + name: 'core/test-block', + attributes: { fruit: 'Bananas' }, + originalContent: 'Bananas', + }, + ] ); + + expect( result ).toBeUndefined(); + } ); + + it( 'mutates the block with the validation result', () => { + registerBlockType( 'core/test-block', defaultBlockSettings ); + + const block = { + name: 'core/test-block', + attributes: { fruit: 'Bananas' }, + originalContent: 'Bananas', + }; + + validate( block ); + + expect( block.isValid ).toBe( true ); + } ); + + it( 'mutates the blocks array with the validation result', () => { + registerBlockType( 'core/test-block', defaultBlockSettings ); + + const block = { + name: 'core/test-block', + attributes: { fruit: 'Bananas' }, + originalContent: 'Bananas', + }; + + validate( [ block ] ); + + expect( block.isValid ).toBe( true ); + } ); + } ); } ); diff --git a/packages/blocks/src/api/validation.js b/packages/blocks/src/api/validation.js index ff37b5a73872ca..43498ab6af2a2f 100644 --- a/packages/blocks/src/api/validation.js +++ b/packages/blocks/src/api/validation.js @@ -9,6 +9,7 @@ import { isEqual, includes, stubTrue, + castArray, } from 'lodash'; /** @@ -21,6 +22,7 @@ import { decodeEntities } from '@wordpress/html-entities'; */ import { getSaveContent } from './serializer'; import { normalizeBlockType } from './utils'; +import { getBlockType } from './registration'; /** * Globally matches any consecutive whitespace @@ -649,3 +651,23 @@ export function isValidBlockContent( blockTypeOrName, attributes, originalBlockC return isValid; } + +/** + * Given a block or array of blocks, assigns the `isValid` property of each + * block corresponding to the validation result. This mutates the original + * array or object. + * + * @param {(WPBlock|WPBlock[])} blocks Block or array of blocks to validate. + */ +export default function validate( blocks ) { + // Normalize value to array (support singular argument). + blocks = castArray( blocks ); + + for ( const block of blocks ) { + block.isValid = isValidBlockContent( + getBlockType( block.name ), + block.attributes, + block.originalContent + ); + } +} diff --git a/packages/editor/src/components/post-text-editor/index.js b/packages/editor/src/components/post-text-editor/index.js index 667aaa76fcd700..861758eb95a78a 100644 --- a/packages/editor/src/components/post-text-editor/index.js +++ b/packages/editor/src/components/post-text-editor/index.js @@ -99,8 +99,8 @@ export default compose( [ editPost( { content } ); }, onPersist( content ) { - const blocks = parse( content ); - resetEditorBlocks( blocks ); + const blocks = parse( content, { validate: false } ); + resetEditorBlocks( blocks, { validate: true } ); }, }; } ), diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 8ea2c9a408de84..64171b689d9d32 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -11,6 +11,7 @@ import deprecated from '@wordpress/deprecated'; import { dispatch, select, apiFetch } from '@wordpress/data-controls'; import { parse, + validate, synchronizeBlocksWithTemplate, } from '@wordpress/blocks'; import isShallowEqual from '@wordpress/is-shallow-equal'; @@ -36,6 +37,17 @@ import { import { awaitNextStateChange, getRegistry } from './controls'; import * as sources from './block-sources'; +/** + * Default values for `resetEditorBlocks` action creator options. + * + * @typedef {Object} WPEditorResetEditorBlocksActionOptions + * + * @property {boolean} validate Whether to run validator over provided blocks. + */ +const DEFAULT_RESET_EDITOR_BLOCKS_OPTIONS = { + validate: false, +}; + /** * Map of Registry instance to WeakMap of dependencies by custom source. * @@ -167,7 +179,7 @@ export function* setupEditor( post, edits, template ) { content = post.content.raw; } - let blocks = parse( content ); + let blocks = parse( content, { validate: false } ); // Apply a template for new posts only, if exists. const isNewPost = post.status === 'auto-draft'; @@ -183,7 +195,7 @@ export function* setupEditor( post, edits, template ) { edits, template, }; - yield resetEditorBlocks( blocks ); + yield resetEditorBlocks( blocks, { validate: ! isNewPost } ); yield setupEditorState( post ); yield* __experimentalSubscribeSources(); } @@ -901,12 +913,16 @@ export function unlockPostSaving( lockName ) { /** * Returns an action object used to signal that the blocks have been updated. * - * @param {Array} blocks Block Array. - * @param {?Object} options Optional options. + * @param {Array} blocks Block Array. + * @param {?WPEditorResetEditorBlocksActionOptions} options Optional options. * * @return {Object} Action object */ -export function* resetEditorBlocks( blocks, options = {} ) { +export function* resetEditorBlocks( blocks, options = DEFAULT_RESET_EDITOR_BLOCKS_OPTIONS ) { + if ( options !== DEFAULT_RESET_EDITOR_BLOCKS_OPTIONS ) { + options = { ...DEFAULT_RESET_EDITOR_BLOCKS_OPTIONS, ...options }; + } + const lastBlockAttributesChange = yield select( 'core/block-editor', '__experimentalGetLastBlockAttributeChanges' ); // Sync to sources from block attributes updates. @@ -943,6 +959,10 @@ export function* resetEditorBlocks( blocks, options = {} ) { yield* resetLastBlockSourceDependencies( Array.from( updatedSources ) ); } + if ( options.validate ) { + validate( blocks ); + } + return { type: 'RESET_EDITOR_BLOCKS', blocks: yield* getBlocksWithSourcedAttributes( blocks ),