Skip to content

Commit 9a7f64e

Browse files
committed
HTML API: Add support for list elements.
1 parent 32dd59b commit 9a7f64e

7 files changed

+272
-46
lines changed

phpcs.xml.dist

+10-1
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@
224224
#############################################################################
225225
SELECTIVE EXCLUSIONS
226226
Exclude specific files for specific sniffs and/or exclude sub-groups in sniffs.
227-
227+
228228
These exclusions are listed ordered by alphabetic sniff name.
229229
#############################################################################
230230
-->
@@ -250,6 +250,15 @@
250250
<exclude-pattern>/wp-tests-config-sample\.php</exclude-pattern>
251251
</rule>
252252

253+
<!-- Exclude forbidding goto in the HTML Processor, which mimics algorithms that are written
254+
this way in the HTML specification, and these particular algorithms are complex and
255+
highly imperative. Avoiding the goto introduces a number of risks that could make it
256+
more difficult to maintain the relationship to the standard, lead to subtle differences
257+
in the parsing, and distance the code from its standard. -->
258+
<rule ref="Generic.PHP.DiscourageGoto.Found">
259+
<exclude-pattern>/wp-includes/html-api/class-wp-html-processor\.php</exclude-pattern>
260+
</rule>
261+
253262
<!-- Exclude sample config from modernization to prevent breaking CI workflows based on WP-CLI scaffold.
254263
See: https://core.trac.wordpress.org/ticket/48082#comment:16 -->
255264
<rule ref="Modernize.FunctionCalls.Dirname.FileConstant">

src/wp-includes/html-api/class-wp-html-open-elements.php

