From 248697a5fc95b92bd7d2fc08fdaf8de1d44fd492 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 28 Jun 2018 14:12:26 -0400 Subject: [PATCH] Parser: Parse superfluous classes as custom classes (#7538) * Parser: Parse superfluous classes as custom classes * Documentation: Document blocks.getBlockAttributes filter --- blocks/api/index.js | 1 + blocks/api/parser.js | 9 +++- docs/extensibility/extending-blocks.md | 4 ++ editor/hooks/custom-class-name.js | 67 +++++++++++++++++++++++++- editor/hooks/test/custom-class-name.js | 62 +++++++++++++++++++++--- 5 files changed, 133 insertions(+), 10 deletions(-) diff --git a/blocks/api/index.js b/blocks/api/index.js index 1863c5f4284ba..33bee91bfbc17 100644 --- a/blocks/api/index.js +++ b/blocks/api/index.js @@ -18,6 +18,7 @@ export { getBlockDefaultClassName, getBlockMenuDefaultClassName, getSaveElement, + getSaveContent, } from './serializer'; export { isValidBlock } from './validation'; export { diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 60863e0829495..a45dedb1bd516 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -8,6 +8,7 @@ import { flow, castArray, mapValues, omit } from 'lodash'; * WordPress dependencies */ import { autop } from '@wordpress/autop'; +import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies @@ -182,7 +183,13 @@ export function getBlockAttributes( blockType, innerHTML, attributes ) { return getBlockAttribute( attributeKey, attributeSchema, innerHTML, attributes ); } ); - return blockAttributes; + return applyFilters( + 'blocks.getBlockAttributes', + blockAttributes, + blockType, + innerHTML, + attributes + ); } /** diff --git a/docs/extensibility/extending-blocks.md b/docs/extensibility/extending-blocks.md index e671c1f9dc673..94c0b66573896 100644 --- a/docs/extensibility/extending-blocks.md +++ b/docs/extensibility/extending-blocks.md @@ -66,6 +66,10 @@ Used internally by the default block (paragraph) to exclude the attributes from Used to filters an individual transform result from block transformation. All of the original blocks are passed, since transformations are many-to-many, not one-to-one. +#### `blocks.getBlockAttributes` + +Called immediately after the default parsing of a block's attributes and before validation to allow a plugin to manipulate attribute values in time for validation and/or the initial values rendering of the block in the editor. + #### `editor.BlockEdit` Used to modify the block's `edit` component. It receives the original block `BlockEdit` component and returns a new wrapped component. diff --git a/editor/hooks/custom-class-name.js b/editor/hooks/custom-class-name.js index 9d94b40e65398..0244405448d86 100644 --- a/editor/hooks/custom-class-name.js +++ b/editor/hooks/custom-class-name.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { assign } from 'lodash'; +import { assign, difference, compact } from 'lodash'; import classnames from 'classnames'; /** @@ -11,7 +11,11 @@ import { createHigherOrderComponent, Fragment } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; import { TextControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { hasBlockSupport } from '@wordpress/blocks'; +import { + hasBlockSupport, + parseWithAttributeSchema, + getSaveContent, +} from '@wordpress/blocks'; /** * Internal dependencies @@ -93,6 +97,65 @@ export function addSaveProps( extraProps, blockType, attributes ) { return extraProps; } +/** + * Given an HTML string, returns an array of class names assigned to the root + * element in the markup. + * + * @param {string} innerHTML Markup string from which to extract classes. + * + * @return {string[]} Array of class names assigned to the root element. + */ +export function getHTMLRootElementClasses( innerHTML ) { + innerHTML = `
${ innerHTML }
`; + + const parsed = parseWithAttributeSchema( innerHTML, { + type: 'string', + source: 'attribute', + selector: '[data-custom-class-name] > *', + attribute: 'class', + } ); + + return parsed ? parsed.trim().split( /\s+/ ) : []; +} + +/** + * Given a parsed set of block attributes, if the block supports custom class + * names and an unknown class (per the block's serialization behavior) is + * found, the unknown classes are treated as custom classes. This prevents the + * block from being considered as invalid. + * + * @param {Object} blockAttributes Original block attributes. + * @param {Object} blockType Block type settings. + * @param {string} innerHTML Original block markup. + * + * @return {Object} Filtered block attributes. + */ +export function addParsedDifference( blockAttributes, blockType, innerHTML ) { + if ( hasBlockSupport( blockType, 'customClassName', true ) ) { + // To determine difference, serialize block given the known set of + // attributes. If there are classes which are mismatched with the + // incoming HTML of the block, add to filtered result. + const serialized = getSaveContent( blockType, blockAttributes ); + const classes = getHTMLRootElementClasses( serialized ); + const parsedClasses = getHTMLRootElementClasses( innerHTML ); + const customClasses = difference( parsedClasses, classes ); + + const filteredClassName = compact( [ + blockAttributes.className, + ...customClasses, + ] ).join( ' ' ); + + if ( filteredClassName ) { + blockAttributes.className = filteredClassName; + } else { + delete blockAttributes.className; + } + } + + return blockAttributes; +} + addFilter( 'blocks.registerBlockType', 'core/custom-class-name/attribute', addAttribute ); addFilter( 'editor.BlockEdit', 'core/editor/custom-class-name/with-inspector-control', withInspectorControl ); addFilter( 'blocks.getSaveContent.extraProps', 'core/custom-class-name/save-props', addSaveProps ); +addFilter( 'blocks.getBlockAttributes', 'core/custom-class-name/addParsedDifference', addParsedDifference ); diff --git a/editor/hooks/test/custom-class-name.js b/editor/hooks/test/custom-class-name.js index fc946d03c13d6..3414889aff7c5 100644 --- a/editor/hooks/test/custom-class-name.js +++ b/editor/hooks/test/custom-class-name.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { noop } from 'lodash'; - /** * WordPress dependencies */ @@ -11,11 +6,11 @@ import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ -import '../custom-class-name'; +import { getHTMLRootElementClasses } from '../custom-class-name'; describe( 'custom className', () => { const blockSettings = { - save: noop, + save: () =>
, category: 'common', title: 'block title', }; @@ -63,4 +58,57 @@ describe( 'custom className', () => { expect( extraProps.className ).toBe( 'foo bar' ); } ); } ); + + describe( 'getHTMLRootElementClasses', () => { + it( 'should return an empty array if there are no classes', () => { + const classes = getHTMLRootElementClasses( '
' ); + + expect( classes ).toEqual( [] ); + } ); + + it( 'return an array of parsed classes from inner HTML', () => { + const classes = getHTMLRootElementClasses( '
' ); + + expect( classes ).toEqual( [ 'foo', 'bar' ] ); + } ); + } ); + + describe( 'addParsedDifference', () => { + const addParsedDifference = applyFilters.bind( null, 'blocks.getBlockAttributes' ); + + it( 'should do nothing if the block settings do not define custom className support', () => { + const attributes = addParsedDifference( + { className: 'foo' }, + { + ...blockSettings, + supports: { + customClassName: false, + }, + }, + '
' + ); + + expect( attributes.className ).toBe( 'foo' ); + } ); + + it( 'should inject the className differences from parsed attributes', () => { + const attributes = addParsedDifference( + { className: 'foo' }, + blockSettings, + '
' + ); + + expect( attributes.className ).toBe( 'foo bar baz' ); + } ); + + it( 'should assign as undefined if there are no classes', () => { + const attributes = addParsedDifference( + {}, + blockSettings, + '
' + ); + + expect( attributes.className ).toBeUndefined(); + } ); + } ); } );