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: Add select handling #5908

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b867b58
Implement SELECT and related tags handling
sirreal Jan 19, 2024
9245930
Remove SELECT from unsupported elements test
sirreal Jul 3, 2024
267a27b
Remove SELECT scope optgroup/option tests
sirreal Jul 3, 2024
b0475fb
Remove SELECT, OPTION, OPTGROUP from unsupported tags
sirreal Jul 3, 2024
1944b66
Remove OPTION,OPTGROUP from gen implied end tags unsupported test
sirreal Jul 3, 2024
8d4b4df
Add OPTION, OPTGROUP to implied end tags
sirreal Jul 4, 2024
1404119
Update step_in_select since tag
sirreal Jul 4, 2024
dc0bbb9
Update since tags on in_select_scope
sirreal Jul 4, 2024
9d99844
Implement in_select_scope via …in_specific_scope
sirreal Jul 4, 2024
3085405
Revert "Implement in_select_scope via …in_specific_scope"
sirreal Jul 4, 2024
e095c62
Add current_node_is method
sirreal Jul 4, 2024
4026493
Merge branch 'html-api/add-current-node-is' into html-api/handle-sele…
sirreal Jul 4, 2024
76c899a
Add text, comment, doctype handling
sirreal Jul 4, 2024
52fd8e5
Handle +HTML case
sirreal Jul 4, 2024
6a10eb0
Update several cases to use current_node_is
sirreal Jul 4, 2024
8b3a9e4
Simplify -OPTGROUP handling implementation
sirreal Jul 4, 2024
87c6596
Add input,keygen,textarea,script,template handling
sirreal Jul 4, 2024
d464330
Add "anything else" handling
sirreal Jul 4, 2024
ddbd676
Add step_in_head stub
sirreal Jul 4, 2024
f6d686d
Multi-line comments should be block comments
sirreal Jul 4, 2024
783cef6
fixup! Update several cases to use current_node_is
sirreal Jul 4, 2024
3222275
Fix step_in_head documentation block
sirreal Jul 4, 2024
eb126e8
Merge branch 'trunk' into html-api/handle-select-tag
dmsnell Jul 4, 2024
f2e1d03
Apply feedback requests.
dmsnell Jul 5, 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
23 changes: 19 additions & 4 deletions src/wp-includes/html-api/class-wp-html-open-elements.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,19 +269,34 @@ public function has_element_in_table_scope( $tag_name ) {
/**
* Returns whether a particular element is in select scope.
*
* @since 6.4.0
* @since 6.4.0 Stub implementation (throws).
* @since 6.7.0 Full implementation.
*
* @see https://html.spec.whatwg.org/#has-an-element-in-select-scope
*
* @throws WP_HTML_Unsupported_Exception Always until this function is implemented.
* > The stack of open elements is said to have a particular element in select scope when it has
* > that element in the specific scope consisting of all element types except the following:
* > - optgroup in the HTML namespace
* > - option in the HTML namespace
*
* @param string $tag_name Name of tag to check.
* @return bool Whether given element is in scope.
*/
public function has_element_in_select_scope( $tag_name ) {
throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on select scope.' );
foreach ( $this->walk_up() as $node ) {
if ( $node->node_name === $tag_name ) {
return true;
}

return false; // The linter requires this unreachable code until the function is implemented and can return.
if (
'OPTION' !== $node->node_name &&
'OPTGROUP' !== $node->node_name
) {
return false;
}
}

return false;
}

/**
Expand Down
169 changes: 165 additions & 4 deletions src/wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
*
* - Containers: ADDRESS, BLOCKQUOTE, DETAILS, DIALOG, DIV, FOOTER, HEADER, MAIN, MENU, SPAN, SUMMARY.
* - Custom elements: All custom elements are supported. :)
* - Form elements: BUTTON, DATALIST, FIELDSET, INPUT, LABEL, LEGEND, METER, PROGRESS, SEARCH.
* - Form elements: BUTTON, DATALIST, FIELDSET, INPUT, LABEL, LEGEND, METER, OPTGROUP, OPTION, PROGRESS, SEARCH, SELECT.
* - Formatting elements: B, BIG, CODE, EM, FONT, I, PRE, SMALL, STRIKE, STRONG, TT, U, WBR.
* - Heading elements: H1, H2, H3, H4, H5, H6, HGROUP.
* - Links: A.
Expand Down Expand Up @@ -757,6 +757,9 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ) {
case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY:
return $this->step_in_body();

case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT:
return $this->step_in_select();

default:
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
Expand Down Expand Up @@ -1336,6 +1339,50 @@ private function step_in_body() {
case '+TRACK':
$this->insert_html_element( $this->state->current_token );
return true;

/*
* > A start tag whose tag name is "select"
*/
case '+SELECT':
$this->reconstruct_active_formatting_elements();
$this->insert_html_element( $this->state->current_token );
$this->state->frameset_ok = false;

// If the insertion mode is one of
// - "in table"
// - "in caption"
// - "in table body"
// - "in row"
// - "in cell"
// then switch the insertion mode to "in select in table"
//
// Otherwise, switch the insertion mode to "in select".
switch ( $this->state->insertion_mode ) {
case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE:
case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION:
case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY:
case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW:
case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL:
$this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE;
break;
default:
$this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT;
break;
}
return true;

/*
* > A start tag whose tag name is one of: "optgroup", "option"
*/
case '+OPTGROUP':
case '+OPTION':
$current_node = $this->state->stack_of_open_elements->current_node();
if ( $current_node && 'OPTION' === $current_node->node_name ) {
$this->state->stack_of_open_elements->pop();
}
$this->reconstruct_active_formatting_elements();
$this->insert_html_element( $this->state->current_token );
return true;
}