+36-9
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public function current_node() {
106106
*
107107
* @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope
108108
*
109-
* @param string $tag_name Name of tag check.
109+
* @param string $tag_name Name of tag check, or the class constant HEADING_ELEMENTS to specify H1-H6.
110110
* @param string[] $termination_list List of elements that terminate the search.
111111
* @return bool Whether the element was found in a specific scope.
112112
*/
@@ -169,15 +169,18 @@ public function has_element_in_scope( $tag_name ) {
169169
*
170170
* @see https://html.spec.whatwg.org/#has-an-element-in-list-item-scope
171171
*
172-
* @throws WP_HTML_Unsupported_Exception Always until this function is implemented.
173-
*
174172
* @param string $tag_name Name of tag to check.
175173
* @return bool Whether given element is in scope.
176174
*/
177175
public function has_element_in_list_item_scope( $tag_name ) {
178-
throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on list item scope.' );
179-
180-
return false; // The linter requires this unreachable code until the function is implemented and can return.
176+
return $this->has_element_in_specific_scope(
177+
$tag_name,
178+
array(
179+
// There are more elements that belong here which aren't currently supported.
180+
'OL',
181+
'UL',
182+
)
183+
);
181184
}
182185

183186
/**
@@ -270,7 +273,8 @@ public function pop() {
270273
*
271274
* @see WP_HTML_Open_Elements::pop
272275
*
273-
* @param string $tag_name Name of tag that needs to be popped off of the stack of open elements.
276+
* @param string $tag_name Name of tag that needs to be popped off of the stack of open elements,
277+
* or the class constant HEADING_ELEMENTS to specify any of H1-H6.
274278
* @return bool Whether a tag of the given name was found and popped off of the stack of open elements.
275279
*/
276280
public function pop_until( $tag_name ) {
@@ -375,10 +379,22 @@ public function walk_down() {
375379
* see WP_HTML_Open_Elements::walk_down().
376380
*
377381
* @since 6.4.0
382+
* @since 6.5.0 Accepts $above_this_node to start traversal above a given node, if it exists.
383+
*
384+
* @param ?WP_HTML_Token $above_this_node Start traversing above this node, if provided and if the node exists.
378385
*/
379-
public function walk_up() {
386+
public function walk_up( $above_this_node = null ) {
387+
$has_found_node = null === $above_this_node;
388+
380389
for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) {
381-
yield $this->stack[ $i ];
390+
$node = $this->stack[ $i ];
391+
392+
if ( ! $has_found_node ) {
393+
$has_found_node = $node === $above_this_node;
394+
continue;
395+
}
396+
397+
yield $node;
382398
}
383399
}
384400

@@ -443,4 +459,15 @@ public function after_element_pop( $item ) {
443459
break;
444460
}
445461
}
462+
463+
/**
464+
* Represents the collection of H1-H6 elements.
465+
*
466+
* @since 6.5.0
467+
*
468+
* @see has_element_in_scope()
469+
*
470+
* @var string
471+
*/
472+
const HEADING_ELEMENTS = 'heading-elements';
446473
}

src/wp-includes/html-api/class-wp-html-processor.php

+101-1
Original file line numberDiff line numberDiff line change
@@ -644,10 +644,12 @@ private function step_in_body() {
644644
case '+MAIN':
645645
case '+MENU':
646646
case '+NAV':
647+
case '+OL':
647648
case '+P':
648649
case '+SEARCH':
649650
case '+SECTION':
650651
case '+SUMMARY':
652+
case '+UL':
651653
if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
652654
$this->close_a_p_element();
653655
}
@@ -681,9 +683,11 @@ private function step_in_body() {
681683
case '-MAIN':
682684
case '-MENU':
683685
case '-NAV':
686+
case '-OL':
684687
case '-SEARCH':
685688
case '-SECTION':
686689
case '-SUMMARY':
690+
case '-UL':
687691
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $tag_name ) ) {
688692
// @TODO: Report parse error.
689693
// Ignore the token.
@@ -733,7 +737,11 @@ private function step_in_body() {
733737
case '-H4':
734738
case '-H5':
735739
case '-H6':
736-
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( '(internal: H1 through H6 - do not use)' ) ) {
740+
if (
741+
! $this->state->stack_of_open_elements->has_element_in_scope(
742+
'(internal: H1 through H6 - do not use)'
743+
)
744+
) {
737745
/*
738746
* This is a parse error; ignore the token.
739747
*
@@ -751,6 +759,92 @@ private function step_in_body() {
751759
$this->state->stack_of_open_elements->pop_until( '(internal: H1 through H6 - do not use)' );
752760
return true;
753761

762+
/*
763+
* > A start tag whose tag name is "li"
764+
* > A start tag whose tag name is one of: "dd", "dt"
765+
*/
766+
case '+DD':
767+
case '+DT':
768+
case '+LI':
769+
$this->state->frameset_ok = false;
770+
$node = $this->state->stack_of_open_elements->current_node();
771+
772+
in_body_list_loop:
773+
if ( $tag_name === $node->node_name ) {
774+
$this->generate_implied_end_tags();
775+
if ( $tag_name !== $this->state->stack_of_open_elements->current_node()->node_name ) {
776+
// @TODO: Indicate a parse error once it's possible. This error does not impact the logic here.
777+
}
778+
779+
$this->state->stack_of_open_elements->pop_until( $tag_name );
780+
goto in_body_list_done;
781+
}
782+
783+
if (
784+
'ADDRESS' !== $node->node_name &&
785+
'DIV' !== $node->node_name &&
786+
'P' !== $node->node_name &&
787+
$this->is_special( $node->node_name )
788+
) {
789+
/*
790+
* > If node is in the special category, but is not an address, div,
791+
* > or p element, then jump to the step labeled done below.
792+
*/
793+
goto in_body_list_done;
794+
} else {
795+
/*
796+
* > Otherwise, set node to the previous entry in the stack of open elements
797+
* > and return to the step labeled loop.
798+
*/
799+
foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $item ) {
800+
$node = $item;
801+
break;
802+
}
803+
goto in_body_list_loop;
804+
}
805+
806+
in_body_list_done:
807+
if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
808+
$this->close_a_p_element();
809+
}
810+
811+
$this->insert_html_element( $this->state->current_token );
812+
return true;
813+
814+
/*
815+
* > An end tag whose tag name is "li"
816+
* > An end tag whose tag name is one of: "dd", "dt"
817+
*/
818+
case '-DD':
819+
case '-DT':
820+
case '-LI':
821+
if (
822+
(
823+
'LI' === $tag_name &&
824+
! $this->state->stack_of_open_elements->has_element_in_list_item_scope( 'LI' )
825+
) ||
826+
(
827+
'LI' !== $tag_name &&
828+
! $this->state->stack_of_open_elements->has_element_in_scope( $tag_name )
829+
)
830+
) {
831+
/*
832+
* This is a parse error, ignore the token.
833+
*
834+
* @TODO: Indicate a parse error once it's possible.
835+
*/
836+
return $this->step();
837+
}
838+
839+
$this->generate_implied_end_tags( $tag_name );
840+
841+
if ( $tag_name !== $this->state->stack_of_open_elements->current_node()->node_name ) {
842+
// @TODO: Indicate a parse error once it's possible. This error does not impact the logic here.
843+
}
844+
845+
$this->state->stack_of_open_elements->pop_until( $tag_name );
846+
return true;
847+
754848
/*
755849
* > An end tag whose tag name is "p"
756850
*/
@@ -1128,6 +1222,9 @@ private function close_a_p_element() {
11281222
*/
11291223
private function generate_implied_end_tags( $except_for_this_element = null ) {
11301224
$elements_with_implied_end_tags = array(
1225+
'DD',
1226+
'DT',
1227+
'LI',
11311228
'P',
11321229
);
11331230

@@ -1153,6 +1250,9 @@ private function generate_implied_end_tags( $except_for_this_element = null ) {
11531250
*/
11541251
private function generate_implied_end_tags_thoroughly() {
11551252
$elements_with_implied_end_tags = array(
1253+
'DD',
1254+
'DT',
1255+
'LI',
11561256
'P',
11571257
);
11581258

0 commit comments

Comments
 (0)