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

Server directive processing: Improve how block references are saved #56107

Merged
merged 8 commits into from Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -27,17 +27,26 @@ class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_4 {
*
* @var array
*/
public static $root_blocks = array();
public static $root_block = null;

/**
* Add a root block to the list.
* Add a root block to the variable.
*
* @param array $block The block to add.
*
* @return void
*/
public static function add_root_block( $block ) {
self::$root_blocks[] = md5( serialize( $block ) );
public static function mark_root_block( $block ) {
self::$root_block = md5( serialize( $block ) );
}

/**
* Remove a root block to the variable.
*
* @return void
*/
public static function unmark_root_block() {
self::$root_block = null;
}

/**
Expand All @@ -47,8 +56,17 @@ public static function add_root_block( $block ) {
*
* @return bool True if block is a root block, false otherwise.
*/
public static function is_root_block( $block ) {
return in_array( md5( serialize( $block ) ), self::$root_blocks, true );
public static function is_marked_as_root_block( $block ) {
return md5( serialize( $block ) ) === self::$root_block;
}

/**
* Check if a root block has already been defined.
*
* @return bool True if block is a root block, false otherwise.
*/
public static function has_root_block() {
return isset( self::$root_block );
}


Expand Down Expand Up @@ -92,6 +110,75 @@ public function next_balanced_closer() {
return false;
}

/**
* Traverses the HTML searching for Interactivity API directives and processing
* them.
*
* @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor.
* @param string $prefix Attribute prefix.
* @param string[] $directives Directives.
*
* @return WP_Directive_Processor The modified instance of the
* WP_Directive_Processor.
*/
public function process_rendered_html( $tags, $prefix, $directives ) {
$context = new WP_Directive_Context();
$tag_stack = array();

while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
$tag_name = $tags->get_tag();

// Is this a tag that closes the latest opening tag?
if ( $tags->is_tag_closer() ) {
if ( 0 === count( $tag_stack ) ) {
continue;
}

list( $latest_opening_tag_name, $attributes ) = end( $tag_stack );
if ( $latest_opening_tag_name === $tag_name ) {
array_pop( $tag_stack );

// If the matching opening tag didn't have any directives, we move on.
if ( 0 === count( $attributes ) ) {
continue;
}
}
} else {
$attributes = array();
foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) {
/*
* Removes the part after the double hyphen before looking for
* the directive processor inside `$directives`, e.g., "wp-bind"
* from "wp-bind--src" and "wp-context" from "wp-context" etc...
*/
list( $type ) = WP_Directive_Processor::parse_attribute_name( $name );
if ( array_key_exists( $type, $directives ) ) {
$attributes[] = $type;
}
}

/*
* If this is an open tag, and if it either has directives, or if
* we're inside a tag that does, take note of this tag and its
* directives so we can call its directive processor once we
* encounter the matching closing tag.
*/
if (
! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) &&
( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) )
) {
$tag_stack[] = array( $tag_name, $attributes );
}
}

foreach ( $attributes as $attribute ) {
call_user_func( $directives[ $attribute ], $tags, $context );
}
}

return $tags;
}

/**
* Return the content between two balanced tags.
*
Expand Down
133 changes: 28 additions & 105 deletions lib/experimental/interactivity-api/directive-processing.php
Expand Up @@ -8,131 +8,54 @@
*/

