@@ -424,6 +424,18 @@ class WP_HTML_Tag_Processor {
424424 */
425425 private $ lexical_updates = array ();
426426
427+ /**
428+ * Attribute replacements to apply to input HTML document.
429+ *
430+ * Unlike more generic lexical updates, attribute updates are stored
431+ * in an associative array, where the keys are (lowercase-normalized)
432+ * attribute names, in order to avoid duplication.
433+ *
434+ * @since 6.2.0
435+ * @var WP_HTML_Text_Replacement[]
436+ */
437+ private $ attribute_updates = array ();
438+
427439 /**
428440 * Tracks how many times we've performed a `seek()`
429441 * so that we can prevent accidental infinite loops.
@@ -1103,7 +1115,8 @@ private function skip_whitespace() {
11031115 * @return void
11041116 */
11051117 private function after_tag () {
1106- $ this ->class_name_updates_to_lexical_updates ();
1118+ $ this ->class_name_updates_to_attribute_updates ();
1119+ $ this ->attribute_updates_to_lexical_updates ();
11071120 $ this ->apply_lexical_updates ();
11081121 $ this ->tag_name_starts_at = null ;
11091122 $ this ->tag_name_length = null ;
@@ -1113,20 +1126,20 @@ private function after_tag() {
11131126 }
11141127
11151128 /**
1116- * Converts class name updates into tag lexical updates
1129+ * Converts class name updates into tag attribute updates
11171130 * (they are accumulated in different data formats for performance).
11181131 *
1119- * This method is only meant to run right before the lexical updates are applied.
1132+ * This method is only meant to run right before the attribute updates are applied.
11201133 * The behavior in all other cases is undefined.
11211134 *
11221135 * @return void
11231136 * @since 6.2.0
11241137 *
11251138 * @see $classname_updates
1126- * @see $lexical_updates
1139+ * @see $attribute_updates
11271140 */
1128- private function class_name_updates_to_lexical_updates () {
1129- if ( count ( $ this ->classname_updates ) === 0 || isset ( $ this ->lexical_updates ['class ' ] ) ) {
1141+ private function class_name_updates_to_attribute_updates () {
1142+ if ( count ( $ this ->classname_updates ) === 0 || isset ( $ this ->attribute_updates ['class ' ] ) ) {
11301143 $ this ->classname_updates = array ();
11311144 return ;
11321145 }
@@ -1241,6 +1254,26 @@ private function class_name_updates_to_lexical_updates() {
12411254 }
12421255 }
12431256
1257+ /**
1258+ * Converts attribute updates into lexical updates.
1259+ *
1260+ * This method is only meant to run right before the attribute updates are applied.
1261+ * The behavior in all other cases is undefined.
1262+ *
1263+ * @return void
1264+ * @since 6.2.0
1265+ *
1266+ * @see $attribute_updates
1267+ * @see $lexical_updates
1268+ */
1269+ private function attribute_updates_to_lexical_updates () {
1270+ $ this ->lexical_updates = array_merge (
1271+ $ this ->lexical_updates ,
1272+ array_values ( $ this ->attribute_updates )
1273+ );
1274+ $ this ->attribute_updates = array ();
1275+ }
1276+
12441277 /**
12451278 * Applies updates to attributes.
12461279 *
@@ -1501,6 +1534,18 @@ public function is_tag_closer() {
15011534 return $ this ->is_closing_tag ;
15021535 }
15031536
1537+ /**
1538+ * Add a lexical update, i.e. a replacement of HTML at a given position.
1539+ *
1540+ * @param int $start The start offset of the replacement.
1541+ * @param int $end The end offset of the replacement.
1542+ * @param string $text The replacement.
1543+ * @return void
1544+ */
1545+ protected function add_lexical_update ( $ start , $ end , $ text ) {
1546+ $ this ->lexical_updates [] = new WP_HTML_Text_Replacement ( $ start , $ end , $ text );
1547+ }
1548+
15041549 /**
15051550 * Updates or creates a new attribute on the currently matched tag with the value passed.
15061551 *
@@ -1604,8 +1649,8 @@ public function set_attribute( $name, $value ) {
16041649 *
16051650 * Result: <div id="new"/>
16061651 */
1607- $ existing_attribute = $ this ->attributes [ $ comparable_name ];
1608- $ this ->lexical_updates [ $ name ] = new WP_HTML_Text_Replacement (
1652+ $ existing_attribute = $ this ->attributes [ $ comparable_name ];
1653+ $ this ->attribute_updates [ $ name ] = new WP_HTML_Text_Replacement (
16091654 $ existing_attribute ->start ,
16101655 $ existing_attribute ->end ,
16111656 $ updated_attribute
@@ -1622,7 +1667,7 @@ public function set_attribute( $name, $value ) {
16221667 *
16231668 * Result: <div id="new"/>
16241669 */
1625- $ this ->lexical_updates [ $ comparable_name ] = new WP_HTML_Text_Replacement (
1670+ $ this ->attribute_updates [ $ comparable_name ] = new WP_HTML_Text_Replacement (
16261671 $ this ->tag_name_starts_at + $ this ->tag_name_length ,
16271672 $ this ->tag_name_starts_at + $ this ->tag_name_length ,
16281673 ' ' . $ updated_attribute
@@ -1662,7 +1707,7 @@ public function remove_attribute( $name ) {
16621707 *
16631708 * Result: <div />
16641709 */
1665- $ this ->lexical_updates [ $ name ] = new WP_HTML_Text_Replacement (
1710+ $ this ->attribute_updates [ $ name ] = new WP_HTML_Text_Replacement (
16661711 $ this ->attributes [ $ name ]->start ,
16671712 $ this ->attributes [ $ name ]->end ,
16681713 ''
@@ -1724,7 +1769,11 @@ public function __toString() {
17241769 */
17251770 public function get_updated_html () {
17261771 // Short-circuit if there are no new updates to apply.
1727- if ( ! count ( $ this ->classname_updates ) && ! count ( $ this ->lexical_updates ) ) {
1772+ if (
1773+ ! count ( $ this ->classname_updates ) &&
1774+ ! count ( $ this ->attribute_updates ) &&
1775+ ! count ( $ this ->lexical_updates )
1776+ ) {
17281777 return $ this ->updated_html . substr ( $ this ->html , $ this ->updated_bytes );
17291778 }
17301779
@@ -1737,7 +1786,8 @@ public function get_updated_html() {
17371786 $ updated_html_up_to_current_tag_name_end = $ this ->updated_html . $ delta_between_updated_html_end_and_current_tag_end ;
17381787
17391788 // 1. Apply the attributes updates to the original HTML
1740- $ this ->class_name_updates_to_lexical_updates ();
1789+ $ this ->class_name_updates_to_attribute_updates ();
1790+ $ this ->attribute_updates_to_lexical_updates ();
17411791 $ this ->apply_lexical_updates ();
17421792
17431793 // 2. Replace the original HTML with the updated HTML
0 commit comments