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 ),