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 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 @@ -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 );
}
}
@@ -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.

76 changes: 51 additions & 25 deletions lib/experimental/interactivity-api/directive-processing.php
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