diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 63ad01f98ab35..63af6229e3991 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -10,6 +10,7 @@ import { pickBy } from 'lodash'; import { parse as grammarParse } from './post.pegjs'; import { getBlockType, getUnknownTypeHandler } from './registration'; import { createBlock } from './factory'; +import { getBeautifulContent, getSaveContent } from './serializer'; /** * Returns the block attributes parsed from raw content. @@ -81,18 +82,59 @@ export function createBlockWithFallback( name, rawContent, attributes ) { // Include in set only if type were determined. // TODO do we ever expect there to not be an unknown type handler? - if ( blockType && ( rawContent.trim() || name !== fallbackBlock ) ) { + if ( blockType && ( rawContent || name !== fallbackBlock ) ) { // TODO allow blocks to opt-in to receiving a tree instead of a string. // Gradually convert all blocks to this new format, then remove the // string serialization. const block = createBlock( name, - getBlockAttributes( blockType, rawContent.trim(), attributes ) + getBlockAttributes( blockType, rawContent, attributes ) ); + + // Validate that the parsed block is valid, meaning that if we were to + // reserialize it given the assumed attributes, the markup matches the + // original value. Otherwise, preserve original to avoid destruction. + block.isValid = isValidBlock( rawContent, blockType, block.attributes ); + if ( ! block.isValid ) { + block.originalContent = rawContent; + } + return block; } } +/** + * Returns true if the parsed block is valid given the input content. A block + * is considered valid if, when serialized with assumed attributes, the content + * matches the original value. + * + * Logs to console in development environments when invalid. + * + * @param {String} rawContent Original block content + * @param {String} blockType Block type + * @param {Object} attributes Parsed block attributes + * @return {Boolean} Whether block is valid + */ +export function isValidBlock( rawContent, blockType, attributes ) { + const [ actual, expected ] = [ + rawContent, + getSaveContent( blockType, attributes ), + ].map( getBeautifulContent ); + + const isValid = ( actual === expected ); + + if ( ! isValid && 'development' === process.env.NODE_ENV ) { + // eslint-disable-next-line no-console + console.error( + 'Invalid block parse\n' + + '\tExpected: ' + expected + '\n' + + '\tActual: ' + actual + ); + } + + return isValid; +} + /** * Parses the post content with a PegJS grammar and returns a list of blocks. * @@ -102,7 +144,7 @@ export function createBlockWithFallback( name, rawContent, attributes ) { export function parseWithGrammar( content ) { return grammarParse( content ).reduce( ( memo, blockNode ) => { const { blockName, rawContent, attrs } = blockNode; - const block = createBlockWithFallback( blockName, rawContent, attrs ); + const block = createBlockWithFallback( blockName, rawContent.trim(), attrs ); if ( block ) { memo.push( block ); } diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index aed8cb12f3c88..2657e727c30f5 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -111,10 +111,33 @@ export function serializeAttributes( attrs ) { .replace( /&/g, '\\u0026' ); // ibid } +/** + * Returns HTML markup processed by a markup beautifier configured for use in + * block serialization. + * + * @param {String} content Original HTML + * @return {String} Beautiful HTML + */ +export function getBeautifulContent( content ) { + return beautifyHtml( content, { + indent_inner_html: true, + wrap_line_length: 0, + } ); +} + export function serializeBlock( block ) { const blockName = block.name; const blockType = getBlockType( blockName ); - const saveContent = getSaveContent( blockType, block.attributes ); + + let saveContent; + if ( block.isValid ) { + saveContent = getSaveContent( blockType, block.attributes ); + } else { + // If block was parsed as invalid, skip serialization behavior and opt + // to use original content instead so we don't destroy user content. + saveContent = block.originalContent; + } + const saveAttributes = getCommentAttributes( block.attributes, parseBlockAttributes( saveContent, blockType.attributes ) ); if ( 'wp:core/more' === blockName ) { @@ -131,13 +154,7 @@ export function serializeBlock( block ) { return ( `\n` + - - /** make more readable - @see https://github.com/WordPress/gutenberg/pull/663 */ - beautifyHtml( saveContent, { - indent_inner_html: true, - wrap_line_length: 0, - } ) + - + getBeautifulContent( saveContent ) + `\n` ); } diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 4d2ba51318180..17632802137d7 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -10,6 +10,7 @@ import { text } from '../query'; import { getBlockAttributes, parseBlockAttributes, + isValidBlock, createBlockWithFallback, default as parse, } from '../parser'; @@ -17,11 +18,14 @@ import { registerBlockType, unregisterBlockType, getBlockTypes, + getBlockType, setUnknownTypeHandler, } from '../registration'; describe( 'block parser', () => { - const defaultBlockSettings = { save: noop }; + const defaultBlockSettings = { + save: ( { attributes } ) => attributes.fruit, + }; afterEach( () => { setUnknownTypeHandler( undefined ); @@ -89,6 +93,28 @@ describe( 'block parser', () => { } ); } ); + describe( 'isValidBlock()', () => { + it( 'returns false is block is not valid', () => { + registerBlockType( 'core/test-block', defaultBlockSettings ); + + expect( isValidBlock( + 'Apples', + getBlockType( 'core/test-block' ), + { fruit: 'Bananas' } + ) ).toBe( false ); + } ); + + it( 'returns true is block is valid', () => { + registerBlockType( 'core/test-block', defaultBlockSettings ); + + expect( isValidBlock( + 'Bananas', + getBlockType( 'core/test-block' ), + { fruit: 'Bananas' } + ) ).toBe( true ); + } ); + } ); + describe( 'createBlockWithFallback', () => { it( 'should create the requested block if it exists', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); diff --git a/blocks/api/test/serializer.js b/blocks/api/test/serializer.js index 73705b0846c10..97dda9b7ea30a 100644 --- a/blocks/api/test/serializer.js +++ b/blocks/api/test/serializer.js @@ -6,7 +6,12 @@ import { createElement, Component } from 'element'; /** * Internal dependencies */ -import serialize, { getCommentAttributes, getSaveContent, serializeAttributes } from '../serializer'; +import serialize, { + getCommentAttributes, + getBeautifulContent, + getSaveContent, + serializeAttributes, +} from '../serializer'; import { getBlockTypes, registerBlockType, unregisterBlockType } from '../registration'; describe( 'block serializer', () => { @@ -16,6 +21,14 @@ describe( 'block serializer', () => { } ); } ); + describe( 'getBeautifulContent()', () => { + it( 'returns beautiful content', () => { + const content = getBeautifulContent( '
Ribs & Chicken
\n'; diff --git a/blocks/test/fixtures/core-embed__animoto.json b/blocks/test/fixtures/core-embed__animoto.json index 28c7c26cd2cc2..71eac6be8e756 100644 --- a/blocks/test/fixtures/core-embed__animoto.json +++ b/blocks/test/fixtures/core-embed__animoto.json @@ -7,6 +7,7 @@ "caption": [ "Embedded content from animoto" ] - } + }, + "isValid": true } ] diff --git a/blocks/test/fixtures/core-embed__animoto.serialized.html b/blocks/test/fixtures/core-embed__animoto.serialized.html index e6af9836343d1..2d00b4dc7b234 100644 --- a/blocks/test/fixtures/core-embed__animoto.serialized.html +++ b/blocks/test/fixtures/core-embed__animoto.serialized.html @@ -3,4 +3,4 @@ https://animoto.com/export default function MyButton() {
return <Button>Click Me!</Button>;
}
-
+
\ No newline at end of file
diff --git a/blocks/test/fixtures/core__cover-image.json b/blocks/test/fixtures/core__cover-image.json
index e76615bc0388e..513aedf310c6b 100644
--- a/blocks/test/fixtures/core__cover-image.json
+++ b/blocks/test/fixtures/core__cover-image.json
@@ -5,6 +5,7 @@
"attributes": {
"url": "https://cldup.com/uuUqE_dXzy.jpg",
"title": "Guten Berg!"
- }
+ },
+ "isValid": true
}
]
diff --git a/blocks/test/fixtures/core__cover-image.serialized.html b/blocks/test/fixtures/core__cover-image.serialized.html
index 71dbaaf017f04..e56ce920fbf16 100644
--- a/blocks/test/fixtures/core__cover-image.serialized.html
+++ b/blocks/test/fixtures/core__cover-image.serialized.html
@@ -4,4 +4,4 @@
Some preformatted text..." } ] diff --git a/blocks/test/fixtures/core__preformatted.serialized.html b/blocks/test/fixtures/core__preformatted.serialized.html index b8bceecf41f82..0064f0f01cf01 100644 --- a/blocks/test/fixtures/core__preformatted.serialized.html +++ b/blocks/test/fixtures/core__preformatted.serialized.html @@ -1,3 +1,3 @@ -
And more!
Some preformatted text...- +
And more!
Some preformatted text...+ \ No newline at end of file diff --git a/blocks/test/fixtures/core__pullquote.json b/blocks/test/fixtures/core__pullquote.json index 088e26a2d92f0..53e5e64c35c05 100644 --- a/blocks/test/fixtures/core__pullquote.json +++ b/blocks/test/fixtures/core__pullquote.json @@ -11,6 +11,8 @@ "citation": [ "...with a caption" ] - } + }, + "isValid": false, + "originalContent": "
And more!
\n" } ] diff --git a/blocks/test/fixtures/core__pullquote.serialized.html b/blocks/test/fixtures/core__pullquote.serialized.html index 2957236d56242..be431d695e3be 100644 --- a/blocks/test/fixtures/core__pullquote.serialized.html +++ b/blocks/test/fixtures/core__pullquote.serialized.html @@ -1,6 +1,6 @@ -Testing pullquote block...
\n
+- + \ No newline at end of file diff --git a/blocks/test/fixtures/core__quote__style-2.json b/blocks/test/fixtures/core__quote__style-2.json index 114a32462aea3..89950d45b7167 100644 --- a/blocks/test/fixtures/core__quote__style-2.json +++ b/blocks/test/fixtures/core__quote__style-2.json @@ -3,16 +3,17 @@ "uid": "_uid_0", "name": "core/quote", "attributes": { - "style": "2", "value": [ { - "children": "There is no greater agony than bearing an untold story inside you.", - "type": "p" + "type": "p", + "children": "There is no greater agony than bearing an untold story inside you." } ], + "style": "2", "citation": [ "Maya Angelou" ] - } + }, + "isValid": true } ] diff --git a/blocks/test/fixtures/core__quote__style-2.serialized.html b/blocks/test/fixtures/core__quote__style-2.serialized.html index 109f466bbd19c..8b567a80e8371 100644 --- a/blocks/test/fixtures/core__quote__style-2.serialized.html +++ b/blocks/test/fixtures/core__quote__style-2.serialized.html @@ -3,4 +3,4 @@- + \ No newline at end of file diff --git a/blocks/test/fixtures/core__quote__style-1.json b/blocks/test/fixtures/core__quote__style-1.json index dd76ad46a3da7..f317088d34d1a 100644 --- a/blocks/test/fixtures/core__quote__style-1.json +++ b/blocks/test/fixtures/core__quote__style-1.json @@ -3,16 +3,17 @@ "uid": "_uid_0", "name": "core/quote", "attributes": { - "style": "1", "value": [ { - "children": "The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery.", - "type": "p" + "type": "p", + "children": "The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery." } ], + "style": "1", "citation": [ "Matt Mullenweg, 2017" ] - } + }, + "isValid": true } ] diff --git a/blocks/test/fixtures/core__quote__style-1.serialized.html b/blocks/test/fixtures/core__quote__style-1.serialized.html index b6f8ab20095fc..6f6d6c0ae1507 100644 --- a/blocks/test/fixtures/core__quote__style-1.serialized.html +++ b/blocks/test/fixtures/core__quote__style-1.serialized.html @@ -3,4 +3,4 @@Testing pullquote block...
The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery.
There is no greater agony than bearing an untold story inside you.
- + \ No newline at end of file diff --git a/blocks/test/fixtures/core__separator.json b/blocks/test/fixtures/core__separator.json index c068dfcf2de5a..53bdd684bcb6f 100644 --- a/blocks/test/fixtures/core__separator.json +++ b/blocks/test/fixtures/core__separator.json @@ -2,6 +2,7 @@ { "uid": "_uid_0", "name": "core/separator", - "attributes": {} + "attributes": {}, + "isValid": true } ] diff --git a/blocks/test/fixtures/core__separator.serialized.html b/blocks/test/fixtures/core__separator.serialized.html index d835a483147f1..5e3a7c0714b35 100644 --- a/blocks/test/fixtures/core__separator.serialized.html +++ b/blocks/test/fixtures/core__separator.serialized.html @@ -1,3 +1,3 @@Version | Musician | Date |
---|---|---|
.70 | No musician chosen. | May 27, 2003 |
1.0 | Miles Davis | January 3, 2004 |
Lots of versions skipped, see the full list | … | … |
4.4 | Clifford Brown | December 8, 2015 |
4.5 | Coleman Hawkins | April 12, 2016 |
4.6 | Pepper Adams | August 16, 2016 |
4.7 | Sarah Vaughan | December 6, 2016 |
Version | @@ -20,8 +20,8 @@||||
---|---|---|---|---|
Lots of versions skipped, see the full list | -… | -… | +… | +… |
4.4 | diff --git a/blocks/test/fixtures/core__text__align-right.json b/blocks/test/fixtures/core__text__align-right.json index e41e915f38069..49b1d5e887f05 100644 --- a/blocks/test/fixtures/core__text__align-right.json +++ b/blocks/test/fixtures/core__text__align-right.json @@ -5,8 +5,11 @@ "attributes": { "align": "right", "content": [ - [ "... like this one, which is separate from the above and right aligned." ] + [ + "... like this one, which is separate from the above and right aligned." + ] ] - } + }, + "isValid": true } ] diff --git a/blocks/test/fixtures/core__text__align-right.serialized.html b/blocks/test/fixtures/core__text__align-right.serialized.html index 65a819fd91c6f..e841a0450bb7c 100644 --- a/blocks/test/fixtures/core__text__align-right.serialized.html +++ b/blocks/test/fixtures/core__text__align-right.serialized.html @@ -1,3 +1,3 @@