/*
Expand Down Expand Up @@ -1378,16 +1425,13 @@ private function step_in_body() {
case 'NOFRAMES':
case 'NOSCRIPT':
case 'OBJECT':
case 'OPTGROUP':
case 'OPTION':
case 'PLAINTEXT':
case 'RB':
case 'RP':
case 'RT':
case 'RTC':
case 'SARCASM':
case 'SCRIPT':
case 'SELECT':
case 'STYLE':
case 'SVG':
case 'TABLE':
Expand Down Expand Up @@ -1448,6 +1492,118 @@ private function step_in_body() {
}
}

/**
* Parses next element in the 'in select' insertion mode.
*
* This internal function performs the 'in select' insertion mode
* logic for the generalized WP_HTML_Processor::step() function.
*
* @since 6.7.0
*
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect
* @see WP_HTML_Processor::step
*
* @return bool Whether an element was found.
*/
private function step_in_select() {
$tag_name = $this->get_tag();
$op_sigil = $this->is_tag_closer() ? '-' : '+';
$op = "{$op_sigil}{$tag_name}";

switch ( $op ) {
/*
* > A start tag whose tag name is "html"
*/
case '+HTML':
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "Cannot process {$tag_name} element." );

/*
* > A start tag whose tag name is "option"
*/
case '+OPTION':
$current_node = $this->state->stack_of_open_elements->current_node();
if ( $current_node && 'OPTION' === $current_node->node_name ) {
$this->state->stack_of_open_elements->pop();
}
$this->insert_html_element( $this->state->current_token );
return true;

/*
* > A start tag whose tag name is "optgroup"
* > A start tag whose tag name is "hr"
*/
case '+OPTGROUP':
case '+HR':
$current_node = $this->state->stack_of_open_elements->current_node();
if ( $current_node && 'OPTION' === $current_node->node_name ) {
$this->state->stack_of_open_elements->pop();
// If we've popped, update the current_node
$current_node = $this->state->stack_of_open_elements->current_node();
}

if ( $current_node && 'OPTGROUP' === $current_node->node_name ) {
$this->state->stack_of_open_elements->pop();
}

$this->insert_html_element( $this->state->current_token );
return true;

/*
* > An end tag whose tag name is "optgroup"
*/
case '-OPTGROUP':
$walker = $this->state->stack_of_open_elements->walk_up();
$current_node = $walker->current();
if ( ! $current_node ) {
return $this->step();
}
if ( 'OPTGROUP' === $current_node->node_name ) {
$this->state->stack_of_open_elements->pop();
return true;
}

$walker->next();
$current_node_parent = $walker->current();
if ( 'OPTION' === $current_node->node_name && 'OPTGROUP' === $current_node_parent->node_name ) {
$this->state->stack_of_open_elements->pop();
$this->state->stack_of_open_elements->pop();
return true;
}
return $this->step();

/*
* > An end tag whose tag name is "option"
*/
case '-OPTION':
$current_node = $this->state->stack_of_open_elements->current_node();
if ( $current_node && 'OPTION' === $current_node->node_name ) {
$this->state->stack_of_open_elements->pop();
return true;
}
return $this->step();

/*
* > An end tag whose tag name is "select"
* > A start tag whose tag name is "select"
Copy link
Member

Choose a reason for hiding this comment

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

It's worth it IMO to add the quote from the spec, the "Note"

/*
 * > It just gets treated like an end tag.
 */

*/
case '-SELECT':
case '+SELECT':
if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) {
return $this->step();
Copy link
Member

Choose a reason for hiding this comment

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

Comment: Ignore the token

}
$this->state->stack_of_open_elements->pop_until( 'SELECT' );
$this->state->stack_of_open_elements->pop();
Copy link
Member

