From a497b087037535c5609ff8ab13143253834e916c Mon Sep 17 00:00:00 2001 From: James Nylen Date: Mon, 12 Jun 2017 17:39:48 -0400 Subject: [PATCH 1/4] Remove TinyMCE-based parser --- blocks/api/parser.js | 125 ------------------- blocks/api/test/parser.js | 247 ++++++++++++++++++-------------------- bootstrap-test.js | 1 + webpack.config.js | 3 - 4 files changed, 121 insertions(+), 255 deletions(-) diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 006f942c1fb01a..47a7269f93ce81 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import tinymce from 'tinymce'; import { parse as hpqParse } from 'hpq'; import { escape, unescape, pickBy } from 'lodash'; @@ -95,130 +94,6 @@ export function createBlockWithFallback( name, rawContent, attributes ) { } } -/** - * 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 ) { - // First, convert comment delimiters into temporary "tags" so - // that TinyMCE can parse them. Examples: - // In : - // Out : - // In : - // Out : - // In : - // Out : - content = content.replace( - //g, - function( match, closingSlash, slug, attributes ) { - if ( closingSlash ) { - return ''; - } - - if ( attributes ) { - attributes = ' attributes="' + escape( attributes.trim() ) + '"'; - } - return ''; - } - ); - - // Create a custom HTML schema. - const schema = new tinymce.html.Schema(); - - // Add "tags" to our schema. - schema.addCustomElements( 'wp-block' ); - - // Add valid "attributes" also. - schema.addValidElements( 'wp-block[slug|attributes]' ); - - // Initialize the parser with our custom schema. - const parser = new tinymce.html.DomParser( { validate: true }, schema ); - - // 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 ); - - // Walk the tree and initialize blocks. - const blocks = []; - - // Store markup we found in between blocks. - let contentBetweenBlocks = null; - function flushContentBetweenBlocks() { - if ( contentBetweenBlocks && contentBetweenBlocks.firstChild ) { - const block = createBlockWithFallback( - null, // default: unknown type handler - serializer.serialize( contentBetweenBlocks ), - null // no known attributes - ); - if ( block ) { - blocks.push( block ); - } - } - contentBetweenBlocks = new tinymce.html.Node( 'body', 11 ); - } - flushContentBetweenBlocks(); - - let currentNode = tree.firstChild; - while ( currentNode ) { - if ( currentNode.name === 'wp-block' ) { - // Set node type to document fragment so that the TinyMCE - // serializer doesn't output its markup. - currentNode.type = 11; - - // Serialize the content - const rawContent = serializer.serialize( currentNode ); - - // Retrieve the attributes from the tag. - const nodeAttributes = currentNode.attributes.reduce( ( memo, attr ) => { - memo[ attr.name ] = attr.value; - return memo; - }, {} ); - - // Retrieve the block attributes from the original delimiters. - const attributesMatcher = /([a-z0-9_-]+)(="([^"]*)")?/g; - const blockAttributes = {}; - let match; - do { - match = attributesMatcher.exec( unescape( nodeAttributes.attributes || '' ) ); - if ( match ) { - blockAttributes[ match[ 1 ] ] = !! match[ 2 ] ? match[ 3 ] : true; - } - } while ( !! match ); - - // Try to create the block. - const block = createBlockWithFallback( - nodeAttributes.slug, - rawContent, - blockAttributes - ); - if ( block ) { - flushContentBetweenBlocks(); - blocks.push( block ); - } - - currentNode = currentNode.next; - } else { - // We have some HTML content outside of block delimiters. Save it - // so that we can initialize it using `getUnknownTypeHandler`. - const toAppend = currentNode; - // Advance the DOM tree pointer before calling `append` because - // this is a destructive operation. - currentNode = currentNode.next; - contentBetweenBlocks.append( toAppend ); - } - } - - flushContentBetweenBlocks(); - - return blocks; -} - /** * Parses the post content with a PegJS grammar and returns a list of blocks. * diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 506b33abdd429a..2deb2e25a8e8d6 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -11,8 +11,7 @@ import { getBlockAttributes, parseBlockAttributes, createBlockWithFallback, - parseWithGrammar, - parseWithTinyMCE, + default as parse, } from '../parser'; import { registerBlockType, @@ -141,132 +140,126 @@ describe( 'block parser', () => { } ); describe( 'parse()', () => { - const parsers = { parseWithTinyMCE, parseWithGrammar }; - Object.keys( parsers ).forEach( ( parser ) => { - const parse = parsers[ parser ]; - describe( parser, () => { - it( 'should parse the post content, including block attributes', () => { - registerBlockType( 'core/test-block', { - // Currently this is the only way to test block content parsing? - attributes: function( rawContent ) { - return { - content: rawContent, - }; - }, - } ); - - const parsed = parse( - '' + - 'Brisket' + - '' - ); - - expect( parsed ).to.have.lengthOf( 1 ); - expect( parsed[ 0 ].name ).to.equal( 'core/test-block' ); - expect( parsed[ 0 ].attributes ).to.eql( { - content: 'Brisket', - smoked: 'yes', - url: 'http://google.com', - chicken: 'ribs & \'wings\'', - } ); - expect( parsed[ 0 ].uid ).to.be.a( 'string' ); - } ); - - it( 'should parse empty post content', () => { - const parsed = parse( '' ); - - expect( parsed ).to.eql( [] ); - } ); - - it( 'should parse the post content, ignoring unknown blocks', () => { - registerBlockType( 'core/test-block', { - attributes: function( rawContent ) { - return { - content: rawContent + ' & Chicken', - }; - }, - } ); - - const parsed = parse( - '\nRibs\n' + - '

Broccoli

' + - 'Ribs' - ); - - expect( parsed ).to.have.lengthOf( 1 ); - expect( parsed[ 0 ].name ).to.equal( 'core/test-block' ); - expect( parsed[ 0 ].attributes ).to.eql( { - content: 'Ribs & Chicken', - } ); - expect( parsed[ 0 ].uid ).to.be.a( 'string' ); - } ); - - it( 'should parse the post content, using unknown block handler', () => { - registerBlockType( 'core/test-block', {} ); - registerBlockType( 'core/unknown-block', {} ); - - setUnknownTypeHandler( 'core/unknown-block' ); - - const parsed = parse( - 'Ribs' + - '

Broccoli

' + - 'Ribs' - ); - - expect( parsed ).to.have.lengthOf( 3 ); - expect( parsed.map( ( { name } ) => name ) ).to.eql( [ - 'core/test-block', - 'core/unknown-block', - 'core/unknown-block', - ] ); - } ); - - it( 'should parse the post content, including raw HTML at each end', () => { - registerBlockType( 'core/test-block', {} ); - registerBlockType( 'core/unknown-block', { - // Currently this is the only way to test block content parsing? - attributes: function( rawContent ) { - return { - content: rawContent, - }; - }, - } ); - - setUnknownTypeHandler( 'core/unknown-block' ); - - const parsed = parse( - '

Cauliflower

' + - 'Ribs' + - '\n

Broccoli

\n' + - 'Ribs' + - '

Romanesco

' - ); - - expect( parsed ).to.have.lengthOf( 5 ); - expect( parsed.map( ( { name } ) => name ) ).to.eql( [ - 'core/unknown-block', - 'core/test-block', - 'core/unknown-block', - 'core/test-block', - 'core/unknown-block', - ] ); - expect( parsed[ 0 ].attributes.content ).to.eql( '

Cauliflower

' ); - expect( parsed[ 2 ].attributes.content ).to.eql( '

Broccoli

' ); - expect( parsed[ 4 ].attributes.content ).to.eql( '

Romanesco

' ); - } ); - - it( 'should parse blocks with empty content', () => { - registerBlockType( 'core/test-block', {} ); - const parsed = parse( - '' - ); - - expect( parsed ).to.have.lengthOf( 1 ); - expect( parsed.map( ( { name } ) => name ) ).to.eql( [ - 'core/test-block', - ] ); - } ); + it( 'should parse the post content, including block attributes', () => { + registerBlockType( 'core/test-block', { + // Currently this is the only way to test block content parsing? + attributes: function( rawContent ) { + return { + content: rawContent, + }; + }, + } ); + + const parsed = parse( + '' + + 'Brisket' + + '' + ); + + expect( parsed ).to.have.lengthOf( 1 ); + expect( parsed[ 0 ].name ).to.equal( 'core/test-block' ); + expect( parsed[ 0 ].attributes ).to.eql( { + content: 'Brisket', + smoked: 'yes', + url: 'http://google.com', + chicken: 'ribs & \'wings\'', + } ); + expect( parsed[ 0 ].uid ).to.be.a( 'string' ); + } ); + + it( 'should parse empty post content', () => { + const parsed = parse( '' ); + + expect( parsed ).to.eql( [] ); + } ); + + it( 'should parse the post content, ignoring unknown blocks', () => { + registerBlockType( 'core/test-block', { + attributes: function( rawContent ) { + return { + content: rawContent + ' & Chicken', + }; + }, + } ); + + const parsed = parse( + '\nRibs\n' + + '

Broccoli

' + + 'Ribs' + ); + + expect( parsed ).to.have.lengthOf( 1 ); + expect( parsed[ 0 ].name ).to.equal( 'core/test-block' ); + expect( parsed[ 0 ].attributes ).to.eql( { + content: 'Ribs & Chicken', } ); + expect( parsed[ 0 ].uid ).to.be.a( 'string' ); + } ); + + it( 'should parse the post content, using unknown block handler', () => { + registerBlockType( 'core/test-block', {} ); + registerBlockType( 'core/unknown-block', {} ); + + setUnknownTypeHandler( 'core/unknown-block' ); + + const parsed = parse( + 'Ribs' + + '

Broccoli

' + + 'Ribs' + ); + + expect( parsed ).to.have.lengthOf( 3 ); + expect( parsed.map( ( { name } ) => name ) ).to.eql( [ + 'core/test-block', + 'core/unknown-block', + 'core/unknown-block', + ] ); + } ); + + it( 'should parse the post content, including raw HTML at each end', () => { + registerBlockType( 'core/test-block', {} ); + registerBlockType( 'core/unknown-block', { + // Currently this is the only way to test block content parsing? + attributes: function( rawContent ) { + return { + content: rawContent, + }; + }, + } ); + + setUnknownTypeHandler( 'core/unknown-block' ); + + const parsed = parse( + '

Cauliflower

' + + 'Ribs' + + '\n

Broccoli

\n' + + 'Ribs' + + '

Romanesco

' + ); + + expect( parsed ).to.have.lengthOf( 5 ); + expect( parsed.map( ( { name } ) => name ) ).to.eql( [ + 'core/unknown-block', + 'core/test-block', + 'core/unknown-block', + 'core/test-block', + 'core/unknown-block', + ] ); + expect( parsed[ 0 ].attributes.content ).to.eql( '

Cauliflower

' ); + expect( parsed[ 2 ].attributes.content ).to.eql( '

Broccoli

' ); + expect( parsed[ 4 ].attributes.content ).to.eql( '

Romanesco

' ); + } ); + + it( 'should parse blocks with empty content', () => { + registerBlockType( 'core/test-block', {} ); + const parsed = parse( + '' + ); + + expect( parsed ).to.have.lengthOf( 1 ); + expect( parsed.map( ( { name } ) => name ) ).to.eql( [ + 'core/test-block', + ] ); } ); } ); } ); diff --git a/bootstrap-test.js b/bootstrap-test.js index 181dcbbf338b82..e50256f2b0b82c 100644 --- a/bootstrap-test.js +++ b/bootstrap-test.js @@ -31,6 +31,7 @@ global.window.tinyMCEPreInit = { //