diff --git a/src/wp-includes/html-api/class-wp-html-template.php b/src/wp-includes/html-api/class-wp-html-template.php
new file mode 100644
index 0000000000000..c3161a4d3895b
--- /dev/null
+++ b/src/wp-includes/html-api/class-wp-html-template.php
@@ -0,0 +1,228 @@
+lexical_updates;
+ $this->remove_attribute( $name );
+ $span = $this->lexical_updates[ strtolower( $name ) ];
+ $this->lexical_updates = $lexical_updates;
+ $span->text = substr( $this->html, $span->start, $span->length );
+ return $span;
+ }
+
+ public function raw_replace_token( $content ) {
+ $this->set_bookmark( 'here' );
+ $here = $this->bookmarks['_here'];
+ $this->deferred_updates[] = new WP_HTML_Text_Replacement(
+ $here->start,
+ $here->length,
+ $content
+ );
+ }
+
+ public function set_attribute( $name, $value ): bool {
+ if ( ! parent::set_attribute( $name, $value ) ) {
+ return false;
+ }
+ $lower_name = strtolower( $name );
+ $this->deferred_updates[ $lower_name ] = $this->lexical_updates[ $lower_name ];
+ return true;
+ }
+
+ public function remove_attribute( $name ): bool {
+ if ( ! parent::remove_attribute( $name ) ) {
+ return false;
+ }
+
+ $lower_name = strtolower( $name );
+ $this->deferred_updates[ $lower_name ] = $this->lexical_updates[ $lower_name ];
+ return true;
+ }
+ };
+
+ $processor = $builder::create_fragment( $template );
+ $bit_pattern = '%(?P[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)';
+
+ $refers_to = function ( $input ) use ( $args, $bit_pattern ){
+ if ( 1 === preg_match( "~^{$bit_pattern}>$~", $input, $matches ) ) {
+ return isset( $args[ $matches['VAR'] ] ) ? $matches['VAR'] : null;
+ }
+
+ return false;
+ };
+
+ while ( $processor->next_token() ) {
+ $token_type = $processor->get_token_type();
+
+ // Skip over entire elements when instructed.
+ if ( '#tag' === $token_type ) {
+ if ( is_string( $processor->get_attribute( 'data-wp-if' ) ) ) {
+ $ignorable = $processor->get_raw_attribute( 'data-wp-if' )->text;
+ $quote = $ignorable[ strlen( $ignorable ) - 1 ];
+ $ignorable = substr(
+ $ignorable,
+ strcspn( $ignorable, $quote ) + 1,
+ -1
+ );
+ $processor->remove_attribute( 'data-wp-if' );
+ $condition = $refers_to( $ignorable );
+
+ if ( isset( $condition, $args[ $condition ] ) && in_array( $args[ $condition ], array( false, null, '' ), true ) ) {
+ $depth = $processor->get_current_depth();
+ while ( $processor->next_token() && $processor->get_current_depth() > $depth ) {
+ continue;
+ }
+
+ continue;
+ }
+ }
+
+ // Replace Bits in attributes and spread attributes.
+ foreach ( $processor->get_attribute_names_with_prefix( '' ) ?? array() as $name ) {
+ // Spread attributes which are boolean; don’t replace those with values.
+ if ( str_starts_with( $name, '...' ) && true === $processor->get_attribute( $name ) ) {
+ $processor->remove_attribute( $name );
+ $spread_name = substr( $name, 3 );
+ if ( isset( $spread_name, $args[ $spread_name ] ) && is_array( $args[ $spread_name ] ) ) {
+ $spread_args = $args[ $spread_name ];
+ foreach ( $spread_args as $arg_name => $arg_value ) {
+ if ( is_string( $arg_name ) && ( true === $arg_value || is_string( $arg_value ) ) ) {
+ $processor->set_attribute( $arg_name, $arg_value );
+ } else if ( is_string( $arg_name ) && in_array( $arg_value, array( false, null ), true ) ) {
+ $processor->remove_attribute( $arg_name );
+ }
+ }
+ }
+
+ continue;
+ }
+
+ $raw_attr = $processor->get_raw_attribute( $name )->text;
+ $last_c = $raw_attr[ strlen( $raw_attr ) - 1 ];
+
+ // Bit syntax cannot appear in unquoted attributes.
+ if ( '"' !== $last_c && "'" !== $last_c ) {
+ continue;
+ }
+
+ $value = substr( $raw_attr, strcspn( $raw_attr, $last_c ) + 1, -1 );
+ $matches = null;
+ $bits = preg_match_all( "~{$bit_pattern}>~", $value, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE );
+ if ( 0 === $bits || false === $bits ) {
+ continue;
+ }
+
+ $updated = array();
+ $was_at = 0;
+ foreach ( $matches as $match ) {
+ $updated[] = substr( $value, $was_at, $match[0][1] - $was_at );
+ $was_at = $match[0][1] + strlen( $match[0][0] );
+ $arg_name = $match['VAR'][0];
+
+ if ( isset( $arg_name, $args[ $arg_name ] ) && is_string( $args[ $arg_name ] ) ) {
+ $updated[] = self::escape( $name, $args[ $arg_name ] );
+ }
+ }
+
+ $updated[] = substr( $value, $was_at );
+ $decoded = WP_HTML_Decoder::decode_attribute( implode( '', $updated ) );
+ if ( ! $processor->set_attribute( $name, $decoded ) ) {
+ $processor->remove_attribute( $name );
+ }
+ }
+ }
+
+ if ( '#funky-comment' === $token_type ) {
+ $text = $processor->get_modifiable_text();
+ if ( 1 !== preg_match( "~^{$bit_pattern}$~", $text, $match ) ) {
+ continue;
+ }
+
+ if ( isset( $match['VAR'], $args[ $match['VAR'] ] ) && is_string( $args[ $match['VAR'] ] ) ) {
+ $processor->raw_replace_token( self::escape( null, $args[ $match['VAR'] ] ) );
+ }
+ }
+ }
+
+ return self::$sentinel_class::wrap( $template, $processor->deferred_updates );
+ }
+
+ public static function render( $compiled ): string {
+ return $compiled->unwrap();
+ }
+
+ private static function escape( $attr_name, string $plaintext ): string {
+ if ( isset( $attr_name ) && in_array( strtolower( $attr_name ), wp_kses_uri_attributes(), true ) ) {
+ return esc_url( $plaintext );
+ }
+
+ return strtr(
+ $plaintext,
+ array(
+ '<' => '<',
+ '>' => '>',
+ '&' => '&',
+ '"' => '"',
+ "'" => ''',
+ )
+ );
+ }
+
+ /**
+ * Ensures that the sentinel class is dynamically generated at boot.
+ * This class is to never be serialized or instantiated outside of
+ * this parent class.
+ *
+ * @since 7.0
+ */
+ private static function ensure_sentinel(): void {
+ if ( isset( self::$sentinel_class ) ) {
+ return;
+ }
+
+ self::$sentinel_class = new class () {
+ private $html = '';
+
+ private $updates = array();
+
+ public static function wrap( string $html, array $updates ): self {
+ $wrapper = new self();
+ $wrapper->html = $html;
+ $wrapper->updates = $updates;
+ return $wrapper;
+ }
+
+ public function unwrap(): string {
+ $processor = new class( $this->html ) extends WP_HTML_Tag_Processor {
+ public function flood( $updates ) {
+ $this->lexical_updates = $updates;
+ }
+ };
+
+ $processor->flood( $this->updates );
+ return $processor->get_updated_html();
+ }
+ };
+ }
+}