-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Changes from 20 commits
b867b58
9245930
267a27b
b0475fb
1944b66
8d4b4df
1404119
dc0bbb9
9d99844
3085405
e095c62
4026493
76c899a
52fd8e5
6a10eb0
8b3a9e4
87c6596
d464330
ddbd676
f6d686d
783cef6
3222275
eb126e8
f2e1d03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -144,6 +144,28 @@ public function current_node() { | |
return $current_node ? $current_node : null; | ||
} | ||
|
||
/** | ||
* Checks if the node at the top of the stack matches provided node name. | ||
* | ||
* @example | ||
* // Is the current node a text node: | ||
* $stack->current_node_is( '#text' ); | ||
* | ||
* // Is the current node a DIV element: | ||
* $stack->current_node_is( 'DIV' ); | ||
* | ||
* @since 6.7.0 | ||
* | ||
* @param string $node_name The node name to match. Provide a tag name for tags or a | ||
* token name for other types of tokens. | ||
* @return bool True if there are nodes on the stack and the top node has | ||
* a matching node_name. | ||
*/ | ||
public function current_node_is( string $node_name ): bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is from #6968. |
||
$current_node = end( $this->stack ); | ||
return $current_node && $current_node->node_name === $node_name; | ||
} | ||
|
||
/** | ||
* Returns whether an element is in a specific scope. | ||
* | ||
|
@@ -269,19 +291,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; | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
sirreal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* - 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. | ||
|
@@ -757,6 +757,12 @@ 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_HEAD: | ||
return $this->step_in_head(); | ||
|
||
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." ); | ||
|
@@ -1336,6 +1342,52 @@ 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; | ||
} | ||
|
||
/* | ||
|
@@ -1378,16 +1430,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': | ||
|
@@ -1448,6 +1497,184 @@ private function step_in_body() { | |
} | ||
} | ||
|
||
/** | ||
* Parses next element in the 'in head' insertion mode. | ||
* | ||
* this internal function performs the 'in select' insertion mode | ||
* logic for the generalized wp_html_processor::step() function. | ||
* | ||
* @since 6.7.0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can note that this is a stub. |
||
* | ||
* @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_head() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should stub out the rest of these |
||
$this->last_error = self::ERROR_UNSUPPORTED; | ||
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." ); | ||
} | ||
|
||
/** | ||
* 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() { | ||
$token_name = $this->get_token_name(); | ||
$token_type = $this->get_token_type(); | ||
$op_sigil = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : ''; | ||
$op = "{$op_sigil}{$token_name}"; | ||
|
||
switch ( $op ) { | ||
sirreal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/* | ||
* > Any other character token | ||
*/ | ||
case '#text': | ||
$this->insert_html_element( $this->state->current_token ); | ||
return true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should perform the same check as we do in IN BODY for null text, as we don't really want to create a new node or pause just for them. that is, if the text node comprises only null bytes, ignore the token. note that this rule only applies to null bytes directly. it does not apply if the null byte is encoded as a character reference. in that case, the character references |
||
|
||
/* | ||
* > A comment token | ||
*/ | ||
case '#comment': | ||
case '#funky-comment': | ||
case '#presumptuous-tag': | ||
$this->insert_html_element( $this->state->current_token ); | ||
return true; | ||
|
||
/* | ||
* > A DOCTYPE token | ||
*/ | ||
case 'html': | ||
// Parse error. Ignore the token. | ||
return $this->step(); | ||
|
||
/* | ||
* > A start tag whose tag name is "html" | ||
*/ | ||
case '+HTML': | ||
return $this->step_in_body(); | ||
|
||
/* | ||
* > A start tag whose tag name is "option" | ||
*/ | ||
case '+OPTION': | ||
if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { | ||
$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" | ||
* | ||
* These rules are identical except for the treatment of the self-closing flag and | ||
* the subsequent pop of the HR void element, all of which is handled elsewhere in the processor. | ||
*/ | ||
case '+OPTGROUP': | ||
case '+HR': | ||
if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { | ||
$this->state->stack_of_open_elements->pop(); | ||
} | ||
|
||
if ( $this->state->stack_of_open_elements->current_node_is( 'OPTGROUP' ) ) { | ||
$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': | ||
$current_node = $this->state->stack_of_open_elements->current_node(); | ||
if ( $current_node && 'OPTION' === $current_node->node_name ) { | ||
foreach ( $this->state->stack_of_open_elements->walk_up( $current_node ) as $parent ) { | ||
break; | ||
} | ||
if ( $parent && 'OPTGROUP' === $parent->node_name ) { | ||
$this->state->stack_of_open_elements->pop(); | ||
} | ||
} | ||
|
||
if ( $this->state->stack_of_open_elements->current_node_is( 'OPTGROUP' ) ) { | ||
$this->state->stack_of_open_elements->pop(); | ||
return true; | ||
} | ||
// Parse error: ignore the token. | ||
return $this->step(); | ||
|
||
/* | ||
* > An end tag whose tag name is "option" | ||
*/ | ||
case '-OPTION': | ||
if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { | ||
$this->state->stack_of_open_elements->pop(); | ||
return true; | ||
} | ||
// Parse error: ignore the token. | ||
return $this->step(); | ||
|
||
/* | ||
* > An end tag whose tag name is "select" | ||
* > A start tag whose tag name is "select" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is superfluous, because
|
||
$this->reset_insertion_mode(); | ||
return true; | ||
|
||
/* | ||
* > A start tag whose tag name is one of: "input", "keygen", "textarea" | ||
*/ | ||
case '+INPUT': | ||
case '+KEYGEN': | ||
case '+TEXTAREA': | ||
// Parse error. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This look like it's describing the next line, the |
||
if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) { | ||
return $this->step(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment: Ignore token. |
||
} | ||
$this->state->stack_of_open_elements->pop_until( 'SELECT' ); | ||
$this->reset_insertion_mode(); | ||
return $this->step( self::REPROCESS_CURRENT_NODE ); | ||
|
||
/* | ||
* > A start tag whose tag name is one of: "script", "template" | ||
* > An end tag whose tag name is "template" | ||
*/ | ||
case '+SCRIPT': | ||
case '+TEMPLATE': | ||
case '-TEMPLATE': | ||
return $this->step_in_head(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment: Process the token using the rules for the "in head" insertion mode. |
||
} | ||
|
||
/* | ||
* > Anything else | ||
* > Parse error: ignore the token. | ||
*/ | ||
return $this->step(); | ||
} | ||
|
||
/* | ||
* Internal helpers | ||
*/ | ||
|
@@ -2036,6 +2263,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 | ||
* | ||
|
@@ -2046,6 +2274,8 @@ private function generate_implied_end_tags( $except_for_this_element = null ) { | |
'DD', | ||
'DT', | ||
'LI', | ||
'OPTGROUP', | ||
'OPTION', | ||
'P', | ||
); | ||
|
||
|
@@ -2074,6 +2304,8 @@ private function generate_implied_end_tags_thoroughly() { | |
'DD', | ||
'DT', | ||
'LI', | ||
'OPTGROUP', | ||
'OPTION', | ||
'P', | ||
); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that this style of example only comes over from the Gutenberg end, and to play well with the WordPress documentation we should follow this pattern
That is:
Example:
, a blank line, and then four spaces to indent the code. This works well with various IDE support thankfully.