diff --git a/blocks/README.md b/blocks/README.md index 94661b4a1a8a56..b2bdcc85db12bd 100644 --- a/blocks/README.md +++ b/blocks/README.md @@ -130,7 +130,7 @@ Registers a new block provided a unique slug and an object defining its behavior - `attributes: Object | Function` - An object of [matchers](http://github.com/aduth/hpq) or a function which, when passed the raw content of the block, returns block attributes as an object. When defined as an object of matchers, the attributes object is generated with values corresponding to the shape of the matcher object keys. - `category: string` - Slug of the block's category. The category is used to organize the blocks in the block inserter. - `edit( { attributes: Object, setAttributes: Function } ): WPElement` - Returns an element describing the markup of a block to be shown in the editor. A block can update its own state in response to events using the `setAttributes` function, passing an object of properties to be applied as a partial update. -- `save( { attributes: Object } ): WPElement` - Returns an element describing the markup of a block to be saved in the published content. This function is called before save and when switching to an editor's HTML view. +- `save( { attributes: Object } ): WPElement | String` - Returns an element describing the markup of a block to be saved in the published content. This function is called before save and when switching to an editor's HTML view. - `controls: string[]` - Slugs for controls to be made available to block. See also: [`wp.blocks.registerControl`](#wpblocksregistercontrol-slug-string-settings-object-) - `encodeAttributes( attributes: Object ): Object` - Called when save markup is generated, this function allows you to control which attributes are to be encoded in the block comment metadata. By default, all attribute values not defined in the block's `attributes` property are serialized to the comment metadata. If defined, this function should return the subset of attributes to encode, or `null` to bypass default behavior. diff --git a/blocks/index.js b/blocks/index.js index 714407089947f0..42a732768df987 100644 --- a/blocks/index.js +++ b/blocks/index.js @@ -6,6 +6,7 @@ import * as query from 'hpq'; export { query }; export { default as Editable } from './components/editable'; export { default as parse } from './parser'; +export { default as serialize } from './serializer'; export { getCategories } from './categories'; export { registerBlock, diff --git a/blocks/serializer.js b/blocks/serializer.js new file mode 100644 index 00000000000000..daaf96607a259e --- /dev/null +++ b/blocks/serializer.js @@ -0,0 +1,87 @@ +/** + * External dependencies + */ +import { difference } from 'lodash'; + +/** + * Internal dependencies + */ +import { getBlockSettings } from './registration'; +import { getBlockAttributes } from './parser'; + +/** + * Given a block's save render implementation and attributes, returns the + * static markup to be saved. + * + * @param {Function|WPComponent} save Save render implementation + * @param {Object} attributes Block attributes + * @return {string} Save content + */ +export function getSaveContent( save, attributes ) { + let rawContent; + + if ( save.prototype instanceof wp.element.Component ) { + rawContent = wp.element.createElement( save, { attributes } ); + } else { + rawContent = save( { attributes } ); + + // Special-case function render implementation to allow raw HTML return + if ( 'string' === typeof rawContent ) { + return rawContent; + } + } + + // Otherwise, infer as element + return wp.element.renderToString( rawContent ); +} + +/** + * Returns comment attributes as serialized string, determined by subset of + * difference between actual attributes of a block and those expected based + * on its settings. + * + * @param {Object} realAttributes Actual block attributes + * @param {Object} expectedAttributes Expected block attributes + * @return {string} Comment attributes + */ +export function getCommentAttributes( realAttributes, expectedAttributes ) { + // Find difference and build into object subset of attributes. + const keys = difference( + Object.keys( realAttributes ), + Object.keys( expectedAttributes ) + ); + + // Serialize the comment attributes + return keys.reduce( ( memo, key ) => { + const value = realAttributes[ key ]; + return memo + `${ key }:${ value } `; + }, '' ); +} + +/** + * Takes a block list and returns the serialized post content + * + * @param {Array} blocks Block list + * @return {String} The post content + */ +export default function serialize( blocks ) { + return blocks.reduce( ( memo, block ) => { + const blockType = block.blockType; + const settings = getBlockSettings( blockType ); + + return memo + ( + '' + + getSaveContent( settings.save, block.attributes ) + + '' + ); + }, '' ); +} diff --git a/blocks/test/serializer.js b/blocks/test/serializer.js new file mode 100644 index 00000000000000..17556c7501e963 --- /dev/null +++ b/blocks/test/serializer.js @@ -0,0 +1,105 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import serialize, { getCommentAttributes, getSaveContent } from '../serializer'; +import { getBlocks, registerBlock, unregisterBlock } from '../registration'; + +describe( 'block serializer', () => { + afterEach( () => { + getBlocks().forEach( block => { + unregisterBlock( block.slug ); + } ); + } ); + + describe( 'getSaveContent()', () => { + context( 'function save', () => { + it( 'should return string verbatim', () => { + const saved = getSaveContent( + ( { attributes } ) => attributes.fruit, + { fruit: 'Bananas' } + ); + + expect( saved ).to.equal( 'Bananas' ); + } ); + + it( 'should return element as string if save returns element', () => { + const { createElement } = wp.element; + const saved = getSaveContent( + ( { attributes } ) => createElement( 'div', null, attributes.fruit ), + { fruit: 'Bananas' } + ); + + expect( saved ).to.equal( '
Bananas
' ); + } ); + } ); + + context( 'component save', () => { + it( 'should return element as string', () => { + const { Component, createElement } = wp.element; + const saved = getSaveContent( + class extends Component { + render() { + return createElement( 'div', null, this.props.attributes.fruit ); + } + }, + { fruit: 'Bananas' } + ); + + expect( saved ).to.equal( '
Bananas
' ); + } ); + } ); + } ); + + describe( 'getCommentAttributes()', () => { + it( 'should return empty string if no difference', () => { + const attributes = getCommentAttributes( {}, {} ); + + expect( attributes ).to.equal( '' ); + } ); + + it( 'should return joined string of key:value pairs by difference subset', () => { + const attributes = getCommentAttributes( { + fruit: 'bananas', + category: 'food', + ripeness: 'ripe' + }, { + fruit: 'bananas' + } ); + + expect( attributes ).to.equal( 'category:food ripeness:ripe ' ); + } ); + } ); + + describe( 'serialize()', () => { + it( 'should serialize the post content properly', () => { + const blockSettings = { + attributes: ( rawContent ) => { + return { + content: rawContent + }; + }, + save( { attributes } ) { + return

; + } + }; + registerBlock( 'core/test-block', blockSettings ); + const blockList = [ + { + blockType: 'core/test-block', + attributes: { + content: 'Ribs & Chicken', + align: 'left' + } + } + ]; + const expectedPostContent = '

