Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blocks: Preserve unknown block, remove freeform block comment delimiters #2513

Merged
merged 3 commits into from
Aug 29, 2017
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions blocks/api/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { parse as grammarParse } from './post.pegjs';
import { getBlockType, getUnknownTypeHandlerName } from './registration';
import { createBlock } from './factory';
import { isValidBlock } from './validation';
import { getCommentDelimitedContent } from './serializer';

/**
* Returns true if the provided function is a valid attribute source, or false
Expand Down Expand Up @@ -177,6 +178,12 @@ export function createBlockWithFallback( name, rawContent, attributes ) {
let blockType = getBlockType( name );
const fallbackBlock = getUnknownTypeHandlerName();
if ( ! blockType ) {
// If detected as a block which is not registered, preserve comment
// delimiters in content of unknown type handler.
if ( name ) {
rawContent = getCommentDelimitedContent( name, attributes, rawContent );
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to manually preserve it?

Copy link
Member Author

Choose a reason for hiding this comment

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

The result of a parse doesn't include the comment demarcations. Take this example:

<!-- wp:core/unknown -->
Unknown block
<!-- /wp:core/unknown -->

Upon parse, this is passed to createBlockWithFallback with arguments: core/unknown (name), Unknown block (rawContent), { foo: 'bar' } (attributes). When it fails to find the block matching this slug, the name and behavior assumes that of the fallback block. Future reserialization will not know to save this as the core/unknown block. So the logic here re-adds the comments to the rawContent field, so even though this assumes the role of the fallback block, the fallback block's content includes the original block text, and since the fallback block no longer writes its own comment demarcation, it is saved exactly as it was originally.

Part of this assumes we want the freeform block behaviors to be made available for unknown blocks. The original pull request comment explores whether this is desirable, or if we'd want to just flag it as "unknown", leave it alone, and notify the user as such.

Because Freeform renders a TinyMCE, it is very easy to mistakenly edit the content of an unknown block type. I'm wondering if we ought to either create a separate block representation for un-blocked content vs. block content of an unknown type (e.g. since-disabled plugins blocks).

Alternatively, or in either case, we may want to convey messaging to the user about the unknown block instead of providing them the option to edit.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the explanation. The previous behaviour would transform the unknown block to freeform but still retain the comment within it, hence my question.

Copy link
Member Author

Choose a reason for hiding this comment

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

The previous behaviour would transform the unknown block to freeform but still retain the comment within it

No, the problem was that it didn't retain the comment. The logic added here is what achieves this.

}

name = fallbackBlock;
blockType = getBlockType( name );
}
Expand Down
55 changes: 40 additions & 15 deletions blocks/api/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Component, createElement, renderToString, cloneElement, Children } from
/**
* Internal dependencies
*/
import { getBlockType } from './registration';
import { getBlockType, getUnknownTypeHandlerName } from './registration';

/**
* Returns the block's default classname from its name
Expand Down Expand Up @@ -139,6 +139,37 @@ export function getBeautifulContent( content ) {
} );
}

/**
* Returns the content of a block, including comment delimiters.
*
* @param {String} blockName Block name
* @param {Object} attributes Block attributes
* @param {String} content Block save content
* @return {String} Comment-delimited block content
*/
export function getCommentDelimitedContent( blockName, attributes, content ) {
const serializedAttributes = ! isEmpty( attributes )
? serializeAttributes( attributes ) + ' '
: '';

if ( ! content ) {
return `<!-- wp:${ blockName } ${ serializedAttributes }/-->`;
}

return (
`<!-- wp:${ blockName } ${ serializedAttributes }-->\n` +
getBeautifulContent( content ) +
`\n<!-- /wp:${ blockName } -->`
);
}

