From bf17aab9df20d245fd2deb12b7daf38858599862 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 21 Sep 2018 22:14:01 -0400 Subject: [PATCH] Parser: Normalize data types and fix default implementation Resolves #10041 Resolves #10047 A few inconsistencies have remained in the grammar specification concerning freeform blocks and blocks without attributes in the block delimiters. Freeform blocks were returned without block names and blocks without attributes returned `null` instead of an empty set of attributes. Further, the default parser implementation (from #8083) was returning an array of block objects instead of an array of generic arrays. This resulted in mismatches in PHP of accessing properties with `$block[ 'attrs' ]` syntax vs `$block->attrs` syntax. In this patch I've updatd the specification to remove all of the type ambiguity and have updated the default parser to match it. After this patch every block should be accessible as a normal array in PHP and have all properties: `blockName`, `attrs`, `innerBlocks`, and `innerHTML`. If no attributes are specified then `attrs` will be an empty set (in JavaScript `{}` and in PHP `array()`). --- lib/blocks.php | 6 ++- lib/parser.php | 35 ++++++++---- .../parser.php | 53 ++++++++++--------- .../src/index.js | 25 +++------ .../grammar.pegjs | 43 ++++++++++----- .../block-serialization-spec-parser/index.js | 43 ++++++++++----- 6 files changed, 126 insertions(+), 79 deletions(-) diff --git a/lib/blocks.php b/lib/blocks.php index 92e89abddd646..bf2dee358c8df 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -60,8 +60,10 @@ function gutenberg_parse_blocks( $content ) { if ( ! has_blocks( $content ) ) { return array( array( - 'attrs' => array(), - 'innerHTML' => $content, + 'blockName' => 'core/freeform', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $content, ), ); } diff --git a/lib/parser.php b/lib/parser.php index 25d37f3c37b8b..a1fb922936619 100644 --- a/lib/parser.php +++ b/lib/parser.php @@ -259,26 +259,26 @@ private function peg_f1($pre, $bs, $post) { return peg_join_blocks( $pre, $bs, $ private function peg_f2($blockName, $a) { return $a; } private function peg_f3($blockName, $attrs) { return array( - 'blockName' => $blockName, - 'attrs' => $attrs, + 'blockName' => $blockName, + 'attrs' => isset( $attrs ) ? $attrs : array(), 'innerBlocks' => array(), - 'innerHTML' => '', + 'innerHTML' => '', ); } private function peg_f4($s, $children, $e) { list( $innerHTML, $innerBlocks ) = peg_array_partition( $children, 'is_string' ); return array( - 'blockName' => $s['blockName'], - 'attrs' => $s['attrs'], + 'blockName' => $s['blockName'], + 'attrs' => $s['attrs'], 'innerBlocks' => $innerBlocks, - 'innerHTML' => implode( '', $innerHTML ), + 'innerHTML' => implode( '', $innerHTML ), ); } private function peg_f5($blockName, $attrs) { return array( 'blockName' => $blockName, - 'attrs' => $attrs, + 'attrs' => isset( $attrs ) ? $attrs : array(), ); } private function peg_f6($blockName) { @@ -1461,7 +1461,12 @@ function peg_join_blocks( $pre, $tokens, $post ) { $blocks = array(); if ( ! empty( $pre ) ) { - $blocks[] = array( 'attrs' => array(), 'innerHTML' => $pre ); + $blocks[] = array( + 'blockName' => 'core/freeform', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $pre + ); } foreach ( $tokens as $token ) { @@ -1470,12 +1475,22 @@ function peg_join_blocks( $pre, $tokens, $post ) { $blocks[] = $token; if ( ! empty( $html ) ) { - $blocks[] = array( 'attrs' => array(), 'innerHTML' => $html ); + $blocks[] = array( + 'blockName' => 'core/freeform', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $html + ); } } if ( ! empty( $post ) ) { - $blocks[] = array( 'attrs' => array(), 'innerHTML' => $post ); + $blocks[] = array( + 'blockName' => 'core/freeform', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $post + ); } return $blocks; diff --git a/packages/block-serialization-default-parser/parser.php b/packages/block-serialization-default-parser/parser.php index 9ea5b6ab78919..e39ade0ab29d9 100644 --- a/packages/block-serialization-default-parser/parser.php +++ b/packages/block-serialization-default-parser/parser.php @@ -121,6 +121,7 @@ function __construct( $block, $token_start, $token_length, $prev_offset = null, * Parses a document and constructs a list of parsed block objects * * @since 3.8.0 + * @since 4.0.0 returns arrays not objects, all attributes are arrays */ class WP_Block_Parser { /** @@ -244,17 +245,14 @@ function proceed() { */ if ( 0 === $stack_depth ) { if ( isset( $leading_html_start ) ) { - $this->output[] = array( - 'attrs' => array(), - 'innerHTML' => substr( - $this->document, - $leading_html_start, - $start_offset - $leading_html_start - ), - ); + $this->output[] = (array) self::freeform( substr( + $this->document, + $leading_html_start, + $start_offset - $leading_html_start + ) ); } - $this->output[] = new WP_Block_Parser_Block( $block_name, $attrs, array(), '' ); + $this->output[] = (array) new WP_Block_Parser_Block( $block_name, $attrs, array(), '' ); $this->offset = $start_offset + $token_length; return true; } @@ -370,7 +368,7 @@ function next_token() { $namespace = ( isset( $namespace ) && -1 !== $namespace[ 1 ] ) ? $namespace[ 0 ] : 'core/'; $name = $namespace . $matches[ 'name' ][ 0 ]; $has_attrs = isset( $matches[ 'attrs' ] ) && -1 !== $matches[ 'attrs' ][ 1 ]; - $attrs = $has_attrs ? json_decode( $matches[ 'attrs' ][ 0 ] ) : null; + $attrs = $has_attrs ? json_decode( $matches[ 'attrs' ][ 0 ], /* as-associative */ true ) : array(); /* * This state isn't allowed @@ -391,6 +389,19 @@ function next_token() { return array( 'block-opener', $name, $attrs, $started_at, $length ); } + /** + * Returns a new block object for freeform HTML + * + * @internal + * @since 3.9.0 + * + * @param string $innerHTML HTML content of block + * @return WP_Block_Parser_Block freeform block object + */ + static function freeform( $innerHTML ) { + return new WP_Block_Parser_Block( 'core/freeform', array(), array(), $innerHTML ); + } + /** * Pushes a length of text from the input document * to the output list as a freeform block @@ -406,10 +417,7 @@ function add_freeform( $length = null ) { return; } - $this->output[] = array( - 'attrs' => new stdClass(), - 'innerHTML' => substr( $this->document, $this->offset, $length ), - ); + $this->output[] = (array) self::freeform( substr( $this->document, $this->offset, $length ) ); } /** @@ -423,7 +431,7 @@ function add_freeform( $length = null ) { * @param int $token_length byte length of entire block from start of opening token to end of closing token * @param int|null $last_offset last byte offset into document if continuing form earlier output */ - function add_inner_block(WP_Block_Parser_Block $block, $token_start, $token_length, $last_offset = null ) { + function add_inner_block( WP_Block_Parser_Block $block, $token_start, $token_length, $last_offset = null ) { $parent = $this->stack[ count( $this->stack ) - 1 ]; $parent->block->innerBlocks[] = $block; $parent->block->innerHTML .= substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset ); @@ -446,16 +454,13 @@ function add_block_from_stack( $end_offset = null ) { : substr( $this->document, $prev_offset ); if ( isset( $stack_top->leading_html_start ) ) { - $this->output[] = array( - 'attrs' => array(), - 'innerHTML' => substr( - $this->document, - $stack_top->leading_html_start, - $stack_top->token_start - $stack_top->leading_html_start - ), - ); + $this->output[] = (array) self::freeform( substr( + $this->document, + $stack_top->leading_html_start, + $stack_top->token_start - $stack_top->leading_html_start + ) ); } - $this->output[] = $stack_top->block; + $this->output[] = (array) $stack_top->block; } } diff --git a/packages/block-serialization-default-parser/src/index.js b/packages/block-serialization-default-parser/src/index.js index a4cec7aecd8ef..fef329298d384 100644 --- a/packages/block-serialization-default-parser/src/index.js +++ b/packages/block-serialization-default-parser/src/index.js @@ -13,6 +13,10 @@ function Block( blockName, attrs, innerBlocks, innerHTML ) { }; } +function Freeform( innerHTML ) { + return Block( 'core/freeform', {}, [], innerHTML ); +} + function Frame( block, tokenStart, tokenLength, prevOffset, leadingHtmlStart ) { return { block, @@ -78,10 +82,7 @@ function proceed() { // in the top-level of the document if ( 0 === stackDepth ) { if ( null !== leadingHtmlStart ) { - output.push( { - attrs: {}, - innerHTML: document.substr( leadingHtmlStart, startOffset - leadingHtmlStart ), - } ); + output.push( Freeform( document.substr( leadingHtmlStart, startOffset - leadingHtmlStart ) ) ); } output.push( Block( blockName, attrs, [], '' ) ); offset = startOffset + tokenLength; @@ -196,7 +197,7 @@ function nextToken() { const namespace = namespaceMatch || 'core/'; const name = namespace + nameMatch; const hasAttrs = !! attrsMatch; - const attrs = hasAttrs ? parseJSON( attrsMatch ) : null; + const attrs = hasAttrs ? parseJSON( attrsMatch ) : {}; // This state isn't allowed // This is an error @@ -223,14 +224,7 @@ function addFreeform( rawLength ) { return; } - // why is this not a Frame? it's because the current grammar - // specifies an object that's different. we can update the - // specification and change here if we want to but for now we - // want this parser to be spec-compliant - output.push( { - attrs: {}, - innerHTML: document.substr( offset, length ), - } ); + output.push( Freeform( document.substr( offset, length ) ) ); } function addInnerBlock( block, tokenStart, tokenLength, lastOffset ) { @@ -253,10 +247,7 @@ function addBlockFromStack( endOffset ) { } if ( null !== leadingHtmlStart ) { - output.push( { - attrs: {}, - innerHTML: document.substr( leadingHtmlStart, tokenStart - leadingHtmlStart ), - } ); + output.push( Freeform( document.substr( leadingHtmlStart, tokenStart - leadingHtmlStart ) ) ); } output.push( block ); diff --git a/packages/block-serialization-spec-parser/grammar.pegjs b/packages/block-serialization-spec-parser/grammar.pegjs index aa1dc1b70a29e..ece4a45600a49 100644 --- a/packages/block-serialization-spec-parser/grammar.pegjs +++ b/packages/block-serialization-spec-parser/grammar.pegjs @@ -71,7 +71,12 @@ if ( ! function_exists( 'peg_join_blocks' ) ) { $blocks = array(); if ( ! empty( $pre ) ) { - $blocks[] = array( 'attrs' => array(), 'innerHTML' => $pre ); + $blocks[] = array( + 'blockName' => 'core/freeform', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $pre + ); } foreach ( $tokens as $token ) { @@ -80,12 +85,22 @@ if ( ! function_exists( 'peg_join_blocks' ) ) { $blocks[] = $token; if ( ! empty( $html ) ) { - $blocks[] = array( 'attrs' => array(), 'innerHTML' => $html ); + $blocks[] = array( + 'blockName' => 'core/freeform', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $html + ); } } if ( ! empty( $post ) ) { - $blocks[] = array( 'attrs' => array(), 'innerHTML' => $post ); + $blocks[] = array( + 'blockName' => 'core/freeform', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $post + ); } return $blocks; @@ -96,8 +111,10 @@ if ( ! function_exists( 'peg_join_blocks' ) ) { function freeform( s ) { return s.length && { + blockName: 'core/freeform', attrs: {}, - innerHTML: s + innerBlocks: [], + innerHTML: s, }; } @@ -180,16 +197,16 @@ Block_Void { /** $blockName, - 'attrs' => $attrs, + 'blockName' => $blockName, + 'attrs' => isset( $attrs ) ? $attrs : array(), 'innerBlocks' => array(), - 'innerHTML' => '', + 'innerHTML' => '', ); ?> **/ return { blockName: blockName, - attrs: attrs, + attrs: attrs || {}, innerBlocks: [], innerHTML: '' }; @@ -202,10 +219,10 @@ Block_Balanced list( $innerHTML, $innerBlocks ) = peg_array_partition( $children, 'is_string' ); return array( - 'blockName' => $s['blockName'], - 'attrs' => $s['attrs'], + 'blockName' => $s['blockName'], + 'attrs' => $s['attrs'], 'innerBlocks' => $innerBlocks, - 'innerHTML' => implode( '', $innerHTML ), + 'innerHTML' => implode( '', $innerHTML ), ); ?> **/ @@ -230,13 +247,13 @@ Block_Start /** $blockName, - 'attrs' => $attrs, + 'attrs' => isset( $attrs ) ? $attrs : array(), ); ?> **/ return { blockName: blockName, - attrs: attrs + attrs: attrs || {} }; } diff --git a/packages/block-serialization-spec-parser/index.js b/packages/block-serialization-spec-parser/index.js index 95a476bc3cbdb..5106a8b87d4e4 100644 --- a/packages/block-serialization-spec-parser/index.js +++ b/packages/block-serialization-spec-parser/index.js @@ -165,16 +165,16 @@ peg$c10 = function(blockName, attrs) { /** $blockName, - 'attrs' => $attrs, + 'blockName' => $blockName, + 'attrs' => isset( $attrs ) ? $attrs : array(), 'innerBlocks' => array(), - 'innerHTML' => '', + 'innerHTML' => '', ); ?> **/ return { blockName: blockName, - attrs: attrs, + attrs: attrs || {}, innerBlocks: [], innerHTML: '' }; @@ -184,10 +184,10 @@ list( $innerHTML, $innerBlocks ) = peg_array_partition( $children, 'is_string' ); return array( - 'blockName' => $s['blockName'], - 'attrs' => $s['attrs'], + 'blockName' => $s['blockName'], + 'attrs' => $s['attrs'], 'innerBlocks' => $innerBlocks, - 'innerHTML' => implode( '', $innerHTML ), + 'innerHTML' => implode( '', $innerHTML ), ); ?> **/ @@ -208,13 +208,13 @@ /** $blockName, - 'attrs' => $attrs, + 'attrs' => isset( $attrs ) ? $attrs : array(), ); ?> **/ return { blockName: blockName, - attrs: attrs + attrs: attrs || {} }; }, peg$c15 = "/wp:", @@ -1498,7 +1498,12 @@ $blocks = array(); if ( ! empty( $pre ) ) { - $blocks[] = array( 'attrs' => array(), 'innerHTML' => $pre ); + $blocks[] = array( + 'blockName' => 'core/freeform', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $pre + ); } foreach ( $tokens as $token ) { @@ -1507,12 +1512,22 @@ $blocks[] = $token; if ( ! empty( $html ) ) { - $blocks[] = array( 'attrs' => array(), 'innerHTML' => $html ); + $blocks[] = array( + 'blockName' => 'core/freeform', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $html + ); } } if ( ! empty( $post ) ) { - $blocks[] = array( 'attrs' => array(), 'innerHTML' => $post ); + $blocks[] = array( + 'blockName' => 'core/freeform', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $post + ); } return $blocks; @@ -1523,8 +1538,10 @@ function freeform( s ) { return s.length && { + blockName: 'core/freeform', attrs: {}, - innerHTML: s + innerBlocks: [], + innerHTML: s, }; }