Skip to content

Commit 53e3df3

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

7 files changed

+253
-42
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

+23-7
Original file line numberDiff line numberDiff line change
@@ -166,18 +166,22 @@ public function has_element_in_scope( $tag_name ) {
166166
* Returns whether a particular element is in list item scope.
167167
*
168168
* @since 6.4.0
169+
* @since 6.5.0 Implemented: no longer throws on every invocation.
169170
*
170171
* @see https://html.spec.whatwg.org/#has-an-element-in-list-item-scope
171172
*
172-
* @throws WP_HTML_Unsupported_Exception Always until this function is implemented.
173-
*
174173
* @param string $tag_name Name of tag to check.
175174
* @return bool Whether given element is in scope.
176175
*/
177176
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.
177+
return $this->has_element_in_specific_scope(
178+
$tag_name,
179+
array(
180+
// There are more elements that belong here which aren't currently supported.
181+
'OL',
182+
'UL',
183+
)
184+
);
181185
}
182186

183187
/**
@@ -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

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

+96
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.
@@ -751,6 +755,92 @@ private function step_in_body() {
751755
$this->state->stack_of_open_elements->pop_until( '(internal: H1 through H6 - do not use)' );
752756
return true;
753757

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

@@ -1153,6 +1246,9 @@ private function generate_implied_end_tags( $except_for_this_element = null ) {
11531246
*/
11541247
private function generate_implied_end_tags_thoroughly() {
11551248
$elements_with_implied_end_tags = array(
1249+
'DD',
1250+
'DT',
1251+
'LI',
11561252
'P',
11571253
);
11581254

tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php

+23-21
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ public function data_single_tag_of_supported_elements() {
4343
'B',
4444
'BIG',
4545
'BUTTON',
46-
'CENTER', // Neutralized
46+
'CENTER', // Neutralized.
4747
'CODE',
48+
'DD',
4849
'DETAILS',
4950
'DIALOG',
5051
'DIR',
5152
'DIV',
5253
'DL',
54+
'DT',
5355
'EM',
5456
'FIELDSET',
5557
'FIGCAPTION',
@@ -66,9 +68,11 @@ public function data_single_tag_of_supported_elements() {
6668
'HGROUP',
6769
'I',
6870
'IMG',
71+
'LI',
6972
'MAIN',
7073
'MENU',
7174
'NAV',
75+
'OL',
7276
'P',
7377
'SEARCH',
7478
'SECTION',
@@ -79,6 +83,7 @@ public function data_single_tag_of_supported_elements() {
7983
'SUMMARY',
8084
'TT',
8185
'U',
86+
'UL',
8287
);
8388

8489
$data = array();
@@ -122,15 +127,15 @@ public function test_fails_when_encountering_unsupported_tag( $html ) {
122127
public function data_unsupported_elements() {
123128
$unsupported_elements = array(
124129
'ABBR',
125-
'ACRONYM', // Neutralized
126-
'APPLET', // Deprecated
130+
'ACRONYM', // Neutralized.
131+
'APPLET', // Deprecated.
127132
'AREA',
128133
'AUDIO',
129134
'BASE',
130135
'BDI',
131136
'BDO',
132137
'BGSOUND', // Deprecated; self-closing if self-closing flag provided, otherwise normal.
133-
'BLINK', // Deprecated
138+
'BLINK', // Deprecated.
134139
'BODY',
135140
'BR',
136141
'CANVAS',
@@ -140,10 +145,8 @@ public function data_unsupported_elements() {
140145
'COLGROUP',
141146
'DATA',
142147
'DATALIST',
143-
'DD',
144148
'DEL',
145149
'DEFN',
146-
'DT',
147150
'EMBED',
148151
'FORM',
149152
'FRAME',
@@ -154,47 +157,45 @@ public function data_unsupported_elements() {
154157
'IFRAME',
155158
'INPUT',
156159
'INS',
157-
'ISINDEX', // Deprecated
160+
'ISINDEX', // Deprecated.
158161
'KBD',
159-
'KEYGEN', // Deprecated; void
162+
'KEYGEN', // Deprecated; void.
160163
'LABEL',
161164
'LEGEND',
162-
'LI',
163165
'LINK',
164166
'LISTING', // Deprecated, use PRE instead.
165167
'MAP',
166168
'MARK',
167-
'MARQUEE', // Deprecated
169+
'MARQUEE', // Deprecated.
168170
'MATH',
169171
'META',
170172
'METER',
171-
'MULTICOL', // Deprecated
172-
'NEXTID', // Deprecated
173-
'NOBR', // Neutralized
174-
'NOEMBED', // Neutralized
175-
'NOFRAMES', // Neutralized
173+
'MULTICOL', // Deprecated.
174+
'NEXTID', // Deprecated.
175+
'NOBR', // Neutralized.
176+
'NOEMBED', // Neutralized.
177+
'NOFRAMES', // Neutralized.
176178
'NOSCRIPT',
177179
'OBJECT',
178-
'OL',
179180
'OPTGROUP',
180181
'OPTION',
181182
'OUTPUT',
182183
'PICTURE',
183-
'PLAINTEXT', // Neutralized
184+
'PLAINTEXT', // Neutralized.
184185
'PRE',
185186
'PROGRESS',
186187
'Q',
187-
'RB', // Neutralized
188+
'RB', // Neutralized.
188189
'RP',
189190
'RT',
190-
'RTC', // Neutralized
191+
'RTC', // Neutralized.
191192
'RUBY',
192193
'SAMP',
193194
'SCRIPT',
194195
'SELECT',
195196
'SLOT',
196197
'SOURCE',
197-
'SPACER', // Deprecated
198+
'SPACER', // Deprecated.
198199
'STYLE',
199200
'SUB',
200201
'SUP',
@@ -211,7 +212,6 @@ public function data_unsupported_elements() {
211212
'TITLE',
212213
'TR',
213214
'TRACK',
214-
'UL',
215215
'VAR',
216216
'VIDEO',
217217
'WBR',
@@ -360,6 +360,8 @@ public function data_html_target_with_breadcrumbs() {
360360
'H4 inside H2' => array( '<h2><span>Major<h4 target>Minor</h3></span>', array( 'HTML', 'BODY', 'H2', 'SPAN', 'H4' ), 1 ),
361361
'H5 after unclosed H4 inside H2' => array( '<h2><span>Major<h4>Minor</span></h3><h5 target>', array( 'HTML', 'BODY', 'H2', 'SPAN', 'H5' ), 1 ),
362362
'H5 after H4 inside H2' => array( '<h2><span>Major<h4>Minor</h4></span></h3><h5 target>', array( 'HTML', 'BODY', 'H5' ), 1 ),
363+
'LI after unclosed LI' => array( '<li>one<li>two<li target>three', array( 'HTML', 'BODY', 'LI' ), 3 ),
364+
'LI in UL in LI' => array( '<ul><li>one<ul><li target>two', array( 'HTML', 'BODY', 'UL', 'LI', 'UL', 'LI' ), 1 ),
363365
);
364366
}
365367

0 commit comments

Comments
 (0)