Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTML API: Improve implementation of adoption agency algorithm #6983

Open
wants to merge 19 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
38c29c8
HTML API: Simplify breadcrumb accounting.
dmsnell Jul 6, 2024
e11f9ee
HTML API: Expand Unsupported class and make it available for debugging.
dmsnell Jul 6, 2024
e97f678
HTML API: Support more of the adoption agency algorithm.
dmsnell Jul 3, 2024
2b2d6fe
HTML API: Simplify breadcrumb accounting.
dmsnell Jul 6, 2024
6962fa2
HTML API: Expand Unsupported class and make it available for debugging.
dmsnell Jul 6, 2024
ab1096f
HTML API: Implement "reconstruct the active formatting elements" algo…
dmsnell Jul 6, 2024
ad82d3a
Merge branch 'trunk' into html-api/improve-active-element-reconstruction
dmsnell Aug 8, 2024
99d2175
Merge branch 'trunk' into html-api/improve-adoption-agency-algorithm
dmsnell Aug 8, 2024
22856d3
More iteration
dmsnell Aug 9, 2024
398ce37
Bail when needing to ignore token during adoption.
dmsnell Aug 9, 2024
374cba3
Continue iterating
dmsnell Aug 9, 2024
9bf12f4
Merge branch 'trunk' into html-api/improve-active-element-reconstruction
sirreal Aug 29, 2024
d7dac5e
Goto instead of ignore
dmsnell Sep 2, 2024
cabe36e
Remove dead code
dmsnell Sep 2, 2024
5e4bab9
Merge branch 'trunk' into html-api/improve-active-element-reconstruction
dmsnell Sep 3, 2024
301438f
Merge branch 'trunk' into html-api/improve-adoption-agency-algorithm
dmsnell Sep 3, 2024
9856fce
Merge branch 'trunk' into html-api/improve-active-element-reconstruction
dmsnell Sep 4, 2024
6d60b3a
Merge branch 'trunk' into html-api/improve-adoption-agency-algorithm
dmsnell Sep 4, 2024
dd0f59f
Merge branch 'html-api/improve-active-element-reconstruction' into ht…
dmsnell Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/wp-includes/html-api/class-wp-html-open-elements.php
Original file line number Diff line number Diff line change
Expand Up @@ -622,12 +622,23 @@ public function remove_node( WP_HTML_Token $token ): bool {
* 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix PHPdoc:

Suggested change
* @param ?WP_HTML_Token $below_this_node Start traversing below this node, if provided and if the node exists.
* @param WP_HTML_Token|null $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 ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function walk_down( $below_this_node = null ) {
public function walk_down( WP_HTML_Token $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;
}
}

Expand Down
247 changes: 202 additions & 45 deletions src/wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2360,7 +2360,9 @@ private function step_in_body(): bool {
break;

case 'A':
$this->run_adoption_agency_algorithm();
if ( 'ignore' === $this->run_adoption_agency_algorithm() ) {
$this->bail( 'Cannot ignore token after running adoption agency algorithm.' );
}
$this->state->active_formatting_elements->remove_node( $item );
$this->state->stack_of_open_elements->remove_node( $item );
break;
Expand Down Expand Up @@ -2401,7 +2403,9 @@ private function step_in_body(): bool {

if ( $this->state->stack_of_open_elements->has_element_in_scope( 'NOBR' ) ) {
// Parse error.
$this->run_adoption_agency_algorithm();
if ( 'ignore' === $this->run_adoption_agency_algorithm() ) {
$this->bail( 'Cannot ignore token after running adoption agency algorithm.' );
}
$this->reconstruct_active_formatting_elements();
}

Expand All @@ -2426,7 +2430,9 @@ private function step_in_body(): bool {
case '-STRONG':
case '-TT':
case '-U':
$this->run_adoption_agency_algorithm();
if ( 'ignore' === $this->run_adoption_agency_algorithm() ) {
$this->bail( 'Cannot ignore token after running adoption agency algorithm.' );
}
return true;

/*
Expand Down Expand Up @@ -5312,32 +5318,31 @@ public function reset_insertion_mode(): void {
*
* @see https://html.spec.whatwg.org/#adoption-agency-algorithm
*/
private function run_adoption_agency_algorithm(): void {
private function run_adoption_agency_algorithm(): ?string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would need a @return tag about the strings and their meanings.

$budget = 1000;
$subject = $this->get_tag();
$current_node = $this->state->stack_of_open_elements->current_node();

/*
* > 2. If the current node is an HTML element whose tag name is subject,
* > and the current node is not in the list of active formatting elements,
* > then pop the current node off the stack of open elements and return.
*/
if (
// > If the current node is an HTML element whose tag name is subject
$current_node && $subject === $current_node->node_name &&
// > the current node is not in the list of active formatting elements
'html' === $current_node->namespace &&
$subject === $current_node->node_name &&
! $this->state->active_formatting_elements->contains_node( $current_node )
) {
$this->state->stack_of_open_elements->pop();
return;
return null;
}

$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,
* > if any, or the start of the list otherwise,
* > - and has the tag name subject.
* > 3. 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,
* > if any, or the start of the list otherwise,
* > - and has the tag name subject.
*/
$formatting_element = null;
foreach ( $this->state->active_formatting_elements->walk_up() as $item ) {
Expand All @@ -5351,64 +5356,216 @@ private function run_adoption_agency_algorithm(): void {
}
}

// > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
/*
* > 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->bail( 'Cannot run adoption agency when "any other end tag" is required.' );
/*
* These steps are copied here from above. This may remove the node
* or ignore it, meaning the following code must respect that.
*/
foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
if ( 'html' === $node->namespace && $subject === $node->node_name ) {
break;
}

if ( self::is_special( $node ) ) {
return 'ignore';
}
}

$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 null;
}
}
}

// > 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.
/*
* > 4. 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.
*/
if ( ! $this->state->stack_of_open_elements->contains_node( $formatting_element ) ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
return;
return null;
}

// > If formatting element is in the stack of open elements, but the element is not in scope, then this is a parse error; return.
/*
* > 5. If formatting element is in the stack of open elements, but the element
* > is not in scope, then this is a parse error; return.
*/
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $formatting_element->node_name ) ) {
return;
return null;
}

