diff --git a/lib/block-parser.php b/lib/block-parser.php index 670e78acb52172..622e229b8a7e45 100644 --- a/lib/block-parser.php +++ b/lib/block-parser.php @@ -1,18 +1,10 @@ )'; + const BLOCK_COMMENT_OPEN = '(^)'; const BLOCK_NAME = '(^[[:alpha:]](?:[[:alnum:]]|/[[:alnum:]])*)i'; const BLOCK_ATTRIBUTES = '(^{(?:((?!}[ \t\r\n]+/?-->).)*)})'; const WS = '(^[ \t\r\n])'; @@ -20,29 +12,123 @@ class Gutenberg_Block_Parser { 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( '(^)' ) ) ), + 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 ); @@ -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( '(^' - ) + [ [ 'core/void', [] ], '' ], + Gutenberg_Block_Parser::block_opening( '' ) + ); + } + + function test_block_opening_with_empty_attrs() { + $this->assertEquals( + [ [ 'core/void', [] ], '' ], + Gutenberg_Block_Parser::block_opening( '' ) ); } - 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( - '' + [ [ 'core/void', [ 'val' => 1337 ] ], '' ], + Gutenberg_Block_Parser::block_opening( '' ) + ); + } + + function test_block_opening_with_extra_space() { + $this->assertEquals( + [ [ 'core/void', [ 'weird' => true ] ], '' ], + Gutenberg_Block_Parser::block_opening( + "" ) ); } - function test_block_void_with_non_empty_attrs() { + function test_block_opening_leaves_remaining() { + list( /* result */, $remaining ) = Gutenberg_Block_Parser::block_opening( + '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( '' ) ); + } + + function test_block_opening_fails_html_comment() { + $this->assertEquals( [], Gutenberg_Block_Parser::block_opening( '' ) ); + } + + function test_block_closing() { + $this->assertEquals( + [ 'core/void', '' ], + Gutenberg_Block_Parser::block_closing( '' ) + ); + } + + function test_block_closing_fails_with_opening() { + $this->assertEquals( [], Gutenberg_Block_Parser::block_closing( '' ) ); + } + + function test_raw_chunk_text() { + $this->assertEquals( + [ 'test', '' ], + Gutenberg_Block_Parser::raw_chunk('test' ) + ); + } + + function test_raw_chunk_with_opening() { + $this->assertEquals( + [ 'test', '' ], + Gutenberg_Block_Parser::raw_chunk( 'test' ) + ); + } + + function test_raw_chunk_with_closing() { + $this->assertEquals( + [ 'test', '' ], + Gutenberg_Block_Parser::raw_chunk( 'test' ) + ); + } + + function test_raw_chunk_eats_html_comments() { + $this->assertEquals( + [ 'testtext', '' ], + Gutenberg_Block_Parser::raw_chunk( 'testtext' ) + ); + } + + 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( '' ) + ); + } + + function test_parse_simple_block_with_attrs() { + $this->assertEquals( + [ [ + 'blockName' => 'core/void', + 'attrs' => [ 'method' => 'GET' ], + 'rawContent' => '' + ] ], + Gutenberg_Block_Parser::parse( '' ) + ); + } + + function test_parse_two_simple_blocks() { + $this->assertEquals( + [ [ + 'blockName' => 'core/one', + 'attrs' => [], + 'rawContent' => '' + ], [ + 'blockName' => 'core/two', + 'attrs' => [], + 'rawContent' => '' + ] ], + Gutenberg_Block_Parser::parse( '' ) + ); + } + + 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' => '
HTML
' + ], [ 'blockName' => 'core/void', - 'attrs' => [ - 'val' => 1337 - ], + 'attrs' => [], 'rawContent' => '' - ], '' ], - Gutenberg_Block_Parser::block_void( - '' + ] ], + Gutenberg_Block_Parser::parse( 'HTML
' ) + ); + } + + // currently we don't allow nesting + function test_parse_nested_block() { + $this->assertEquals( + [ [ + 'blockName' => 'core/outer', + 'attrs' => [], + 'rawContent' => 'beforeinsideafter' + ] ], + Gutenberg_Block_Parser::parse( + 'beforeinsideafter' ) ); }