Skip to content

Commit

Permalink
Add select support to tag processor
Browse files Browse the repository at this point in the history
Split up main CSS selector class and support more restricted selectors in the tag processor.
  • Loading branch information
sirreal committed Dec 4, 2024
1 parent 9299288 commit 9fe8b84
Show file tree
Hide file tree
Showing 15 changed files with 520 additions and 162 deletions.
12 changes: 7 additions & 5 deletions src/wp-includes/html-api/class-wp-css-attribute-selector.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?php

final class WP_CSS_Attribute_Selector implements WP_CSS_HTML_Processor_Matcher {
public function matches( WP_HTML_Processor $processor ): bool {
final class WP_CSS_Attribute_Selector implements WP_CSS_HTML_Tag_Processor_Matcher {
const WHITESPACE_CHARACTERS = " \t\r\n\f";

public function matches( WP_HTML_Tag_Processor $processor ): bool {
$att_value = $processor->get_attribute( $this->name );
if ( null === $att_value ) {
return false;
Expand Down Expand Up @@ -76,17 +78,17 @@ public function matches( WP_HTML_Processor $processor ): bool {
* @return Generator<string>
*/
private function whitespace_delimited_list( string $input ): Generator {
$offset = strspn( $input, WP_CSS_Selector::WHITESPACE_CHARACTERS );
$offset = strspn( $input, self::WHITESPACE_CHARACTERS );

while ( $offset < strlen( $input ) ) {
// Find the byte length until the next boundary.
$length = strcspn( $input, WP_CSS_Selector::WHITESPACE_CHARACTERS, $offset );
$length = strcspn( $input, self::WHITESPACE_CHARACTERS, $offset );
if ( 0 === $length ) {
return;
}

$value = substr( $input, $offset, $length );
$offset += $length + strspn( $input, WP_CSS_Selector::WHITESPACE_CHARACTERS, $offset + $length );
$offset += $length + strspn( $input, self::WHITESPACE_CHARACTERS, $offset + $length );

yield $value;
}
Expand Down
4 changes: 2 additions & 2 deletions src/wp-includes/html-api/class-wp-css-class-selector.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

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

Expand Down
165 changes: 165 additions & 0 deletions src/wp-includes/html-api/class-wp-css-complex-selector-list.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<?php
/**
* HTML API: WP_CSS_Complex_Selector_List class
*
* @package WordPress
* @subpackage HTML-API
* @since TBD
*/

/**
* Core class used by the {@see WP_HTML_Processor} to parse and match CSS selectors.
*
* This class is designed for internal use by the HTML processor.
*
* For usage, see {@see WP_HTML_Processor::select()} or {@see WP_HTML_Processor::select_all()}.
*
* This class is instantiated via the {@see WP_CSS_Complex_Selector_List::from_selectors()} method.
* It takes a CSS selector string and returns an instance of itself or `null` if the selector
* is invalid or unsupported.
*
* A subset of the CSS selector grammar is supported. The grammar is defined in the CSS Syntax
* specification, which is available at {@link https://www.w3.org/TR/selectors/#grammar}.
*
* 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.
*
* This class supports the same selector syntax as {@see WP_CSS_Compound_Selector_List} as well as:
* - The following combinators:
* - Next sibling (`el + el`)
* - Subsequent sibling (`el ~ el`)
*
* @since TBD
*
* @access private
*/
class WP_CSS_Complex_Selector_List extends WP_CSS_Compound_Selector_List implements WP_CSS_HTML_Processor_Matcher {
/**
* Takes a CSS selector string and returns an instance of itself or `null` if the selector
* string is invalid or unsupported.
*
* @since TBD
*
* @param string $input CSS selectors.
* @return static|null
*/
public static function from_selectors( string $input ) {
// > A selector string is a list of one or more complex selectors ([SELECTORS4], section 3.1) that may be surrounded by whitespace…
$input = trim( $input, " \t\r\n\r" );

if ( '' === $input ) {
return null;
}

/*
* > The input stream consists of the filtered code points pushed into it as the input byte stream is decoded.
* >
* > To filter code points from a stream of (unfiltered) code points input:
* > 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.
* > Replace any U+0000 NULL or surrogate code points in input with U+FFFD REPLACEMENT CHARACTER (�).
*
* https://www.w3.org/TR/css-syntax-3/#input-preprocessing
*/
$input = str_replace( array( "\r\n" ), "\n", $input );
$input = str_replace( array( "\r", "\f" ), "\n", $input );
$input = str_replace( "\0", "\u{FFFD}", $input );

$offset = 0;

$selector = self::parse_complex_selector( $input, $offset );
if ( null === $selector ) {
return null;
}
self::parse_whitespace( $input, $offset );

$selectors = array( $selector );
while ( $offset < strlen( $input ) ) {
// Each loop should stop on a `,` selector list delimiter.
if ( ',' !== $input[ $offset ] ) {
return null;
}
++$offset;
self::parse_whitespace( $input, $offset );
$selector = self::parse_complex_selector( $input, $offset );
if ( null === $selector ) {
return null;
}
$selectors[] = $selector;
self::parse_whitespace( $input, $offset );
}

return new self( $selectors );
}

/*
* ------------------------------
* Selector parsing functionality
* ------------------------------
*/

/**
* Parses a complex selector.
*
* > <complex-selector> = [ <type-selector> <combinator>? ]* <compound-selector>
*
* @return WP_CSS_Complex_Selector|null
*/
final protected static function parse_complex_selector( string $input, int &$offset ): ?WP_CSS_Complex_Selector {
if ( $offset >= strlen( $input ) ) {
return null;
}

$updated_offset = $offset;
$selector = self::parse_compound_selector( $input, $updated_offset );
if ( null === $selector ) {
return null;
}

$selectors = array( $selector );
$has_preceding_subclass_selector = null !== $selector->subclass_selectors;

$found_whitespace = self::parse_whitespace( $input, $updated_offset );
while ( $updated_offset < strlen( $input ) ) {
if (
WP_CSS_Complex_Selector::COMBINATOR_CHILD === $input[ $updated_offset ] ||
WP_CSS_Complex_Selector::COMBINATOR_NEXT_SIBLING === $input[ $updated_offset ] ||
WP_CSS_Complex_Selector::COMBINATOR_SUBSEQUENT_SIBLING === $input[ $updated_offset ]
) {
$combinator = $input[ $updated_offset ];
++$updated_offset;
self::parse_whitespace( $input, $updated_offset );

// Failure to find a selector here is a parse error
$selector = self::parse_compound_selector( $input, $updated_offset );
} elseif ( $found_whitespace ) {
/*
* Whitespace is ambiguous, it could be a descendant combinator or
* insignificant whitespace.
*/
$selector = self::parse_compound_selector( $input, $updated_offset );
if ( null === $selector ) {
break;
}
$combinator = WP_CSS_Complex_Selector::COMBINATOR_DESCENDANT;
} else {
break;
}

if ( null === $selector ) {
return null;
}

// `div > .className` is valid, but `.className > div` is not.
if ( $has_preceding_subclass_selector ) {
throw new Exception( 'Unsupported non-final subclass selector.' );
}
$has_preceding_subclass_selector = null !== $selector->subclass_selectors;

$selectors[] = $combinator;
$selectors[] = $selector;

$found_whitespace = self::parse_whitespace( $input, $updated_offset );
}
$offset = $updated_offset;
return new WP_CSS_Complex_Selector( $selectors );
}
}
Loading

0 comments on commit 9fe8b84

Please sign in to comment.