Skip to content

Commit

Permalink
Interactivity API: Implement wp_initial_state() (#57556)
Browse files Browse the repository at this point in the history
* Update directive processing to handle namespaces

* Add required namespace param to evaluate reference

* Handle references with namespaces

* Rename parse_reference to parse_attribute_value

* Update directive implementations

* Improve comments in `parse_attribute_value`

* Restore changes from `store()` migration PR

* Do not make namespace optional in `get_state()`

* Replace Store with Initial_State

* Fix set up and tear down of Initial State tests

* Update directive tests after implementation changes

* Add TODO comment to derived state

* Refactor directive processing tests

* Move namespace logic to a directive

* Add tests for namespaces logic

* Fix typo in comments

* Handle empty attributes in wp-interactive

* Handle boolean attributes in wp-context

* Do not support derived state yet

* Add root blocks bail out quick by name

---------

Co-authored-by: Carlos Bravo <carlos.bravo@automattic.com>
  • Loading branch information
DAreRodz and cbravobernal authored Jan 9, 2024
1 parent 9323cdd commit 76e1642
Show file tree
Hide file tree
Showing 21 changed files with 723 additions and 431 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_5 {
* @param array $block The block to add.
*/
public static function mark_root_block( $block ) {
self::$root_block = md5( serialize( $block ) );
if ( null !== $block['blockName'] ) {
self::$root_block = $block['blockName'] . md5( serialize( $block ) );
} else {
self::$root_block = md5( serialize( $block ) );
}
}

/**
Expand All @@ -52,6 +56,14 @@ public static function unmark_root_block() {
* @return bool True if block is a root block, false otherwise.
*/
public static function is_marked_as_root_block( $block ) {
// If self::$root_block is null, is impossible that any block has been marked as root.
if ( is_null( self::$root_block ) ) {
return false;
}
// Blocks whose blockName is null are specifically intended to convey - "this is a freeform HTML block."
if ( null !== $block['blockName'] ) {
return str_contains( self::$root_block, $block['blockName'] ) && $block['blockName'] . md5( serialize( $block ) ) === self::$root_block;
}
return md5( serialize( $block ) ) === self::$root_block;
}

Expand Down Expand Up @@ -256,4 +268,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();
}

/**
* 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.

76 changes: 51 additions & 25 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,16 +187,18 @@ 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',
'data-wp-class' => 'gutenberg_interactivity_process_wp_class',
'data-wp-style' => 'gutenberg_interactivity_process_wp_style',
'data-wp-text' => 'gutenberg_interactivity_process_wp_text',
'data-wp-interactive' => 'gutenberg_interactivity_process_wp_interactive',
'data-wp-context' => 'gutenberg_interactivity_process_wp_context',
'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind',
'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( $html );
Expand All @@ -207,9 +212,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 @@ -270,7 +275,15 @@ function gutenberg_process_interactive_html( $html, $context, $inner_blocks = ar
);

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

Expand All @@ -290,17 +303,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.
*
* @param string $path Path.
* 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 $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,7 +350,12 @@ 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 ) {
$current = call_user_func( $current, $store );
/*
* TODO: Figure out a way to implement derived state without having to
* pass the store as argument:
*
* $current = call_user_func( $current );
*/
}

// Returns the opposite if it has a negator operator (!).
Expand Down
Loading

0 comments on commit 76e1642

Please sign in to comment.