mirror of
git://develop.git.wordpress.org/
synced 2025-01-16 20:38:35 +01:00
HTML API: Add support for list elements.
Adds support for the following HTML elements to the HTML Processor: - LI, OL, UL. - DD, DL, DT. Previously, these elements were not supported and the HTML Processor would bail when encountering them. With this patch it will proceed to parse an HTML document when encountering those tags as long as other normal conditions don't cause it to bail (such as complicated format reconstruction). Props audrasjb, jonsurrell, bernhard-reiter. Fixes #60215. git-svn-id: https://develop.svn.wordpress.org/trunk@57264 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
parent
eff1a3d24b
commit
7c1c48c141
@ -250,6 +250,15 @@
|
||||
<exclude-pattern>/wp-tests-config-sample\.php</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
<!-- Exclude forbidding goto in the HTML Processor, which mimics algorithms that are written
|
||||
this way in the HTML specification, and these particular algorithms are complex and
|
||||
highly imperative. Avoiding the goto introduces a number of risks that could make it
|
||||
more difficult to maintain the relationship to the standard, lead to subtle differences
|
||||
in the parsing, and distance the code from its standard. -->
|
||||
<rule ref="Generic.PHP.DiscourageGoto.Found">
|
||||
<exclude-pattern>/wp-includes/html-api/class-wp-html-processor\.php</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
<!-- Exclude sample config from modernization to prevent breaking CI workflows based on WP-CLI scaffold.
|
||||
See: https://core.trac.wordpress.org/ticket/48082#comment:16 -->
|
||||
<rule ref="Modernize.FunctionCalls.Dirname.FileConstant">
|
||||
|
@ -129,7 +129,7 @@ class WP_HTML_Open_Elements {
|
||||
}
|
||||
|
||||
if ( in_array( $node->node_name, $termination_list, true ) ) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,18 +166,22 @@ class WP_HTML_Open_Elements {
|
||||
* Returns whether a particular element is in list item scope.
|
||||
*
|
||||
* @since 6.4.0
|
||||
* @since 6.5.0 Implemented: no longer throws on every invocation.
|
||||
*
|
||||
* @see https://html.spec.whatwg.org/#has-an-element-in-list-item-scope
|
||||
*
|
||||
* @throws WP_HTML_Unsupported_Exception Always until this function is implemented.
|
||||
*
|
||||
* @param string $tag_name Name of tag to check.
|
||||
* @return bool Whether given element is in scope.
|
||||
*/
|
||||
public function has_element_in_list_item_scope( $tag_name ) {
|
||||
throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on list item scope.' );
|
||||
|
||||
return false; // The linter requires this unreachable code until the function is implemented and can return.
|
||||
return $this->has_element_in_specific_scope(
|
||||
$tag_name,
|
||||
array(
|
||||
// There are more elements that belong here which aren't currently supported.
|
||||
'OL',
|
||||
'UL',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -375,10 +379,22 @@ class WP_HTML_Open_Elements {
|
||||
* see WP_HTML_Open_Elements::walk_down().
|
||||
*
|
||||
* @since 6.4.0
|
||||
* @since 6.5.0 Accepts $above_this_node to start traversal above a given node, if it exists.
|
||||
*
|
||||
* @param ?WP_HTML_Token $above_this_node Start traversing above this node, if provided and if the node exists.
|
||||
*/
|
||||
public function walk_up() {
|
||||
public function walk_up( $above_this_node = null ) {
|
||||
$has_found_node = null === $above_this_node;
|
||||
|
||||
for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) {
|
||||
yield $this->stack[ $i ];
|
||||
$node = $this->stack[ $i ];
|
||||
|
||||
if ( ! $has_found_node ) {
|
||||
$has_found_node = $node === $above_this_node;
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $node;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,7 +105,7 @@
|
||||
* - Formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U.
|
||||
* - Heading elements: H1, H2, H3, H4, H5, H6, HGROUP.
|
||||
* - Links: A.
|
||||
* - Lists: DL.
|
||||
* - Lists: DD, DL, DT, LI, OL, LI.
|
||||
* - Media elements: AUDIO, CANVAS, FIGCAPTION, FIGURE, IMG, MAP, PICTURE, VIDEO.
|
||||
* - Paragraph: P.
|
||||
* - Phrasing elements: ABBR, BDI, BDO, CITE, DATA, DEL, DFN, INS, MARK, OUTPUT, Q, SAMP, SUB, SUP, TIME, VAR.
|
||||
@ -648,10 +648,12 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
|
||||
case '+MAIN':
|
||||
case '+MENU':
|
||||
case '+NAV':
|
||||
case '+OL':
|
||||
case '+P':
|
||||
case '+SEARCH':
|
||||
case '+SECTION':
|
||||
case '+SUMMARY':
|
||||
case '+UL':
|
||||
if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
|
||||
$this->close_a_p_element();
|
||||
}
|
||||
@ -685,9 +687,11 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
|
||||
case '-MAIN':
|
||||
case '-MENU':
|
||||
case '-NAV':
|
||||
case '-OL':
|
||||
case '-SEARCH':
|
||||
case '-SECTION':
|
||||
case '-SUMMARY':
|
||||
case '-UL':
|
||||
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $tag_name ) ) {
|
||||
// @todo Report parse error.
|
||||
// Ignore the token.
|
||||
@ -755,6 +759,109 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
|
||||
$this->state->stack_of_open_elements->pop_until( '(internal: H1 through H6 - do not use)' );
|
||||
return true;
|
||||
|
||||
/*
|
||||
* > A start tag whose tag name is "li"
|
||||
* > A start tag whose tag name is one of: "dd", "dt"
|
||||
*/
|
||||
case '+DD':
|
||||
case '+DT':
|
||||
case '+LI':
|
||||
$this->state->frameset_ok = false;
|
||||
$node = $this->state->stack_of_open_elements->current_node();
|
||||
$is_li = 'LI' === $tag_name;
|
||||
|
||||
in_body_list_loop:
|
||||
/*
|
||||
* The logic for LI and DT/DD is the same except for one point: LI elements _only_
|
||||
* close other LI elements, but a DT or DD element closes _any_ open DT or DD element.
|
||||
*/
|
||||
if ( $is_li ? 'LI' === $node->node_name : ( 'DD' === $node->node_name || 'DT' === $node->node_name ) ) {
|
||||
$node_name = $is_li ? 'LI' : $node->node_name;
|
||||
$this->generate_implied_end_tags( $node_name );
|
||||
if ( $node_name !== $this->state->stack_of_open_elements->current_node()->node_name ) {
|
||||
// @todo Indicate a parse error once it's possible. This error does not impact the logic here.
|
||||
}
|
||||
|
||||
$this->state->stack_of_open_elements->pop_until( $node_name );
|
||||
goto in_body_list_done;
|
||||
}
|
||||
|
||||
if (
|
||||
'ADDRESS' !== $node->node_name &&
|
||||
'DIV' !== $node->node_name &&
|
||||
'P' !== $node->node_name &&
|
||||
$this->is_special( $node->node_name )
|
||||
) {
|
||||
/*
|
||||
* > If node is in the special category, but is not an address, div,
|
||||
* > or p element, then jump to the step labeled done below.
|
||||
*/
|
||||
goto in_body_list_done;
|
||||
} else {
|
||||
/*
|
||||
* > Otherwise, set node to the previous entry in the stack of open elements
|
||||
* > and return to the step labeled loop.
|
||||
*/
|
||||
foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $item ) {
|
||||
$node = $item;
|
||||
break;
|
||||
}
|
||||
goto in_body_list_loop;
|
||||
}
|
||||
|
||||
in_body_list_done:
|
||||
if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
|
||||
$this->close_a_p_element();
|
||||
}
|
||||
|
||||
$this->insert_html_element( $this->state->current_token );
|
||||
return true;
|
||||
|
||||
/*
|
||||
* > An end tag whose tag name is "li"
|
||||
* > An end tag whose tag name is one of: "dd", "dt"
|
||||
*/
|
||||
case '-DD':
|
||||
case '-DT':
|
||||
case '-LI':
|
||||
if (
|
||||
/*
|
||||
* An end tag whose tag name is "li":
|
||||
* If the stack of open elements does not have an li element in list item scope,
|
||||
* then this is a parse error; ignore the token.
|
||||
*/
|
||||
(
|
||||
'LI' === $tag_name &&
|
||||
! $this->state->stack_of_open_elements->has_element_in_list_item_scope( 'LI' )
|
||||
) ||
|
||||
/*
|
||||
* An end tag whose tag name is one of: "dd", "dt":
|
||||
* If the stack of open elements does not have an element in scope that is an
|
||||
* HTML element with the same tag name as that of the token, then this is a
|
||||
* parse error; ignore the token.
|
||||
*/
|
||||
(
|
||||
'LI' !== $tag_name &&
|
||||
! $this->state->stack_of_open_elements->has_element_in_scope( $tag_name )
|
||||
)
|
||||
) {
|
||||
/*
|
||||
* This is a parse error, ignore the token.
|
||||
*
|
||||
* @todo Indicate a parse error once it's possible.
|
||||
*/
|
||||
return $this->step();
|
||||
}
|
||||
|
||||
$this->generate_implied_end_tags( $tag_name );
|
||||
|
||||
if ( $tag_name !== $this->state->stack_of_open_elements->current_node()->node_name ) {
|
||||
// @todo Indicate a parse error once it's possible. This error does not impact the logic here.
|
||||
}
|
||||
|
||||
$this->state->stack_of_open_elements->pop_until( $tag_name );
|
||||
return true;
|
||||
|
||||
/*
|
||||
* > An end tag whose tag name is "p"
|
||||
*/
|
||||
@ -1223,6 +1330,9 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
|
||||
*/
|
||||
private function generate_implied_end_tags( $except_for_this_element = null ) {
|
||||
$elements_with_implied_end_tags = array(
|
||||
'DD',
|
||||
'DT',
|
||||
'LI',
|
||||
'P',
|
||||
);
|
||||
|
||||
@ -1248,6 +1358,9 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
|
||||
*/
|
||||
private function generate_implied_end_tags_thoroughly() {
|
||||
$elements_with_implied_end_tags = array(
|
||||
'DD',
|
||||
'DT',
|
||||
'LI',
|
||||
'P',
|
||||
);
|
||||
|
||||
|
@ -168,8 +168,6 @@ class Tests_HtmlApi_WpHtmlProcessor extends WP_UnitTestCase {
|
||||
'CAPTION' => array( 'CAPTION' ),
|
||||
'COL' => array( 'COL' ),
|
||||
'COLGROUP' => array( 'COLGROUP' ),
|
||||
'DD' => array( 'DD' ),
|
||||
'DT' => array( 'DT' ),
|
||||
'EMBED' => array( 'EMBED' ),
|
||||
'FORM' => array( 'FORM' ),
|
||||
'FRAME' => array( 'FRAME' ),
|
||||
@ -180,7 +178,6 @@ class Tests_HtmlApi_WpHtmlProcessor extends WP_UnitTestCase {
|
||||
'IFRAME' => array( 'IFRAME' ),
|
||||
'INPUT' => array( 'INPUT' ),
|
||||
'KEYGEN' => array( 'KEYGEN' ),
|
||||
'LI' => array( 'LI' ),
|
||||
'LINK' => array( 'LINK' ),
|
||||
'LISTING' => array( 'LISTING' ),
|
||||
'MARQUEE' => array( 'MARQUEE' ),
|
||||
@ -191,7 +188,6 @@ class Tests_HtmlApi_WpHtmlProcessor extends WP_UnitTestCase {
|
||||
'NOFRAMES' => array( 'NOFRAMES' ),
|
||||
'NOSCRIPT' => array( 'NOSCRIPT' ),
|
||||
'OBJECT' => array( 'OBJECT' ),
|
||||
'OL' => array( 'OL' ),
|
||||
'OPTGROUP' => array( 'OPTGROUP' ),
|
||||
'OPTION' => array( 'OPTION' ),
|
||||
'PARAM' => array( 'PARAM' ),
|
||||
@ -218,7 +214,6 @@ class Tests_HtmlApi_WpHtmlProcessor extends WP_UnitTestCase {
|
||||
'TITLE' => array( 'TITLE' ),
|
||||
'TR' => array( 'TR' ),
|
||||
'TRACK' => array( 'TRACK' ),
|
||||
'UL' => array( 'UL' ),
|
||||
'WBR' => array( 'WBR' ),
|
||||
'XMP' => array( 'XMP' ),
|
||||
);
|
||||
|
@ -38,7 +38,7 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
$supported_elements = array(
|
||||
'A',
|
||||
'ABBR',
|
||||
'ACRONYM', // Neutralized
|
||||
'ACRONYM', // Neutralized.
|
||||
'ADDRESS',
|
||||
'ARTICLE',
|
||||
'ASIDE',
|
||||
@ -47,13 +47,14 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
'BDI',
|
||||
'BDO',
|
||||
'BIG',
|
||||
'BLINK', // Deprecated
|
||||
'BLINK', // Deprecated.
|
||||
'BUTTON',
|
||||
'CANVAS',
|
||||
'CENTER', // Neutralized
|
||||
'CENTER', // Neutralized.
|
||||
'CITE',
|
||||
'CODE',
|
||||
'DATA',
|
||||
'DD',
|
||||
'DATALIST',
|
||||
'DFN',
|
||||
'DEL',
|
||||
@ -62,6 +63,7 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
'DIR',
|
||||
'DIV',
|
||||
'DL',
|
||||
'DT',
|
||||
'EM',
|
||||
'FIELDSET',
|
||||
'FIGCAPTION',
|
||||
@ -79,6 +81,7 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
'I',
|
||||
'IMG',
|
||||
'INS',
|
||||
'LI',
|
||||
'ISINDEX', // Deprecated
|
||||
'KBD',
|
||||
'LABEL',
|
||||
@ -91,6 +94,7 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
'MULTICOL', // Deprecated
|
||||
'NAV',
|
||||
'NEXTID', // Deprecated
|
||||
'OL',
|
||||
'OUTPUT',
|
||||
'P',
|
||||
'PICTURE',
|
||||
@ -112,6 +116,7 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
'TIME',
|
||||
'TT',
|
||||
'U',
|
||||
'UL',
|
||||
'VAR',
|
||||
'VIDEO',
|
||||
);
|
||||
@ -156,7 +161,7 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
*/
|
||||
public function data_unsupported_elements() {
|
||||
$unsupported_elements = array(
|
||||
'APPLET', // Deprecated
|
||||
'APPLET', // Deprecated.
|
||||
'AREA',
|
||||
'BASE',
|
||||
'BGSOUND', // Deprecated; self-closing if self-closing flag provided, otherwise normal.
|
||||
@ -165,8 +170,6 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
'CAPTION',
|
||||
'COL',
|
||||
'COLGROUP',
|
||||
'DD',
|
||||
'DT',
|
||||
'EMBED',
|
||||
'FORM',
|
||||
'FRAME',
|
||||
@ -176,27 +179,25 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
'HTML',
|
||||
'IFRAME',
|
||||
'INPUT',
|
||||
'KEYGEN', // Deprecated; void
|
||||
'LI',
|
||||
'KEYGEN', // Deprecated; void.
|
||||
'LINK',
|
||||
'LISTING', // Deprecated, use PRE instead.
|
||||
'MARQUEE', // Deprecated
|
||||
'MARQUEE', // Deprecated.
|
||||
'MATH',
|
||||
'META',
|
||||
'NOBR', // Neutralized
|
||||
'NOEMBED', // Neutralized
|
||||
'NOFRAMES', // Neutralized
|
||||
'NOBR', // Neutralized.
|
||||
'NOEMBED', // Neutralized.
|
||||
'NOFRAMES', // Neutralized.
|
||||
'NOSCRIPT',
|
||||
'OBJECT',
|
||||
'OL',
|
||||
'OPTGROUP',
|
||||
'OPTION',
|
||||
'PLAINTEXT', // Neutralized
|
||||
'PLAINTEXT', // Neutralized.
|
||||
'PRE',
|
||||
'RB', // Neutralized
|
||||
'RB', // Neutralized.
|
||||
'RP',
|
||||
'RT',
|
||||
'RTC', // Neutralized
|
||||
'RTC', // Neutralized.
|
||||
'SCRIPT',
|
||||
'SELECT',
|
||||
'SOURCE',
|
||||
@ -213,7 +214,6 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
'TITLE',
|
||||
'TR',
|
||||
'TRACK',
|
||||
'UL',
|
||||
'WBR',
|
||||
'XMP', // Deprecated, use PRE instead.
|
||||
);
|
||||
@ -348,6 +348,12 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase {
|
||||
),
|
||||
'MAIN inside MAIN inside SPAN' => array( '<span><main><main target>', array( 'HTML', 'BODY', 'SPAN', 'MAIN', 'MAIN' ), 1 ),
|
||||
'MAIN next to unclosed P' => array( '<p><main target>', array( 'HTML', 'BODY', 'MAIN' ), 1 ),
|
||||
'LI after unclosed LI' => array( '<li>one<li>two<li target>three', array( 'HTML', 'BODY', 'LI' ), 3 ),
|
||||
'LI in UL in LI' => array( '<ul><li>one<ul><li target>two', array( 'HTML', 'BODY', 'UL', 'LI', 'UL', 'LI' ), 1 ),
|
||||
'DD and DT mutually close, LI self-closes (dt 2)' => array( '<dd><dd><dt><dt target><dd><li><li>', array( 'HTML', 'BODY', 'DT' ), 2 ),
|
||||
'DD and DT mutually close, LI self-closes (dd 3)' => array( '<dd><dd><dt><dt><dd target><li><li>', array( 'HTML', 'BODY', 'DD' ), 3 ),
|
||||
'DD and DT mutually close, LI self-closes (li 1)' => array( '<dd><dd><dt><dt><dd><li target><li>', array( 'HTML', 'BODY', 'DD', 'LI' ), 1 ),
|
||||
'DD and DT mutually close, LI self-closes (li 2)' => array( '<dd><dd><dt><dt><dd><li><li target>', array( 'HTML', 'BODY', 'DD', 'LI' ), 2 ),
|
||||
|
||||
// H1 - H6 close out _any_ H1 - H6 when encountering _any_ of H1 - H6, making this section surprising.
|
||||
'EM inside H3 after unclosed P' => array( '<p><h3><em target>Important Message</em></h3>', array( 'HTML', 'BODY', 'H3', 'EM' ), 1 ),
|
||||
|
@ -224,6 +224,109 @@ class Tests_HtmlApi_WpHtmlProcessorSemanticRules extends WP_UnitTestCase {
|
||||
$this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for third button.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that H1 through H6 elements close an open P element.
|
||||
*
|
||||
* @ticket 60215
|
||||
*
|
||||
* @dataProvider data_heading_elements
|
||||
*
|
||||
* @param string $tag_name Name of H1 - H6 element under test.
|
||||
*/
|
||||
public function test_in_body_heading_element_closes_open_p_tag( $tag_name ) {
|
||||
$processor = WP_HTML_Processor::create_fragment(
|
||||
"<p>Open<{$tag_name}>Closed P</{$tag_name}><img></p>"
|
||||
);
|
||||
|
||||
$processor->next_tag( $tag_name );
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', $tag_name ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"Expected {$tag_name} to be a direct child of the BODY, having closed the open P element."
|
||||
);
|
||||
|
||||
$processor->next_tag( 'IMG' );
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'IMG' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
'Expected IMG to be a direct child of BODY, having closed the open P element.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider.
|
||||
*
|
||||
* @return array[].
|
||||
*/
|
||||
public function data_heading_elements() {
|
||||
return array(
|
||||
'H1' => array( 'H1' ),
|
||||
'H2' => array( 'H2' ),
|
||||
'H3' => array( 'H3' ),
|
||||
'H4' => array( 'H4' ),
|
||||
'H5' => array( 'H5' ),
|
||||
'H6' => array( 'H5' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that H1 through H6 elements close an open H1 through H6 element.
|
||||
*
|
||||
* @ticket 60215
|
||||
*
|
||||
* @dataProvider data_heading_combinations
|
||||
*
|
||||
* @param string $first_heading H1 - H6 element appearing (unclosed) before the second.
|
||||
* @param string $second_heading H1 - H6 element appearing after the first.
|
||||
*/
|
||||
public function test_in_body_heading_element_closes_other_heading_elements( $first_heading, $second_heading ) {
|
||||
$processor = WP_HTML_Processor::create_fragment(
|
||||
"<div><{$first_heading} first> then <{$second_heading} second> and end </{$second_heading}><img></{$first_heading}></div>"
|
||||
);
|
||||
|
||||
while ( $processor->next_tag() && null === $processor->get_attribute( 'second' ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'second' ),
|
||||
"Failed to find expected {$second_heading} tag."
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DIV', $second_heading ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"Expected {$second_heading} to be a direct child of the DIV, having closed the open {$first_heading} element."
|
||||
);
|
||||
|
||||
$processor->next_tag( 'IMG' );
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DIV', 'IMG' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"Expected IMG to be a direct child of DIV, having closed the open {$first_heading} element."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function data_heading_combinations() {
|
||||
$headings = array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' );
|
||||
|
||||
$combinations = array();
|
||||
|
||||
// Create all unique pairs of H1 - H6 elements.
|
||||
foreach ( $headings as $first_tag ) {
|
||||
foreach ( $headings as $second_tag ) {
|
||||
$combinations[ "{$first_tag} then {$second_tag}" ] = array( $first_tag, $second_tag );
|
||||
}
|
||||
}
|
||||
|
||||
return $combinations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when "in body" and encountering "any other end tag"
|
||||
* that the HTML processor ignores the end tag if there's a special
|
||||
|
@ -0,0 +1,431 @@
|
||||
<?php
|
||||
/**
|
||||
* Unit tests covering WP_HTML_Processor compliance with HTML5 semantic parsing rules
|
||||
* for the list elements, including DD, DL, DT, LI, OL, and UL.
|
||||
*
|
||||
* @package WordPress
|
||||
* @subpackage HTML-API
|
||||
*
|
||||
* @since 6.5.0
|
||||
*
|
||||
* @group html-api
|
||||
*
|
||||
* @coversDefaultClass WP_HTML_Processor
|
||||
*/
|
||||
class Tests_HtmlApi_WpHtmlProcessorSemanticRulesListElements extends WP_UnitTestCase {
|
||||
/*******************************************************************
|
||||
* RULES FOR "IN BODY" MODE
|
||||
*******************************************************************/
|
||||
|
||||
/**
|
||||
* Ensures that an opening LI element implicitly closes an open LI element.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_li_closes_open_li() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<li><li><li target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'LI' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"LI should have closed open LI, but didn't."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening LI element implicitly closes other open elements with optional closing tags.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_li_generates_implied_end_tags_inside_open_li() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<li><li><div><li target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'LI' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"LI should have closed open LI, but didn't."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that when closing tags with optional tag closers, an opening LI tag doesn't close beyond a special boundary.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_li_generates_implied_end_tags_inside_open_li_but_stopping_at_special_tags() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<li><li><blockquote><li target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'LI', 'BLOCKQUOTE', 'LI' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
'LI should have left the BLOCKQOUTE open, but closed it.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening LI closes an open P in button scope.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_li_in_li_closes_p_in_button_scope() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<li><li><p><button><p><li target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'LI', 'P', 'BUTTON', 'LI' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
'LI should have left the outer P open, but closed it.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening DD closes an open DD element.
|
||||
*
|
||||
* Note that a DD closes an open DD and also an open DT.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_dd_closes_open_dd() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<dd><dd><dd target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DD' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"DD should have closed open DD, but didn't."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening DD closes an open DT element.
|
||||
*
|
||||
* Note that a DD closes an open DD and also an open DT.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_dd_closes_open_dt() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<dt><dt><dd target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DD' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"DD should have closed open DD, but didn't."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening DD implicitly closes open elements with optional closing tags.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_dd_generates_implied_end_tags_inside_open_dd() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<dd><dd><div><dd target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DD' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"DD should have closed open DD, but didn't."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening DD implicitly closes open elements with optional closing tags,
|
||||
* but doesn't close beyond a special boundary.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_dd_generates_implied_end_tags_inside_open_dd_but_stopping_at_special_tags() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<dd><dd><blockquote><dd target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DD', 'BLOCKQUOTE', 'DD' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
'DD should have left the BLOCKQOUTE open, but closed it.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening DD inside a DD closes a P in button scope.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_dd_in_dd_closes_p_in_button_scope() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<dd><dd><p><button><p><dd target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DD', 'P', 'BUTTON', 'DD' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
'DD should have left the outer P open, but closed it.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening DT closes an open DT element.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_dt_closes_open_dt() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<dt><dt><dt target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DT' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"DT should have closed open DT, but didn't."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening DT closes an open DD.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_dt_closes_open_dd() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<dd><dd><dt target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DT' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"DT should have closed open DT, but didn't."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening DT implicitly closes open elements with optional closing tags.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_dt_generates_implied_end_tags_inside_open_dt() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<dt><dt><div><dt target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DT' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
"DT should have closed open DT, but didn't."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening DT implicitly closes open elements with optional closing tags,
|
||||
* but doesn't close beyond a special boundary.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_dt_generates_implied_end_tags_inside_open_dt_but_stopping_at_special_tags() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<dt><dt><blockquote><dt target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DT', 'BLOCKQUOTE', 'DT' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
'DT should have left the BLOCKQOUTE open, but closed it.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an opening DT inside a DT closes a P in button scope.
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_in_body_dt_in_dt_closes_p_in_button_scope() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<dt><dt><p><button><p><dt target>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'DT', 'P', 'BUTTON', 'DT' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
'DT should have left the outer P open, but closed it.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an unexpected LI doesn't close more elements than it should, that it doesn't
|
||||
* close open LI elements that are beyond a special element (in this case, the UL).
|
||||
*
|
||||
* @ticket 60215
|
||||
*/
|
||||
public function test_unexpected_li_close_tag_is_properly_contained() {
|
||||
$processor = WP_HTML_Processor::create_fragment( '<ul><li><ul></li><li target>a</li></ul></li></ul>' );
|
||||
|
||||
while (
|
||||
null === $processor->get_attribute( 'target' ) &&
|
||||
$processor->next_tag()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$processor->get_attribute( 'target' ),
|
||||
'Failed to find target node.'
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
array( 'HTML', 'BODY', 'UL', 'LI', 'UL', 'LI' ),
|
||||
$processor->get_breadcrumbs(),
|
||||
'Unexpected LI close tag should have left its containing UL open, but closed it.'
|
||||
);
|
||||
}
|
||||
}
|
@ -58,9 +58,6 @@ class Tests_HtmlApi_WpHtmlSupportRequiredHtmlProcessor extends WP_UnitTestCase {
|
||||
* @covers WP_HTML_Processor::generate_implied_end_tags
|
||||
*/
|
||||
public function test_generate_implied_end_tags_needs_support() {
|
||||
$this->ensure_support_is_added_everywhere( 'DD' );
|
||||
$this->ensure_support_is_added_everywhere( 'DT' );
|
||||
$this->ensure_support_is_added_everywhere( 'LI' );
|
||||
$this->ensure_support_is_added_everywhere( 'OPTGROUP' );
|
||||
$this->ensure_support_is_added_everywhere( 'OPTION' );
|
||||
$this->ensure_support_is_added_everywhere( 'RB' );
|
||||
@ -82,9 +79,6 @@ class Tests_HtmlApi_WpHtmlSupportRequiredHtmlProcessor extends WP_UnitTestCase {
|
||||
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( 'DD' );
|
||||
$this->ensure_support_is_added_everywhere( 'DT' );
|
||||
$this->ensure_support_is_added_everywhere( 'LI' );
|
||||
$this->ensure_support_is_added_everywhere( 'OPTGROUP' );
|
||||
$this->ensure_support_is_added_everywhere( 'OPTION' );
|
||||
$this->ensure_support_is_added_everywhere( 'RB' );
|
||||
|
@ -120,13 +120,6 @@ class Tests_HtmlApi_WpHtmlSupportRequiredOpenElements extends WP_UnitTestCase {
|
||||
* FOREIGNOBJECT, DESC, TITLE.
|
||||
*/
|
||||
$this->ensure_support_is_added_everywhere( 'SVG' );
|
||||
|
||||
// These elements are specific to list item scope.
|
||||
$this->ensure_support_is_added_everywhere( 'OL' );
|
||||
$this->ensure_support_is_added_everywhere( 'UL' );
|
||||
|
||||
// This element is the only element that depends on list item scope.
|
||||
$this->ensure_support_is_added_everywhere( 'LI' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user