/*
* > 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.
* > 6. If formatting element is not the current node, this is a parse error. (But do not return.)
*/
$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;
}

/*
* > 7. 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.
*/
$furthest_block = null;
foreach ( $this->state->stack_of_open_elements->walk_down( $formatting_element ) as $item ) {
if ( self::is_special( $item ) ) {
$furthest_block = $item;
break;
}
}

/*
* > If there is no furthest block, then the UA must first pop all the nodes from the bottom of the
* > stack of open elements, from the current node up to and including formatting element, then
* > remove formatting element from the list of active formatting elements, and finally return.
* > 8. If there is no furthest block, then the UA must first pop all the nodes from the bottom of
* > the stack of open elements, from the current node up to and including formatting element,
* > then remove formatting element from the list of active formatting elements, and finally return.
*/
if ( null === $furthest_block ) {
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;
return null;
}
}
}

$this->bail( 'Cannot extract common ancestor in adoption agency algorithm.' );
}
/*
* > 9. 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;
}

/*
* > 10. 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;
}

/*
* > 11. Let node and last node be furthest block.
*/
$node = $furthest_block;
$last_node = $furthest_block;

$inner_loop_counter = 0;
while ( $budget-- > 0 ) {
/*
* > 1. Increment innerLoopCounter by 1.
*/
++$inner_loop_counter;

$this->bail( 'Cannot run adoption agency when looping required.' );
/*
* > 2. Let node be the element immediately above node in the stack of open elements,
* > or if node is no longer in the stack of open elements (e.g. because it got
* > removed by this algorithm), the element that was immediately above node in
* > the stack of open elements before node was removed.
*/
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->bail( 'Cannot adjust node pointer above removed node.' );
}

/*
* > 3. If node is formattingElement, the break.
*/
if ( $formatting_element === $node ) {
break;
}

/*
* > 4. If innerLoopCounter is greater than 3 and node is in the list of active formatting
* > elements, then remove node from the list of active formatting elements.
*/
if ( $inner_loop_counter > 3 && $this->state->active_formatting_elements->contains_node( $node ) ) {
$this->state->active_formatting_elements->remove_node( $node );
}

/*
* > 5. If node is not in the list of active formatting elements, then remove node from
* > the stack of open elements and continue.
*/
if ( ! $this->state->active_formatting_elements->contains_node( $node ) ) {
$this->state->stack_of_open_elements->remove_node( $node );
continue;
}

/*
* > 6. 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->bail( 'Cannot create and reference new element for which no token exists.' );

/*
* > 7. If last node is furthestBlock, then move the aforementioned bookmark to
* > be immediately after the new node in the list of active formatting elements.
*/

/*
* > 8. Append lastNode to node.
*/

/*
* > 9. Set lastNode to node.
*/
$last_node = $node;
}

/*
* > 14. Insert whatever last node ended up being in the previous step at the appropriate
* > place for inserting a node, but using common ancestor as the override target.
*/
$this->bail( 'Cannot create and reference new element for which no token exists.' );

/*
* > 15. Create an element for the token for which formattingElement was created,
* > in the HTML namespace, with furthestBlock as the intended parent.
*/

/*
* > 16. Take all of the child nodes of furthestBlock and append them to the element
* > created in the last step.
*/

/*
* > 17. Append that new element to furthestBlock.
*/

/*
* > 18. Remove formattingElement from the list of active formatting elements,
* > and insert the new element into the list of active formatting elements
* > at the position of the aforementioned bookmark.
*/

/*
* > 19. Remove formattingElement from the stack of open elements, and insert the
* > new element into the stack of open elements immediately below the position
* > of furthestBlock in that stack.
*/
}
}

/**
Expand Down
Loading