/**
* Returns the content of a block, including comment delimiters, determining
* serialized attributes and content form from the current state of the block.
*
* @param {Object} block Block instance
* @return {String} Serialized block
*/
export function serializeBlock( block ) {
const blockName = block.name;
const blockType = getBlockType( blockName );
Expand All @@ -154,23 +185,17 @@ export function serializeBlock( block ) {

const saveAttributes = getCommentAttributes( block.attributes, blockType.attributes );

if ( 'core/more' === blockName ) {
return `<!--more${ saveAttributes.text ? ` ${ saveAttributes.text }` : '' }-->${ saveAttributes.noTeaser ? '\n<!--noteaser-->' : '' }`;
}
switch ( blockName ) {
case 'core/more':
const { text, noTeaser } = saveAttributes;
return `<!--more${ text ? ` ${ text }` : '' }-->${ noTeaser ? '\n<!--noteaser-->' : '' }`;

const serializedAttributes = ! isEmpty( saveAttributes )
? serializeAttributes( saveAttributes ) + ' '
: '';
case getUnknownTypeHandlerName():
return saveContent;

if ( ! saveContent ) {
return `<!-- wp:${ blockName } ${ serializedAttributes }/-->`;
default:
return getCommentDelimitedContent( blockName, saveAttributes, saveContent );
}

return (
`<!-- wp:${ blockName } ${ serializedAttributes }-->\n` +
getBeautifulContent( saveContent ) +
`\n<!-- /wp:${ blockName } -->`
);
}

/**
Expand Down
19 changes: 16 additions & 3 deletions blocks/api/test/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,29 @@ describe( 'block parser', () => {
} );

it( 'should fall back to the unknown type handler for unknown blocks if present', () => {
registerBlockType( 'core/unknown-block', defaultBlockSettings );
registerBlockType( 'core/unknown-block', {
category: 'common',
attributes: {
content: {
type: 'string',
source: html(),
},
fruit: {
type: 'string',
},
},
save: ( { attributes } ) => attributes.content,
} );
setUnknownTypeHandlerName( 'core/unknown-block' );

const block = createBlockWithFallback(
'core/test-block',
'content',
{ fruit: 'Bananas' }
);
expect( block.name ).toEqual( 'core/unknown-block' );
expect( block.attributes ).toEqual( { fruit: 'Bananas' } );
expect( block.name ).toBe( 'core/unknown-block' );
expect( block.attributes.fruit ).toBe( 'Bananas' );
expect( block.attributes.content ).toContain( 'core/test-block' );
} );

it( 'should fall back to the unknown type handler if block type not specified', () => {
Expand Down
124 changes: 123 additions & 1 deletion blocks/api/test/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ import serialize, {
getBeautifulContent,
getSaveContent,
serializeAttributes,
getCommentDelimitedContent,
serializeBlock,
} from '../serializer';
import { getBlockTypes, registerBlockType, unregisterBlockType } from '../registration';
import {
getBlockTypes,
registerBlockType,
unregisterBlockType,
setUnknownTypeHandlerName,
} from '../registration';
import { createBlock } from '../';

describe( 'block serializer', () => {
afterEach( () => {
setUnknownTypeHandlerName( undefined );
getBlockTypes().forEach( block => {
unregisterBlockType( block.name );
} );
Expand Down Expand Up @@ -210,6 +218,120 @@ describe( 'block serializer', () => {
} );
} );

describe( 'getCommentDelimitedContent()', () => {
it( 'should generate empty attributes void', () => {
const content = getCommentDelimitedContent(
'core/test-block',
{},
''
);

expect( content ).toBe( '<!-- wp:core/test-block /-->' );
} );

it( 'should generate empty attributes non-void', () => {
const content = getCommentDelimitedContent(
'core/test-block',
{},
'Delicious'
);

expect( content ).toBe( '<!-- wp:core/test-block -->\nDelicious\n<!-- /wp:core/test-block -->' );
} );

it( 'should generate non-empty attributes void', () => {
const content = getCommentDelimitedContent(
'core/test-block',
{ fruit: 'Banana' },
''
);

expect( content ).toBe(
'<!-- wp:core/test-block {"fruit":"Banana"} /-->'
);
} );

it( 'should generate non-empty attributes non-void', () => {
const content = getCommentDelimitedContent(
'core/test-block',
{ fruit: 'Banana' },
'Delicious'
);

expect( content ).toBe(
'<!-- wp:core/test-block {"fruit":"Banana"} -->\nDelicious\n<!-- /wp:core/test-block -->'
);
} );
} );

describe( 'serializeBlock()', () => {
describe( '"more" block', () => {
beforeEach( () => {
registerBlockType( 'core/more', {
category: 'layout',

attributes: {
text: {
type: 'string',
},
noTeaser: {
type: 'boolean',
default: false,
},
},

save: ( { attributes } ) => attributes.text,
} );
} );

it( 'serializes without text', () => {
const block = createBlock( 'core/more', {} );

const content = serializeBlock( block );

expect( content ).toBe( '<!--more-->' );
} );

it( 'serializes with text', () => {
const block = createBlock( 'core/more', {
text: 'Read more!',
} );

const content = serializeBlock( block );

expect( content ).toBe( '<!--more Read more!-->' );
} );

it( 'serializes with no teaser', () => {
const block = createBlock( 'core/more', {
noTeaser: true,
} );

const content = serializeBlock( block );

expect( content ).toBe( '<!--more-->\n<!--noteaser-->' );
} );
} );

it( 'serializes the fallback block without comment delimiters', () => {
registerBlockType( 'core/unknown-block', {
category: 'common',
attributes: {
fruit: {
type: 'string',
},
},
save: ( { attributes } ) => attributes.fruit,
} );
setUnknownTypeHandlerName( 'core/unknown-block' );
const block = createBlock( 'core/unknown-block', { fruit: 'Bananas' } );

const content = serializeBlock( block );

expect( content ).toBe( 'Bananas' );
} );
} );

describe( 'serialize()', () => {
it( 'should serialize the post content properly', () => {
const blockType = {
Expand Down
2 changes: 1 addition & 1 deletion blocks/test/fixtures/core__audio.parsed.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"blockName": "core/audio",
"attrs": {
"align": "right"
"align": "right"
},
"rawContent": "\n<div class=\"wp-block-audio alignright\">\n <audio controls=\"\" src=\"https://media.simplecast.com/episodes/audio/80564/draft-podcast-51-livePublish2.mp3\"></audio>\n</div>\n"
},
Expand Down
4 changes: 1 addition & 3 deletions blocks/test/fixtures/core__freeform.serialized.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<!-- wp:core/freeform -->
Testing freeform block with some
<div class="wp-some-class">
HTML <span style="color: red;">content</span>
HTML <span style="color: red;">content</span>
</div>
<!-- /wp:core/freeform -->