Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Next Next commit
WIP: HTML API: Add support for H1-H6 elements in HTML Processor
  • Loading branch information
dmsnell committed Dec 13, 2023
commit 8a2a4f66f777a3749f6a2c1917f2faddc9bbb8d0
30 changes: 28 additions & 2 deletions src/wp-includes/html-api/class-wp-html-open-elements.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public function current_node() {
*
* @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope
*
* @param string $tag_name Name of tag check.
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I missed this; this looks accidental, no?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, thanks. Not sure what happened here.
added back in 6400821

* @param string $tag_name Name of tag check, or the class constant HEADING_ELEMENTS to specify H1-H6.
* @param string[] $termination_list List of elements that terminate the search.
* @return bool Whether the element was found in a specific scope.
*/
Expand All @@ -116,6 +116,13 @@ public function has_element_in_specific_scope( $tag_name, $termination_list ) {
return true;
}

if (
self::HEADING_ELEMENTS === $tag_name &&
in_array( $node->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
) {
return true;
}

switch ( $node->node_name ) {
case 'HTML':
return false;
Expand Down Expand Up @@ -263,13 +270,21 @@ public function pop() {
*
* @see WP_HTML_Open_Elements::pop
*
* @param string $tag_name Name of tag that needs to be popped off of the stack of open elements.
* @param string $tag_name Name of tag that needs to be popped off of the stack of open elements,
* or the class constant HEADING_ELEMENTS to specify any of H1-H6.
* @return bool Whether a tag of the given name was found and popped off of the stack of open elements.
*/
public function pop_until( $tag_name ) {
foreach ( $this->walk_up() as $item ) {
$this->pop();

if (
self::HEADING_ELEMENTS === $tag_name &&
in_array( $item->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
) {
return true;
}

if ( $tag_name === $item->node_name ) {
return true;
}
Expand Down Expand Up @@ -429,4 +444,15 @@ public function after_element_pop( $item ) {
break;
}
}

/**
* Represents the collection of H1-H6 elements.
*
* @since 6.5.0
*
* @see has_element_in_scope()
*
* @var string
*/
const HEADING_ELEMENTS = 'heading-elements';
}
60 changes: 59 additions & 1 deletion src/wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
* - Containers: ADDRESS, BLOCKQUOTE, DETAILS, DIALOG, DIV, FOOTER, HEADER, MAIN, MENU, SPAN, SUMMARY.
* - Form elements: BUTTON, FIELDSET, SEARCH.
* - Formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U.
* - Heading elements: HGROUP.
* - Heading elements: H1, H2, H3, H4, H5, H6, HGROUP.
* - Links: A.
* - Lists: DL.
* - Media elements: FIGCAPTION, FIGURE, IMG.
Expand Down Expand Up @@ -697,6 +697,64 @@ private function step_in_body() {
$this->state->stack_of_open_elements->pop_until( $tag_name );
return true;

/*
* > A start tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6"
*/
case '+H1':
case '+H2':
case '+H3':
case '+H4':
case '+H5':
case '+H6':
if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
$this->close_a_p_element();
}

if (
in_array(
$this->state->stack_of_open_elements->current_node()->node_name,
array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ),
true
)
) {
// @TODO: Indicate a parse error once it's possible.
$this->state->stack_of_open_elements->pop();
}

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

/*
* > An end tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6"
*/
case '-H1':
case '-H2':
case '-H3':
case '-H4':
case '-H5':
case '-H6':
if (
! $this->state->stack_of_open_elements->has_element_in_scope(
WP_HTML_Open_Elements::HEADING_ELEMENTS
)
) {
/*
* 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();

if ( $this->state->stack_of_open_elements->current_node()->node_name !== $tag_name ) {
// @TODO: Record parse error: this error doesn't impact parsing.
}

$this->state->stack_of_open_elements->pop_until( WP_HTML_Open_Elements::HEADING_ELEMENTS );
return true;

/*
* > An end tag whose tag name is "p"
*/
Expand Down
2 changes: 0 additions & 2 deletions tests/phpunit/tests/html-api/wpHtmlProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@ public function test_stops_processing_after_unsupported_elements() {
*
* @covers WP_HTML_Processor::next_tag
* @covers WP_HTML_Processor::seek
*
* @throws WP_HTML_Unsupported_Exception
*/
public function test_clear_to_navigate_after_seeking() {
$p = WP_HTML_Processor::create_fragment( '<div one><strong></strong></div><p><strong two></strong></p>' );
Expand Down
12 changes: 6 additions & 6 deletions tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ public function data_single_tag_of_supported_elements() {
'FIGURE',
'FONT',
'FOOTER',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'HEADER',
'HGROUP',
'I',
Expand Down Expand Up @@ -142,12 +148,6 @@ public function data_unsupported_elements() {
'FORM',
'FRAME',
'FRAMESET',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'HEAD',
'HR',
'HTML',
Expand Down
48 changes: 34 additions & 14 deletions tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,6 @@ public function data_article_container_group() {
* element in scope, that it skips the tag entirely.
*
* @ticket 58961
*
* @since 6.4.0
*
* @throws Exception
*/
public function test_in_body_skips_unexpected_button_closer() {
$p = WP_HTML_Processor::create_fragment( '<div>Test</button></div>' );
Expand All @@ -145,10 +141,6 @@ public function test_in_body_skips_unexpected_button_closer() {
* Verifies insertion of a BUTTON element when no existing BUTTON is already in scope.
*
* @ticket 58961
*
* @since 6.4.0
*
* @throws WP_HTML_Unsupported_Exception
*/
public function test_in_body_button_with_no_button_in_scope() {
$p = WP_HTML_Processor::create_fragment( '<div><p>Click the button <button one>here</button>!</p></div><button two>not here</button>' );
Expand All @@ -174,8 +166,6 @@ public function test_in_body_button_with_no_button_in_scope() {
* @ticket 58961
*
* @since 6.4.0
*
* @throws WP_HTML_Unsupported_Exception
*/
public function test_in_body_button_with_button_in_scope_as_parent() {
$p = WP_HTML_Processor::create_fragment( '<div><p>Click the button <button one>almost<button two>here</button>!</p></div><button three>not here</button>' );
Expand Down Expand Up @@ -209,8 +199,6 @@ public function test_in_body_button_with_button_in_scope_as_parent() {
* @ticket 58961
*
* @since 6.4.0
*
* @throws WP_HTML_Unsupported_Exception
*/
public function test_in_body_button_with_button_in_scope_as_ancestor() {
$p = WP_HTML_Processor::create_fragment( '<div><button one><p>Click the button <span><button two>here</button>!</span></p></div><button three>not here</button>' );
Expand All @@ -236,7 +224,39 @@ public function test_in_body_button_with_button_in_scope_as_ancestor() {
$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 {TICKET_NUMBER}
*
* @dataProvider data_heading_elements
*
* @param string $tag_name Name of H1 - H6 element under test.
*/
public function test_heading_element_closes_open_p_tag( $tag_name ) {
$p = WP_HTML_Processor::create_fragment( "<p>Open<{$tag_name}>Closed P</{$tag_name}></p>" );

$p->next_tag( $tag_name );
$this->assertSame( array( 'HTML', 'BODY', $tag_name ), $p->get_breadcrumbs() );
}

/**
* Data provider.
*
* @return array[].
*/
public function data_heading_elements() {
return array(
array( 'H1' ),
array( 'H2' ),
array( 'H3' ),
array( 'H4' ),
array( 'H5' ),
array( 'H5' ),
);
}

/**
* Verifies that when "in body" and encountering "any other end tag"
* that the HTML processor ignores the end tag if there's a special
* element on the stack of open elements before the matching opening.
Expand All @@ -259,7 +279,7 @@ public function test_in_body_any_other_end_tag_with_unclosed_special_element() {
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'DIV' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting: SPAN should still be open and DIV should be its child.' );
}

/*
/**
* Verifies that when "in body" and encountering "any other end tag"
* that the HTML processor closes appropriate elements on the stack of
* open elements up to the matching opening.
Expand Down