diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 63ad01f98ab358..63af6229e39912 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 aed8cb12f3c88e..2657e727c30f56 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 4d2ba513181800..17632802137d79 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 73705b0846c108..97dda9b7ea30a3 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 28c7c26cd2cc2a..71eac6be8e7561 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 e6af9836343d1e..2d00b4dc7b234c 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 e76615bc0388e9..513aedf310c6b7 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 71dbaaf017f04c..e56ce920fbf162 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 b8bceecf41f82e..0064f0f01cf017 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 088e26a2d92f0a..53e5e64c35c05c 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 2957236d56242b..be431d695e3be8 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 114a32462aea3a..89950d45b7167a 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 109f466bbd19c5..8b567a80e83715 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 dd76ad46a3da74..f317088d34d1a4 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 b6f8ab20095fc7..6f6d6c0ae15077 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 c068dfcf2de5a3..53bdd684bcb6fe 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 d835a483147f13..5e3a7c0714b35d 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 e41e915f38069c..49b1d5e887f055 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 65a819fd91c6f2..e841a0450bb7c7 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 @@