Skip to content

Commit 43f6865

Browse files
committed
HTML API: Track self-closing flag during tokenization
1 parent 26e86cb commit 43f6865

2 files changed

Lines changed: 65 additions & 37 deletions

File tree

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

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,15 @@ class WP_HTML_Tag_Processor {
627627
*/
628628
private $token_length;
629629

630+
/**
631+
* Whether the current tag token has the self-closing flag.
632+
*
633+
* @since 6.9.0
634+
*
635+
* @var bool
636+
*/
637+
private $self_closing_flag = false;
638+
630639
/**
631640
* Byte offset in input document where current tag name starts.
632641
*
@@ -1079,6 +1088,7 @@ private function base_class_next_token(): bool {
10791088
$tag_name_starts_at = $this->tag_name_starts_at;
10801089
$tag_name_length = $this->tag_name_length;
10811090
$tag_ends_at = $this->token_starts_at + $this->token_length;
1091+
$self_closing_flag = $this->self_closing_flag;
10821092
$attributes = $this->attributes;
10831093
$duplicate_attributes = $this->duplicate_attributes;
10841094

@@ -1136,6 +1146,7 @@ private function base_class_next_token(): bool {
11361146
$this->text_length = $this->tag_name_starts_at - $this->text_starts_at;
11371147
$this->tag_name_starts_at = $tag_name_starts_at;
11381148
$this->tag_name_length = $tag_name_length;
1149+
$this->self_closing_flag = $self_closing_flag;
11391150
$this->attributes = $attributes;
11401151
$this->duplicate_attributes = $duplicate_attributes;
11411152

@@ -2143,7 +2154,19 @@ private function parse_next_attribute(): bool {
21432154
$doc_length = strlen( $this->html );
21442155

21452156
// Skip whitespace and slashes.
2146-
$this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed );
2157+
$skipped_start = $this->bytes_already_parsed;
2158+
$this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $skipped_start );
2159+
2160+
// A slash inside an unquoted attribute value will not have been skipped here.
2161+
if (
2162+
$this->bytes_already_parsed < $doc_length &&
2163+
$this->bytes_already_parsed > $skipped_start &&
2164+
'/' === $this->html[ $this->bytes_already_parsed - 1 ] &&
2165+
'>' === $this->html[ $this->bytes_already_parsed ]
2166+
) {
2167+
$this->self_closing_flag = true;
2168+
}
2169+
21472170
if ( $this->bytes_already_parsed >= $doc_length ) {
21482171
$this->parser_state = self::STATE_INCOMPLETE_INPUT;
21492172

@@ -2327,6 +2350,7 @@ private function after_tag(): void {
23272350

23282351
$this->token_starts_at = null;
23292352
$this->token_length = null;
2353+
$this->self_closing_flag = false;
23302354
$this->tag_name_starts_at = null;
23312355
$this->tag_name_length = null;
23322356
$this->text_starts_at = 0;
@@ -3335,42 +3359,7 @@ public function has_self_closing_flag(): bool {
33353359
return false;
33363360
}
33373361

3338-
/*
3339-
* The self-closing flag is the solidus at the _end_ of the tag, not the beginning.
3340-
*
3341-
* Example:
3342-
*
3343-
* <figure />
3344-
* ^ this appears one character before the end of the closing ">".
3345-
*/
3346-
$self_closing_flag_at = $this->token_starts_at + $this->token_length - 2;
3347-
if ( '/' !== $this->html[ $self_closing_flag_at ] ) {
3348-
return false;
3349-
}
3350-
3351-
foreach ( $this->attributes as $attribute ) {
3352-
$attribute_ends_at = $attribute->start + $attribute->length;
3353-
if (
3354-
$self_closing_flag_at >= $attribute->start &&
3355-
$self_closing_flag_at < $attribute_ends_at
3356-
) {
3357-
return false;
3358-
}
3359-
}
3360-
3361-
foreach ( $this->duplicate_attributes ?? array() as $duplicate_attributes ) {
3362-
foreach ( $duplicate_attributes as $attribute ) {
3363-
$attribute_ends_at = $attribute->start + $attribute->length;
3364-
if (
3365-
$self_closing_flag_at >= $attribute->start &&
3366-
$self_closing_flag_at < $attribute_ends_at
3367-
) {
3368-
return false;
3369-
}
3370-
}
3371-
}
3372-
3373-
return true;
3362+
return $this->self_closing_flag;
33743363
}
33753364

33763365
/**

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,45 @@ public static function data_has_self_closing_flag() {
119119
);
120120
}
121121

122+
/**
123+
* Ensures internally consumed special-element closers do not affect the opener's
124+
* self-closing flag.
125+
*
126+
* @ticket 61576
127+
*
128+
* @covers WP_HTML_Tag_Processor::has_self_closing_flag
129+
*
130+
* @dataProvider data_special_atomic_self_closing_flags
131+
*
132+
* @param string $html Input HTML whose first tag might contain the self-closing flag `/`.
133+
* @param bool $flag_is_set Whether the input HTML's first tag contains the self-closing flag.
134+
*/
135+
public function test_special_atomic_elements_report_opening_tag_self_closing_flag( string $html, bool $flag_is_set ) {
136+
$processor = new WP_HTML_Tag_Processor( $html );
137+
138+
$this->assertTrue( $processor->next_token(), 'Expected to find complete special atomic tag.' );
139+
140+
if ( $flag_is_set ) {
141+
$this->assertTrue( $processor->has_self_closing_flag(), 'Did not find the self-closing flag on the opening tag.' );
142+
} else {
143+
$this->assertFalse( $processor->has_self_closing_flag(), 'Reported the internally consumed closing tag self-closing flag on the opening tag.' );
144+
}
145+
}
146+
147+
/**
148+
* Data provider.
149+
*
150+
* @return array[]
151+
*/
152+
public static function data_special_atomic_self_closing_flags() {
153+
return array(
154+
'SCRIPT closer self-closing flag' => array( '<script>x</script/>', false ),
155+
'STYLE closer self-closing flag' => array( '<style>x</style/>', false ),
156+
'TITLE closer self-closing flag' => array( '<title>x</title/>', false ),
157+
'TITLE opener self-closing flag' => array( '<title/>x</title>', true ),
158+
);
159+
}
160+
122161
/**
123162
* Ensures a trailing slash in an unquoted attribute value is part of the value.
124163
*

0 commit comments

Comments
 (0)