From 4b4b014c429315d5a1f39b8cb1fd97ec03c68737 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 11 Sep 2018 16:58:05 -0400 Subject: [PATCH 1/2] 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 +++++-- .../splitting-merging.test.js.snap | 6 +- test/e2e/specs/splitting-merging.test.js | 10 + 6 files changed, 346 insertions(+), 12 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 81bc282d774e6..5c6f9b7c19bd8 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, hasChildBlocksWithInserterSupport, getUnknownTypeHandlerName } from '@wordpress/blocks'; +import { + serialize, + getBlockType, + getBlockTypes, + hasBlockSupport, + hasChildBlocksWithInserterSupport, +} 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, diff --git a/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap b/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap index 2fddf0347d889..f449ce86a0568 100644 --- a/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap +++ b/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap @@ -2,7 +2,11 @@ exports[`splitting and merging blocks should delete an empty first line 1`] = ` " -

+

First

+ + + +

Still Second

" `; diff --git a/test/e2e/specs/splitting-merging.test.js b/test/e2e/specs/splitting-merging.test.js index 2a1bf6337e01e..fdb773826188f 100644 --- a/test/e2e/specs/splitting-merging.test.js +++ b/test/e2e/specs/splitting-merging.test.js @@ -75,7 +75,13 @@ describe( 'splitting and merging blocks', () => { // should remove the first. // // See: https://github.com/WordPress/gutenberg/issues/8388 + + // First paragraph await insertBlock( 'Paragraph' ); + await page.keyboard.type( 'First' ); + await page.keyboard.press( 'Enter' ); + + // Second paragraph await page.keyboard.down( 'Shift' ); await page.keyboard.press( 'Enter' ); await page.keyboard.up( 'Shift' ); @@ -83,6 +89,10 @@ describe( 'splitting and merging blocks', () => { // Delete the soft line break. await page.keyboard.press( 'Backspace' ); + // Typing at this point should occur still within the second paragraph, + // while before the regression fix it would have occurred in the first. + await page.keyboard.type( 'Still Second' ); + expect( await getEditedPostContent() ).toMatchSnapshot(); } ); From f8e729becf3adbd09b9e03ddeffff4e1158ae4be Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 14 Sep 2018 17:11:50 -0400 Subject: [PATCH 2/2] Editor: Implement content processing inline --- 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 | 72 +++--- packages/editor/src/store/test/selectors.js | 214 +++++++++++++++++- 5 files changed, 243 insertions(+), 314 deletions(-) delete mode 100644 packages/editor/src/hooks/process-content.js delete 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 10ad5e1bdd286..e464747bf6e95 100644 --- a/packages/editor/src/hooks/index.js +++ b/packages/editor/src/hooks/index.js @@ -7,4 +7,3 @@ 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 deleted file mode 100644 index 11908ad068199..0000000000000 --- a/packages/editor/src/hooks/process-content.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * 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 deleted file mode 100644 index 21d9ada14a67a..0000000000000 --- a/packages/editor/src/hooks/test/process-content.js +++ /dev/null @@ -1,179 +0,0 @@ -/** - * 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 5c6f9b7c19bd8..42d11366638d9 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -29,9 +29,11 @@ import { getBlockTypes, hasBlockSupport, hasChildBlocksWithInserterSupport, + getUnknownTypeHandlerName, + isUnmodifiedDefaultBlock, } from '@wordpress/blocks'; import { moment } from '@wordpress/date'; -import { applyFilters } from '@wordpress/hooks'; +import { removep } from '@wordpress/autop'; /*** * Module constants @@ -368,7 +370,7 @@ export function isEditedPostEmpty( state ) { // operation by comparison. Since this function can be called frequently, // optimize for the fast case where saveable blocks are non-empty. return ( - ! getBlocksForSave( state ).length && + ! getBlocksForSerialization( state ).length && ! getEditedPostAttribute( state, 'content' ) ); } @@ -1358,47 +1360,28 @@ export function getSuggestedPostFormat( state ) { } /** - * Returns a filtered set of blocks which are assumed to be used in - * consideration of the post's generated save content. + * Returns a set of blocks which are 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 ), - ); -} +export function getBlocksForSerialization( state ) { + const blocks = 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 + // A single unmodified default block is assumed to be equivalent to an + // empty post. + const isSingleUnmodifiedDefaultBlock = ( + blocks.length === 1 && + isUnmodifiedDefaultBlock( blocks[ 0 ] ) ); + + if ( isSingleUnmodifiedDefaultBlock ) { + return []; + } + + return blocks; } /** @@ -1416,7 +1399,22 @@ export const getEditedPostContent = createSelector( return edits.content; } - return getBlockContent( state, getBlocksForSave( state ) ); + const blocks = getBlocksForSerialization( state ); + const content = serialize( blocks ); + + // For compatibility purposes, treat a post consisting of a single + // unknown block as legacy content and downgrade to a pre-block-editor + // removep'd content format. + const isSingleUnknownBlock = ( + blocks.length === 1 && + blocks[ 0 ].name === getUnknownTypeHandlerName() + ); + + if ( isSingleUnknownBlock ) { + return removep( content ); + } + + return content; }, ( state ) => [ state.editor.present.edits.content, diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 60bde150d492a..d02f9092778da 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -6,8 +6,18 @@ import { filter, without } from 'lodash'; /** * WordPress dependencies */ -import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; +import { + registerBlockType, + unregisterBlockType, + createBlock, + getBlockTypes, + getDefaultBlockName, + setDefaultBlockName, + getUnknownTypeHandlerName, + setUnknownTypeHandlerName, +} from '@wordpress/blocks'; import { moment } from '@wordpress/date'; +import { RawHTML } from '@wordpress/element'; /** * Internal dependencies @@ -74,6 +84,7 @@ const { didPostSaveRequestSucceed, didPostSaveRequestFail, getSuggestedPostFormat, + getEditedPostContent, getNotices, getReusableBlock, isSavingReusableBlock, @@ -101,6 +112,10 @@ describe( 'selectors', () => { let cachedSelectors; beforeAll( () => { + cachedSelectors = filter( selectors, ( selector ) => selector.clear ); + } ); + + beforeEach( () => { registerBlockType( 'core/block', { save: () => null, category: 'reusable', @@ -138,14 +153,10 @@ describe( 'selectors', () => { parent: [ 'core/test-block-b' ], } ); - cachedSelectors = filter( selectors, ( selector ) => selector.clear ); - } ); - - beforeEach( () => { cachedSelectors.forEach( ( { clear } ) => clear() ); } ); - afterAll( () => { + afterEach( () => { unregisterBlockType( 'core/block' ); unregisterBlockType( 'core/test-block-a' ); unregisterBlockType( 'core/test-block-b' ); @@ -2872,6 +2883,197 @@ describe( 'selectors', () => { } ); } ); + describe( 'getEditedPostContent', () => { + 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', + attributes: { + html: { + type: 'string', + }, + }, + save: ( { attributes } ) => { attributes.html }, + } ); + setDefaultBlockName( 'core/default' ); + setUnknownTypeHandlerName( 'core/unknown' ); + } ); + + afterAll( () => { + setDefaultBlockName( originalDefaultBlockName ); + setUnknownTypeHandlerName( originalUnknownTypeHandlerName ); + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); + } ); + + it( 'defers to returning an edited post attribute', () => { + const block = createBlock( 'core/block' ); + + const state = { + editor: { + present: { + blockOrder: { + '': [ block.clientId ], + }, + blocksByClientId: { + [ block.clientId ]: block, + }, + edits: { + content: 'custom edit', + }, + }, + }, + currentPost: {}, + }; + + const content = getEditedPostContent( state ); + + expect( content ).toBe( 'custom edit' ); + } ); + + it( 'returns serialization of blocks', () => { + const block = createBlock( 'core/block' ); + + const state = { + editor: { + present: { + blockOrder: { + '': [ block.clientId ], + }, + blocksByClientId: { + [ block.clientId ]: block, + }, + edits: {}, + }, + }, + currentPost: {}, + }; + + const content = getEditedPostContent( state ); + + expect( content ).toBe( '' ); + } ); + + it( 'returns removep\'d serialization of blocks for single unknown', () => { + const unknownBlock = createBlock( getUnknownTypeHandlerName(), { + html: '

foo

', + } ); + const state = { + editor: { + present: { + blockOrder: { + '': [ unknownBlock.clientId ], + }, + blocksByClientId: { + [ unknownBlock.clientId ]: unknownBlock, + }, + edits: {}, + }, + }, + currentPost: {}, + }; + + const content = getEditedPostContent( state ); + + expect( content ).toBe( 'foo' ); + } ); + + it( 'returns non-removep\'d serialization of blocks for multiple unknown', () => { + const firstUnknown = createBlock( getUnknownTypeHandlerName(), { + html: '

foo

', + } ); + const secondUnknown = createBlock( getUnknownTypeHandlerName(), { + html: '

bar

', + } ); + const state = { + editor: { + present: { + blockOrder: { + '': [ firstUnknown.clientId, secondUnknown.clientId ], + }, + blocksByClientId: { + [ firstUnknown.clientId ]: firstUnknown, + [ secondUnknown.clientId ]: secondUnknown, + }, + edits: {}, + }, + }, + currentPost: {}, + }; + + const content = getEditedPostContent( state ); + + expect( content ).toBe( '

foo

\n\n

bar

' ); + } ); + + it( 'returns empty string for single unmodified default block', () => { + const defaultBlock = createBlock( getDefaultBlockName() ); + const state = { + editor: { + present: { + blockOrder: { + '': [ defaultBlock.clientId ], + }, + blocksByClientId: { + [ defaultBlock.clientId ]: defaultBlock, + }, + edits: {}, + }, + }, + currentPost: {}, + }; + + const content = getEditedPostContent( state ); + + expect( content ).toBe( '' ); + } ); + + it( 'should not return empty string for modified default block', () => { + const defaultBlock = createBlock( getDefaultBlockName() ); + const state = { + editor: { + present: { + blockOrder: { + '': [ defaultBlock.clientId ], + }, + blocksByClientId: { + [ defaultBlock.clientId ]: { + ...defaultBlock, + attributes: { + ...defaultBlock.attributes, + modified: true, + }, + }, + }, + edits: {}, + }, + }, + currentPost: {}, + }; + + const content = getEditedPostContent( state ); + + expect( content ).toBe( '' ); + } ); + } ); + describe( 'getNotices', () => { it( 'should return the notices array', () => { const state = {