From a634252470ce307cf95d4c6c251c863dc4ff6f8d Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 11 Sep 2018 16:58:05 -0400 Subject: [PATCH] Editor: Consider single unmodified default block as empty content --- packages/editor/src/hooks/index.js | 1 + packages/editor/src/hooks/process-content.js | 91 +++++++++ .../editor/src/hooks/test/process-content.js | 179 ++++++++++++++++++ packages/editor/src/store/selectors.js | 71 +++++-- 4 files changed, 331 insertions(+), 11 deletions(-) create mode 100644 packages/editor/src/hooks/process-content.js create mode 100644 packages/editor/src/hooks/test/process-content.js diff --git a/packages/editor/src/hooks/index.js b/packages/editor/src/hooks/index.js index e464747bf6e95..10ad5e1bdd286 100644 --- a/packages/editor/src/hooks/index.js +++ b/packages/editor/src/hooks/index.js @@ -7,3 +7,4 @@ import './custom-class-name'; import './default-autocompleters'; import './generated-class-name'; import './layout'; +import './process-content'; diff --git a/packages/editor/src/hooks/process-content.js b/packages/editor/src/hooks/process-content.js new file mode 100644 index 0000000000000..11908ad068199 --- /dev/null +++ b/packages/editor/src/hooks/process-content.js @@ -0,0 +1,91 @@ +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { removep } from '@wordpress/autop'; +import { + isUnmodifiedDefaultBlock, + getUnknownTypeHandlerName, +} from '@wordpress/blocks'; + +/** + * Returns true if the given array of blocks consists of a single block of the + * unknown type, or false otherwise. + * + * @param {WPBlock[]} blocks Array of block objects. + * + * @return {boolean} Whether array consists of single unknown block. + */ +export function isSingleUnknownBlock( blocks ) { + return ( + blocks.length === 1 && + blocks[ 0 ].name === getUnknownTypeHandlerName() + ); +} + +/** + * Returns true if the given array of blocks consists of a single unmodified + * default block, or false otherwise. + * + * @param {WPBlock[]} blocks Array of block objects. + * + * @return {boolean} Whether array consists of single unmodified default block. + */ +export function isSingleUnmodifiedDefaultBlock( blocks ) { + return ( + blocks.length === 1 && + isUnmodifiedDefaultBlock( blocks[ 0 ] ) + ); +} + +/** + * Given an array of blocks, returns either an empty array if the blocks + * consist only of a single unmodified default block. Otherwise returns the + * original array unmodified. + * + * @param {WPBlock[]} blocks Array of blocks. + * + * @return {WPBlock[]} Empty array, or original passed set of blocks. + */ +export function omitSingleUnmodifiedDefaultBlock( blocks ) { + if ( isSingleUnmodifiedDefaultBlock( blocks ) ) { + blocks = []; + } + + return blocks; +} + +/** + * Given an HTML string and array of blocks, returns a formatted HTML string + * with paragraph tags removed via `removep` behavior if the array of blocks + * consists only of a single block of the unknown type. + * + * @link https://www.npmjs.com/package/@wordpress/autop + * + * @param {string} content HTML content. + * @param {WPBlock[]} blocks Array of blocks from which HTML content has been + * generated. + * + * @return {string} HTML content, with `removep` filtering applied if the array + * of blocks from which it was generated consists only of a + * single block of the unknown type. + */ +export function removepSingleUnknownBlock( content, blocks ) { + if ( isSingleUnknownBlock( blocks ) ) { + content = removep( content ); + } + + return content; +} + +addFilter( + 'editor.selectors.getBlockContent', + 'core/processContent/removepSingleUnknownBlock', + removepSingleUnknownBlock +); + +addFilter( + 'editor.selectors.getBlocksForSave', + 'core/processContent/omitSingleUnmodifiedDefaultBlock', + omitSingleUnmodifiedDefaultBlock +); diff --git a/packages/editor/src/hooks/test/process-content.js b/packages/editor/src/hooks/test/process-content.js new file mode 100644 index 0000000000000..21d9ada14a67a --- /dev/null +++ b/packages/editor/src/hooks/test/process-content.js @@ -0,0 +1,179 @@ +/** + * WordPress dependencies + */ +import { + getBlockTypes, + unregisterBlockType, + registerBlockType, + createBlock, + getDefaultBlockName, + setDefaultBlockName, + getUnknownTypeHandlerName, + setUnknownTypeHandlerName, +} from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { + isSingleUnknownBlock, + isSingleUnmodifiedDefaultBlock, + omitSingleUnmodifiedDefaultBlock, + removepSingleUnknownBlock, +} from '../process-content'; + +describe( 'processContent', () => { + let originalDefaultBlockName, originalUnknownTypeHandlerName; + + beforeAll( () => { + originalDefaultBlockName = getDefaultBlockName(); + originalUnknownTypeHandlerName = getUnknownTypeHandlerName(); + + registerBlockType( 'core/default', { + category: 'common', + title: 'default', + attributes: { + modified: { + type: 'boolean', + default: false, + }, + }, + save: () => null, + } ); + registerBlockType( 'core/unknown', { + category: 'common', + title: 'unknown', + save: () => null, + } ); + setDefaultBlockName( 'core/default' ); + setUnknownTypeHandlerName( 'core/unknown' ); + } ); + + afterAll( () => { + setDefaultBlockName( originalDefaultBlockName ); + setUnknownTypeHandlerName( originalUnknownTypeHandlerName ); + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); + } ); + + describe( 'isSingleUnknownBlock', () => { + it( 'returns false if multiple blocks passed', () => { + const blocks = [ + createBlock( getUnknownTypeHandlerName() ), + createBlock( getUnknownTypeHandlerName() ), + ]; + + const result = isSingleUnknownBlock( blocks ); + + expect( result ).toBe( false ); + } ); + + it( 'returns false if single block not of unknown type', () => { + const blocks = [ + createBlock( getDefaultBlockName() ), + ]; + + const result = isSingleUnknownBlock( blocks ); + + expect( result ).toBe( false ); + } ); + + it( 'returns true if single block of unknown type', () => { + const blocks = [ + createBlock( getUnknownTypeHandlerName() ), + ]; + + const result = isSingleUnknownBlock( blocks ); + + expect( result ).toBe( true ); + } ); + } ); + + describe( 'isSingleUnmodifiedDefaultBlock', () => { + it( 'returns false if multiple blocks passed', () => { + const blocks = [ + createBlock( getDefaultBlockName() ), + createBlock( getDefaultBlockName() ), + ]; + + const result = isSingleUnmodifiedDefaultBlock( blocks ); + + expect( result ).toBe( false ); + } ); + + it( 'returns false if single non-default block', () => { + const blocks = [ + createBlock( getUnknownTypeHandlerName() ), + ]; + + const result = isSingleUnmodifiedDefaultBlock( blocks ); + + expect( result ).toBe( false ); + } ); + + it( 'returns false if single modified default block', () => { + const blocks = [ + createBlock( getDefaultBlockName(), { modified: true } ), + ]; + + const result = isSingleUnmodifiedDefaultBlock( blocks ); + + expect( result ).toBe( false ); + } ); + + it( 'returns true if single unmodified default block', () => { + const blocks = [ + createBlock( getDefaultBlockName() ), + ]; + + const result = isSingleUnmodifiedDefaultBlock( blocks ); + + expect( result ).toBe( true ); + } ); + } ); + + describe( 'omitSingleUnmodifiedDefaultBlock', () => { + it( 'returns original array of blocks if not single unmodified default block', () => { + const blocks = [ + createBlock( getUnknownTypeHandlerName() ), + ]; + + const result = omitSingleUnmodifiedDefaultBlock( blocks ); + + expect( result ).toBe( blocks ); + } ); + + it( 'returns an empty array if single unmodified default block', () => { + const blocks = [ + createBlock( getDefaultBlockName() ), + ]; + + const result = omitSingleUnmodifiedDefaultBlock( blocks ); + + expect( result ).toEqual( [] ); + } ); + } ); + + describe( 'removepSingleUnknownBlock', () => { + it( 'returns original content if not single block of unknown type', () => { + const blocks = [ + createBlock( getDefaultBlockName() ), + ]; + + const result = removepSingleUnknownBlock( '

foo

', blocks ); + + expect( result ).toBe( '

foo

' ); + } ); + + it( 'returns removep-formatted content if single block of unknown type', () => { + const blocks = [ + createBlock( getUnknownTypeHandlerName() ), + ]; + + const result = removepSingleUnknownBlock( '

foo

', blocks ); + + expect( result ).toBe( 'foo' ); + } ); + } ); +} ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 823fbe522f363..1f38f2dbb30ab 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -23,9 +23,15 @@ import createSelector from 'rememo'; /** * WordPress dependencies */ -import { serialize, getBlockType, getBlockTypes, hasBlockSupport, hasChildBlocks, getUnknownTypeHandlerName } from '@wordpress/blocks'; +import { + serialize, + getBlockType, + getBlockTypes, + hasBlockSupport, + hasChildBlocks, +} from '@wordpress/blocks'; import { moment } from '@wordpress/date'; -import { removep } from '@wordpress/autop'; +import { applyFilters } from '@wordpress/hooks'; /*** * Module constants @@ -349,15 +355,20 @@ export function isEditedPostSaveable( state ) { /** * Returns true if the edited post has content. A post has content if it has at - * least one block or otherwise has a non-empty content property assigned. + * least one saveable block or otherwise has a non-empty content property + * assigned. * * @param {Object} state Global application state. * * @return {boolean} Whether post has content. */ export function isEditedPostEmpty( state ) { + // While the condition of truthy content string would be sufficient for + // determining emptiness, testing saveable blocks length is a trivial + // operation by comparison. Since this function can be called frequently, + // optimize for the fast case where saveable blocks are non-empty. return ( - ! getBlockCount( state ) && + ! getBlocksForSave( state ).length && ! getEditedPostAttribute( state, 'content' ) ); } @@ -1346,6 +1357,50 @@ export function getSuggestedPostFormat( state ) { return null; } +/** + * Returns a filtered set of blocks which are assumed to be used in + * consideration of the post's generated save content. + * + * @param {Object} state Editor state. + * + * @return {WPBlock[]} Filtered set of blocks for save. + */ +export function getBlocksForSave( state ) { + /** + * Filters the set of blocks assumed to be used in consideration of the + * post's generated save content. + * + * @param {Object[]} blocks Original editor state blocks. + */ + return applyFilters( + 'editor.selectors.getBlocksForSave', + getBlocks( state ), + ); +} + +/** + * Returns a filtered HTML string for serialized given array of blocks. + * + * @param {Object} state Editor state. + * @param {WPBlock[]} blocks Array of blocks from which to generate content. + * + * @return {string} Filtered HTML string for serialized blocks. + */ +export function getBlockContent( state, blocks ) { + /** + * Filters the serialized content result of blocks. + * + * @param {string} content Original serialized value. + * @param {WPBlock[]} blocks Blocks from which serialized value is + * generated. + */ + return applyFilters( + 'editor.selectors.getBlockContent', + serialize( blocks ), + blocks + ); +} + /** * Returns the content of the post being edited, preferring raw string edit * before falling back to serialization of block state. @@ -1361,13 +1416,7 @@ export const getEditedPostContent = createSelector( return edits.content; } - const blocks = getBlocks( state ); - - if ( blocks.length === 1 && blocks[ 0 ].name === getUnknownTypeHandlerName() ) { - return removep( serialize( blocks ) ); - } - - return serialize( blocks ); + return getBlockContent( state, getBlocksForSave( state ) ); }, ( state ) => [ state.editor.present.edits.content,