@@ -528,6 +528,18 @@ class Gutenberg_HTML_Tag_Processor_6_3 {
528528 */
529529 protected $ lexical_updates = array ();
530530
531+ /**
532+ * Attribute replacements to apply to input HTML document.
533+ *
534+ * Unlike more generic lexical updates, attribute updates are stored
535+ * in an associative array, where the keys are (lowercase-normalized)
536+ * attribute names, in order to avoid duplication.
537+ *
538+ * @since 6.3.0
539+ * @var WP_HTML_Text_Replacement[]
540+ */
541+ private $ attribute_updates = array ();
542+
531543 /**
532544 * Tracks and limits `seek()` calls to prevent accidental infinite loops.
533545 *
@@ -1237,15 +1249,16 @@ private function skip_whitespace() {
12371249 }
12381250
12391251 /**
1240- * Applies attribute updates and cleans up once a tag is fully parsed.
1252+ * Applies lexical updates and cleans up once a tag is fully parsed.
12411253 *
12421254 * @since 6.2.0
12431255 *
12441256 * @return void
12451257 */
12461258 private function after_tag () {
1247- $ this ->class_name_updates_to_attributes_updates ();
1248- $ this ->apply_attributes_updates ();
1259+ $ this ->class_name_updates_to_attribute_updates ();
1260+ $ this ->attribute_updates_to_lexical_updates ();
1261+ $ this ->apply_lexical_updates ();
12491262 $ this ->tag_name_starts_at = null ;
12501263 $ this ->tag_name_length = null ;
12511264 $ this ->tag_ends_at = null ;
@@ -1254,17 +1267,17 @@ private function after_tag() {
12541267 }
12551268
12561269 /**
1257- * Converts class name updates into tag attributes updates
1270+ * Converts class name updates into tag attribute updates
12581271 * (they are accumulated in different data formats for performance).
12591272 *
1260- * @see $lexical_updates
1273+ * @see $attribute_updates
12611274 * @see $classname_updates
12621275 *
12631276 * @since 6.2.0
12641277 *
12651278 * @return void
12661279 */
1267- private function class_name_updates_to_attributes_updates () {
1280+ private function class_name_updates_to_attribute_updates () {
12681281 if ( count ( $ this ->classname_updates ) === 0 ) {
12691282 return ;
12701283 }
@@ -1398,14 +1411,33 @@ private function class_name_updates_to_attributes_updates() {
13981411 }
13991412
14001413 /**
1401- * Applies attribute updates to HTML document.
1414+ * Converts attribute updates into lexical updates.
1415+ *
1416+ * This method is only meant to run right before the attribute updates are applied.
1417+ * The behavior in all other cases is undefined.
1418+ *
1419+ * @return void
1420+ * @since 6.3.0
1421+ *
1422+ * @see $attribute_updates
1423+ * @see $lexical_updates
1424+ */
1425+ private function attribute_updates_to_lexical_updates () {
1426+ foreach ( $ this ->attribute_updates as $ update ) {
1427+ $ this ->lexical_updates [] = $ update ;
1428+ }
1429+ $ this ->attribute_updates = array ();
1430+ }
1431+
1432+ /**
1433+ * Applies lexical updates to HTML document.
14021434 *
14031435 * @since 6.2.0
14041436 * @since 6.3.0 Invalidate any bookmarks whose targets are overwritten.
14051437 *
14061438 * @return void
14071439 */
1408- private function apply_attributes_updates () {
1440+ private function apply_lexical_updates () {
14091441 if ( ! count ( $ this ->lexical_updates ) ) {
14101442 return ;
14111443 }
@@ -1527,8 +1559,8 @@ public function seek( $bookmark_name ) {
15271559 *
15281560 * @since 6.2.0
15291561 *
1530- * @param WP_HTML_Text_Replacement $a First attribute update.
1531- * @param WP_HTML_Text_Replacement $b Second attribute update.
1562+ * @param WP_HTML_Text_Replacement $a First lexical update.
1563+ * @param WP_HTML_Text_Replacement $b Second lexical update.
15321564 * @return int Comparison value for string order.
15331565 */
15341566 private static function sort_start_ascending ( $ a , $ b ) {
@@ -1564,11 +1596,11 @@ private static function sort_start_ascending( $a, $b ) {
15641596 * @return string|boolean|null Value of enqueued update if present, otherwise false.
15651597 */
15661598 private function get_enqueued_attribute_value ( $ comparable_name ) {
1567- if ( ! isset ( $ this ->lexical_updates [ $ comparable_name ] ) ) {
1599+ if ( ! isset ( $ this ->attribute_updates [ $ comparable_name ] ) ) {
15681600 return false ;
15691601 }
15701602
1571- $ enqueued_text = $ this ->lexical_updates [ $ comparable_name ]->text ;
1603+ $ enqueued_text = $ this ->attribute_updates [ $ comparable_name ]->text ;
15721604
15731605 // Removed attributes erase the entire span.
15741606 if ( '' === $ enqueued_text ) {
@@ -1641,7 +1673,7 @@ public function get_attribute( $name ) {
16411673
16421674 /*
16431675 * For every attribute other than `class` it's possible to perform a quick check if
1644- * there's an enqueued lexical update whose value takes priority over what's found in
1676+ * there's an enqueued attribute update whose value takes priority over what's found in
16451677 * the input document.
16461678 *
16471679 * The `class` attribute is special though because of the exposed helpers `add_class`
@@ -1651,7 +1683,7 @@ public function get_attribute( $name ) {
16511683 * into an attribute value update.
16521684 */
16531685 if ( 'class ' === $ name ) {
1654- $ this ->class_name_updates_to_attributes_updates ();
1686+ $ this ->class_name_updates_to_attribute_updates ();
16551687 }
16561688
16571689 // Return any enqueued attribute value updates if they exist.
@@ -1879,8 +1911,8 @@ public function set_attribute( $name, $value ) {
18791911 *
18801912 * Result: <div id="new"/>
18811913 */
1882- $ existing_attribute = $ this ->attributes [ $ comparable_name ];
1883- $ this ->lexical_updates [ $ name ] = new WP_HTML_Text_Replacement (
1914+ $ existing_attribute = $ this ->attributes [ $ comparable_name ];
1915+ $ this ->attribute_updates [ $ name ] = new WP_HTML_Text_Replacement (
18841916 $ existing_attribute ->start ,
18851917 $ existing_attribute ->end ,
18861918 $ updated_attribute
@@ -1897,7 +1929,7 @@ public function set_attribute( $name, $value ) {
18971929 *
18981930 * Result: <div id="new"/>
18991931 */
1900- $ this ->lexical_updates [ $ comparable_name ] = new WP_HTML_Text_Replacement (
1932+ $ this ->attribute_updates [ $ comparable_name ] = new WP_HTML_Text_Replacement (
19011933 $ this ->tag_name_starts_at + $ this ->tag_name_length ,
19021934 $ this ->tag_name_starts_at + $ this ->tag_name_length ,
19031935 ' ' . $ updated_attribute
@@ -1955,8 +1987,8 @@ public function remove_attribute( $name ) {
19551987 * and when that attribute wasn't originally present.
19561988 */
19571989 if ( ! isset ( $ this ->attributes [ $ name ] ) ) {
1958- if ( isset ( $ this ->lexical_updates [ $ name ] ) ) {
1959- unset( $ this ->lexical_updates [ $ name ] );
1990+ if ( isset ( $ this ->attribute_updates [ $ name ] ) ) {
1991+ unset( $ this ->attribute_updates [ $ name ] );
19601992 }
19611993 return false ;
19621994 }
@@ -1972,7 +2004,7 @@ public function remove_attribute( $name ) {
19722004 *
19732005 * Result: <div />
19742006 */
1975- $ this ->lexical_updates [ $ name ] = new WP_HTML_Text_Replacement (
2007+ $ this ->attribute_updates [ $ name ] = new WP_HTML_Text_Replacement (
19762008 $ this ->attributes [ $ name ]->start ,
19772009 $ this ->attributes [ $ name ]->end ,
19782010 ''
@@ -2041,7 +2073,10 @@ public function __toString() {
20412073 * @return string The processed HTML.
20422074 */
20432075 public function get_updated_html () {
2044- $ requires_no_updating = 0 === count ( $ this ->classname_updates ) && 0 === count ( $ this ->lexical_updates );
2076+ $ requires_no_updating =
2077+ 0 === count ( $ this ->classname_updates ) &&
2078+ 0 === count ( $ this ->attribute_updates ) &&
2079+ 0 === count ( $ this ->lexical_updates );
20452080
20462081 /*
20472082 * When there is nothing more to update and nothing has already been
@@ -2072,8 +2107,9 @@ public function get_updated_html() {
20722107 *
20732108 * Note: `apply_attributes_updates()` modifies `$this->output_buffer`.
20742109 */
2075- $ this ->class_name_updates_to_attributes_updates ();
2076- $ this ->apply_attributes_updates ();
2110+ $ this ->class_name_updates_to_attribute_updates ();
2111+ $ this ->attribute_updates_to_lexical_updates ();
2112+ $ this ->apply_lexical_updates ();
20772113
20782114 /*
20792115 * 2. Replace the original HTML with the now-updated HTML so that it's possible to
0 commit comments