/**
* Process the Interactivity API directives using the root blocks of the
* outermost rendering, ignoring the root blocks of inner blocks like Patterns,
* Template Parts or Content.
* Mark if the block is a root block. Checks that there is already a root block
* in order not to mark template-parts or synced patterns as root blocks, where
* the parent is null.
*
* @param array $parsed_block The parsed block.
* @param array $source_block The source block.
* @param array $parent_block The parent block.
*
* @return array The parsed block.
*/
function gutenberg_interactivity_process_directives( $parsed_block, $source_block, $parent_block ) {
static $is_inside_root_block = false;
static $process_directives_in_root_blocks = null;

if ( ! isset( $process_directives_in_root_blocks ) ) {
/**
* Process directives in each root block.
*
* @param string $block_content The block content.
* @param array $block The full block.
*
* @return string Filtered block content.
*/
$process_directives_in_root_blocks = static function ( $block_content, $block ) use ( &$is_inside_root_block ) {

if ( WP_Directive_Processor::is_root_block( $block ) ) {

$directives = array(
'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind',
'data-wp-context' => 'gutenberg_interactivity_process_wp_context',
'data-wp-class' => 'gutenberg_interactivity_process_wp_class',
'data-wp-style' => 'gutenberg_interactivity_process_wp_style',
'data-wp-text' => 'gutenberg_interactivity_process_wp_text',
);

$tags = new WP_Directive_Processor( $block_content );
$tags = gutenberg_interactivity_process_rendered_html( $tags, 'data-wp-', $directives );
$is_inside_root_block = false;
return $tags->get_updated_html();

}

return $block_content;
};
add_filter( 'render_block', $process_directives_in_root_blocks, 10, 2 );
}

if ( ! isset( $parent_block ) && ! $is_inside_root_block ) {
WP_Directive_Processor::add_root_block( $parsed_block );
$is_inside_root_block = true;
function gutenberg_interactivity_mark_root_blocks( $parsed_block, $source_block, $parent_block ) {
if ( ! isset( $parent_block ) && ! WP_Directive_Processor::has_root_block() ) {
WP_Directive_Processor::mark_root_block( $parsed_block );
}

return $parsed_block;
}
add_filter( 'render_block_data', 'gutenberg_interactivity_process_directives', 10, 3 );

add_filter( 'render_block_data', 'gutenberg_interactivity_mark_root_blocks', 10, 3 );

/**
* Traverses the HTML searching for Interactivity API directives and processing
* them.
* Process directives in each root block.
*
* @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor.
* @param string $prefix Attribute prefix.
* @param string[] $directives Directives.
* @param string $block_content The block content.
* @param array $block The full block.
*
* @return WP_Directive_Processor The modified instance of the
* WP_Directive_Processor.
* @return string Filtered block content.
*/
function gutenberg_interactivity_process_rendered_html( $tags, $prefix, $directives ) {
$context = new WP_Directive_Context();
$tag_stack = array();
function gutenberg_process_directives_in_root_blocks( $block_content, $block ) {
if ( WP_Directive_Processor::is_marked_as_root_block( $block ) ) {
WP_Directive_Processor::unmark_root_block();
$directives = array(
'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind',
'data-wp-context' => 'gutenberg_interactivity_process_wp_context',
'data-wp-class' => 'gutenberg_interactivity_process_wp_class',
'data-wp-style' => 'gutenberg_interactivity_process_wp_style',
'data-wp-text' => 'gutenberg_interactivity_process_wp_text',
);

$tags = new WP_Directive_Processor( $block_content );
$tags = $tags->process_rendered_html( $tags, 'data-wp-', $directives );
return $tags->get_updated_html();

while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
$tag_name = $tags->get_tag();

// Is this a tag that closes the latest opening tag?
if ( $tags->is_tag_closer() ) {
if ( 0 === count( $tag_stack ) ) {
continue;
}

list( $latest_opening_tag_name, $attributes ) = end( $tag_stack );
if ( $latest_opening_tag_name === $tag_name ) {
array_pop( $tag_stack );

// If the matching opening tag didn't have any directives, we move on.
if ( 0 === count( $attributes ) ) {
continue;
}
}
} else {
$attributes = array();
foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) {
/*
* Removes the part after the double hyphen before looking for
* the directive processor inside `$directives`, e.g., "wp-bind"
* from "wp-bind--src" and "wp-context" from "wp-context" etc...
*/
list( $type ) = WP_Directive_Processor::parse_attribute_name( $name );
if ( array_key_exists( $type, $directives ) ) {
$attributes[] = $type;
}
}

/*
* If this is an open tag, and if it either has directives, or if
* we're inside a tag that does, take note of this tag and its
* directives so we can call its directive processor once we
* encounter the matching closing tag.
*/
if (
! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) &&
( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) )
) {
$tag_stack[] = array( $tag_name, $attributes );
}
}

foreach ( $attributes as $attribute ) {
call_user_func( $directives[ $attribute ], $tags, $context );
}
}

return $tags;
return $block_content;
}
add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 10, 2 );


/**
* Resolve the reference using the store and the context from the provided path.
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e-tests/plugins/interactive-blocks.php
Expand Up @@ -40,7 +40,7 @@ function () {
if ( 'true' === $_GET['disable_directives_ssr'] ) {
remove_filter(
'render_block_data',
'gutenberg_interactivity_process_directives'
'gutenberg_interactivity_mark_root_blocks'
);
}
}
Expand Down