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

Interactivity API: Implement wp_initial_state() #57556

Merged
merged 20 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from 13 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
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,43 @@ public static function is_html_void_element( $tag_name ) {
public static function parse_attribute_name( $name ) {
return explode( '--', $name, 2 );
}

/**
* Parse and extract the namespace and path from the given value.
*
* If the value contains a JSON instead of a path, the function parses it
* and returns the resulting array.
*
* @param string $value Passed value.
* @param string $ns Namespace fallback.
* @return array The resulting array
*/
public static function parse_attribute_value( $value, $ns = null ) {
$matches = array();
$has_ns = preg_match( '/^([\w\-_\/]+)::(.+)$/', $value, $matches );

/*
* Overwrite both `$ns` and `$value` variables if `$value` explicitly
* contains a namespace.
*/
if ( $has_ns ) {
list( , $ns, $value ) = $matches;
}

/*
* Try to decode `$value` as a JSON object. If it works, `$value` is
* replaced with the resulting array. The original string is preserved
* otherwise.
*
* Note that `json_decode` returns `null` both for an invalid JSON or
* the `'null'` string (a valid JSON). In the latter case, `$value` is
* replaced with `null`.
*/
$data = json_decode( $value, true );
if ( null !== $data || 'null' === trim( $value ) ) {
$value = $data;
}

return array( $ns, $value );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
/**
* WP_Interactivity_Initial_State class
*
* @package Gutenberg
* @subpackage Interactivity API
*/

if ( class_exists( 'WP_Interactivity_Initial_State' ) ) {
return;
}

/**
* Manages the initial state of the Interactivity API store in the server and
* its serialization so it can be restored in the browser upon hydration.
*
* @package Gutenberg
* @subpackage Interactivity API
*/
class WP_Interactivity_Initial_State {
/**
* Map of initial state by namespace.
*
* @var array
*/
private static $initial_state = array();

/**
* Get state from a given namespace.
*
* @param string $store_ns Namespace.
*
* @return array The requested state.
*/
public static function get_state( $store_ns ) {
return self::$initial_state[ $store_ns ] ?? array();
}

/**
* Merge data into the state with the given namespace.
*
* @param string $store_ns Namespace.
* @param array $data State to merge.
*
* @return void
*/
public static function merge_state( $store_ns, $data ) {
self::$initial_state[ $store_ns ] = array_replace_recursive(
self::get_state( $store_ns ),
$data
);
}

/**
* Get store data.
*
* @return array
*/
public static function get_data() {
return self::$initial_state;
}

/**
* Reset the initial state.
*/
public static function reset() {
self::$initial_state = array();
luisherranz marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Render the initial state.
*/
public static function render() {
if ( empty( self::$initial_state ) ) {
return;
}
echo sprintf(
'<script id="wp-interactivity-initial-state" type="application/json">%s</script>',
wp_json_encode( self::$initial_state, JSON_HEX_TAG | JSON_HEX_AMP )
);
}
}

This file was deleted.

69 changes: 50 additions & 19 deletions lib/experimental/interactivity-api/directive-processing.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ function gutenberg_process_directives_in_root_blocks( $block_content, $block ) {
$parsed_blocks = parse_blocks( $block_content );
$context = new WP_Directive_Context();
$processed_content = '';
$namespace_stack = array();

foreach ( $parsed_blocks as $parsed_block ) {
if ( 'core/interactivity-wrapper' === $parsed_block['blockName'] ) {
$processed_content .= gutenberg_process_interactive_block( $parsed_block, $context );
$processed_content .= gutenberg_process_interactive_block( $parsed_block, $context, $namespace_stack );
} elseif ( 'core/non-interactivity-wrapper' === $parsed_block['blockName'] ) {
$processed_content .= gutenberg_process_non_interactive_block( $parsed_block, $context );
$processed_content .= gutenberg_process_non_interactive_block( $parsed_block, $context, $namespace_stack );
} else {
$processed_content .= $parsed_block['innerHTML'];
}
Expand Down Expand Up @@ -118,10 +119,11 @@ function gutenberg_mark_block_interactivity( $block_content, $block, $block_inst
*
* @param array $interactive_block The interactive block to process.
* @param WP_Directive_Context $context The context to use when processing.
* @param array $namespace_stack Stack of namespackes passed by reference.
*
* @return string The processed HTML.
*/
function gutenberg_process_interactive_block( $interactive_block, $context ) {
function gutenberg_process_interactive_block( $interactive_block, $context, &$namespace_stack ) {
$block_index = 0;
$content = '';
$interactive_inner_blocks = array();
Expand All @@ -137,7 +139,7 @@ function gutenberg_process_interactive_block( $interactive_block, $context ) {
}
}

return gutenberg_process_interactive_html( $content, $context, $interactive_inner_blocks );
return gutenberg_process_interactive_html( $content, $context, $interactive_inner_blocks, $namespace_stack );
}

/**
Expand All @@ -147,10 +149,11 @@ function gutenberg_process_interactive_block( $interactive_block, $context ) {
*
* @param array $non_interactive_block The non-interactive block to process.
* @param WP_Directive_Context $context The context to use when processing.
* @param array $namespace_stack Stack of namespackes passed by reference.
*
* @return string The processed HTML.
*/
function gutenberg_process_non_interactive_block( $non_interactive_block, $context ) {
function gutenberg_process_non_interactive_block( $non_interactive_block, $context, &$namespace_stack ) {
$block_index = 0;
$content = '';
foreach ( $non_interactive_block['innerContent'] as $inner_content ) {
Expand All @@ -164,9 +167,9 @@ function gutenberg_process_non_interactive_block( $non_interactive_block, $conte
$inner_block = $non_interactive_block['innerBlocks'][ $block_index++ ];

if ( 'core/interactivity-wrapper' === $inner_block['blockName'] ) {
$content .= gutenberg_process_interactive_block( $inner_block, $context );
$content .= gutenberg_process_interactive_block( $inner_block, $context, $namespace_stack );
} elseif ( 'core/non-interactivity-wrapper' === $inner_block['blockName'] ) {
$content .= gutenberg_process_non_interactive_block( $inner_block, $context );
$content .= gutenberg_process_non_interactive_block( $inner_block, $context, $namespace_stack );
}
}
}
Expand All @@ -184,10 +187,11 @@ function gutenberg_process_non_interactive_block( $non_interactive_block, $conte
* @param string $html The HTML to process.
* @param mixed $context The context to use when processing.
* @param array $inner_blocks The inner blocks to process.
* @param array $namespace_stack Stack of namespackes passed by reference.
*
* @return string The processed HTML.
*/
function gutenberg_process_interactive_html( $html, $context, $inner_blocks = array() ) {
function gutenberg_process_interactive_html( $html, $context, $inner_blocks = array(), &$namespace_stack = array() ) {
static $directives = array(
'data-wp-context' => 'gutenberg_interactivity_process_wp_context',
'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind',
Expand All @@ -207,9 +211,9 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar
// Processes the inner blocks.
if ( str_contains( $tag_name, 'WP-INNER-BLOCKS' ) && ! empty( $inner_blocks ) && ! $tags->is_tag_closer() ) {
if ( 'core/interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) {
$inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context );
$inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context, $namespace_stack );
} elseif ( 'core/non-interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) {
$inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_non_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context );
$inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_non_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context, $namespace_stack );
}
}
if ( $tags->is_tag_closer() ) {
Expand Down Expand Up @@ -269,8 +273,25 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar
$attributes
);

// Push the new namespace if the current tag is an island.
$island = $tags->get_attribute( 'data-wp-interactive' );
if ( isset( $island ) && ! $tags->is_tag_closer() ) {
$island_data = json_decode( $island, true );
$namespace_stack[] = $island_data['namespace'];
}

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

// Pop the current namespace if this is the closing tag of an island.
if ( isset( $island ) && $tags->is_tag_closer() ) {
array_pop( $namespace_stack );
}
}

Expand All @@ -290,17 +311,25 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar
}

/**
* Resolves the reference using the store and the context from the provided
* path.
* Resolves the passed reference from the store and the context under the given
* namespace.
*
* A reference could be either a single path or a namespace followed by a path,
* separated by two colons, i.e, `namespace::path.to.prop`. If the reference
* contains a namespace, that namespace overrides the one passed as argument.
*
* @param string $path Path.
* @param string $reference Reference value.
* @param string $ns Inherited namespace.
* @param array $context Context data.
* @return mixed
* @return mixed Resolved value.
*/
function gutenberg_interactivity_evaluate_reference( $path, array $context = array() ) {
$store = array_merge(
WP_Interactivity_Store::get_data(),
array( 'context' => $context )
function gutenberg_interactivity_evaluate_reference( $reference, $ns, array $context = array() ) {
// Extract the namespace from the reference (if present).
list( $ns, $path ) = WP_Directive_Processor::parse_attribute_value( $reference, $ns );

$store = array(
'state' => WP_Interactivity_Initial_State::get_state( $ns ),
'context' => $context[ $ns ] ?? array(),
);

/*
Expand Down Expand Up @@ -329,6 +358,8 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr
* E.g., "file" is an string and a "callable" (the "file" function exists).
*/
if ( $current instanceof Closure ) {
// TODO: Do not pass the store as argument. Implement something similar
// to what we have in the JS runtime (`getContext()`, etc.).
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@luisherranz, do you remember the idea we had for computing the derived state in the server? I added this comment just in case we want to change the current implementation (I'm pretty sure we want). 🤔

Copy link
Member

Choose a reason for hiding this comment

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

Let's remove support for closures for now.

$current = call_user_func( $current, $store );
}

Expand Down
7 changes: 4 additions & 3 deletions lib/experimental/interactivity-api/directives/wp-bind.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
*
* @param WP_Directive_Processor $tags Tags.
* @param WP_Directive_Context $context Directive context.
* @param string $ns Namespace.
*/
function gutenberg_interactivity_process_wp_bind( $tags, $context ) {
function gutenberg_interactivity_process_wp_bind( $tags, $context, $ns ) {
if ( $tags->is_tag_closer() ) {
return;
}
Expand All @@ -25,8 +26,8 @@ function gutenberg_interactivity_process_wp_bind( $tags, $context ) {
continue;
}

$expr = $tags->get_attribute( $attr );
$value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() );
$reference = $tags->get_attribute( $attr );
$value = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() );
$tags->set_attribute( $bound_attr, $value );
}
}
7 changes: 4 additions & 3 deletions lib/experimental/interactivity-api/directives/wp-class.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
*
* @param WP_Directive_Processor $tags Tags.
* @param WP_Directive_Context $context Directive context.
* @param string $ns Namespace.
*/
function gutenberg_interactivity_process_wp_class( $tags, $context ) {
function gutenberg_interactivity_process_wp_class( $tags, $context, $ns ) {
if ( $tags->is_tag_closer() ) {
return;
}
Expand All @@ -25,8 +26,8 @@ function gutenberg_interactivity_process_wp_class( $tags, $context ) {
continue;
}

$expr = $tags->get_attribute( $attr );
$add_class = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() );
$reference = $tags->get_attribute( $attr );
$add_class = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() );
if ( $add_class ) {
$tags->add_class( $class_name );
} else {
Expand Down
Loading
Loading