11<?php
2-
2+ /**
3+ * HTML API: WP_CSS_Attribute_Selector class
4+ *
5+ * @package WordPress
6+ * @subpackage HTML-API
7+ * @since TBD
8+ */
9+
10+ /**
11+ * CSS attribute selector.
12+ *
13+ * This class implements a CSS attribute selector and is used to test for matching HTML tags
14+ * in a {@see WP_HTML_Tag_Processor}.
15+ *
16+ * @since TBD
17+ *
18+ * @access private
19+ */
320final 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 {
7- $ att_value = $ processor ->get_attribute ( $ this ->name );
8- if ( null === $ att_value ) {
9- return false ;
10- }
11-
12- if ( null === $ this ->value ) {
13- return true ;
14- }
15-
16- if ( true === $ att_value ) {
17- $ att_value = '' ;
18- }
19-
20- $ case_insensitive = self ::MODIFIER_CASE_INSENSITIVE === $ this ->modifier ;
21-
22- switch ( $ this ->matcher ) {
23- case self ::MATCH_EXACT :
24- return $ case_insensitive
25- ? 0 === strcasecmp ( $ att_value , $ this ->value )
26- : $ att_value === $ this ->value ;
27-
28- case self ::MATCH_ONE_OF_EXACT :
29- foreach ( $ this ->whitespace_delimited_list ( $ att_value ) as $ val ) {
30- if (
31- $ case_insensitive
32- ? 0 === strcasecmp ( $ val , $ this ->value )
33- : $ val === $ this ->value
34- ) {
35- return true ;
36- }
37- }
38- return false ;
39-
40- case self ::MATCH_EXACT_OR_EXACT_WITH_HYPHEN :
41- // Attempt the full match first
42- if (
43- $ case_insensitive
44- ? 0 === strcasecmp ( $ att_value , $ this ->value )
45- : $ att_value === $ this ->value
46- ) {
47- return true ;
48- }
49-
50- // Partial match
51- if ( strlen ( $ att_value ) < strlen ( $ this ->value ) + 1 ) {
52- return false ;
53- }
54-
55- $ starts_with = "{$ this ->value }- " ;
56- return 0 === substr_compare ( $ att_value , $ starts_with , 0 , strlen ( $ starts_with ), $ case_insensitive );
57-
58- case self ::MATCH_PREFIXED_BY :
59- return 0 === substr_compare ( $ att_value , $ this ->value , 0 , strlen ( $ this ->value ), $ case_insensitive );
60-
61- case self ::MATCH_SUFFIXED_BY :
62- return 0 === substr_compare ( $ att_value , $ this ->value , -strlen ( $ this ->value ), null , $ case_insensitive );
63-
64- case self ::MATCH_CONTAINS :
65- return false !== (
66- $ case_insensitive
67- ? stripos ( $ att_value , $ this ->value )
68- : strpos ( $ att_value , $ this ->value )
69- );
70- }
71- }
72-
73- /**
74- * @param string $input
75- *
76- * @return Generator<string>
77- */
78- private function whitespace_delimited_list ( string $ input ): Generator {
79- // Start by skipping whitespace.
80- $ offset = strspn ( $ input , self ::WHITESPACE_CHARACTERS );
81-
82- while ( $ offset < strlen ( $ input ) ) {
83- // Find the byte length until the next boundary.
84- $ length = strcspn ( $ input , self ::WHITESPACE_CHARACTERS , $ offset );
85- $ value = substr ( $ input , $ offset , $ length );
86-
87- // Move past trailing whitespace.
88- $ offset += $ length + strspn ( $ input , self ::WHITESPACE_CHARACTERS , $ offset + $ length );
89-
90- yield $ value ;
91- }
92- }
93-
9421 /**
9522 * [att=val]
9623 * Represents an element with the att attribute whose value is exactly "val".
@@ -145,36 +72,41 @@ private function whitespace_delimited_list( string $input ): Generator {
14572 */
14673 const MODIFIER_CASE_INSENSITIVE = 'case-insensitive ' ;
14774
148-
14975 /**
15076 * The attribute name.
15177 *
15278 * @var string
79+ * @readonly
15380 */
15481 public $ name ;
15582
15683 /**
15784 * The attribute matcher.
15885 *
15986 * @var null|self::MATCH_*
87+ * @readonly
16088 */
16189 public $ matcher ;
16290
16391 /**
16492 * The attribute value.
16593 *
16694 * @var string|null
95+ * @readonly
16796 */
16897 public $ value ;
16998
17099 /**
171100 * The attribute modifier.
172101 *
173102 * @var null|self::MODIFIER_*
103+ * @readonly
174104 */
175105 public $ modifier ;
176106
177107 /**
108+ * Constructor.
109+ *
178110 * @param string $name
179111 * @param null|self::MATCH_* $matcher
180112 * @param null|string $value
@@ -186,4 +118,102 @@ public function __construct( string $name, ?string $matcher = null, ?string $val
186118 $ this ->value = $ value ;
187119 $ this ->modifier = $ modifier ;
188120 }
121+
122+ /**
123+ * Determines if the processor's current position matches the selector.
124+ *
125+ * @param WP_HTML_Tag_Processor $processor
126+ * @return bool True if the processor's current position matches the selector.
127+ */
128+ public function matches ( WP_HTML_Tag_Processor $ processor ): bool {
129+ $ att_value = $ processor ->get_attribute ( $ this ->name );
130+ if ( null === $ att_value ) {
131+ return false ;
132+ }
133+
134+ if ( null === $ this ->value ) {
135+ return true ;
136+ }
137+
138+ if ( true === $ att_value ) {
139+ $ att_value = '' ;
140+ }
141+
142+ $ case_insensitive = self ::MODIFIER_CASE_INSENSITIVE === $ this ->modifier ;
143+
144+ switch ( $ this ->matcher ) {
145+ case self ::MATCH_EXACT :
146+ return $ case_insensitive
147+ ? 0 === strcasecmp ( $ att_value , $ this ->value )
148+ : $ att_value === $ this ->value ;
149+
150+ case self ::MATCH_ONE_OF_EXACT :
151+ foreach ( $ this ->whitespace_delimited_list ( $ att_value ) as $ val ) {
152+ if (
153+ $ case_insensitive
154+ ? 0 === strcasecmp ( $ val , $ this ->value )
155+ : $ val === $ this ->value
156+ ) {
157+ return true ;
158+ }
159+ }
160+ return false ;
161+
162+ case self ::MATCH_EXACT_OR_EXACT_WITH_HYPHEN :
163+ // Attempt the full match first
164+ if (
165+ $ case_insensitive
166+ ? 0 === strcasecmp ( $ att_value , $ this ->value )
167+ : $ att_value === $ this ->value
168+ ) {
169+ return true ;
170+ }
171+
172+ // Partial match
173+ if ( strlen ( $ att_value ) < strlen ( $ this ->value ) + 1 ) {
174+ return false ;
175+ }
176+
177+ $ starts_with = "{$ this ->value }- " ;
178+ return 0 === substr_compare ( $ att_value , $ starts_with , 0 , strlen ( $ starts_with ), $ case_insensitive );
179+
180+ case self ::MATCH_PREFIXED_BY :
181+ return 0 === substr_compare ( $ att_value , $ this ->value , 0 , strlen ( $ this ->value ), $ case_insensitive );
182+
183+ case self ::MATCH_SUFFIXED_BY :
184+ return 0 === substr_compare ( $ att_value , $ this ->value , -strlen ( $ this ->value ), null , $ case_insensitive );
185+
186+ case self ::MATCH_CONTAINS :
187+ return false !== (
188+ $ case_insensitive
189+ ? stripos ( $ att_value , $ this ->value )
190+ : strpos ( $ att_value , $ this ->value )
191+ );
192+ }
193+ }
194+
195+ /**
196+ * Splits a string into a list of whitespace delimited values.
197+ *
198+ * This is useful for the {@see WP_CSS_Attribute_Selector::MATCH_ONE_OF_EXACT} matcher.
199+ *
200+ * @param string $input
201+ *
202+ * @return Generator<string>
203+ */
204+ private function whitespace_delimited_list ( string $ input ): Generator {
205+ // Start by skipping whitespace.
206+ $ offset = strspn ( $ input , " \t\r\n\f" );
207+
208+ while ( $ offset < strlen ( $ input ) ) {
209+ // Find the byte length until the next boundary.
210+ $ length = strcspn ( $ input , " \t\r\n\f" , $ offset );
211+ $ value = substr ( $ input , $ offset , $ length );
212+
213+ // Move past trailing whitespace.
214+ $ offset += $ length + strspn ( $ input , " \t\r\n\f" , $ offset + $ length );
215+
216+ yield $ value ;
217+ }
218+ }
189219}
0 commit comments