Choose a reason for hiding this comment

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

this is superfluous, because pop_until() pops the requested node.

Pops nodes off of the stack of open elements until one with the given tag name has been popped.

$this->reset_insertion_mode();
return true;
}

$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "Cannot process {$tag_name} element." );
}

/*
* Internal helpers
*/
Expand Down Expand Up @@ -2036,6 +2192,7 @@ private function close_a_p_element() {
* Closes elements that have implied end tags.
*
* @since 6.4.0
* @since 6.7.0 Support "option" and "optgroup".
*
* @see https://html.spec.whatwg.org/#generate-implied-end-tags
*
Expand All @@ -2046,6 +2203,8 @@ private function generate_implied_end_tags( $except_for_this_element = null ) {
'DD',
'DT',
'LI',
'OPTGROUP',
'OPTION',
'P',
);

Expand Down Expand Up @@ -2074,6 +2233,8 @@ private function generate_implied_end_tags_thoroughly() {
'DD',
'DT',
'LI',
'OPTGROUP',
'OPTION',
'P',
);

Expand Down
3 changes: 0 additions & 3 deletions tests/phpunit/tests/html-api/wpHtmlProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -406,16 +406,13 @@ public static function data_unsupported_special_in_body_tags() {
'NOFRAMES' => array( 'NOFRAMES' ),
'NOSCRIPT' => array( 'NOSCRIPT' ),
'OBJECT' => array( 'OBJECT' ),
'OPTGROUP' => array( 'OPTGROUP' ),
'OPTION' => array( 'OPTION' ),
'PLAINTEXT' => array( 'PLAINTEXT' ),
'RB' => array( 'RB' ),
'RP' => array( 'RP' ),
'RT' => array( 'RT' ),
'RTC' => array( 'RTC' ),
'SARCASM' => array( 'SARCASM' ),
'SCRIPT' => array( 'SCRIPT' ),
'SELECT' => array( 'SELECT' ),
'STYLE' => array( 'STYLE' ),
'SVG' => array( 'SVG' ),
'TABLE' => array( 'TABLE' ),
Expand Down
3 changes: 0 additions & 3 deletions tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,12 @@ public static function data_unsupported_elements() {
'NOFRAMES', // Neutralized.
'NOSCRIPT',
'OBJECT',
'OPTGROUP',
'OPTION',
'PLAINTEXT', // Neutralized.
'RB', // Neutralized.
'RP',
'RT',
'RTC', // Neutralized.
'SCRIPT',
'SELECT',
'STYLE',
'SVG',
'TABLE',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ private function ensure_support_is_added_everywhere( $tag_name ) {
* @covers WP_HTML_Processor::generate_implied_end_tags
*/
public function test_generate_implied_end_tags_needs_support() {
$this->ensure_support_is_added_everywhere( 'OPTGROUP' );
$this->ensure_support_is_added_everywhere( 'OPTION' );
$this->ensure_support_is_added_everywhere( 'RB' );
$this->ensure_support_is_added_everywhere( 'RP' );
$this->ensure_support_is_added_everywhere( 'RT' );
Expand All @@ -79,8 +77,6 @@ public function test_generate_implied_end_tags_needs_support() {
public function test_generate_implied_end_tags_thoroughly_needs_support() {
$this->ensure_support_is_added_everywhere( 'CAPTION' );
$this->ensure_support_is_added_everywhere( 'COLGROUP' );
$this->ensure_support_is_added_everywhere( 'OPTGROUP' );
$this->ensure_support_is_added_everywhere( 'OPTION' );
$this->ensure_support_is_added_everywhere( 'RB' );
$this->ensure_support_is_added_everywhere( 'RP' );
$this->ensure_support_is_added_everywhere( 'RT' );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,5 @@ public function test_has_element_in_select_scope_needs_support() {
* FOREIGNOBJECT, DESC, TITLE.
*/
$this->ensure_support_is_added_everywhere( 'SVG' );

// These elements are specific to SELECT scope.
$this->ensure_support_is_added_everywhere( 'OPTGROUP' );
$this->ensure_support_is_added_everywhere( 'OPTION' );
}
}
Loading