Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
HTML API: Introduce HTML Template Renderer
Currently only renders text data:

 - does not render nested HTML (escapes everything)
 - does not escape URLs
  • Loading branch information
dmsnell committed Jan 25, 2024
commit ba8701ba6c60fb8a605c1c3c0432a4b33a69ba66
4 changes: 2 additions & 2 deletions src/wp-includes/html-api/class-wp-html-tag-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2017,8 +2017,8 @@ private function after_tag() {
$this->token_length = null;
$this->tag_name_starts_at = null;
$this->tag_name_length = null;
$this->text_starts_at = 0;
$this->text_length = 0;
$this->text_starts_at = null;
$this->text_length = null;
$this->is_closing_tag = null;
$this->attributes = array();
$this->comment_type = null;
Expand Down
166 changes: 166 additions & 0 deletions src/wp-includes/html-api/class-wp-html-template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php
/**
* HTML API: WP_HTML_Template helper class
*
* Provides the rendering code for the WP_HTML class. This needs to exist separately as
* implemented so that it can subclass the WP_HTML_Tag_Processor class and gain access
* to the bookmarks and lexical updates, which it uses to perform string operations.
*
* @package WordPress
* @subpackage HTML-API
* @since 6.5.0
*/

/**
* WP_HTML_Template class.
*
* To be used only by the WP_HTML class.
*
* @since 6.5.0
*
* @access private
*/
class WP_HTML_Template extends WP_HTML_Tag_Processor {
/**
* Renders an HTML template, replacing the placeholders with the provided values.
*
* This function looks for placeholders in the template string and will replace
* them with appropriately-escaped substitutions from the given arguments, if
* provided and if those arguments are strings.
*
* Example:
*
* echo WP_HTML_Template::render(
* '<a href="</%profile_url>"></%name></a>',
* array(
* 'profile_url' => 'https://profiles.example.com/username',
* 'name' => $user->display_name
* )
* );
* // Outputs: <a href="https://profiles.example.com/username">Bobby Tables</a>
*
* Do not escape the values supplied to the argument array! This function will escape each
* parameter's value as needed and additional manual escaping may lead to incorrect output.
*
* ## Syntax.
*
* ### Substitution Placeholders.
*
* - `</%named_arg>` finds `named_arg` in the arguments array, escapes its value if possible,
* and replaces the placeholder with the escaped value. These may exist inside double-quoted
* HTML tag attributes or in HTML text content between tags. They cannot be used to output a tag
* name or content inside a comment.
*
* ### Spread Attributes.
*
* - `...named_arg` when found within an HTML tag will lookup `named_arg` in the arguments array
* and, if it's an array, will set the attribute on the tag for each key/value pair whose value
* is a string. The
*
* ## Notes.
*
* - Attributes may only be supplied for a limited set of types: a string value assigns a double-quoted
* attribute value; `true` sets the attribute as a boolean attribute; `null` removes the attribute.
* If provided any other type of value the attribute will be ignored and its existing value persists.
*
* - If multiple HTML attributes are specified for a given tag they will be applied as if calling
* `set_attribute()` in the order they are specified in the temlpate. This includes any attributes
* assigned through the attribute spread syntax.
*
* - Substitutions in text nodes may only contain string values. If provided any other type of value
* the placeholder will be removed with nothing in its place.
*
* - This function currently escapes all value provided in the arguments array. In the future
* it may provide the ability to nest pre-rendered HTML into the template, but this functionality
* is deferred for a future update.
*
* - This function will not replace content inside of TEXTAREA, TITLE, SCRIPT, or STYLE elements.
*
* @since 6.5.0
*
* @access private
*
* @param string $template The HTML template.
* @param string $args Array of key/value pairs providing substitue values for the placeholders.
* @return string The rendered HTML.
*/
public static function render( $template, $args = array() ) {
$processor = new self( $template );
while ( $processor->next_token() ) {
$type = $processor->get_token_type();
$text = $processor->get_modifiable_text();

if ( '#funky-comment' === $type && strlen( $text ) > 0 && '%' === $text[0] ) {
$name = substr( $text, 1 );
$value = isset( $args[ $name ] ) && is_string( $args[ $name ] ) ? $args[ $name ] : null;
$processor->set_bookmark( 'here' );
$processor->lexical_updates[] = new WP_HTML_Text_Replacement(
$processor->bookmarks['here']->start,
$processor->bookmarks['here']->length,
null === $value ? '' : esc_html( $value )
);
continue;
}

if ( '#tag' === $type ) {
foreach ( $processor->get_attribute_names_with_prefix( '' ) ?? array() as $attribute_name ) {
if ( str_starts_with( $attribute_name, '...' ) ) {
$spread_name = substr( $attribute_name, 3 );
if ( isset( $args[ $spread_name ] ) && is_array( $args[ $spread_name ] ) ) {
foreach ( $args[ $spread_name ] as $key => $value ) {
if ( true === $value || null === $value || is_string( $value ) ) {
$processor->set_attribute( $key, $value );
}
}
}
$processor->remove_attribute( $attribute_name );
}

$value = $processor->get_attribute( $attribute_name );

if ( ! is_string( $value ) ) {
continue;
}

$full_match = null;
if ( preg_match( '~^</%([^>]+)>$~', $value, $full_match ) ) {
$name = $full_match[1];

if ( array_key_exists( $name, $args ) ) {
$value = $args[ $name ];
if ( null === $value ) {
$processor->remove_attribute( $attribute_name );
} elseif ( true === $value ) {
$processor->set_attribute( $attribute_name, true );
} elseif ( is_string( $value ) ) {
$processor->set_attribute( $attribute_name, esc_attr( $args[ $name ] ) );
} else {
$processor->remove_attribute( $attribute_name );
}
} else {
$processor->remove_attribute( $attribute_name );
}

continue;
}

$new_value = preg_replace_callback(
'~</%([^>]+)>~',
static function ( $matches ) use ( $args ) {
return is_string( $args[ $matches[1] ] )
? esc_attr( $args[ $matches[1] ] )
: '';
},
$value
);

if ( $new_value !== $value ) {
$processor->set_attribute( $attribute_name, $new_value );
}
}
}
}

return $processor->get_updated_html();
}
}
82 changes: 82 additions & 0 deletions src/wp-includes/html-api/class-wp-html.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
/**
* HTML API: WP_HTML class
*
* Provides a public interface for HTML-related functionality in WordPress.
*
* @package WordPress
* @subpackage HTML-API
* @since 6.5.0
*/

/**
* WP_HTML class.
*
* @since 6.5.0
*/
class WP_HTML {
/**
* Renders an HTML template, replacing the placeholders with the provided values.
*
* This function looks for placeholders in the template string and will replace
* them with appropriately-escaped substitutions from the given arguments, if
* provided and if those arguments are strings.
*
* Example:
*
* echo WP_HTML::render(
* '<a href="</%profile_url>"></%name></a>',
* array(
* 'profile_url' => 'https://profiles.example.com/username',
* 'name' => $user->display_name
* )
* );
* // Outputs: <a href="https://profiles.example.com/username">Bobby Tables</a>
*
* Do not escape the values supplied to the argument array! This function will escape each
* parameter's value as needed and additional manual escaping may lead to incorrect output.
*
* ## Syntax.
*
* ### Substitution Placeholders.
*
* - `</%named_arg>` finds `named_arg` in the arguments array, escapes its value if possible,
* and replaces the placeholder with the escaped value. These may exist inside double-quoted
* HTML tag attributes or in HTML text content between tags. They cannot be used to output a tag
* name or content inside a comment.
*
* ### Spread Attributes.
*
* - `...named_arg` when found within an HTML tag will lookup `named_arg` in the arguments array
* and, if it's an array, will set the attribute on the tag for each key/value pair whose value
* is a string. The
*
* ## Notes.
*
* - Attributes may only be supplied for a limited set of types: a string value assigns a double-quoted
* attribute value; `true` sets the attribute as a boolean attribute; `null` removes the attribute.
* If provided any other type of value the attribute will be ignored and its existing value persists.
*
* - If multiple HTML attributes are specified for a given tag they will be applied as if calling
* `set_attribute()` in the order they are specified in the temlpate. This includes any attributes
* assigned through the attribute spread syntax.
*
* - Substitutions in text nodes may only contain string values. If provided any other type of value
* the placeholder will be removed with nothing in its place.
*
* - This function currently escapes all value provided in the arguments array. In the future
* it may provide the ability to nest pre-rendered HTML into the template, but this functionality
* is deferred for a future update.
*
* @since 6.5.0
*
* @access private
*
* @param string $template The HTML template.
* @param string $args Array of key/value pairs providing substitue values for the placeholders.
* @return string The rendered HTML.
*/
public static function render( $template, $args ) {
return WP_HTML_Template::render( $template, $args );
}
}
2 changes: 2 additions & 0 deletions src/wp-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@
require ABSPATH . WPINC . '/html-api/class-wp-html-token.php';
require ABSPATH . WPINC . '/html-api/class-wp-html-processor-state.php';
require ABSPATH . WPINC . '/html-api/class-wp-html-processor.php';
require ABSPATH . WPINC . '/html-api/class-wp-html-template.php';
require ABSPATH . WPINC . '/html-api/class-wp-html.php';
require ABSPATH . WPINC . '/class-wp-http.php';
require ABSPATH . WPINC . '/class-wp-http-streams.php';
require ABSPATH . WPINC . '/class-wp-http-curl.php';
Expand Down
41 changes: 41 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlTemplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* Unit tests covering WP_HTML_Template functionality.
*
* @package WordPress
* @subpackage HTML-API
*
* @since 6.5.0
*
* @group html-api
*
* @coversDefaultClass WP_HTML_Template
*/

class Tests_HtmlApi_WpHtmlTemplate extends WP_UnitTestCase {
/**
* Demonstrates how to pass values into an HTML template.
*
* @ticket 60229
*/
public function test_basic_render() {
$html = WP_HTML_Template::render(
'<div class="is-test </%class>" ...div-args inert="</%is_inert>">Just a </%count> test</div>',
array(
'count' => '<strong>Hi <3</strong>',
'class' => '5>4',
'is_inert' => 'inert',
'div-args' => array(
'class' => 'hoover',
'disabled' => true,
),
)
);

$this->assertSame(
'<div disabled class="hoover" inert="inert">Just a &lt;strong&gt;Hi &lt;3&lt;/strong&gt; test</div>',
$html,
'Failed to properly render template.'
);
}
}