diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index d1585cdea5bf5..66b7561d0e686 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -470,12 +470,23 @@ public function remove_node( $token ) { * see WP_HTML_Open_Elements::walk_up(). * * @since 6.4.0 + * @since 6.7.0 Accepts $below_this_node to start traversal below a given node, if it exists. + * + * @param ?WP_HTML_Token $below_this_node Start traversing below this node, if provided and if the node exists. */ - public function walk_down() { - $count = count( $this->stack ); + public function walk_down( $below_this_node = null ) { + $has_found_node = null === $below_this_node; + $count = count( $this->stack ); for ( $i = 0; $i < $count; $i++ ) { - yield $this->stack[ $i ]; + $node = $this->stack[ $i ]; + + if ( ! $has_found_node ) { + $has_found_node = $node === $below_this_node; + continue; + } + + yield $node; } } diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 588d2fbe7d7c9..204c9e97813f6 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -3062,7 +3062,7 @@ private function run_adoption_agency_algorithm() { if ( // > If the current node is an HTML element whose tag name is subject - $current_node && $subject === $current_node->node_name && + isset( $current_node ) && $subject === $current_node->node_name && // > the current node is not in the list of active formatting elements ! $this->state->active_formatting_elements->contains_node( $current_node ) ) { @@ -3070,12 +3070,7 @@ private function run_adoption_agency_algorithm() { return; } - $outer_loop_counter = 0; - while ( $budget-- > 0 ) { - if ( $outer_loop_counter++ >= 8 ) { - return; - } - + for ( $outer_loop_counter = 0; $outer_loop_counter < 8; $outer_loop_counter++ ) { /* * > Let formatting element be the last element in the list of active formatting elements that: * > - is between the end of the list and the last marker in the list, @@ -3096,8 +3091,35 @@ private function run_adoption_agency_algorithm() { // > If there is no such element, then return and instead act as described in the "any other end tag" entry above. if ( null === $formatting_element ) { - $this->last_error = self::ERROR_UNSUPPORTED; - throw new WP_HTML_Unsupported_Exception( 'Cannot run adoption agency when "any other end tag" is required.' ); + /* + * > Any other end tag + */ + + /* + * Find the corresponding tag opener in the stack of open elements, if + * it exists before reaching a special element, which provides a kind + * of boundary in the stack. For example, a `` should not + * close anything beyond its containing `P` or `DIV` element. + */ + foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) { + if ( $subject === $node->node_name ) { + break; + } + + if ( self::is_special( $node->node_name ) ) { + // This is a parse error, ignore the token. + return; + } + } + + $this->generate_implied_end_tags( $subject ); + + foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) { + $this->state->stack_of_open_elements->pop(); + if ( $node === $item ) { + return; + } + } } // > If formatting element is not in the stack of open elements, then this is a parse error; remove the element from the list, and return. @@ -3111,22 +3133,16 @@ private function run_adoption_agency_algorithm() { return; } + /* + * > If formatting element is not the current node, this is a parse error. (But do not return.) + */ + /* * > Let furthest block be the topmost node in the stack of open elements that is lower in the stack * > than formatting element, and is an element in the special category. There might not be one. */ - $is_above_formatting_element = true; - $furthest_block = null; - foreach ( $this->state->stack_of_open_elements->walk_down() as $item ) { - if ( $is_above_formatting_element && $formatting_element->bookmark_name !== $item->bookmark_name ) { - continue; - } - - if ( $is_above_formatting_element ) { - $is_above_formatting_element = false; - continue; - } - + $furthest_block = null; + foreach ( $this->state->stack_of_open_elements->walk_down( $formatting_element ) as $item ) { if ( self::is_special( $item->node_name ) ) { $furthest_block = $item; break; @@ -3142,19 +3158,87 @@ private function run_adoption_agency_algorithm() { foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) { $this->state->stack_of_open_elements->pop(); - if ( $formatting_element->bookmark_name === $item->bookmark_name ) { + if ( $formatting_element === $item ) { $this->state->active_formatting_elements->remove_node( $formatting_element ); return; } } } + /* + * > Let common ancestor be the element immediately above formatting element in the stack of open elements. + */ + $common_ancestor = null; + foreach ( $this->state->stack_of_open_elements->walk_up( $formatting_element ) as $item ) { + $common_ancestor = $item; + break; + } + + /* + * Let a bookmark note the position of formatting element in the list of active formatting elements relative to the elements on either side of it in the list. + */ + $formatting_element_index = 0; + foreach ( $this->state->active_formatting_elements->walk_down() as $item ) { + if ( $formatting_element === $item ) { + break; + } + + ++$formatting_element_index; + } + + /* + * > Let node and last node be furthest block. + */ + $node = $furthest_block; + $last_node = $furthest_block; + + $inner_loop_counter = 0; + while ( $budget-- > 0 ) { + ++$inner_loop_counter; + + if ( $this->state->stack_of_open_elements->contains_node( $node ) ) { + foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $item ) { + $node = $item; + break; + } + } else { + $this->last_error = self::ERROR_UNSUPPORTED; + throw new WP_HTML_Unsupported_Exception( 'Cannot adjust node pointer above removed node.' ); + } + + if ( $formatting_element === $node ) { + break; + } + + if ( $inner_loop_counter > 3 && $this->state->active_formatting_elements->contains_node( $node ) ) { + $this->state->active_formatting_elements->remove_node( $node ); + } + + if ( ! $this->state->active_formatting_elements->contains_node( $node ) ) { + $this->state->stack_of_open_elements->remove_node( $node ); + continue; + } + + /* + * > Create an element for the token for which the element node was created, + * in the HTML namespace, with common ancestor as the intended parent; + * replace the entry for node in the list of active formatting elements + * with an entry for the new element, replace the entry for node in the + * stack of open elements with an entry for the new element, and let node + * be the new element. + */ + $this->last_error = self::ERROR_UNSUPPORTED; + throw new WP_HTML_Unsupported_Exception( 'Cannot create and reference new element for which no token exists.' ); + } + + /* + * > Insert whatever last node ended up being in the previous step at the appropriate + * > palce for inserting a node, but using common ancestor as the override target. + */ + $this->last_error = self::ERROR_UNSUPPORTED; - throw new WP_HTML_Unsupported_Exception( 'Cannot extract common ancestor in adoption agency algorithm.' ); + throw new WP_HTML_Unsupported_Exception( 'Cannot create and reference new element for which no token exists.' ); } - - $this->last_error = self::ERROR_UNSUPPORTED; - throw new WP_HTML_Unsupported_Exception( 'Cannot run adoption agency when looping required.' ); } /**