Skip to content

Commit

Permalink
HTML API: Introduce HTML Template Renderer
Browse files Browse the repository at this point in the history
Currently only renders text data:

 - does not render nested HTML (escapes everything)
 - does not escape URLs
  • Loading branch information
dmsnell committed Jan 12, 2024
1 parent 10ed5d8 commit caa9a03
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 2 deletions.
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 @@ -1983,8 +1983,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->duplicate_attributes = null;
Expand Down
96 changes: 96 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,96 @@
<?php

/**
* WP_HTML_Template class.
*
* @since 6.5.0
*/
class WP_HTML_Template extends WP_HTML_Tag_Processor {
/**
* Renders an HTML template, replacing the placeholders with the provided values.
*
* @since 6.5.0
*
* @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 )
);
}

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();
}
}
1 change: 1 addition & 0 deletions src/wp-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
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 . '/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 {TICKET_NUMBER}
*/
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.'
);
}
}

0 comments on commit caa9a03

Please sign in to comment.