Skip to content

Commit

Permalink
Interactivity API: initial support for SSR (#51229)
Browse files Browse the repository at this point in the history
* Initial version working with basic support for wp-bind

* Add wp-context

* Add wp-class

* Add wp-style

* Add wp-text

* Add directive processing tests

* Add WP_Directive_Processor class tests

* Add wp-bind tests

* Add wp-context tests

* Add wp-class tests

* Add wp-style tests

* Add wp-text tests

* Add evaluate tests

* Fix PHP lint

* Prevent errors with incorrect JSON objects

* Add support for functions in the server
  • Loading branch information
luisherranz authored Jun 6, 2023
1 parent 66c4c58 commit e740fff
Show file tree
Hide file tree
Showing 19 changed files with 1,218 additions and 3 deletions.
3 changes: 2 additions & 1 deletion lib/experimental/interactivity-api/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* Extend WordPress core navigation block to use the Interactivity API.
* Interactivity API directives are added using the Tag Processor while it is experimental.
*
* @package gutenberg
* @package Gutenberg
* @subpackage Interactivity API
*/

/**
Expand Down
77 changes: 77 additions & 0 deletions lib/experimental/interactivity-api/class-wp-directive-context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php
/**
* Context data implementation.
*
* @package Gutenberg
* @subpackage Interactivity API
*/

/**
* This is a data structure to hold the current context.
*
* Whenever encountering a `data-wp-context` directive, we need to update
* the context with the data found in that directive. Conversely,
* when "leaving" that context (by encountering a closing tag), we
* need to reset the context to its previous state. This means that
* we actually need sort of a stack to keep track of all nested contexts.
*
* Example:
*
* <div data-wp-context='{ "foo": 123 }'>
* <!-- foo should be 123 here. -->
* <div data-wp-context='{ "foo": 456 }'>
* <!-- foo should be 456 here. -->
* </div>
* <!-- foo should be reset to 123 here. -->
* </div>
*/
class WP_Directive_Context {
/**
* The stack used to store contexts internally.
*
* @var array An array of contexts.
*/
protected $stack = array( array() );

/**
* Constructor.
*
* Accepts a context as an argument to initialize this with.
*
* @param array $context A context.
*/
function __construct( $context = array() ) {
$this->set_context( $context );
}

/**
* Return the current context.
*
* @return array The current context.
*/
public function get_context() {
return end( $this->stack );
}

/**
* Set the current context.
*
* @param array $context The context to be set.
*
* @return void
*/
public function set_context( $context ) {
if ( $context ) {
array_push( $this->stack, array_replace_recursive( $this->get_context(), $context ) );
}
}

/**
* Reset the context to its previous state.
*
* @return void
*/
public function rewind_context() {
array_pop( $this->stack );
}
}
176 changes: 176 additions & 0 deletions lib/experimental/interactivity-api/class-wp-directive-processor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?php
/**
* WP_Directive_Processor class
*
* @package Gutenberg
* @subpackage Interactivity API
*/

/**
* This processor is built on top of the HTML Tag Processor and augments its
* capabilities to process the Interactivity API directives.
*
* IMPORTANT DISCLAIMER: This code is highly experimental and its only purpose
* is to provide a way to test the server-side rendering of the Interactivity
* API. Most of this code will be discarded once the HTML Processor is
* available. Please restrain from investing unnecessary time and effort trying
* to improve this code.
*/
class WP_Directive_Processor extends WP_HTML_Tag_Processor {
/**
* Find the matching closing tag for an opening tag.
*
* When called while on an open tag, traverse the HTML until we find the
* matching closing tag, respecting any in-between content, including nested
* tags of the same name. Return false when called on a closing or void tag,
* or if no matching closing tag was found.
*
* @return bool Whether a matching closing tag was found.
*/
public function next_balanced_closer() {
$depth = 0;

$tag_name = $this->get_tag();

if ( self::is_html_void_element( $tag_name ) ) {
return false;
}

while ( $this->next_tag(
array(
'tag_name' => $tag_name,
'tag_closers' => 'visit',
)
) ) {
if ( ! $this->is_tag_closer() ) {
$depth++;
continue;
}

if ( 0 === $depth ) {
return true;
}

$depth--;
}

return false;
}

/**
* Return the content between two balanced tags.
*
* When called on an opening tag, return the HTML content found between that
* opening tag and its matching closing tag.
*
* @return string The content between the current opening and its matching
* closing tag.
*/
public function get_inner_html() {
$bookmarks = $this->get_balanced_tag_bookmarks();
if ( ! $bookmarks ) {
return false;
}
list( $start_name, $end_name ) = $bookmarks;

$start = $this->bookmarks[ $start_name ]->end + 1;
$end = $this->bookmarks[ $end_name ]->start;

$this->seek( $start_name ); // Return to original position.
$this->release_bookmark( $start_name );
$this->release_bookmark( $end_name );

return substr( $this->html, $start, $end - $start );
}

/**
* Set the content between two balanced tags.
*
* When called on an opening tag, set the HTML content found between that
* opening tag and its matching closing tag.
*
* @param string $new_html The string to replace the content between the
* matching tags with.
*
* @return bool Whether the content was successfully replaced.
*/
public function set_inner_html( $new_html ) {
$this->get_updated_html(); // Apply potential previous updates.

$bookmarks = $this->get_balanced_tag_bookmarks();
if ( ! $bookmarks ) {
return false;
}
list( $start_name, $end_name ) = $bookmarks;

$start = $this->bookmarks[ $start_name ]->end + 1;
$end = $this->bookmarks[ $end_name ]->start;

$this->seek( $start_name ); // Return to original position.
$this->release_bookmark( $start_name );
$this->release_bookmark( $end_name );

$this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_html );
return true;
}

/**
* Return a pair of bookmarks for the current opening tag and the matching
* closing tag.
*
* @return array|false A pair of bookmarks, or false if there's no matching
* closing tag.
*/
public function get_balanced_tag_bookmarks() {
$i = 0;
while ( array_key_exists( 'start' . $i, $this->bookmarks ) ) {
++$i;
}
$start_name = 'start' . $i;

$this->set_bookmark( $start_name );
if ( ! $this->next_balanced_closer() ) {
$this->release_bookmark( $start_name );
return false;
}

$i = 0;
while ( array_key_exists( 'end' . $i, $this->bookmarks ) ) {
++$i;
}
$end_name = 'end' . $i;
$this->set_bookmark( $end_name );

return array( $start_name, $end_name );
}

/**
* Whether a given HTML element is void (e.g. <br>).
*
* @param string $tag_name The element in question.
* @return bool True if the element is void.
*
* @see https://html.spec.whatwg.org/#elements-2
*/
public static function is_html_void_element( $tag_name ) {
switch ( $tag_name ) {
case 'AREA':
case 'BASE':
case 'BR':
case 'COL':
case 'EMBED':
case 'HR':
case 'IMG':
case 'INPUT':
case 'LINK':
case 'META':
case 'SOURCE':
case 'TRACK':
case 'WBR':
return true;

default:
return false;
}
}
}
Loading

0 comments on commit e740fff

Please sign in to comment.