Skip to content

Commit a1c6d80

Browse files
committed
HTML API: Introduce HTML Template Renderer
Currently only renders text data: - does not render nested HTML (escapes everything) - does not escape URLs
1 parent 8d9b719 commit a1c6d80

5 files changed

Lines changed: 293 additions & 2 deletions

File tree

src/wp-includes/html-api/class-wp-html-tag-processor.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1999,8 +1999,8 @@ private function after_tag() {
19991999
$this->token_length = null;
20002000
$this->tag_name_starts_at = null;
20012001
$this->tag_name_length = null;
2002-
$this->text_starts_at = 0;
2003-
$this->text_length = 0;
2002+
$this->text_starts_at = null;
2003+
$this->text_length = null;
20042004
$this->is_closing_tag = null;
20052005
$this->attributes = array();
20062006
$this->duplicate_attributes = null;
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
/**
3+
* HTML API: WP_HTML_Template helper class
4+
*
5+
* Provides the rendering code for the WP_HTML class. This needs to exist separately as
6+
* implemented so that it can subclass the WP_HTML_Tag_Processor class and gain access
7+
* to the bookmarks and lexical updates, which it uses to perform string operations.
8+
*
9+
* @package WordPress
10+
* @subpackage HTML-API
11+
* @since 6.5.0
12+
*/
13+
14+
/**
15+
* WP_HTML_Template class.
16+
*
17+
* To be used only by the WP_HTML class.
18+
*
19+
* @since 6.5.0
20+
*
21+
* @access private
22+
*/
23+
class WP_HTML_Template extends WP_HTML_Tag_Processor {
24+
/**
25+
* Renders an HTML template, replacing the placeholders with the provided values.
26+
*
27+
* This function looks for placeholders in the template string and will replace
28+
* them with appropriately-escaped substitutions from the given arguments, if
29+
* provided and if those arguments are strings.
30+
*
31+
* Example:
32+
*
33+
* echo WP_HTML_Template::render(
34+
* '<a href="</%profile_url>"></%name></a>',
35+
* array(
36+
* 'profile_url' => 'https://profiles.example.com/username',
37+
* 'name' => $user->display_name
38+
* )
39+
* );
40+
* // Outputs: <a href="https://profiles.example.com/username">Bobby Tables</a>
41+
*
42+
* Do not escape the values supplied to the argument array! This function will escape each
43+
* parameter's value as needed and additional manual escaping may lead to incorrect output.
44+
*
45+
* ## Syntax.
46+
*
47+
* ### Substitution Placeholders.
48+
*
49+
* - `</%named_arg>` finds `named_arg` in the arguments array, escapes its value if possible,
50+
* and replaces the placeholder with the escaped value. These may exist inside double-quoted
51+
* HTML tag attributes or in HTML text content between tags. They cannot be used to output a tag
52+
* name or content inside a comment.
53+
*
54+
* ### Spread Attributes.
55+
*
56+
* - `...named_arg` when found within an HTML tag will lookup `named_arg` in the arguments array
57+
* and, if it's an array, will set the attribute on the tag for each key/value pair whose value
58+
* is a string. The
59+
*
60+
* ## Notes.
61+
*
62+
* - Attributes may only be supplied for a limited set of types: a string value assigns a double-quoted
63+
* attribute value; `true` sets the attribute as a boolean attribute; `null` removes the attribute.
64+
* If provided any other type of value the attribute will be ignored and its existing value persists.
65+
*
66+
* - If multiple HTML attributes are specified for a given tag they will be applied as if calling
67+
* `set_attribute()` in the order they are specified in the temlpate. This includes any attributes
68+
* assigned through the attribute spread syntax.
69+
*
70+
* - Substitutions in text nodes may only contain string values. If provided any other type of value
71+
* the placeholder will be removed with nothing in its place.
72+
*
73+
* - This function currently escapes all value provided in the arguments array. In the future
74+
* it may provide the ability to nest pre-rendered HTML into the template, but this functionality
75+
* is deferred for a future update.
76+
*
77+
* - This function will not replace content inside of TEXTAREA, TITLE, SCRIPT, or STYLE elements.
78+
*
79+
* @since 6.5.0
80+
*
81+
* @access private
82+
*
83+
* @param string $template The HTML template.
84+
* @param string $args Array of key/value pairs providing substitue values for the placeholders.
85+
* @return string The rendered HTML.
86+
*/
87+
public static function render( $template, $args = array() ) {
88+
$processor = new self( $template );
89+
while ( $processor->next_token() ) {
90+
$type = $processor->get_token_type();
91+
$text = $processor->get_modifiable_text();
92+
93+
if ( '#funky-comment' === $type && strlen( $text ) > 0 && '%' === $text[0] ) {
94+
$name = substr( $text, 1 );
95+
$value = isset( $args[ $name ] ) && is_string( $args[ $name ] ) ? $args[ $name ] : null;
96+
$processor->set_bookmark( 'here' );
97+
$processor->lexical_updates[] = new WP_HTML_Text_Replacement(
98+
$processor->bookmarks['here']->start,
99+
$processor->bookmarks['here']->length,
100+
null === $value ? '' : esc_html( $value )
101+
);
102+
continue;
103+
}
104+
105+
if ( '#tag' === $type ) {
106+
foreach ( $processor->get_attribute_names_with_prefix( '' ) ?? array() as $attribute_name ) {
107+
if ( str_starts_with( $attribute_name, '...' ) ) {
108+
$spread_name = substr( $attribute_name, 3 );
109+
if ( isset( $args[ $spread_name ] ) && is_array( $args[ $spread_name ] ) ) {
110+
foreach ( $args[ $spread_name ] as $key => $value ) {
111+
if ( true === $value || null === $value || is_string( $value ) ) {
112+
$processor->set_attribute( $key, $value );
113+
}
114+
}
115+
}
116+
$processor->remove_attribute( $attribute_name );
117+
}
118+
119+
$value = $processor->get_attribute( $attribute_name );
120+
121+
if ( ! is_string( $value ) ) {
122+
continue;
123+
}
124+
125+
$full_match = null;
126+
if ( preg_match( '~^</%([^>]+)>$~', $value, $full_match ) ) {
127+
$name = $full_match[1];
128+
129+
if ( array_key_exists( $name, $args ) ) {
130+
$value = $args[ $name ];
131+
if ( null === $value ) {
132+
$processor->remove_attribute( $attribute_name );
133+
} elseif ( true === $value ) {
134+
$processor->set_attribute( $attribute_name, true );
135+
} elseif ( is_string( $value ) ) {
136+
$processor->set_attribute( $attribute_name, esc_attr( $args[ $name ] ) );
137+
} else {
138+
$processor->remove_attribute( $attribute_name );
139+
}
140+
} else {
141+
$processor->remove_attribute( $attribute_name );
142+
}
143+
144+
continue;
145+
}
146+
147+
$new_value = preg_replace_callback(
148+
'~</%([^>]+)>~',
149+
static function ( $matches ) use ( $args ) {
150+
return is_string( $args[ $matches[1] ] )
151+
? esc_attr( $args[ $matches[1] ] )
152+
: '';
153+
},
154+
$value
155+
);
156+
157+
if ( $new_value !== $value ) {
158+
$processor->set_attribute( $attribute_name, $new_value );
159+
}
160+
}
161+
}
162+
}
163+
164+
return $processor->get_updated_html();
165+
}
166+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
/**
3+
* HTML API: WP_HTML class
4+
*
5+
* Provides a public interface for HTML-related functionality in WordPress.
6+
*
7+
* @package WordPress
8+
* @subpackage HTML-API
9+
* @since 6.5.0
10+
*/
11+
12+
/**
13+
* WP_HTML class.
14+
*
15+
* @since 6.5.0
16+
*/
17+
class WP_HTML {
18+
/**
19+
* Renders an HTML template, replacing the placeholders with the provided values.
20+
*
21+
* This function looks for placeholders in the template string and will replace
22+
* them with appropriately-escaped substitutions from the given arguments, if
23+
* provided and if those arguments are strings.
24+
*
25+
* Example:
26+
*
27+
* echo WP_HTML::render(
28+
* '<a href="</%profile_url>"></%name></a>',
29+
* array(
30+
* 'profile_url' => 'https://profiles.example.com/username',
31+
* 'name' => $user->display_name
32+
* )
33+
* );
34+
* // Outputs: <a href="https://profiles.example.com/username">Bobby Tables</a>
35+
*
36+
* Do not escape the values supplied to the argument array! This function will escape each
37+
* parameter's value as needed and additional manual escaping may lead to incorrect output.
38+
*
39+
* ## Syntax.
40+
*
41+
* ### Substitution Placeholders.
42+
*
43+
* - `</%named_arg>` finds `named_arg` in the arguments array, escapes its value if possible,
44+
* and replaces the placeholder with the escaped value. These may exist inside double-quoted
45+
* HTML tag attributes or in HTML text content between tags. They cannot be used to output a tag
46+
* name or content inside a comment.
47+
*
48+
* ### Spread Attributes.
49+
*
50+
* - `...named_arg` when found within an HTML tag will lookup `named_arg` in the arguments array
51+
* and, if it's an array, will set the attribute on the tag for each key/value pair whose value
52+
* is a string. The
53+
*
54+
* ## Notes.
55+
*
56+
* - Attributes may only be supplied for a limited set of types: a string value assigns a double-quoted
57+
* attribute value; `true` sets the attribute as a boolean attribute; `null` removes the attribute.
58+
* If provided any other type of value the attribute will be ignored and its existing value persists.
59+
*
60+
* - If multiple HTML attributes are specified for a given tag they will be applied as if calling
61+
* `set_attribute()` in the order they are specified in the temlpate. This includes any attributes
62+
* assigned through the attribute spread syntax.
63+
*
64+
* - Substitutions in text nodes may only contain string values. If provided any other type of value
65+
* the placeholder will be removed with nothing in its place.
66+
*
67+
* - This function currently escapes all value provided in the arguments array. In the future
68+
* it may provide the ability to nest pre-rendered HTML into the template, but this functionality
69+
* is deferred for a future update.
70+
*
71+
* @since 6.5.0
72+
*
73+
* @access private
74+
*
75+
* @param string $template The HTML template.
76+
* @param string $args Array of key/value pairs providing substitue values for the placeholders.
77+
* @return string The rendered HTML.
78+
*/
79+
public static function render( $template, $args ) {
80+
return WP_HTML_Template::render( $template, $args );
81+
}
82+
}

src/wp-settings.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@
245245
require ABSPATH . WPINC . '/html-api/class-wp-html-token.php';
246246
require ABSPATH . WPINC . '/html-api/class-wp-html-processor-state.php';
247247
require ABSPATH . WPINC . '/html-api/class-wp-html-processor.php';
248+
require ABSPATH . WPINC . '/html-api/class-wp-html-template.php';
249+
require ABSPATH . WPINC . '/html-api/class-wp-html.php';
248250
require ABSPATH . WPINC . '/class-wp-http.php';
249251
require ABSPATH . WPINC . '/class-wp-http-streams.php';
250252
require ABSPATH . WPINC . '/class-wp-http-curl.php';
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
/**
3+
* Unit tests covering WP_HTML_Template functionality.
4+
*
5+
* @package WordPress
6+
* @subpackage HTML-API
7+
*
8+
* @since 6.5.0
9+
*
10+
* @group html-api
11+
*
12+
* @coversDefaultClass WP_HTML_Template
13+
*/
14+
15+
class Tests_HtmlApi_WpHtmlTemplate extends WP_UnitTestCase {
16+
/**
17+
* Demonstrates how to pass values into an HTML template.
18+
*
19+
* @ticket 60229
20+
*/
21+
public function test_basic_render() {
22+
$html = WP_HTML_Template::render(
23+
'<div class="is-test </%class>" ...div-args inert="</%is_inert>">Just a </%count> test</div>',
24+
array(
25+
'count' => '<strong>Hi <3</strong>',
26+
'class' => '5>4',
27+
'is_inert' => 'inert',
28+
'div-args' => array(
29+
'class' => 'hoover',
30+
'disabled' => true,
31+
),
32+
)
33+
);
34+
35+
$this->assertSame(
36+
'<div disabled class="hoover" inert="inert">Just a &lt;strong&gt;Hi &lt;3&lt;/strong&gt; test</div>',
37+
$html,
38+
'Failed to properly render template.'
39+
);
40+
}
41+
}

0 commit comments

Comments
 (0)