Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions blocks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
87 changes: 87 additions & 0 deletions blocks/serializer.js
Original file line number Diff line number Diff line change
@@ -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 ) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think there's a few functions we could split out of this long block perhaps for easier testing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I noted above, it's hard to do without duplicating code (like computing rawContent and blockSettings several times) or passing those as parameters (which creates weird API)

const blockType = block.blockType;
const settings = getBlockSettings( blockType );

return memo + (
'<!-- wp:' +
blockType +
' ' +
getCommentAttributes(
block.attributes,
getBlockAttributes( block, settings )
) +
'-->' +
getSaveContent( settings.save, block.attributes ) +
'<!-- /wp:' +
blockType +
' -->'
);
}, '' );
}
105 changes: 105 additions & 0 deletions blocks/test/serializer.js
Original file line number Diff line number Diff line change
@@ -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( '<div>Bananas</div>' );
} );
} );

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( '<div>Bananas</div>' );
} );
} );
} );

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 <p dangerouslySetInnerHTML={ { __html: attributes.content } } />;
}
};
registerBlock( 'core/test-block', blockSettings );
const blockList = [
{
blockType: 'core/test-block',
attributes: {
content: 'Ribs & Chicken',
align: 'left'
}
}
];
const expectedPostContent = '<!-- wp:core/test-block align:left --><p>Ribs & Chicken</p><!-- /wp:core/test-block -->';

expect( serialize( blockList ) ).to.eql( expectedPostContent );
} );
} );
} );
2 changes: 1 addition & 1 deletion editor/blocks/text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ wp.blocks.registerBlock( 'core/text', {
},

save( { attributes } ) {
return <p>{ attributes.value }</p>;
return <p dangerouslySetInnerHTML={ { __html: attributes.value } } />;
}
} );
4 changes: 2 additions & 2 deletions editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
18 changes: 8 additions & 10 deletions editor/modes/text-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Textarea
value={ html }
onChange={ changeValue }
defaultValue={ wp.blocks.serialize( blocks ) }
onBlur={ ( event ) => onChange( event.target.value ) }
className="editor-text-editor"
useCacheForDOMMeasurements
/>
Expand All @@ -26,13 +22,15 @@ function TextEditor( { html, onChange } ) {

export default connect(
( state ) => ( {
html: state.html
blocks: state.blocks.order.map( ( uid ) => (
state.blocks.byUid[ uid ]
) )
} ),
( dispatch ) => ( {
onChange( value ) {
dispatch( {
type: 'SET_HTML',
html: value
type: 'REPLACE_BLOCKS',
blockNodes: wp.blocks.parse( value )
} );
}
} )
Expand Down
Loading