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

Add HTML schema for separator block #914

Closed
wants to merge 5 commits into from
Closed
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
7 changes: 5 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
"plugins": [
"lodash",
"transform-object-rest-spread",
[ "transform-react-jsx", {
"pragma": "wp.element.createElement"
[ "transform-jsx-flexible", {
"pragma": "wp.element.createElement",
"tags": {
"Schema": "createSchemaElement"
}
} ]
],
"env": {
Expand Down
122 changes: 108 additions & 14 deletions blocks/api/parser.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-console */

/**
* External dependencies
*/
Expand All @@ -9,7 +11,11 @@ import { escape, unescape, pickBy } from 'lodash';
* Internal dependencies
*/
import { parse as grammarParse } from './post.pegjs';
import { getBlockSettings, getUnknownTypeHandler } from './registration';
import {
getBlocks,
getBlockSettings,
getUnknownTypeHandler,
} from './registration';
import { createBlock } from './factory';

/**
Expand Down Expand Up @@ -63,18 +69,37 @@ export function getBlockAttributes( blockSettings, rawContent, attributes ) {
/**
* Creates a block with fallback to the unknown type handler.
*
* @param {?String} blockType Block type slug
* @param {String} rawContent Raw block content
* @param {?Object} attributes Attributes obtained from block delimiters
* @return {?Object} An initialized block object (if possible)
* @param {?String} blockType Block type slug
* @param {String} rawContent Raw block content
* @param {?Object} attributes Attributes obtained from block delimiters
* @param {?Object} contentObject Cbject for validating against a schema.
* @return {?Object} An initialized block object (if possible)
*/
export function createBlockWithFallback( blockType, rawContent, attributes ) {
export function createBlockWithFallback(
blockType,
rawContent,
attributes,
contentObject
) {
// Use type from block content, otherwise find unknown handler
blockType = blockType || getUnknownTypeHandler();

// Try finding settings for known block type, else again fall back
let blockSettings = getBlockSettings( blockType );
const fallbackBlockType = getUnknownTypeHandler();

// If the block has a schema and we were passed a content tree object,
// validate the content against the block schema. Then, if validation
// fails, ensure that we fall back to the unknown type handler.
if (
blockSettings &&
blockSettings.schema &&
contentObject &&
blockSettings.schema.validateFragment( contentObject ) !== true
) {
blockSettings = null;
}

if ( ! blockSettings ) {
blockType = fallbackBlockType;
blockSettings = getBlockSettings( blockType );
Expand All @@ -94,13 +119,38 @@ export function createBlockWithFallback( blockType, rawContent, attributes ) {
}
}

/**
* Converts an object tree from the format returned by the TinyMCE parser (with
* `firstChild` and `lastChild`) into the format expected by `phs` (with
* `childNodes`).
*
* @param {Object} node A TinyMCE DOM-like node (result of a parse operation).
* @return {Object} An object to validate against a `phs` schema.
*/
export function mceNodeToObjectTree( node ) {
const childNodes = [];
let child = node.firstChild;
while ( child ) {
childNodes.push( mceNodeToObjectTree( child ) );
child = child.next;
}
return {
// TODO nodeName = '#text', tagName = undefined
nodeName: node.name,
tagName: node.name,
childNodes,
};
}

/**
* Parses the post content with TinyMCE and returns a list of blocks.
*
* @param {String} content The post content
* @return {Array} Block list
*/
export function parseWithTinyMCE( content ) {
const blocksWithSchemas = getBlocks().filter( block => !! block.schema );

// First, convert comment delimiters into temporary <wp-block> "tags" so
// that TinyMCE can parse them. Examples:
// In : <!-- wp:core/text -->
Expand All @@ -123,22 +173,22 @@ export function parseWithTinyMCE( content ) {
}
);

// Create a custom HTML schema
const schema = new tinymce.html.Schema();
// Add <wp-block> "tags" to our schema
schema.addCustomElements( 'wp-block' );
// Create a custom TinyMCE parser schema
const parserSchema = new tinymce.html.Schema();
// Add <wp-block> "tags" to our parser schema
parserSchema.addCustomElements( 'wp-block' );
// Add valid <wp-block> "attributes" also
schema.addValidElements( 'wp-block[slug|attributes]' );
parserSchema.addValidElements( 'wp-block[slug|attributes]' );
// Initialize the parser with our custom schema
const parser = new tinymce.html.DomParser( { validate: true }, schema );
const parser = new tinymce.html.DomParser( { validate: true }, parserSchema );

// Parse the content into an object tree
const tree = parser.parse( content );

// Create a serializer that we will use to pass strings to blocks.
// TODO: pass parse trees instead, and verify them against the markup
// shapes that each block can accept.
const serializer = new tinymce.html.Serializer( { validate: true }, schema );
const serializer = new tinymce.html.Serializer( { validate: true }, parserSchema );

// Walk the tree and initialize blocks
const blocks = [];
Expand Down Expand Up @@ -188,10 +238,12 @@ export function parseWithTinyMCE( content ) {
} while ( !! match );

// Try to create the block
const contentObject = mceNodeToObjectTree( currentNode );
const block = createBlockWithFallback(
nodeAttributes.slug,
rawContent,
blockAttributes
blockAttributes,
contentObject
);
if ( block ) {
flushContentBetweenBlocks();
Expand All @@ -200,6 +252,48 @@ export function parseWithTinyMCE( content ) {

currentNode = currentNode.next;
} else {
// We have some HTML content outside of block delimiters. First
// see if the current node validates against any block schemas.
// TODO: We will need a way to match multiple tags against multiple
// schemas, and this should happen in `flushContentBetweenBlocks`
// instead. We'll have to be careful to manage the algorithmic
// complexity well here, as the number of possible matches will
// quickly grow very large.
const currentNodeObject = mceNodeToObjectTree( currentNode );
const matchingBlocks = blocksWithSchemas.filter( block => {
const result = block.schema.validateNode( currentNodeObject );
// TODO: Eventually, `result` will contain any parameters
// deserialized from the HTML, as well as information about
// which branch of the schema was used, extra attributes
// present in the markup, etc.
return ( result === true );
} );

if ( matchingBlocks.length > 1 ) {
console.error(
'More than 1 block found matching HTML string \'%s\': %s',
serializer.serialize( currentNode ),
matchingBlocks.map( block => block.slug ).join( ', ' )
);
} else if ( matchingBlocks.length === 1 ) {
const contentString = serializer.serialize( currentNode );
const block = createBlockWithFallback(
matchingBlocks[ 0 ].slug,
contentString,
null // no known attributes
);
if ( block ) {
flushContentBetweenBlocks();
blocks.push( block );
currentNode = currentNode.next;
continue;
}
}

// If we get here, then we were unable to match the current node
// against a block schema for one reason or another, and we need to
// store it in `contentBetweenBlocks` instead.

// We have some HTML content outside of block delimiters. Save it
// so that we can initialize it using `getUnknownTypeHandler`.
const toAppend = currentNode;
Expand Down
14 changes: 14 additions & 0 deletions blocks/library/separator/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* External dependencies
*/
import {
Schema,
createSchemaElement, // eslint-disable-line no-unused-vars
} from 'phs';

/**
* Internal dependencies
*/
Expand All @@ -7,6 +15,12 @@ import { registerBlock } from '../../api';
registerBlock( 'core/separator', {
title: wp.i18n.__( 'Separator' ),

schema: (
<Schema>
<hr />
</Schema>
),

icon: 'minus',

category: 'layout',
Expand Down
26 changes: 26 additions & 0 deletions blocks/test/fixtures/between-blocks-separators.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<hr />

<!-- wp:core/separator -->
<hr/>
<!-- /wp:core/separator -->

<!-- wp:core/text -->
<p>some simple text</p>
<!-- /wp:core/text -->

<div>this is not a separator</div>
<div>this is not a separator either</div>

<!-- wp:core/text -->
<p>some more simple text</p>
<!-- /wp:core/text -->

<hr />
<hr/>

<!-- wp:core/text -->
<p>even more simple text</p>
<!-- /wp:core/text -->

<hr />
<div>still not a separator</div>
77 changes: 77 additions & 0 deletions blocks/test/fixtures/between-blocks-separators.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
[
{
"uid": "_uid_0",
"blockType": "core/separator",
"attributes": {}
},
{
"uid": "_uid_1",
"blockType": "core/separator",
"attributes": {}
},
{
"uid": "_uid_2",
"blockType": "core/text",
"attributes": {
"content": [
{
"type": "p",
"children": "some simple text"
}
]
}
},
{
"uid": "_uid_3",
"blockType": "core/freeform",
"attributes": {
"html": "<div>this is not a separator</div><div>this is not a separator either</div>"
}
},
{
"uid": "_uid_4",
"blockType": "core/text",
"attributes": {
"content": [
{
"type": "p",
"children": "some more simple text"
}
]
}
},
{
"uid": "_uid_5",
"blockType": "core/separator",
"attributes": {}
},
{
"uid": "_uid_6",
"blockType": "core/separator",
"attributes": {}
},
{
"uid": "_uid_7",
"blockType": "core/text",
"attributes": {
"content": [
{
"type": "p",
"children": "even more simple text"
}
]
}
},
{
"uid": "_uid_8",
"blockType": "core/separator",
"attributes": {}
},
{
"uid": "_uid_9",
"blockType": "core/freeform",
"attributes": {
"html": "<div>still not a separator</div>"
}
}
]
41 changes: 41 additions & 0 deletions blocks/test/fixtures/between-blocks-separators.serialized.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!-- wp:core/separator -->
<hr/>
<!-- /wp:core/separator -->

<!-- wp:core/separator -->
<hr/>
<!-- /wp:core/separator -->

<!-- wp:core/text -->
<p>some simple text</p>
<!-- /wp:core/text -->

<!-- wp:core/freeform -->
<div>this is not a separator</div>
<div>this is not a separator either</div>
<!-- /wp:core/freeform -->

<!-- wp:core/text -->
<p>some more simple text</p>
<!-- /wp:core/text -->

<!-- wp:core/separator -->
<hr/>
<!-- /wp:core/separator -->

<!-- wp:core/separator -->
<hr/>
<!-- /wp:core/separator -->

<!-- wp:core/text -->
<p>even more simple text</p>
<!-- /wp:core/text -->

<!-- wp:core/separator -->
<hr/>
<!-- /wp:core/separator -->

<!-- wp:core/freeform -->
<div>still not a separator</div>
<!-- /wp:core/freeform -->

9 changes: 9 additions & 0 deletions blocks/test/fixtures/invalid-core-separator.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- wp:core/separator -->
<div>this is not a separator</div>
<!-- /wp:core/separator -->

<!-- wp:core/separator -->
<hr>
<div>what even happened here</div>
</hr>
<!-- /wp:core/separator -->
Loading