From 49f05e53a4e9ceff0c08fe646f3b0bb724f58b53 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 30 Jan 2018 22:27:48 +0100 Subject: [PATCH] Framework: Skip server grammar parse, seek dynamic blocks explicitly (#4591) --- lib/blocks.php | 101 ++++++++++++++++-- lib/class-wp-block-type.php | 27 +++-- phpunit/class-block-type-test.php | 36 +++---- phpunit/class-dynamic-blocks-render-test.php | 43 ++------ phpunit/class-registration-test.php | 22 +++- phpunit/class-reusable-blocks-render-test.php | 4 +- 6 files changed, 152 insertions(+), 81 deletions(-) diff --git a/lib/blocks.php b/lib/blocks.php index 43534f2815dae..1d7b76773d8ee 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -70,6 +70,24 @@ function gutenberg_parse_blocks( $content ) { return $parser->parse( _gutenberg_utf8_split( $content ) ); } +/** + * Returns an array of the names of all registered dynamic block types. + * + * @return array Array of dynamic block names. + */ +function get_dynamic_block_names() { + $dynamic_block_names = array(); + + $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); + foreach ( $block_types as $block_type ) { + if ( $block_type->is_dynamic() ) { + $dynamic_block_names[] = $block_type->name; + } + } + + return $dynamic_block_names; +} + /** * Renders a single block into a HTML string. * @@ -85,8 +103,8 @@ function gutenberg_render_block( $block ) { if ( $block_name ) { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); - if ( null !== $block_type ) { - return $block_type->render( $attributes, $raw_content ); + if ( null !== $block_type && $block_type->is_dynamic() ) { + return $block_type->render( $attributes ); } } @@ -106,12 +124,81 @@ function gutenberg_render_block( $block ) { * @return string Updated post content. */ function do_blocks( $content ) { - $blocks = gutenberg_parse_blocks( $content ); + $rendered_content = ''; + + $dynamic_block_names = get_dynamic_block_names(); + $dynamic_block_pattern = ( + '//' + ); + + 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]; + } - $content_after_blocks = ''; - foreach ( $blocks as $block ) { - $content_after_blocks .= gutenberg_render_block( $block ); + // 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; + } + } + + // Replace dynamic block with server-rendered output. + $rendered_content .= $block_type->render( $attributes ); + + 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]; + + $content = substr( $content, $end_offset + strlen( $end_tag ) ); + } } - return $content_after_blocks; + + // Append remaining unmatched content. + $rendered_content .= $content; + + return $rendered_content; } add_filter( 'the_content', 'do_blocks', 9 ); // BEFORE do_shortcode(). diff --git a/lib/class-wp-block-type.php b/lib/class-wp-block-type.php index 67ebc87e84dd8..581eda0bef1c6 100644 --- a/lib/class-wp-block-type.php +++ b/lib/class-wp-block-type.php @@ -98,27 +98,32 @@ public function __construct( $block_type, $args = array() ) { } /** - * Renders the block type output for given attributes and content. + * Renders the block type output for given attributes. * * @since 0.6.0 * @access public * - * @param array $attributes Optional. Block attributes. Default empty array. - * @param string|null $content Optional. Raw block content, or null if none set. Default null. + * @param array $attributes Optional. Block attributes. Default empty array. * @return string Rendered block type output. */ - public function render( $attributes = array(), $content = null ) { - if ( ! is_callable( $this->render_callback ) ) { - if ( ! $content ) { - return ''; - } - - return $content; + public function render( $attributes = array() ) { + if ( ! $this->is_dynamic() ) { + return ''; } $attributes = $this->prepare_attributes_for_render( $attributes ); - return call_user_func( $this->render_callback, $attributes, $content ); + return call_user_func( $this->render_callback, $attributes ); + } + + /** + * Returns true if the block type is dynamic, or false otherwise. A dynamic + * block is one which defers its rendering to occur on-demand at runtime. + * + * @returns boolean Whether block type is dynamic. + */ + public function is_dynamic() { + return is_callable( $this->render_callback ); } /** diff --git a/phpunit/class-block-type-test.php b/phpunit/class-block-type-test.php index 75b340071065e..8a08cb112dcf2 100644 --- a/phpunit/class-block-type-test.php +++ b/phpunit/class-block-type-test.php @@ -36,31 +36,25 @@ function test_render() { $this->assertEquals( $attributes, json_decode( $output, true ) ); } - function test_render_with_content() { - $attributes = array( - 'foo' => 'bar', - 'bar' => 'foo', - ); - $content = '

Test content.

'; + function test_render_for_static_block() { + $block_type = new WP_Block_Type( 'core/dummy', array() ); + $output = $block_type->render(); - $block_type = new WP_Block_Type( 'core/dummy', array( - 'render_callback' => array( $this, 'render_dummy_block_with_content' ), - ) ); - $output = $block_type->render( $attributes, $content ); - $attributes['_content'] = $content; - $this->assertSame( $attributes, json_decode( $output, true ) ); + $this->assertEquals( '', $output ); } - function test_render_without_callback() { - $attributes = array( - 'foo' => 'bar', - 'bar' => 'foo', - ); - $content = '

Test content.

'; + function test_is_dynamic_for_static_block() { + $block_type = new WP_Block_Type( 'core/dummy', array() ); + + $this->assertFalse( $block_type->is_dynamic() ); + } + + function test_is_dynamic_for_dynamic_block() { + $block_type = new WP_Block_Type( 'core/dummy', array( + 'render_callback' => array( $this, 'render_dummy_block' ), + ) ); - $block_type = new WP_Block_Type( 'core/dummy' ); - $output = $block_type->render( $attributes, $content ); - $this->assertSame( $content, $output ); + $this->assertTrue( $block_type->is_dynamic() ); } function test_prepare_attributes() { diff --git a/phpunit/class-dynamic-blocks-render-test.php b/phpunit/class-dynamic-blocks-render-test.php index c3ecf9d6aacf4..6f8a51fdd2458 100644 --- a/phpunit/class-dynamic-blocks-render-test.php +++ b/phpunit/class-dynamic-blocks-render-test.php @@ -21,13 +21,12 @@ class Dynamic_Blocks_Render_Test extends WP_UnitTestCase { * Dummy block rendering function. * * @param array $attributes Block attributes. - * @param array $content Content. * * @return string Block output. */ - function render_dummy_block( $attributes, $content ) { + function render_dummy_block( $attributes ) { $this->dummy_block_instance_number += 1; - return $this->dummy_block_instance_number . ':' . $attributes['value'] . ":$content"; + return $this->dummy_block_instance_number . ':' . $attributes['value']; } /** @@ -69,41 +68,11 @@ function test_dynamic_block_rendering() { $updated_post_content = do_blocks( $post_content ); $this->assertEquals( $updated_post_content, 'before' . - '1:b1:' . - '2:b1:' . + '1:b1' . + '2:b1' . 'between' . - '3:b2:' . - '4:b2:' . - 'after' - ); - } - - /** - * Test dynamic blocks that contain content. - * - * @covers do_blocks - */ - function test_dynamic_block_rendering_with_content() { - $settings = array( - 'render_callback' => array( - $this, - 'render_dummy_block', - ), - ); - register_block_type( 'core/dummy', $settings ); - $post_content = - 'before' . - "this\ncontent\n\nshould\nbe\npassed" . - 'between' . - 'content2' . - 'after'; - - $updated_post_content = do_blocks( $post_content ); - $this->assertEquals( $updated_post_content, - 'before' . - "1:b1:this\ncontent\n\nshould\nbe\npassed" . - 'between' . - '2:b2:content2' . + '3:b2' . + '4:b2' . 'after' ); } diff --git a/phpunit/class-registration-test.php b/phpunit/class-registration-test.php index eb9f4667cde14..504f8c67798fa 100644 --- a/phpunit/class-registration-test.php +++ b/phpunit/class-registration-test.php @@ -6,17 +6,23 @@ */ /** - * Test register_block_type() and unregister_block_type() + * Test register_block_type(), unregister_block_type(), get_dynamic_block_names() */ class Registration_Test extends WP_UnitTestCase { + function render_stub() {} + function tearDown() { parent::tearDown(); $registry = WP_Block_Type_Registry::get_instance(); - if ( $registry->is_registered( 'core/dummy' ) ) { - $registry->unregister( 'core/dummy' ); + foreach ( array( 'dummy', 'dynamic' ) as $block_name ) { + $block_name = 'core/' . $block_name; + + if ( $registry->is_registered( $block_name ) ) { + $registry->unregister( $block_name ); + } } } @@ -44,4 +50,14 @@ function test_unregister_affects_main_registry() { $registry = WP_Block_Type_Registry::get_instance(); $this->assertFalse( $registry->is_registered( $name ) ); } + + function test_get_dynamic_block_names() { + register_block_type( 'core/dummy', array() ); + register_block_type( 'core/dynamic', array( 'render_callback' => array( $this, 'render_stub' ) ) ); + + $dynamic_block_names = get_dynamic_block_names(); + + $this->assertContains( 'core/dynamic', $dynamic_block_names ); + $this->assertNotContains( 'core/dummy', $dynamic_block_names ); + } } diff --git a/phpunit/class-reusable-blocks-render-test.php b/phpunit/class-reusable-blocks-render-test.php index 68ae5afee4d8b..9d4a2119b2915 100644 --- a/phpunit/class-reusable-blocks-render-test.php +++ b/phpunit/class-reusable-blocks-render-test.php @@ -81,7 +81,7 @@ public function test_render() { */ public function test_ref_empty() { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( 'core/block' ); - $output = $block_type->render( array(), 'foo' ); + $output = $block_type->render( array() ); $this->assertSame( '', $output ); } @@ -91,7 +91,7 @@ public function test_ref_empty() { */ public function test_ref_wrong_post_type() { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( 'core/block' ); - $output = $block_type->render( array( 'ref' => self::$post_id ), 'foo' ); + $output = $block_type->render( array( 'ref' => self::$post_id ) ); $this->assertSame( '', $output ); } }