Skip to content

Commit

Permalink
Update parser to support basic blocks
Browse files Browse the repository at this point in the history
Not included in this update:
 - Void block syntax
 - More tag, nofollow tag, nextpage tag
  • Loading branch information
dmsnell committed Jul 10, 2017
1 parent cbe58e2 commit 53543a3
Show file tree
Hide file tree
Showing 2 changed files with 291 additions and 37 deletions.
156 changes: 135 additions & 21 deletions lib/block-parser.php
Original file line number Diff line number Diff line change
@@ -1,48 +1,134 @@
<?php

if (!class_exists('Gutenberg_Block_Parser_State', false)):

class Gutenberg_Block_Parser_State {
public $block_stack;
}

endif;

if (!class_exists('Gutenberg_Block_Parser', false)):

class Gutenberg_Block_Parser {
const BLOCK_COMMENT_OPEN = '(^<!--)';
const BLOCK_COMMENT_CLOSE = '(^/?-->)';
const BLOCK_COMMENT_OPEN = '(^<!--[ \t\r\n]+wp:)';
const BLOCK_COMMENT_CLOSE = '(^[ \t\r\n]+/?-->)';
const BLOCK_NAME = '(^[[:alpha:]](?:[[:alnum:]]|/[[:alnum:]])*)i';
const BLOCK_ATTRIBUTES = '(^{(?:((?!}[ \t\r\n]+/?-->).)*)})';
const WS = '(^[ \t\r\n])';
const WSS = '(^[ \t\r\n]+)';

const MAX_RUNTIME = 1; // give up after one second

public function parse($input) {
public static function parse($input) {
$tic = microtime( true );

$remaining = $input;
$output = array();
$block_stack = array();

// trampoline for stack-safe recursion of the actual parser
while ( $this->input && ( microtime( true ) - $tic ) < self::MAX_RUNTIME ) {
return $this->proceed( $input );
while ( $remaining && ( microtime( true ) - $tic ) < self::MAX_RUNTIME ) {
list(
$remaining,
$output,
$block_stack,
) = self::proceed( $remaining, $output, $block_stack );
}

return $output;
}

public function proceed( $input ) {
return succeed( 'test', $input );
public static function proceed( $input, $output, $block_stack ) {
// open a new block
$opener = self::block_opening( $input );

if ( ! empty( $opener ) ) {
list( list( $block_name, $attrs ), $remaining ) = $opener;

return array(
$remaining,
$output,
array_merge( $block_stack, array( self::block( $block_name, $attrs, '' ) ) )
);
}

$open_blocks = array_slice( $block_stack, 0 );
$this_block = array_pop( $open_blocks );

// close out the block
$closer = self::block_closing( $input );

if ( ! empty( $closer ) ) {
// must be in an open block
if ( ! $this_block ) {
return array(
'',
array_merge( $output, array( self::freeform( $input ) ) ),
$block_stack
);
}

list( $block_name, $remaining ) = $closer;

// we have a block mismatch and can go no further
if ( $block_name !== $this_block[ 'blockName' ] ) {
return array(
'',
array_merge( $output, array( self::freeform( $input ) ) ),
$block_stack
);
}

// close the block and update the parent's raw content
$parent_block = array_pop( $open_blocks );

// not every block has a parent
if ( ! isset( $parent_block ) ) {
return array(
$remaining,
array_merge( $output, array( $this_block ) ),
$open_blocks
);
}

$parent_block[ 'rawContent' ] .= $this_block[ 'rawContent' ];

return array(
$remaining,
$output,
array_merge( $open_blocks, array( $parent_block ) )
);
}

// eat raw content
$chunk = self::raw_chunk( $input );

if ( ! empty( $chunk ) ) {
list( $raw_content, $remaining ) = $chunk;

// we can come before a block opens
if ( ! $this_block ) {
return array(
$remaining,
array_merge( $output, array( self::freeform( $raw_content ) ) ),
$block_stack
);
}

// or we can add to the inside content of an open block
$this_block[ 'rawContent' ] .= $raw_content;

return array(
$remaining,
$output,
array_merge( $open_blocks, array( $this_block ) )
);
}
}

public static function block_void( $input ) {
public static function block_opening( $input ) {
$result = self::sequence( array(
array( 'self::ignore', array( 'self::match', array( '(^<!--[ \t\r\n]+wp:)' ) ) ),
array( 'self::ignore', array( 'self::match', array( self::BLOCK_COMMENT_OPEN ) ) ),
array( 'self::match', array( self::BLOCK_NAME ) ),
array( 'self::first_of', array( array(
array( 'self::ignore', array( 'self::match', array( '(^[ \t\r\n]+/-->)' ) ) ),
array( 'self::ignore', array( 'self::match', array( self::BLOCK_COMMENT_CLOSE ) ) ),
array( 'self::sequence', array( array(
array( 'self::ignore', array( 'self::match', array( self::WSS ) ) ),
array( 'self::match', array( self::BLOCK_ATTRIBUTES ) ),
array( 'self::ignore', array( 'self::match', array( '(^[ \t\r\n]+/-->)' ) ) )
array( 'self::ignore', array( 'self::match', array( self::BLOCK_COMMENT_CLOSE ) ) )
) ) )
) ) )
), $input );
Expand All @@ -56,7 +142,35 @@ public static function block_void( $input ) {
? json_decode( $raw_attrs, true )
: array();

return array( self::block( $blockName, $attrs, '' ), $remaining );
return array( array( $blockName, $attrs ), $remaining );
}

public static function block_closing( $input ) {
$result = self::sequence( array(
array( 'self::ignore', array( 'self::match', array( '(^<!--[ \t\r\n]+/wp:)' ) ) ),
array( 'self::match', array( self::BLOCK_NAME ) ),
array( 'self::ignore', array( 'self::match', array( self::BLOCK_COMMENT_CLOSE ) ) )
), $input );

if ( empty( $result ) ) {
return array();
}

list( list( list( $blockName ) ), $remaining ) = $result;

return array( $blockName, $remaining );
}

public static function raw_chunk( $input ) {
$result = self::match( '(^((?!<!--[ \t\r\n]+/?wp:).)*)', $input );

if ( empty( $result ) ) {
return $result;
}

list( list( $chunk ), $remaining ) = $result;

return array( $chunk, $remaining );
}

//-----------------------------------------
Expand Down Expand Up @@ -198,7 +312,7 @@ public static function block( $blockName, $attrs, $rawContent ) {
}

public static function freeform( $rawContent ) {
return self::block( 'freeform', array(), $rawContent );
return self::block( 'core/freeform', array(), $rawContent );
}
}

Expand Down
172 changes: 156 additions & 16 deletions phpunit/class.block-parser-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,35 +150,175 @@ function test_combinator_sequence_failure() {
);
}

function test_block_void_no_attrs() {
function test_block_opening_no_attrs() {
$this->assertEquals(
[ [ 'blockName' => 'core/void', 'attrs' => [], 'rawContent' => '' ], '' ],
Gutenberg_Block_Parser::block_void(
'<!-- wp:core/void /-->'
)
[ [ 'core/void', [] ], '' ],
Gutenberg_Block_Parser::block_opening( '<!-- wp:core/void /-->' )
);
}

function test_block_opening_with_empty_attrs() {
$this->assertEquals(
[ [ 'core/void', [] ], '' ],
Gutenberg_Block_Parser::block_opening( '<!-- wp:core/void {} /-->' )
);
}

function test_block_void_with_empty_attrs() {
function test_block_opening_with_non_empty_attrs() {
$this->assertEquals(
[ [ 'blockName' => 'core/void', 'attrs' => [], 'rawContent' => '' ], '' ],
Gutenberg_Block_Parser::block_void(
'<!-- wp:core/void {} /-->'
[ [ 'core/void', [ 'val' => 1337 ] ], '' ],
Gutenberg_Block_Parser::block_opening( '<!-- wp:core/void { "val": 1337 } /-->' )
);
}

function test_block_opening_with_extra_space() {
$this->assertEquals(
[ [ 'core/void', [ 'weird' => true ] ], '' ],
Gutenberg_Block_Parser::block_opening(
"<!-- \t \n\r \nwp:core/void\n{ \"weird\":true }\n\t \t \r/-->"
)
);
}

function test_block_void_with_non_empty_attrs() {
function test_block_opening_leaves_remaining() {
list( /* result */, $remaining ) = Gutenberg_Block_Parser::block_opening(
'<!-- wp:core/void /-->just some text'
);

$this->assertEquals( 'just some text', $remaining );
}

function test_block_opening_fails_text() {
$this->assertEquals( [], Gutenberg_Block_Parser::block_opening( 'just a test' ) );
}

function test_block_opening_fails_closer() {
$this->assertEquals( [], Gutenberg_Block_Parser::block_opening( '<!-- /wp:closer -->' ) );
}

function test_block_opening_fails_html_comment() {
$this->assertEquals( [], Gutenberg_Block_Parser::block_opening( '<!-- just a comment -->' ) );
}

function test_block_closing() {
$this->assertEquals(
[ 'core/void', '' ],
Gutenberg_Block_Parser::block_closing( '<!-- /wp:core/void -->' )
);
}

function test_block_closing_fails_with_opening() {
$this->assertEquals( [], Gutenberg_Block_Parser::block_closing( '<!-- wp:core/void /-->' ) );
}

function test_raw_chunk_text() {
$this->assertEquals(
[ 'test', '' ],
Gutenberg_Block_Parser::raw_chunk('test' )
);
}

function test_raw_chunk_with_opening() {
$this->assertEquals(
[ 'test', '<!-- wp:core/void /-->' ],
Gutenberg_Block_Parser::raw_chunk( 'test<!-- wp:core/void /-->' )
);
}

function test_raw_chunk_with_closing() {
$this->assertEquals(
[ 'test', '<!-- /wp:core/void /-->' ],
Gutenberg_Block_Parser::raw_chunk( 'test<!-- /wp:core/void /-->' )
);
}

function test_raw_chunk_eats_html_comments() {
$this->assertEquals(
[ 'test<!-- just a comment -->text', '' ],
Gutenberg_Block_Parser::raw_chunk( 'test<!-- just a comment -->text' )
);
}

function test_parse_empty_document() {
$this->assertEquals(
[],
Gutenberg_Block_Parser::parse( '' )
);
}

function test_parse_simple_block() {
$this->assertEquals(
[ [
'blockName' => 'core/void',
'attrs' => [],
'rawContent' => ''
] ],
Gutenberg_Block_Parser::parse( '<!-- wp:core/void --><!-- /wp:core/void -->' )
);
}

function test_parse_simple_block_with_attrs() {
$this->assertEquals(
[ [
'blockName' => 'core/void',
'attrs' => [ 'method' => 'GET' ],
'rawContent' => ''
] ],
Gutenberg_Block_Parser::parse( '<!-- wp:core/void { "method": "GET" } --><!-- /wp:core/void -->' )
);
}

function test_parse_two_simple_blocks() {
$this->assertEquals(
[ [
'blockName' => 'core/one',
'attrs' => [],
'rawContent' => ''
], [
'blockName' => 'core/two',
'attrs' => [],
'rawContent' => ''
] ],
Gutenberg_Block_Parser::parse( '<!-- wp:core/one --><!-- /wp:core/one --><!-- wp:core/two --><!-- /wp:core/two -->' )
);
}

function test_parse_freeform() {
$this->assertEquals(
[ [
'blockName' => 'core/freeform',
'attrs' => [],
'rawContent' => 'test'
] ],
Gutenberg_Block_Parser::parse( 'test' )
);
}

function test_parse_raw_chunk_prefix() {
$this->assertEquals(
[ [
'blockName' => 'core/freeform',
'attrs' => [],
'rawContent' => '<p>HTML</p>'
], [
'blockName' => 'core/void',
'attrs' => [
'val' => 1337
],
'attrs' => [],
'rawContent' => ''
], '' ],
Gutenberg_Block_Parser::block_void(
'<!-- wp:core/void { "val": 1337 } /-->'
] ],
Gutenberg_Block_Parser::parse( '<p>HTML</p><!-- wp:core/void --><!-- /wp:core/void -->' )
);
}

// currently we don't allow nesting
function test_parse_nested_block() {
$this->assertEquals(
[ [
'blockName' => 'core/outer',
'attrs' => [],
'rawContent' => 'beforeinsideafter'
] ],
Gutenberg_Block_Parser::parse(
'<!-- wp:core/outer -->before<!-- wp:core/inner -->inside<!-- /wp:core/inner -->after<!-- /wp:core/outer -->'
)
);
}
Expand Down

0 comments on commit 53543a3

Please sign in to comment.