Ribs & Chicken

'; + + expect( serialize( blockList ) ).to.eql( expectedPostContent ); + } ); + } ); +} ); diff --git a/editor/blocks/text/index.js b/editor/blocks/text/index.js index 49fde5f357bf25..ba07e1051ede62 100644 --- a/editor/blocks/text/index.js +++ b/editor/blocks/text/index.js @@ -23,6 +23,6 @@ wp.blocks.registerBlock( 'core/text', { }, save( { attributes } ) { - return

{ attributes.value }

; + return

; } } ); diff --git a/editor/index.js b/editor/index.js index 935e96e93570bd..f3f748c43f6779 100644 --- a/editor/index.js +++ b/editor/index.js @@ -20,8 +20,8 @@ import { createReduxStore } from './state'; export function createEditorInstance( id, post ) { const store = createReduxStore(); store.dispatch( { - type: 'SET_HTML', - html: post.content.raw + type: 'REPLACE_BLOCKS', + blockNodes: wp.blocks.parse( post.content.raw ) } ); wp.element.render( diff --git a/editor/modes/text-editor/index.js b/editor/modes/text-editor/index.js index 02e1dc6f6acb39..4d9745550c1e17 100644 --- a/editor/modes/text-editor/index.js +++ b/editor/modes/text-editor/index.js @@ -9,15 +9,11 @@ import Textarea from 'react-textarea-autosize'; */ import './style.scss'; -function TextEditor( { html, onChange } ) { - const changeValue = ( event ) => { - onChange( event.target.value ); - }; - +function TextEditor( { blocks, onChange } ) { return (