diff --git a/lib/blocks.php b/lib/blocks.php index 6ef34c4176eefb..a8083466a04053 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -58,21 +58,6 @@ function unregister_block_type( $name ) { * @return array Array of parsed block objects. */ function gutenberg_parse_blocks( $content ) { - /* - * If there are no blocks in the content, return a single block, rather - * than wasting time trying to parse the string. - */ - if ( ! has_blocks( $content ) ) { - return array( - array( - 'blockName' => null, - 'attrs' => array(), - 'innerBlocks' => array(), - 'innerHTML' => $content, - ), - ); - } - /** * Filter to allow plugins to replace the server-side block parser * @@ -148,27 +133,34 @@ function get_dynamic_blocks_regex() { * Renders a single block into a HTML string. * * @since 1.9.0 + * @since 4.4.0 renders full nested tree of blocks before reassembling into HTML string + * @global WP_Post $post The post to edit. * * @param array $block A single parsed block object. * @return string String of rendered HTML. */ function gutenberg_render_block( $block ) { - $block_name = isset( $block['blockName'] ) ? $block['blockName'] : null; - $attributes = is_array( $block['attrs'] ) ? $block['attrs'] : array(); - $raw_content = isset( $block['innerHTML'] ) ? $block['innerHTML'] : null; + global $post; - if ( $block_name ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); - if ( null !== $block_type && $block_type->is_dynamic() ) { - return $block_type->render( $attributes ); - } + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $is_dynamic = $block['blockName'] && null !== $block_type && $block_type->is_dynamic(); + $inner_content = ''; + $index = 0; + + foreach ( $block['innerContent'] as $chunk ) { + $inner_content .= is_string( $chunk ) ? $chunk : gutenberg_render_block( $block['innerBlocks'][ $index++ ] ); } - if ( $raw_content ) { - return $raw_content; + if ( $is_dynamic ) { + $attributes = is_array( $block['attrs'] ) ? (array) $block['attrs'] : array(); + $global_post = $post; + $output = $block_type->render( $attributes, $inner_content ); + $post = $global_post; + + return $output; } - return ''; + return $inner_content; } if ( ! function_exists( 'do_blocks' ) ) { @@ -176,91 +168,20 @@ function gutenberg_render_block( $block ) { * Parses dynamic blocks out of `post_content` and re-renders them. * * @since 0.1.0 - * @global WP_Post $post The post to edit. + * @since 4.4.0 performs full parse on input post content * * @param string $content Post content. * @return string Updated post content. */ function do_blocks( $content ) { - global $post; - - $rendered_content = ''; - $dynamic_block_pattern = get_dynamic_blocks_regex(); - - /* - * Back up global post, to restore after render callback. - * Allows callbacks to run new WP_Query instances without breaking the global post. - */ - $global_post = $post; - - while ( preg_match( $dynamic_block_pattern, $content, $block_match, PREG_OFFSET_CAPTURE ) ) { - $opening_tag = $block_match[0][0]; - $offset = $block_match[0][1]; - $block_name = $block_match[1][0]; - $is_self_closing = isset( $block_match[4] ); - - // Reset attributes JSON to prevent scope bleed from last iteration. - $block_attributes_json = null; - if ( isset( $block_match[3] ) ) { - $block_attributes_json = $block_match[3][0]; - } + $blocks = gutenberg_parse_blocks( $content ); + $output = ''; - // Since content is a working copy since the last match, append to - // rendered content up to the matched offset... - $rendered_content .= substr( $content, 0, $offset ); - - // ...then update the working copy of content. - $content = substr( $content, $offset + strlen( $opening_tag ) ); - - // Make implicit core namespace explicit. - $is_implicit_core_namespace = ( false === strpos( $block_name, '/' ) ); - $normalized_block_name = $is_implicit_core_namespace ? 'core/' . $block_name : $block_name; - - // Find registered block type. We can assume it exists since we use the - // `get_dynamic_block_names` function as a source for pattern matching. - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $normalized_block_name ); - - // Attempt to parse attributes JSON, if available. - $attributes = array(); - if ( ! empty( $block_attributes_json ) ) { - $decoded_attributes = json_decode( $block_attributes_json, true ); - if ( ! is_null( $decoded_attributes ) ) { - $attributes = $decoded_attributes; - } - } - - $inner_content = ''; - - if ( ! $is_self_closing ) { - $end_tag_pattern = '//'; - if ( ! preg_match( $end_tag_pattern, $content, $block_match_end, PREG_OFFSET_CAPTURE ) ) { - // If no closing tag is found, abort all matching, and continue - // to append remainder of content to rendered output. - break; - } - - // Update content to omit text up to and including closing tag. - $end_tag = $block_match_end[0][0]; - $end_offset = $block_match_end[0][1]; - - $inner_content = substr( $content, 0, $end_offset ); - $content = substr( $content, $end_offset + strlen( $end_tag ) ); - } - - // Replace dynamic block with server-rendered output. - $rendered_content .= $block_type->render( $attributes, $inner_content ); - - // Restore global $post. - $post = $global_post; + foreach ( $blocks as $block ) { + $output .= gutenberg_render_block( $block ); } - // Append remaining unmatched content. - $rendered_content .= $content; - - // Strip remaining block comment demarcations. - $rendered_content = preg_replace( '/\r?\n?/m', '', $rendered_content ); - - return $rendered_content; + return $output; } add_filter( 'the_content', 'do_blocks', 7 ); // BEFORE do_shortcode() and oembed. diff --git a/phpunit/class-do-blocks-test.php b/phpunit/class-do-blocks-test.php index 9da7ddc0fcb27d..147545e65ca0a2 100644 --- a/phpunit/class-do-blocks-test.php +++ b/phpunit/class-do-blocks-test.php @@ -9,6 +9,19 @@ * Test do_blocks */ class Do_Blocks_Test extends WP_UnitTestCase { + /** + * Tear down. + */ + function tearDown() { + parent::tearDown(); + + $registry = WP_Block_Type_Registry::get_instance(); + + if ( $registry->is_registered( 'core/dummy' ) ) { + $registry->unregister( 'core/dummy' ); + } + } + /** * Test do_blocks removes comment demarcations. * @@ -30,7 +43,7 @@ function test_the_content() { add_shortcode( 'someshortcode', array( $this, 'handle_shortcode' ) ); $classic_content = "Foo\n\n[someshortcode]\n\nBar\n\n[/someshortcode]\n\nBaz"; - $block_content = "\n
Foo
\n\n\n[someshortcode]\n\nBar\n\n[/someshortcode]\n\n\nBaz
\n"; + $block_content = "Foo
\n\n\n[someshortcode]\n\nBar\n\n[/someshortcode]\n\n\nBaz
\n"; $classic_filtered_content = apply_filters( 'the_content', $classic_content ); $block_filtered_content = apply_filters( 'the_content', $block_content ); @@ -41,7 +54,43 @@ function test_the_content() { $this->assertEquals( $classic_filtered_content, $block_filtered_content ); } + function test_can_nest_at_least_so_deep() { + $minimum_depth = 99; + + $content = 'deep inside'; + for ( $i = 0; $i < $minimum_depth; $i++ ) { + $content = '' . $content . ''; + } + + $this->assertEquals( 'deep inside', do_blocks( $content ) ); + } + + function test_can_nest_at_least_so_deep_with_dynamic_blocks() { + $minimum_depth = 99; + + $content = '0'; + for ( $i = 0; $i < $minimum_depth; $i++ ) { + $content = '' . $content . ''; + } + + register_block_type( + 'core/dummy', + array( + 'render_callback' => array( + $this, + 'render_dynamic_incrementer', + ), + ) + ); + + $this->assertEquals( $minimum_depth, (int) do_blocks( $content ) ); + } + function handle_shortcode( $atts, $content ) { return $content; } + + function render_dynamic_incrementer( $attrs, $content ) { + return (string) ( 1 + (int) $content ); + } } diff --git a/phpunit/class-dynamic-blocks-render-test.php b/phpunit/class-dynamic-blocks-render-test.php index 812d6aa8d21e1f..35799bc9bf73db 100644 --- a/phpunit/class-dynamic-blocks-render-test.php +++ b/phpunit/class-dynamic-blocks-render-test.php @@ -38,6 +38,10 @@ function render_dummy_block_numeric() { return 10; } + function render_serialize_dynamic_block( $attributes, $content ) { + return base64_encode( serialize( array( $attributes, $content ) ) ); + } + /** * Dummy block rendering function, creating a new WP_Query instance. * @@ -74,7 +78,14 @@ function tearDown() { $this->dummy_block_instance_number = 0; $registry = WP_Block_Type_Registry::get_instance(); - $registry->unregister( 'core/dummy' ); + + if ( $registry->is_registered( 'core/dummy' ) ) { + $registry->unregister( 'core/dummy' ); + } + + if ( $registry->is_registered( 'core/dynamic' ) ) { + $registry->unregister( 'core/dynamic' ); + } } /** @@ -164,4 +175,69 @@ function test_dynamic_block_renders_string() { $this->assertSame( '10', $rendered ); $this->assertInternalType( 'string', $rendered ); } + + function test_dynamic_block_gets_inner_html() { + register_block_type( + 'core/dynamic', + array( + 'render_callback' => array( + $this, + 'render_serialize_dynamic_block', + ), + ) + ); + + $output = do_blocks( 'inner' ); + + list( /* attrs */, $content ) = unserialize( base64_decode( $output ) ); + + $this->assertEquals( 'inner', $content ); + } + + function test_dynamic_block_gets_rendered_inner_blocks() { + register_block_type( + 'core/dummy', + array( + 'render_callback' => array( + $this, + 'render_dummy_block_numeric', + ), + ) + ); + register_block_type( + 'core/dynamic', + array( + 'render_callback' => array( + $this, + 'render_serialize_dynamic_block', + ), + ) + ); + + $output = do_blocks( 'beforeafter' ); + + list( /* attrs */, $content ) = unserialize( base64_decode( $output ) ); + + $this->assertEquals( 'before10after', $content ); + } + + function test_dynamic_block_gets_rendered_inner_dynamic_blocks() { + register_block_type( + 'core/dynamic', + array( + 'render_callback' => array( + $this, + 'render_serialize_dynamic_block', + ), + ) + ); + + $output = do_blocks( 'beforedeep innerafter' ); + + list( /* attrs */, $content ) = unserialize( base64_decode( $output ) ); + + $inner = $this->render_serialize_dynamic_block( array(), 'deep inner' ); + + $this->assertEquals( $content, 'before' . $inner . 'after' ); + } } diff --git a/phpunit/fixtures/do-blocks-expected.html b/phpunit/fixtures/do-blocks-expected.html index 4a3dc379ef48f9..f4131820513347 100644 --- a/phpunit/fixtures/do-blocks-expected.html +++ b/phpunit/fixtures/do-blocks-expected.html @@ -2,13 +2,18 @@ +First Gutenberg Paragraph
+Second Auto Paragraph
+ +Third Gutenberg Paragraph
+Third Auto Paragraph
[someshortcode]