Skip to content

Commit 9fe8b84

Browse files
committed
Add select support to tag processor
Split up main CSS selector class and support more restricted selectors in the tag processor.
1 parent 9299288 commit 9fe8b84

15 files changed

+520
-162
lines changed

src/wp-includes/html-api/class-wp-css-attribute-selector.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<?php
22

3-
final class WP_CSS_Attribute_Selector implements WP_CSS_HTML_Processor_Matcher {
4-
public function matches( WP_HTML_Processor $processor ): bool {
3+
final class WP_CSS_Attribute_Selector implements WP_CSS_HTML_Tag_Processor_Matcher {
4+
const WHITESPACE_CHARACTERS = " \t\r\n\f";
5+
6+
public function matches( WP_HTML_Tag_Processor $processor ): bool {
57
$att_value = $processor->get_attribute( $this->name );
68
if ( null === $att_value ) {
79
return false;
@@ -76,17 +78,17 @@ public function matches( WP_HTML_Processor $processor ): bool {
7678
* @return Generator<string>
7779
*/
7880
private function whitespace_delimited_list( string $input ): Generator {
79-
$offset = strspn( $input, WP_CSS_Selector::WHITESPACE_CHARACTERS );
81+
$offset = strspn( $input, self::WHITESPACE_CHARACTERS );
8082

8183
while ( $offset < strlen( $input ) ) {
8284
// Find the byte length until the next boundary.
83-
$length = strcspn( $input, WP_CSS_Selector::WHITESPACE_CHARACTERS, $offset );
85+
$length = strcspn( $input, self::WHITESPACE_CHARACTERS, $offset );
8486
if ( 0 === $length ) {
8587
return;
8688
}
8789

8890
$value = substr( $input, $offset, $length );
89-
$offset += $length + strspn( $input, WP_CSS_Selector::WHITESPACE_CHARACTERS, $offset + $length );
91+
$offset += $length + strspn( $input, self::WHITESPACE_CHARACTERS, $offset + $length );
9092

9193
yield $value;
9294
}

src/wp-includes/html-api/class-wp-css-class-selector.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

3-
final class WP_CSS_Class_Selector implements WP_CSS_HTML_Processor_Matcher {
4-
public function matches( WP_HTML_Processor $processor ): bool {
3+
final class WP_CSS_Class_Selector implements WP_CSS_HTML_Tag_Processor_Matcher {
4+
public function matches( WP_HTML_Tag_Processor $processor ): bool {
55
return (bool) $processor->has_class( $this->ident );
66
}
77

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
/**
3+
* HTML API: WP_CSS_Complex_Selector_List class
4+
*
5+
* @package WordPress
6+
* @subpackage HTML-API
7+
* @since TBD
8+
*/
9+
10+
/**
11+
* Core class used by the {@see WP_HTML_Processor} to parse and match CSS selectors.
12+
*
13+
* This class is designed for internal use by the HTML processor.
14+
*
15+
* For usage, see {@see WP_HTML_Processor::select()} or {@see WP_HTML_Processor::select_all()}.
16+
*
17+
* This class is instantiated via the {@see WP_CSS_Complex_Selector_List::from_selectors()} method.
18+
* It takes a CSS selector string and returns an instance of itself or `null` if the selector
19+
* is invalid or unsupported.
20+
*
21+
* A subset of the CSS selector grammar is supported. The grammar is defined in the CSS Syntax
22+
* specification, which is available at {@link https://www.w3.org/TR/selectors/#grammar}.
23+
*
24+
* This class is rougly analogous to the <selector-list> in the grammar. See {@see WP_CSS_Compound_Selector_List} for more details on the grammar.
25+
*
26+
* This class supports the same selector syntax as {@see WP_CSS_Compound_Selector_List} as well as:
27+
* - The following combinators:
28+
* - Next sibling (`el + el`)
29+
* - Subsequent sibling (`el ~ el`)
30+
*
31+
* @since TBD
32+
*
33+
* @access private
34+
*/
35+
class WP_CSS_Complex_Selector_List extends WP_CSS_Compound_Selector_List implements WP_CSS_HTML_Processor_Matcher {
36+
/**
37+
* Takes a CSS selector string and returns an instance of itself or `null` if the selector
38+
* string is invalid or unsupported.
39+
*
40+
* @since TBD
41+
*
42+
* @param string $input CSS selectors.
43+
* @return static|null
44+
*/
45+
public static function from_selectors( string $input ) {
46+
// > A selector string is a list of one or more complex selectors ([SELECTORS4], section 3.1) that may be surrounded by whitespace…
47+
$input = trim( $input, " \t\r\n\r" );
48+
49+
if ( '' === $input ) {
50+
return null;
51+
}
52+
53+
/*
54+
* > The input stream consists of the filtered code points pushed into it as the input byte stream is decoded.
55+
* >
56+
* > To filter code points from a stream of (unfiltered) code points input:
57+
* > Replace any U+000D CARRIAGE RETURN (CR) code points, U+000C FORM FEED (FF) code points, or pairs of U+000D CARRIAGE RETURN (CR) followed by U+000A LINE FEED (LF) in input by a single U+000A LINE FEED (LF) code point.
58+
* > Replace any U+0000 NULL or surrogate code points in input with U+FFFD REPLACEMENT CHARACTER (�).
59+
*
60+
* https://www.w3.org/TR/css-syntax-3/#input-preprocessing
61+
*/
62+
$input = str_replace( array( "\r\n" ), "\n", $input );
63+
$input = str_replace( array( "\r", "\f" ), "\n", $input );
64+
$input = str_replace( "\0", "\u{FFFD}", $input );
65+
66+
$offset = 0;
67+
68+
$selector = self::parse_complex_selector( $input, $offset );
69+
if ( null === $selector ) {
70+
return null;
71+
}
72+
self::parse_whitespace( $input, $offset );
73+
74+
$selectors = array( $selector );
75+
while ( $offset < strlen( $input ) ) {
76+
// Each loop should stop on a `,` selector list delimiter.
77+
if ( ',' !== $input[ $offset ] ) {
78+
return null;
79+
}
80+
++$offset;
81+
self::parse_whitespace( $input, $offset );
82+
$selector = self::parse_complex_selector( $input, $offset );
83+
if ( null === $selector ) {
84+
return null;
85+
}
86+
$selectors[] = $selector;
87+
self::parse_whitespace( $input, $offset );
88+
}
89+
90+
return new self( $selectors );
91+
}
92+
93+
/*
94+
* ------------------------------
95+
* Selector parsing functionality
96+
* ------------------------------
97+
*/
98+
99+
/**
100+
* Parses a complex selector.
101+
*
102+
* > <complex-selector> = [ <type-selector> <combinator>? ]* <compound-selector>
103+
*
104+
* @return WP_CSS_Complex_Selector|null
105+
*/
106+
final protected static function parse_complex_selector( string $input, int &$offset ): ?WP_CSS_Complex_Selector {
107+
if ( $offset >= strlen( $input ) ) {
108+
return null;
109+
}
110+
111+
$updated_offset = $offset;
112+
$selector = self::parse_compound_selector( $input, $updated_offset );
113+
if ( null === $selector ) {
114+
return null;
115+
}
116+
117+
$selectors = array( $selector );
118+
$has_preceding_subclass_selector = null !== $selector->subclass_selectors;
119+
120+
$found_whitespace = self::parse_whitespace( $input, $updated_offset );
121+
while ( $updated_offset < strlen( $input ) ) {
122+
if (
123+
WP_CSS_Complex_Selector::COMBINATOR_CHILD === $input[ $updated_offset ] ||
124+
WP_CSS_Complex_Selector::COMBINATOR_NEXT_SIBLING === $input[ $updated_offset ] ||
125+
WP_CSS_Complex_Selector::COMBINATOR_SUBSEQUENT_SIBLING === $input[ $updated_offset ]
126+
) {
127+
$combinator = $input[ $updated_offset ];
128+
++$updated_offset;
129+
self::parse_whitespace( $input, $updated_offset );
130+
131+
// Failure to find a selector here is a parse error
132+
$selector = self::parse_compound_selector( $input, $updated_offset );
133+
} elseif ( $found_whitespace ) {
134+
/*
135+
* Whitespace is ambiguous, it could be a descendant combinator or
136+
* insignificant whitespace.
137+
*/
138+
$selector = self::parse_compound_selector( $input, $updated_offset );
139+
if ( null === $selector ) {
140+
break;
141+
}
142+
$combinator = WP_CSS_Complex_Selector::COMBINATOR_DESCENDANT;
143+
} else {
144+
break;
145+
}
146+
147+
if ( null === $selector ) {
148+
return null;
149+
}
150+
151+
// `div > .className` is valid, but `.className > div` is not.
152+
if ( $has_preceding_subclass_selector ) {
153+
throw new Exception( 'Unsupported non-final subclass selector.' );
154+
}
155+
$has_preceding_subclass_selector = null !== $selector->subclass_selectors;
156+
157+
$selectors[] = $combinator;
158+
$selectors[] = $selector;
159+
160+
$found_whitespace = self::parse_whitespace( $input, $updated_offset );
161+
}
162+
$offset = $updated_offset;
163+
return new WP_CSS_Complex_Selector( $selectors );
164+
}
165+
}

0 commit comments

Comments
 (0)