diff --git a/lib/experimental/html/class-wp-html-tag-processor.php b/lib/experimental/html/class-wp-html-tag-processor.php index 82b5180046879d..0063e742f2b37e 100644 --- a/lib/experimental/html/class-wp-html-tag-processor.php +++ b/lib/experimental/html/class-wp-html-tag-processor.php @@ -424,6 +424,18 @@ class WP_HTML_Tag_Processor { */ private $lexical_updates = array(); + /** + * Attribute replacements to apply to input HTML document. + * + * Unlike more generic lexical updates, attribute updates are stored + * in an associative array, where the keys are (lowercase-normalized) + * attribute names, in order to avoid duplication. + * + * @since 6.2.0 + * @var WP_HTML_Text_Replacement[] + */ + private $attribute_updates = array(); + /** * Tracks how many times we've performed a `seek()` * so that we can prevent accidental infinite loops. @@ -1103,7 +1115,8 @@ private function skip_whitespace() { * @return void */ private function after_tag() { - $this->class_name_updates_to_lexical_updates(); + $this->class_name_updates_to_attribute_updates(); + $this->attribute_updates_to_lexical_updates(); $this->apply_lexical_updates(); $this->tag_name_starts_at = null; $this->tag_name_length = null; @@ -1113,20 +1126,20 @@ private function after_tag() { } /** - * Converts class name updates into tag lexical updates + * Converts class name updates into tag attribute updates * (they are accumulated in different data formats for performance). * - * This method is only meant to run right before the lexical updates are applied. + * This method is only meant to run right before the attribute updates are applied. * The behavior in all other cases is undefined. * * @return void * @since 6.2.0 * * @see $classname_updates - * @see $lexical_updates + * @see $attribute_updates */ - private function class_name_updates_to_lexical_updates() { - if ( count( $this->classname_updates ) === 0 || isset( $this->lexical_updates['class'] ) ) { + private function class_name_updates_to_attribute_updates() { + if ( count( $this->classname_updates ) === 0 || isset( $this->attribute_updates['class'] ) ) { $this->classname_updates = array(); return; } @@ -1241,6 +1254,26 @@ private function class_name_updates_to_lexical_updates() { } } + /** + * Converts attribute updates into lexical updates. + * + * This method is only meant to run right before the attribute updates are applied. + * The behavior in all other cases is undefined. + * + * @return void + * @since 6.2.0 + * + * @see $attribute_updates + * @see $lexical_updates + */ + private function attribute_updates_to_lexical_updates() { + $this->lexical_updates = array_merge( + $this->lexical_updates, + array_values( $this->attribute_updates ) + ); + $this->attribute_updates = array(); + } + /** * Applies updates to attributes. * @@ -1501,6 +1534,18 @@ public function is_tag_closer() { return $this->is_closing_tag; } + /** + * Add a lexical update, i.e. a replacement of HTML at a given position. + * + * @param int $start The start offset of the replacement. + * @param int $end The end offset of the replacement. + * @param string $text The replacement. + * @return void + */ + protected function add_lexical_update( $start, $end, $text ) { + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $text ); + } + /** * Updates or creates a new attribute on the currently matched tag with the value passed. * @@ -1604,8 +1649,8 @@ public function set_attribute( $name, $value ) { * * Result:
*/ - $existing_attribute = $this->attributes[ $comparable_name ]; - $this->lexical_updates[ $name ] = new WP_HTML_Text_Replacement( + $existing_attribute = $this->attributes[ $comparable_name ]; + $this->attribute_updates[ $name ] = new WP_HTML_Text_Replacement( $existing_attribute->start, $existing_attribute->end, $updated_attribute @@ -1622,7 +1667,7 @@ public function set_attribute( $name, $value ) { * * Result:
*/ - $this->lexical_updates[ $comparable_name ] = new WP_HTML_Text_Replacement( + $this->attribute_updates[ $comparable_name ] = new WP_HTML_Text_Replacement( $this->tag_name_starts_at + $this->tag_name_length, $this->tag_name_starts_at + $this->tag_name_length, ' ' . $updated_attribute @@ -1662,7 +1707,7 @@ public function remove_attribute( $name ) { * * Result:
*/ - $this->lexical_updates[ $name ] = new WP_HTML_Text_Replacement( + $this->attribute_updates[ $name ] = new WP_HTML_Text_Replacement( $this->attributes[ $name ]->start, $this->attributes[ $name ]->end, '' @@ -1724,7 +1769,11 @@ public function __toString() { */ public function get_updated_html() { // Short-circuit if there are no new updates to apply. - if ( ! count( $this->classname_updates ) && ! count( $this->lexical_updates ) ) { + if ( + ! count( $this->classname_updates ) && + ! count( $this->attribute_updates ) && + ! count( $this->lexical_updates ) + ) { return $this->updated_html . substr( $this->html, $this->updated_bytes ); } @@ -1737,7 +1786,8 @@ public function get_updated_html() { $updated_html_up_to_current_tag_name_end = $this->updated_html . $delta_between_updated_html_end_and_current_tag_end; // 1. Apply the attributes updates to the original HTML - $this->class_name_updates_to_lexical_updates(); + $this->class_name_updates_to_attribute_updates(); + $this->attribute_updates_to_lexical_updates(); $this->apply_lexical_updates(); // 2. Replace the original HTML with the updated HTML