Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parser: Replace dynamic-block regex in do_blocks #10463

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 74 additions & 64 deletions lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ function get_dynamic_block_names() {
* @return string
*/
function get_dynamic_blocks_regex() {
_deprecated_function( __FUNCTION__, '4.1.0', __( "Dynamic blocks shouldn't be extracted by regex. Use parse_blocks() instead.", 'gutenberg' ) );
$dynamic_block_names = get_dynamic_block_names();
$dynamic_block_pattern = (
'/<!--\s+wp:(' .
Expand Down Expand Up @@ -171,98 +172,107 @@ function gutenberg_render_block( $block ) {
* @return string Updated post content.
*/
function do_blocks( $content ) {
global $post;
$blocks = gutenberg_parse_blocks( $content );
return _recurse_blocks( $blocks, $blocks );
}
add_filter( 'the_content', 'do_blocks', 9 ); // BEFORE do_shortcode().

$rendered_content = '';
$dynamic_block_pattern = get_dynamic_blocks_regex();
/**
* Helper function for do_blocks(), to recurse through the block tree.
*
* @since 4.1.0
* @access private
*
* @param array $blocks Array of blocks from parse_blocks()
* @return string
*/
function _recurse_blocks( $blocks, $all_blocks ) {
global $post;

/*
* 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];
$rendered_content = '';
$dynamic_blocks = get_dynamic_block_names();

foreach ( $blocks as $block ) {
$block = (array) $block;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just do something like this, to allow individual plugins to override as necessary for more custom functionality that Core doesn't support w/r/t content, html, inner blocks, etc?

Suggested change
$block = (array) $block;
$block = (array) $block;
if ( $pre_rendered_content = apply_filters( 'pre_do_block', null, $block, $all_blocks ) ) {
$rendered_content .= $pre_rendered_content;
$post = $global_post;
continue;
}

or potentially even apply_filters( 'pre_do_block_' . $block['blockName'], null, $block, $all_blocks ) to narrow the filter instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be taken advantage of via something like this:

Automattic/jetpack@a12356b

if ( in_array( $block['blockName'], $dynamic_blocks ) ) {
// 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( $block['blockName'] );

// Replace dynamic block with server-rendered output.
$block_content = $block_type->render( (array) $block['attrs'], $block['innerHTML'] );
} elseif ( $block['innerBlocks'] ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way these conditionals are laid, blocks can either be Dynamic Blocks, OR they can have innerBlocks. It doesn't permit Dynamic Blocks to have innerBlocks. Which seems bad.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is true, hence the reason this PR exists.

I'd rather not render inner blocks first then pass them up because then that limits what we can do with inner blocks. it should be good enough to give blocks their inner blocks in structural form, let them decide if they want to modify or filter them, then render the document.

this PR is just one step in the process - we have lots to overhaul here and maybe I'm wrong but I thought that @mcsf decided to keep this as-is until post merge and leave dynamic blocks the way they are - as in, yesterday or the day before he decided. let's see what he says

$block_content = _recurse_blocks( $block['innerBlocks'], $all_blocks );
} else {
$block_content = $block['innerHTML'];
}

// 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 = '/<!--\s+\/wp:' . str_replace( '/', '\/', preg_quote( $block_name ) ) . '\s+-->/';
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 );
/**
* Filters the content of a single block.
*
* During the_content, each block is parsed and added to the output individually. This filter allows
* that content to be altered immediately before it's appended.
*
* @since 5.0.0
*
* @param string $block_content The block content about to be appended.
* @param array $block The full block, including name and attributes.
* @param array $all_blocks The array of all blocks being processed.
*/
$rendered_content .= apply_filters( 'do_block', $block_content, $block, $all_blocks );

// Restore global $post.
$post = $global_post;
}

// Append remaining unmatched content.
$rendered_content .= $content;

// Strip remaining block comment demarcations.
$rendered_content = preg_replace( '/<!--\s+\/?wp:.*?-->\r?\n?/m', '', $rendered_content );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think removing this line is causing the unit test failure in https://travis-ci.org/WordPress/gutenberg/jobs/439435606 as it's changing how some whitespace renders?

The only difference is an extra line break showing up in the output, so not a huge deal, but this may be accounting for it.

$rendered_content = preg_replace( '/<!--\s+\/?wp:.*?-->/m', '', $rendered_content );

return $rendered_content;
}
add_filter( 'the_content', 'do_blocks', 9 ); // BEFORE do_shortcode().

/**
* Remove all dynamic blocks from the given content.
*
* @since 3.6.0
* @since 4.1.0
*
* @param string $content Content of the current post.
* @return string
*/
function strip_dynamic_blocks( $content ) {
return preg_replace( get_dynamic_blocks_regex(), '', $content );
return _recurse_strip_dynamic_blocks( gutenberg_parse_blocks( $content ) );
}

/**
* Helper function for strip_dynamic_blocks(), to recurse through the block tree.
*
* @since 4.1.0
* @access private
*
* @param array $blocks Array of blocks from parse_blocks()
* @return string
*/
function _recurse_strip_dynamic_blocks( $blocks ) {
$clean_content = '';
$dynamic_blocks = get_dynamic_block_names();

foreach ( $blocks as $block ) {
if ( ! in_array( $block['blockName'], $dynamic_blocks ) ) {
if ( $block['innerBlocks'] ) {
$clean_content .= _recurse_strip_dynamic_blocks( $block['innerBlocks'] );
} else {
$clean_content .= $block['innerHTML'];
}
}
}

return $clean_content;
}

/**
Expand Down
21 changes: 17 additions & 4 deletions packages/block-serialization-default-parser/parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,14 @@ function proceed() {
* block and add it as a new innerBlock to the parent
*/
$stack_top = array_pop( $this->stack );
$stack_top->block->innerHTML .= substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset );

$html = substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset );
if ( $stack_top->block->innerBlocks ) {
$stack_top->block->innerBlocks[] = (array) $this->freeform( $html );
} else {
$stack_top->block->innerHTML = $html;
}

$stack_top->prev_offset = $start_offset + $token_length;

$this->add_inner_block(
Expand Down Expand Up @@ -440,8 +447,8 @@ function add_freeform( $length = 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 );
$parent->block->innerBlocks[] = (array) $this->freeform( substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset ) );
$parent->block->innerBlocks[] = (array) $block;
$parent->prev_offset = $last_offset ? $last_offset : $token_start + $token_length;
}

Expand All @@ -456,10 +463,16 @@ function add_block_from_stack( $end_offset = null ) {
$stack_top = array_pop( $this->stack );
$prev_offset = $stack_top->prev_offset;

$stack_top->block->innerHTML .= isset( $end_offset )
$html = isset( $end_offset )
? substr( $this->document, $prev_offset, $end_offset - $prev_offset )
: substr( $this->document, $prev_offset );

if ( $stack_top->block->innerBlocks ) {
$stack_top->block->innerBlocks[] = (array) $this->freeform( $html );
} else {
$stack_top->block->innerHTML = $html;
}

if ( isset( $stack_top->leading_html_start ) ) {
$this->output[] = (array) self::freeform( substr(
$this->document,
Expand Down