From b2c83a745434a848c471a06c8e8eec6ad0bf42a1 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 15 Jun 2017 15:16:03 +0200 Subject: [PATCH] Refactor serializer: rearrange code for clarity and introspection (#1148) * Refactor serializer: rearrange code for clarity and introspection These changes are intended to make the flow of transformation from in-memory block data to serialized `post_content` clearer. Additionally they are intended to ease testing and create points for easy trapping and transformation of the data through the pipeline. --- blocks/api/post.pegjs | 10 +- blocks/api/serializer.js | 132 +++++++++++------- blocks/api/test/serializer.js | 38 ++--- blocks/test/fixtures/core-latestposts.html | 1 - .../fixtures/core-latestposts.serialized.html | 1 - .../core-text-multi-paragraph.serialized.html | 1 - blocks/test/full-content.js | 2 +- post-content.js | 2 +- 8 files changed, 115 insertions(+), 72 deletions(-) diff --git a/blocks/api/post.pegjs b/blocks/api/post.pegjs index 3f3c9e9330710..6b98d88b41282 100644 --- a/blocks/api/post.pegjs +++ b/blocks/api/post.pegjs @@ -1,8 +1,14 @@ { +function untransformValue( value ) { + return 'string' === typeof value + ? value.replace( /\\-/g, '-' ) + : value; +} + function keyValue( key, value ) { const o = {}; - o[ key ] = value; + o[ key ] = untransformValue( value ); return o; } @@ -45,7 +51,7 @@ WP_Block_Html } WP_Block_Start - = "" + = "" { return { blockName: blockName, attrs: attrs diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index 5ccd305befc45..54e64d9ee162b 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { difference } from 'lodash'; +import { isEmpty, map, reduce } from 'lodash'; import { html as beautifyHtml } from 'js-beautify'; /** @@ -36,35 +36,94 @@ export function getSaveContent( save, attributes ) { return wp.element.renderToString( rawContent ); } +const escapeDoubleQuotes = value => value.replace( /"/g, '\"' ); +const escapeHyphens = value => value.replace( /-/g, '\\-' ); + +/** + * Transform value for storage in block comment + * + * Some special characters and sequences should not + * appear in a block comment header. This transformer + * will guarantee that we store the data safely. + * + * @param {*} value attribute value to serialize + * @returns {*} transformed value + */ +export const serializeValue = value => + 'string' === typeof value + ? escapeHyphens( escapeDoubleQuotes( value ) ) + : value; + /** - * Returns comment attributes as serialized string, determined by subset of - * difference between actual attributes of a block and those expected based - * on its settings. + * Returns attributes which ought to be saved + * and serialized into the block comment header + * + * When a block exists in memory it contains as its attributes + * both those which come from the block comment header _and_ + * those which come from parsing the contents of the block. + * + * This function returns only those attributes which are + * needed to persist and which cannot already be inferred + * from the block content. * - * @param {Object} realAttributes Actual block attributes - * @param {Object} expectedAttributes Expected block attributes - * @return {string} Comment attributes + * @param {Object} allAttributes Attributes from in-memory block data + * @param {Object} attributesFromContent Attributes which are inferred from block content + * @returns {Object} filtered set of attributes for minimum save/serialization */ -export function getCommentAttributes( realAttributes, expectedAttributes ) { - // Find difference and build into object subset of attributes. - const keys = difference( - Object.keys( realAttributes ), - Object.keys( expectedAttributes ) +export function getCommentAttributes( allAttributes, attributesFromContent ) { + // Iterate over attributes and produce the set to save + return reduce( + Object.keys( allAttributes ), + ( toSave, key ) => { + const allValue = allAttributes[ key ]; + const contentValue = attributesFromContent[ key ]; + + // save only if attribute if not inferred from the content and if valued + return ! ( contentValue !== undefined || allValue === undefined ) + ? Object.assign( toSave, { [ key ]: allValue } ) + : toSave; + }, + {}, ); +} - // Serialize the comment attributes as `key="value"`. - return keys.reduce( ( memo, key ) => { - const value = realAttributes[ key ]; - if ( undefined === value ) { - return memo; - } +/** + * Lodash iterator which transforms a key: value + * pair into a string of `key="value"` + * + * @param {*} value value to be stringified + * @param {String} key name of value + * @returns {string} stringified equality pair + */ +function asNameValuePair( value, key ) { + return `${ key }="${ serializeValue( value ) }"`; +} - if ( 'string' === typeof value ) { - return memo + `${ key }="${ value.replace( '"', '\"' ) }" `; - } +export function serializeBlock( block ) { + const blockName = block.name; + const blockType = getBlockType( blockName ); + const saveContent = getSaveContent( blockType.save, block.attributes ); + const saveAttributes = getCommentAttributes( block.attributes, parseBlockAttributes( saveContent, blockType ) ); + + const serializedAttributes = ! isEmpty( saveAttributes ) + ? map( saveAttributes, asNameValuePair ).join( ' ' ) + ' ' + : ''; + + if ( ! saveContent ) { + return ``; + } + + return ( + `\n` + + + /** make more readable - @see https://github.com/WordPress/gutenberg/pull/663 */ + beautifyHtml( saveContent, { + indent_inner_html: true, + wrap_line_length: 0, + } ) + - return memo + `${ key }="${ value }" `; - }, '' ); + `\n` + ); } /** @@ -74,30 +133,5 @@ export function getCommentAttributes( realAttributes, expectedAttributes ) { * @return {String} The post content */ export default function serialize( blocks ) { - return blocks.reduce( ( memo, block ) => { - const blockName = block.name; - const blockType = getBlockType( blockName ); - const saveContent = getSaveContent( blockType.save, block.attributes ); - const beautifyOptions = { - indent_inner_html: true, - wrap_line_length: 0, - }; - const blockAttributes = getCommentAttributes( block.attributes, parseBlockAttributes( saveContent, blockType ) ); - - if ( ! saveContent ) { - return memo + '\n\n'; - } - - return memo + ( - '' + - '\n' + beautifyHtml( saveContent, beautifyOptions ) + '\n' + - '' - ) + '\n\n'; - }, '' ); + return blocks.map( serializeBlock ).join( '\n\n' ); } diff --git a/blocks/api/test/serializer.js b/blocks/api/test/serializer.js index e59e15369f65f..ace404fbffdc9 100644 --- a/blocks/api/test/serializer.js +++ b/blocks/api/test/serializer.js @@ -6,7 +6,7 @@ import { expect } from 'chai'; /** * Internal dependencies */ -import serialize, { getCommentAttributes, getSaveContent } from '../serializer'; +import serialize, { getCommentAttributes, getSaveContent, serializeValue } from '../serializer'; import { getBlockTypes, registerBlockType, unregisterBlockType } from '../registration'; describe( 'block serializer', () => { @@ -56,13 +56,13 @@ describe( 'block serializer', () => { } ); describe( 'getCommentAttributes()', () => { - it( 'should return empty string if no difference', () => { + it( 'should return an empty set if no attributes provided', () => { const attributes = getCommentAttributes( {}, {} ); - expect( attributes ).to.equal( '' ); + expect( attributes ).to.eql( {} ); } ); - it( 'should return joined string of key:value pairs by difference subset', () => { + it( 'should only return attributes which cannot be inferred from the content', () => { const attributes = getCommentAttributes( { fruit: 'bananas', category: 'food', @@ -71,25 +71,31 @@ describe( 'block serializer', () => { fruit: 'bananas', } ); - expect( attributes ).to.equal( 'category="food" ripeness="ripe" ' ); + expect( attributes ).to.eql( { + category: 'food', + ripeness: 'ripe', + } ); } ); - it( 'should not append an undefined attribute value', () => { + it( 'should skip attributes whose values are undefined', () => { const attributes = getCommentAttributes( { fruit: 'bananas', - category: 'food', ripeness: undefined, - }, { - fruit: 'bananas', - } ); + }, {} ); + + expect( attributes ).to.eql( { fruit: 'bananas' } ); + } ); + } ); - expect( attributes ).to.equal( 'category="food" ' ); + describe( 'serializeValue()', () => { + it( 'should escape double-quotes', () => { + expect( serializeValue( 'a"b' ) ).to.equal( 'a\"b' ); } ); - it( 'should properly escape attributes with quotes in them', () => { - expect( getCommentAttributes( { - name: 'Kevin "The Yellow Dart" Smith', - }, {} ) ).to.equal( 'name="Kevin \"The Yellow Dart\" Smith" ' ); + it( 'should escape hyphens', () => { + expect( serializeValue( '-' ) ).to.equal( '\u{5c}-' ); + expect( serializeValue( '--' ) ).to.equal( '\u{5c}-\u{5c}-' ); + expect( serializeValue( '\\-' ) ).to.equal( '\u{5c}\u{5c}-' ); } ); } ); @@ -115,7 +121,7 @@ describe( 'block serializer', () => { }, }, ]; - const expectedPostContent = '\n

Ribs & Chicken

\n\n\n'; + const expectedPostContent = '\n

Ribs & Chicken

\n'; expect( serialize( blockList ) ).to.eql( expectedPostContent ); } ); diff --git a/blocks/test/fixtures/core-latestposts.html b/blocks/test/fixtures/core-latestposts.html index 811b2260df738..ee48abd1b4276 100644 --- a/blocks/test/fixtures/core-latestposts.html +++ b/blocks/test/fixtures/core-latestposts.html @@ -1,2 +1 @@ - diff --git a/blocks/test/fixtures/core-latestposts.serialized.html b/blocks/test/fixtures/core-latestposts.serialized.html index 811b2260df738..ee48abd1b4276 100644 --- a/blocks/test/fixtures/core-latestposts.serialized.html +++ b/blocks/test/fixtures/core-latestposts.serialized.html @@ -1,2 +1 @@ - diff --git a/blocks/test/fixtures/core-text-multi-paragraph.serialized.html b/blocks/test/fixtures/core-text-multi-paragraph.serialized.html index e9ab7de13a1cb..d56b01a7c77db 100644 --- a/blocks/test/fixtures/core-text-multi-paragraph.serialized.html +++ b/blocks/test/fixtures/core-text-multi-paragraph.serialized.html @@ -2,4 +2,3 @@

The goal of this new editor is to make adding rich content to WordPress simple and enjoyable. This whole post is composed of pieces of content—somewhat similar to LEGO bricks—that you can move around and interact with. Move your cursor around and you'll notice the different blocks light up with outlines and arrows. Press the arrows to reposition blocks quickly, without fearing about losing things in the process of copying and pasting.

What you are reading now is a text block, the most basic block of all. A text block can have multiple paragraphs, if that's how you prefer to write your posts. But you can also split it by hitting enter twice. Once blocks are split they get their own controls to be moved freely around the post...

- diff --git a/blocks/test/full-content.js b/blocks/test/full-content.js index d4e023d270a06..7a16290ddb872 100644 --- a/blocks/test/full-content.js +++ b/blocks/test/full-content.js @@ -127,7 +127,7 @@ describe( 'full post content fixture', () => { } } - expect( serializedActual ).to.eql( serializedExpected ); + expect( serializedActual.trim() ).to.eql( serializedExpected.trim() ); } ); } ); diff --git a/post-content.js b/post-content.js index 8317d2b64ae3a..3905a9a75b0cd 100644 --- a/post-content.js +++ b/post-content.js @@ -7,7 +7,7 @@ window._wpGutenbergPost = { }, content: { raw: [ - '', + '', '

The goal of this new editor is to make adding rich content to WordPress simple and enjoyable. This whole post is composed of pieces of content—somewhat similar to LEGO bricks—that you can move around and interact with. Move your cursor around and you\'ll notice the different blocks light up with outlines and arrows. Press the arrows to reposition blocks quickly, without fearing about losing things in the process of copying and pasting.

', '

What you are reading now is a text block, the most basic block of all. A text block can have multiple paragraphs, if that\'s how you prefer to write your posts. But you can also split it by hitting enter twice. Once blocks are split they get their own controls to be moved freely around the post...

', '',