From 5cd8af561c3703d6ec71120ef3a3839e59416781 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Mon, 20 Nov 2023 19:32:45 +0100 Subject: [PATCH 1/9] [CssSelector][Serializer][Translation] [Command] Clean unused code --- XPath/XPathExpr.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XPath/XPathExpr.php b/XPath/XPathExpr.php index a76e30b..29f0c8a 100644 --- a/XPath/XPathExpr.php +++ b/XPath/XPathExpr.php @@ -104,7 +104,7 @@ public function join(string $combiner, self $expr): static public function __toString(): string { $path = $this->path.$this->element; - $condition = null === $this->condition || '' === $this->condition ? '' : '['.$this->condition.']'; + $condition = '' === $this->condition ? '' : '['.$this->condition.']'; return $path.$condition; } From 74ca262b973368e25e6178dc263784ddf2e018d6 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 14 Dec 2023 11:03:37 +0100 Subject: [PATCH 2/9] Set `strict` parameter of `in_array` to true where possible --- Parser/Token.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Parser/Token.php b/Parser/Token.php index b50441a..3e926c7 100644 --- a/Parser/Token.php +++ b/Parser/Token.php @@ -72,7 +72,7 @@ public function isDelimiter(array $values = []): bool return true; } - return \in_array($this->value, $values); + return \in_array($this->value, $values, true); } public function isWhitespace(): bool From 3874347e40b1f00c483dc6f02819838b1399049e Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 27 Dec 2023 22:18:42 +0100 Subject: [PATCH 3/9] Fix typo --- Tests/XPath/TranslatorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/XPath/TranslatorTest.php b/Tests/XPath/TranslatorTest.php index c161b80..de15384 100644 --- a/Tests/XPath/TranslatorTest.php +++ b/Tests/XPath/TranslatorTest.php @@ -277,7 +277,7 @@ public static function getHtmlIdsTestData() ['div[foobar~="cd"]', []], ['*[lang|="En"]', ['second-li']], ['[lang|="En-us"]', ['second-li']], - // Attribute values are case sensitive + // Attribute values are case-sensitive ['*[lang|="en"]', []], ['[lang|="en-US"]', []], ['*[lang|="e"]', []], @@ -334,7 +334,7 @@ public static function getHtmlIdsTestData() ['* :root', []], ['*:contains("link")', ['html', 'nil', 'outer-div', 'tag-anchor', 'nofollow-anchor']], [':CONtains("link")', ['html', 'nil', 'outer-div', 'tag-anchor', 'nofollow-anchor']], - ['*:contains("LInk")', []], // case sensitive + ['*:contains("LInk")', []], // case-sensitive ['*:contains("e")', ['html', 'nil', 'outer-div', 'first-ol', 'first-li', 'paragraph', 'p-em']], ['*:contains("E")', []], // case-sensitive ['.a', ['first-ol']], From 5ec03ebb4337064dffc8ca0805b82028c1b4a777 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 2 Jan 2024 14:14:18 +0100 Subject: [PATCH 4/9] [Asset][BrowserKit][Cache][Console][CssSelector] Use CPP --- Node/AttributeNode.php | 20 +++++++------------- Node/ClassNode.php | 11 ++++------- Node/CombinedSelectorNode.php | 14 +++++--------- Node/ElementNode.php | 11 ++++------- Node/FunctionNode.php | 11 +++++------ Node/HashNode.php | 11 ++++------- Node/NegationNode.php | 11 ++++------- Node/PseudoNode.php | 8 ++++---- Node/SelectorNode.php | 8 ++++---- Node/Specificity.php | 14 +++++--------- Parser/Handler/HashHandler.php | 11 ++++------- Parser/Handler/IdentifierHandler.php | 11 ++++------- Parser/Handler/NumberHandler.php | 8 +++----- Parser/Handler/StringHandler.php | 11 ++++------- Parser/Reader.php | 7 +++---- Parser/Token.php | 14 +++++--------- Parser/Tokenizer/TokenizerEscaping.php | 8 +++----- XPath/Extension/NodeExtension.php | 8 +++----- XPath/XPathExpr.php | 16 ++++++---------- 19 files changed, 81 insertions(+), 132 deletions(-) diff --git a/Node/AttributeNode.php b/Node/AttributeNode.php index ba4df31..41b1787 100644 --- a/Node/AttributeNode.php +++ b/Node/AttributeNode.php @@ -23,19 +23,13 @@ */ class AttributeNode extends AbstractNode { - private NodeInterface $selector; - private ?string $namespace; - private string $attribute; - private string $operator; - private ?string $value; - - public function __construct(NodeInterface $selector, ?string $namespace, string $attribute, string $operator, ?string $value) - { - $this->selector = $selector; - $this->namespace = $namespace; - $this->attribute = $attribute; - $this->operator = $operator; - $this->value = $value; + public function __construct( + private NodeInterface $selector, + private ?string $namespace, + private string $attribute, + private string $operator, + private ?string $value, + ) { } public function getSelector(): NodeInterface diff --git a/Node/ClassNode.php b/Node/ClassNode.php index 7174458..2b488c7 100644 --- a/Node/ClassNode.php +++ b/Node/ClassNode.php @@ -23,13 +23,10 @@ */ class ClassNode extends AbstractNode { - private NodeInterface $selector; - private string $name; - - public function __construct(NodeInterface $selector, string $name) - { - $this->selector = $selector; - $this->name = $name; + public function __construct( + private NodeInterface $selector, + private string $name, + ) { } public function getSelector(): NodeInterface diff --git a/Node/CombinedSelectorNode.php b/Node/CombinedSelectorNode.php index 19fecb7..fead5e5 100644 --- a/Node/CombinedSelectorNode.php +++ b/Node/CombinedSelectorNode.php @@ -23,15 +23,11 @@ */ class CombinedSelectorNode extends AbstractNode { - private NodeInterface $selector; - private string $combinator; - private NodeInterface $subSelector; - - public function __construct(NodeInterface $selector, string $combinator, NodeInterface $subSelector) - { - $this->selector = $selector; - $this->combinator = $combinator; - $this->subSelector = $subSelector; + public function __construct( + private NodeInterface $selector, + private string $combinator, + private NodeInterface $subSelector, + ) { } public function getSelector(): NodeInterface diff --git a/Node/ElementNode.php b/Node/ElementNode.php index 39f4d9c..366681f 100644 --- a/Node/ElementNode.php +++ b/Node/ElementNode.php @@ -23,13 +23,10 @@ */ class ElementNode extends AbstractNode { - private ?string $namespace; - private ?string $element; - - public function __construct(string $namespace = null, string $element = null) - { - $this->namespace = $namespace; - $this->element = $element; + public function __construct( + private ?string $namespace = null, + private ?string $element = null, + ) { } public function getNamespace(): ?string diff --git a/Node/FunctionNode.php b/Node/FunctionNode.php index 938a82b..5a04d63 100644 --- a/Node/FunctionNode.php +++ b/Node/FunctionNode.php @@ -25,18 +25,17 @@ */ class FunctionNode extends AbstractNode { - private NodeInterface $selector; private string $name; - private array $arguments; /** * @param Token[] $arguments */ - public function __construct(NodeInterface $selector, string $name, array $arguments = []) - { - $this->selector = $selector; + public function __construct( + private NodeInterface $selector, + string $name, + private array $arguments = [], + ) { $this->name = strtolower($name); - $this->arguments = $arguments; } public function getSelector(): NodeInterface diff --git a/Node/HashNode.php b/Node/HashNode.php index 3af8e84..97e51d6 100644 --- a/Node/HashNode.php +++ b/Node/HashNode.php @@ -23,13 +23,10 @@ */ class HashNode extends AbstractNode { - private NodeInterface $selector; - private string $id; - - public function __construct(NodeInterface $selector, string $id) - { - $this->selector = $selector; - $this->id = $id; + public function __construct( + private NodeInterface $selector, + private string $id, + ) { } public function getSelector(): NodeInterface diff --git a/Node/NegationNode.php b/Node/NegationNode.php index f806758..c4c251d 100644 --- a/Node/NegationNode.php +++ b/Node/NegationNode.php @@ -23,13 +23,10 @@ */ class NegationNode extends AbstractNode { - private NodeInterface $selector; - private NodeInterface $subSelector; - - public function __construct(NodeInterface $selector, NodeInterface $subSelector) - { - $this->selector = $selector; - $this->subSelector = $subSelector; + public function __construct( + private NodeInterface $selector, + private NodeInterface $subSelector, + ) { } public function getSelector(): NodeInterface diff --git a/Node/PseudoNode.php b/Node/PseudoNode.php index c21cd6e..dbf2f85 100644 --- a/Node/PseudoNode.php +++ b/Node/PseudoNode.php @@ -23,12 +23,12 @@ */ class PseudoNode extends AbstractNode { - private NodeInterface $selector; private string $identifier; - public function __construct(NodeInterface $selector, string $identifier) - { - $this->selector = $selector; + public function __construct( + private NodeInterface $selector, + string $identifier, + ) { $this->identifier = strtolower($identifier); } diff --git a/Node/SelectorNode.php b/Node/SelectorNode.php index 0b09ab5..72c9023 100644 --- a/Node/SelectorNode.php +++ b/Node/SelectorNode.php @@ -23,12 +23,12 @@ */ class SelectorNode extends AbstractNode { - private NodeInterface $tree; private ?string $pseudoElement; - public function __construct(NodeInterface $tree, string $pseudoElement = null) - { - $this->tree = $tree; + public function __construct( + private NodeInterface $tree, + string $pseudoElement = null, + ) { $this->pseudoElement = $pseudoElement ? strtolower($pseudoElement) : null; } diff --git a/Node/Specificity.php b/Node/Specificity.php index bb8e5e3..c669a39 100644 --- a/Node/Specificity.php +++ b/Node/Specificity.php @@ -29,15 +29,11 @@ class Specificity public const B_FACTOR = 10; public const C_FACTOR = 1; - private int $a; - private int $b; - private int $c; - - public function __construct(int $a, int $b, int $c) - { - $this->a = $a; - $this->b = $b; - $this->c = $c; + public function __construct( + private int $a, + private int $b, + private int $c, + ) { } public function plus(self $specificity): self diff --git a/Parser/Handler/HashHandler.php b/Parser/Handler/HashHandler.php index b29042f..0be4652 100644 --- a/Parser/Handler/HashHandler.php +++ b/Parser/Handler/HashHandler.php @@ -29,13 +29,10 @@ */ class HashHandler implements HandlerInterface { - private TokenizerPatterns $patterns; - private TokenizerEscaping $escaping; - - public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping) - { - $this->patterns = $patterns; - $this->escaping = $escaping; + public function __construct( + private TokenizerPatterns $patterns, + private TokenizerEscaping $escaping, + ) { } public function handle(Reader $reader, TokenStream $stream): bool diff --git a/Parser/Handler/IdentifierHandler.php b/Parser/Handler/IdentifierHandler.php index 25c0761..7e4356b 100644 --- a/Parser/Handler/IdentifierHandler.php +++ b/Parser/Handler/IdentifierHandler.php @@ -29,13 +29,10 @@ */ class IdentifierHandler implements HandlerInterface { - private TokenizerPatterns $patterns; - private TokenizerEscaping $escaping; - - public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping) - { - $this->patterns = $patterns; - $this->escaping = $escaping; + public function __construct( + private TokenizerPatterns $patterns, + private TokenizerEscaping $escaping, + ) { } public function handle(Reader $reader, TokenStream $stream): bool diff --git a/Parser/Handler/NumberHandler.php b/Parser/Handler/NumberHandler.php index e3eb7af..38cc9b1 100644 --- a/Parser/Handler/NumberHandler.php +++ b/Parser/Handler/NumberHandler.php @@ -28,11 +28,9 @@ */ class NumberHandler implements HandlerInterface { - private TokenizerPatterns $patterns; - - public function __construct(TokenizerPatterns $patterns) - { - $this->patterns = $patterns; + public function __construct( + private TokenizerPatterns $patterns, + ) { } public function handle(Reader $reader, TokenStream $stream): bool diff --git a/Parser/Handler/StringHandler.php b/Parser/Handler/StringHandler.php index 5fd44df..28bc457 100644 --- a/Parser/Handler/StringHandler.php +++ b/Parser/Handler/StringHandler.php @@ -31,13 +31,10 @@ */ class StringHandler implements HandlerInterface { - private TokenizerPatterns $patterns; - private TokenizerEscaping $escaping; - - public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping) - { - $this->patterns = $patterns; - $this->escaping = $escaping; + public function __construct( + private TokenizerPatterns $patterns, + private TokenizerEscaping $escaping, + ) { } public function handle(Reader $reader, TokenStream $stream): bool diff --git a/Parser/Reader.php b/Parser/Reader.php index a280a48..b68d02f 100644 --- a/Parser/Reader.php +++ b/Parser/Reader.php @@ -23,13 +23,12 @@ */ class Reader { - private string $source; private int $length; private int $position = 0; - public function __construct(string $source) - { - $this->source = $source; + public function __construct( + private string $source, + ) { $this->length = \strlen($source); } diff --git a/Parser/Token.php b/Parser/Token.php index 3e926c7..73f7234 100644 --- a/Parser/Token.php +++ b/Parser/Token.php @@ -31,15 +31,11 @@ class Token public const TYPE_NUMBER = 'number'; public const TYPE_STRING = 'string'; - private ?string $type; - private ?string $value; - private ?int $position; - - public function __construct(?string $type, ?string $value, ?int $position) - { - $this->type = $type; - $this->value = $value; - $this->position = $position; + public function __construct( + private ?string $type, + private ?string $value, + private ?int $position, + ) { } public function getType(): ?int diff --git a/Parser/Tokenizer/TokenizerEscaping.php b/Parser/Tokenizer/TokenizerEscaping.php index 8c4b9f7..bb504e4 100644 --- a/Parser/Tokenizer/TokenizerEscaping.php +++ b/Parser/Tokenizer/TokenizerEscaping.php @@ -23,11 +23,9 @@ */ class TokenizerEscaping { - private TokenizerPatterns $patterns; - - public function __construct(TokenizerPatterns $patterns) - { - $this->patterns = $patterns; + public function __construct( + private TokenizerPatterns $patterns, + ) { } public function escapeUnicode(string $value): string diff --git a/XPath/Extension/NodeExtension.php b/XPath/Extension/NodeExtension.php index 49e894a..f2cd617 100644 --- a/XPath/Extension/NodeExtension.php +++ b/XPath/Extension/NodeExtension.php @@ -31,11 +31,9 @@ class NodeExtension extends AbstractExtension public const ATTRIBUTE_NAME_IN_LOWER_CASE = 2; public const ATTRIBUTE_VALUE_IN_LOWER_CASE = 4; - private int $flags; - - public function __construct(int $flags = 0) - { - $this->flags = $flags; + public function __construct( + private int $flags = 0, + ) { } /** diff --git a/XPath/XPathExpr.php b/XPath/XPathExpr.php index 29f0c8a..14f63ef 100644 --- a/XPath/XPathExpr.php +++ b/XPath/XPathExpr.php @@ -23,16 +23,12 @@ */ class XPathExpr { - private string $path; - private string $element; - private string $condition; - - public function __construct(string $path = '', string $element = '*', string $condition = '', bool $starPrefix = false) - { - $this->path = $path; - $this->element = $element; - $this->condition = $condition; - + public function __construct( + private string $path = '', + private string $element = '*', + private string $condition = '', + bool $starPrefix = false, + ) { if ($starPrefix) { $this->addStarPrefix(); } From ce566cad89416dedb8667dbcea57b8c8551658e6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 3 Feb 2024 18:41:27 +0100 Subject: [PATCH 5/9] Fxi markdown --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c035d6b..d452500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ CHANGELOG ========= 6.3 ------ +--- * Add support for `:scope` From 8e8616b51c35678d3bd439812f556277e9fa9460 Mon Sep 17 00:00:00 2001 From: "hubert.lenoir" Date: Wed, 28 Dec 2022 09:59:18 +0100 Subject: [PATCH 6/9] [CssSelector] add support for :is() and :where() --- CHANGELOG.md | 8 ++- Node/MatchingNode.php | 55 ++++++++++++++++++++ Node/SpecificityAdjustmentNode.php | 49 +++++++++++++++++ Parser/Parser.php | 44 ++++++++++++---- Tests/Node/MatchingNodeTest.php | 48 +++++++++++++++++ Tests/Node/SpecificityAdjustmentNodeTest.php | 44 ++++++++++++++++ Tests/Parser/ParserTest.php | 17 ++++++ Tests/XPath/TranslatorTest.php | 14 +++++ XPath/Extension/NodeExtension.php | 32 ++++++++++++ XPath/XPathExpr.php | 4 +- 10 files changed, 303 insertions(+), 12 deletions(-) create mode 100644 Node/MatchingNode.php create mode 100644 Node/SpecificityAdjustmentNode.php create mode 100644 Tests/Node/MatchingNodeTest.php create mode 100644 Tests/Node/SpecificityAdjustmentNodeTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c035d6b..1c8c6cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ CHANGELOG ========= +7.1 +--- + +* Add support for `:is()` +* Add support for `:where()` + 6.3 ------ +--- * Add support for `:scope` diff --git a/Node/MatchingNode.php b/Node/MatchingNode.php new file mode 100644 index 0000000..381ac45 --- /dev/null +++ b/Node/MatchingNode.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a ":is()" node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Hubert Lenoir + * + * @internal + */ +class MatchingNode extends AbstractNode +{ + /** + * @param array $arguments + */ + public function __construct( + public readonly NodeInterface $selector, + public readonly array $arguments = [], + ) { + } + + public function getSpecificity(): Specificity + { + $argumentsSpecificity = array_reduce( + $this->arguments, + fn ($c, $n) => 1 === $n->getSpecificity()->compareTo($c) ? $n->getSpecificity() : $c, + new Specificity(0, 0, 0), + ); + + return $this->selector->getSpecificity()->plus($argumentsSpecificity); + } + + public function __toString(): string + { + $selectorArguments = array_map( + fn ($n): string => ltrim((string) $n, '*'), + $this->arguments, + ); + + return sprintf('%s[%s:is(%s)]', $this->getNodeName(), $this->selector, implode(', ', $selectorArguments)); + } +} diff --git a/Node/SpecificityAdjustmentNode.php b/Node/SpecificityAdjustmentNode.php new file mode 100644 index 0000000..d49ed4c --- /dev/null +++ b/Node/SpecificityAdjustmentNode.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a ":where()" node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Hubert Lenoir + * + * @internal + */ +class SpecificityAdjustmentNode extends AbstractNode +{ + /** + * @param array $arguments + */ + public function __construct( + public readonly NodeInterface $selector, + public readonly array $arguments = [], + ) { + } + + public function getSpecificity(): Specificity + { + return $this->selector->getSpecificity(); + } + + public function __toString(): string + { + $selectorArguments = array_map( + fn ($n) => ltrim((string) $n, '*'), + $this->arguments, + ); + + return sprintf('%s[%s:where(%s)]', $this->getNodeName(), $this->selector, implode(', ', $selectorArguments)); + } +} diff --git a/Parser/Parser.php b/Parser/Parser.php index 5313d34..462714c 100644 --- a/Parser/Parser.php +++ b/Parser/Parser.php @@ -87,13 +87,17 @@ public static function parseSeries(array $tokens): array ]; } - private function parseSelectorList(TokenStream $stream): array + private function parseSelectorList(TokenStream $stream, bool $isArgument = false): array { $stream->skipWhitespace(); $selectors = []; while (true) { - $selectors[] = $this->parserSelectorNode($stream); + if ($isArgument && $stream->getPeek()->isDelimiter([')'])) { + break; + } + + $selectors[] = $this->parserSelectorNode($stream, $isArgument); if ($stream->getPeek()->isDelimiter([','])) { $stream->getNext(); @@ -106,15 +110,19 @@ private function parseSelectorList(TokenStream $stream): array return $selectors; } - private function parserSelectorNode(TokenStream $stream): Node\SelectorNode + private function parserSelectorNode(TokenStream $stream, bool $isArgument = false): Node\SelectorNode { - [$result, $pseudoElement] = $this->parseSimpleSelector($stream); + [$result, $pseudoElement] = $this->parseSimpleSelector($stream, false, $isArgument); while (true) { $stream->skipWhitespace(); $peek = $stream->getPeek(); - if ($peek->isFileEnd() || $peek->isDelimiter([','])) { + if ( + $peek->isFileEnd() + || $peek->isDelimiter([',']) + || ($isArgument && $peek->isDelimiter([')'])) + ) { break; } @@ -129,7 +137,7 @@ private function parserSelectorNode(TokenStream $stream): Node\SelectorNode $combinator = ' '; } - [$nextSelector, $pseudoElement] = $this->parseSimpleSelector($stream); + [$nextSelector, $pseudoElement] = $this->parseSimpleSelector($stream, false, $isArgument); $result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector); } @@ -141,7 +149,7 @@ private function parserSelectorNode(TokenStream $stream): Node\SelectorNode * * @throws SyntaxErrorException */ - private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = false): array + private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = false, bool $isArgument = false): array { $stream->skipWhitespace(); @@ -154,7 +162,7 @@ private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = if ($peek->isWhitespace() || $peek->isFileEnd() || $peek->isDelimiter([',', '+', '>', '~']) - || ($insideNegation && $peek->isDelimiter([')'])) + || ($isArgument && $peek->isDelimiter([')'])) ) { break; } @@ -215,7 +223,7 @@ private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = throw SyntaxErrorException::nestedNot(); } - [$argument, $argumentPseudoElement] = $this->parseSimpleSelector($stream, true); + [$argument, $argumentPseudoElement] = $this->parseSimpleSelector($stream, true, true); $next = $stream->getNext(); if (null !== $argumentPseudoElement) { @@ -227,6 +235,24 @@ private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = } $result = new Node\NegationNode($result, $argument); + } elseif ('is' === strtolower($identifier)) { + $selectors = $this->parseSelectorList($stream, true); + + $next = $stream->getNext(); + if (!$next->isDelimiter([')'])) { + throw SyntaxErrorException::unexpectedToken('")"', $next); + } + + $result = new Node\MatchingNode($result, $selectors); + } elseif ('where' === strtolower($identifier)) { + $selectors = $this->parseSelectorList($stream, true); + + $next = $stream->getNext(); + if (!$next->isDelimiter([')'])) { + throw SyntaxErrorException::unexpectedToken('")"', $next); + } + + $result = new Node\SpecificityAdjustmentNode($result, $selectors); } else { $arguments = []; $next = null; diff --git a/Tests/Node/MatchingNodeTest.php b/Tests/Node/MatchingNodeTest.php new file mode 100644 index 0000000..0bc718c --- /dev/null +++ b/Tests/Node/MatchingNodeTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Tests\Node; + +use Symfony\Component\CssSelector\Node\ClassNode; +use Symfony\Component\CssSelector\Node\ElementNode; +use Symfony\Component\CssSelector\Node\HashNode; +use Symfony\Component\CssSelector\Node\MatchingNode; + +class MatchingNodeTest extends AbstractNodeTestCase +{ + public static function getToStringConversionTestData() + { + return [ + [new MatchingNode(new ElementNode(), [ + new ClassNode(new ElementNode(), 'class'), + new HashNode(new ElementNode(), 'id'), + ]), 'Matching[Element[*]:is(Class[Element[*].class], Hash[Element[*]#id])]'], + ]; + } + + public static function getSpecificityValueTestData() + { + return [ + [new MatchingNode(new ElementNode(), [ + new ClassNode(new ElementNode(), 'class'), + new HashNode(new ElementNode(), 'id'), + ]), 100], + [new MatchingNode(new ClassNode(new ElementNode(), 'class'), [ + new ClassNode(new ElementNode(), 'class'), + new HashNode(new ElementNode(), 'id'), + ]), 110], + [new MatchingNode(new HashNode(new ElementNode(), 'id'), [ + new ClassNode(new ElementNode(), 'class'), + new HashNode(new ElementNode(), 'id'), + ]), 200], + ]; + } +} diff --git a/Tests/Node/SpecificityAdjustmentNodeTest.php b/Tests/Node/SpecificityAdjustmentNodeTest.php new file mode 100644 index 0000000..5c83057 --- /dev/null +++ b/Tests/Node/SpecificityAdjustmentNodeTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Tests\Node; + +use Symfony\Component\CssSelector\Node\ClassNode; +use Symfony\Component\CssSelector\Node\ElementNode; +use Symfony\Component\CssSelector\Node\HashNode; +use Symfony\Component\CssSelector\Node\SpecificityAdjustmentNode; + +class SpecificityAdjustmentNodeTest extends AbstractNodeTestCase +{ + public static function getToStringConversionTestData() + { + return [ + [new SpecificityAdjustmentNode(new ElementNode(), [ + new ClassNode(new ElementNode(), 'class'), + new HashNode(new ElementNode(), 'id'), + ]), 'SpecificityAdjustment[Element[*]:where(Class[Element[*].class], Hash[Element[*]#id])]'], + ]; + } + + public static function getSpecificityValueTestData() + { + return [ + [new SpecificityAdjustmentNode(new ElementNode(), [ + new ClassNode(new ElementNode(), 'class'), + new HashNode(new ElementNode(), 'id'), + ]), 0], + [new SpecificityAdjustmentNode(new ClassNode(new ElementNode(), 'class'), [ + new ClassNode(new ElementNode(), 'class'), + new HashNode(new ElementNode(), 'id'), + ]), 10], + ]; + } +} diff --git a/Tests/Parser/ParserTest.php b/Tests/Parser/ParserTest.php index a8708ce..509f6e3 100644 --- a/Tests/Parser/ParserTest.php +++ b/Tests/Parser/ParserTest.php @@ -152,6 +152,10 @@ public static function getParserTestData() [':scope', ['Pseudo[Element[*]:scope]']], ['foo bar, :scope > div', ['CombinedSelector[Element[foo] Element[bar]]', 'CombinedSelector[Pseudo[Element[*]:scope] > Element[div]]']], ['foo bar,:scope > div', ['CombinedSelector[Element[foo] Element[bar]]', 'CombinedSelector[Pseudo[Element[*]:scope] > Element[div]]']], + ['div:is(.foo, #bar)', ['Matching[Element[div]:is(Selector[Class[Element[*].foo]], Selector[Hash[Element[*]#bar]])]']], + [':is(:hover, :visited)', ['Matching[Element[*]:is(Selector[Pseudo[Element[*]:hover]], Selector[Pseudo[Element[*]:visited]])]']], + ['div:where(.foo, #bar)', ['SpecificityAdjustment[Element[div]:where(Selector[Class[Element[*].foo]], Selector[Hash[Element[*]#bar]])]']], + [':where(:hover, :visited)', ['SpecificityAdjustment[Element[*]:where(Selector[Pseudo[Element[*]:hover]], Selector[Pseudo[Element[*]:visited]])]']], ]; } @@ -183,6 +187,7 @@ public static function getParserExceptionTestData() [':contains("foo', SyntaxErrorException::unclosedString(10)->getMessage()], ['foo!', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '!', 3))->getMessage()], [':scope > div :scope header', SyntaxErrorException::notAtTheStartOfASelector('scope')->getMessage()], + [':not(:not(a))', SyntaxErrorException::nestedNot()->getMessage()], ]; } @@ -233,6 +238,18 @@ public static function getSpecificityTestData() ['foo::before', 2], ['foo:empty::before', 12], ['#lorem + foo#ipsum:first-child > bar:first-line', 213], + [':is(*)', 0], + [':is(foo)', 1], + [':is(.foo)', 10], + [':is(#foo)', 100], + [':is(#foo, :empty, foo)', 100], + ['#foo:is(#bar:empty)', 210], + [':where(*)', 0], + [':where(foo)', 0], + [':where(.foo)', 0], + [':where(#foo)', 0], + [':where(#foo, :empty, foo)', 0], + ['#foo:where(#bar:empty)', 100], ]; } diff --git a/Tests/XPath/TranslatorTest.php b/Tests/XPath/TranslatorTest.php index bfb9072..55a2b10 100644 --- a/Tests/XPath/TranslatorTest.php +++ b/Tests/XPath/TranslatorTest.php @@ -221,6 +221,8 @@ public static function getCssToXPathTestData() ['div#container p', "div[@id = 'container']/descendant-or-self::*/p"], [':scope > div[dataimg=""]', "*[1]/div[@dataimg = '']"], [':scope', '*[1]'], + ['e:is(section, article) h1', "e[(name() = 'section') or (name() = 'article')]/descendant-or-self::*/h1"], + ['e:where(section, article) h1', "e[(name() = 'section') or (name() = 'article')]/descendant-or-self::*/h1"], ]; } @@ -355,6 +357,17 @@ public static function getHtmlIdsTestData() [':not(*)', []], ['a:not([href])', ['name-anchor']], ['ol :Not(li[class])', ['first-li', 'second-li', 'li-div', 'fifth-li', 'sixth-li', 'seventh-li']], + [':is(#first-li, #second-li)', ['first-li', 'second-li']], + ['a:is(#name-anchor, #tag-anchor)', ['name-anchor', 'tag-anchor']], + [':is(.c)', ['first-ol', 'third-li', 'fourth-li']], + ['a:is(:not(#name-anchor))', ['tag-anchor', 'nofollow-anchor']], + ['a:not(:is(#name-anchor))', ['tag-anchor', 'nofollow-anchor']], + [':where(#first-li, #second-li)', ['first-li', 'second-li']], + ['a:where(#name-anchor, #tag-anchor)', ['name-anchor', 'tag-anchor']], + [':where(.c)', ['first-ol', 'third-li', 'fourth-li']], + ['a:where(:not(#name-anchor))', ['tag-anchor', 'nofollow-anchor']], + ['a:not(:where(#name-anchor))', ['tag-anchor', 'nofollow-anchor']], + ['a:where(:is(#name-anchor), :where(#tag-anchor))', ['name-anchor', 'tag-anchor']], // HTML-specific [':link', ['link-href', 'tag-anchor', 'nofollow-anchor', 'area-href']], [':visited', []], @@ -416,6 +429,7 @@ public static function getHtmlShakespearTestData() [':scope > div', 1], [':scope > div > div[class=dialog]', 1], [':scope > div div', 242], + ['div:is(div#test .dialog) .direction', 4], ]; } } diff --git a/XPath/Extension/NodeExtension.php b/XPath/Extension/NodeExtension.php index 49e894a..174d009 100644 --- a/XPath/Extension/NodeExtension.php +++ b/XPath/Extension/NodeExtension.php @@ -65,6 +65,8 @@ public function getNodeTranslators(): array 'Selector' => $this->translateSelector(...), 'CombinedSelector' => $this->translateCombinedSelector(...), 'Negation' => $this->translateNegation(...), + 'Matching' => $this->translateMatching(...), + 'SpecificityAdjustment' => $this->translateSpecificityAdjustment(...), 'Function' => $this->translateFunction(...), 'Pseudo' => $this->translatePseudo(...), 'Attribute' => $this->translateAttribute(...), @@ -97,6 +99,36 @@ public function translateNegation(Node\NegationNode $node, Translator $translato return $xpath->addCondition('0'); } + public function translateMatching(Node\MatchingNode $node, Translator $translator): XPathExpr + { + $xpath = $translator->nodeToXPath($node->selector); + + foreach ($node->arguments as $argument) { + $expr = $translator->nodeToXPath($argument); + $expr->addNameTest(); + if ($condition = $expr->getCondition()) { + $xpath->addCondition($condition, 'or'); + } + } + + return $xpath; + } + + public function translateSpecificityAdjustment(Node\SpecificityAdjustmentNode $node, Translator $translator): XPathExpr + { + $xpath = $translator->nodeToXPath($node->selector); + + foreach ($node->arguments as $argument) { + $expr = $translator->nodeToXPath($argument); + $expr->addNameTest(); + if ($condition = $expr->getCondition()) { + $xpath->addCondition($condition, 'or'); + } + } + + return $xpath; + } + public function translateFunction(Node\FunctionNode $node, Translator $translator): XPathExpr { $xpath = $translator->nodeToXPath($node->getSelector()); diff --git a/XPath/XPathExpr.php b/XPath/XPathExpr.php index a76e30b..8cde461 100644 --- a/XPath/XPathExpr.php +++ b/XPath/XPathExpr.php @@ -46,9 +46,9 @@ public function getElement(): string /** * @return $this */ - public function addCondition(string $condition): static + public function addCondition(string $condition, string $operator = 'and'): static { - $this->condition = $this->condition ? sprintf('(%s) and (%s)', $this->condition, $condition) : $condition; + $this->condition = $this->condition ? sprintf('(%s) %s (%s)', $this->condition, $operator, $condition) : $condition; return $this; } From f5e2522186b8d2c094a94b3af1ce33afac23f13a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 3 Feb 2024 21:05:29 +0100 Subject: [PATCH 7/9] fix markdown --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c8c6cf..d2b7fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ CHANGELOG 7.1 --- -* Add support for `:is()` -* Add support for `:where()` + * Add support for `:is()` + * Add support for `:where()` 6.3 --- From ea43887e9afd2029509662d4f95e8b5ef6fc9bbb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 31 May 2024 16:33:22 +0200 Subject: [PATCH 8/9] Revert "minor #54653 Auto-close PRs on subtree-splits (nicolas-grekas)" This reverts commit 2c9352dd91ebaf37b8a3e3c26fd8e1306df2fb73, reversing changes made to 18c3e87f1512be2cc50e90235b144b13bc347258. --- .gitattributes | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 8 ----- .github/workflows/check-subtree-split.yml | 37 ----------------------- 3 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/workflows/check-subtree-split.yml diff --git a/.gitattributes b/.gitattributes index 14c3c35..84c7add 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.git* export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 4689c4d..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,8 +0,0 @@ -Please do not submit any Pull Requests here. They will be closed. ---- - -Please submit your PR here instead: -https://github.com/symfony/symfony - -This repository is what we call a "subtree split": a read-only subset of that main repository. -We're looking forward to your PR there! diff --git a/.github/workflows/check-subtree-split.yml b/.github/workflows/check-subtree-split.yml deleted file mode 100644 index 16be48b..0000000 --- a/.github/workflows/check-subtree-split.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Check subtree split - -on: - pull_request_target: - -jobs: - close-pull-request: - runs-on: ubuntu-latest - - steps: - - name: Close pull request - uses: actions/github-script@v6 - with: - script: | - if (context.repo.owner === "symfony") { - github.rest.issues.createComment({ - owner: "symfony", - repo: context.repo.repo, - issue_number: context.issue.number, - body: ` - Thanks for your Pull Request! We love contributions. - - However, you should instead open your PR on the main repository: - https://github.com/symfony/symfony - - This repository is what we call a "subtree split": a read-only subset of that main repository. - We're looking forward to your PR there! - ` - }); - - github.rest.pulls.update({ - owner: "symfony", - repo: context.repo.repo, - pull_number: context.issue.number, - state: "closed" - }); - } From 4f7f3c35fba88146b56d0025d20ace3f3901f097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Sep 2024 11:24:18 +0200 Subject: [PATCH 9/9] Add PR template and auto-close PR on subtree split repositories --- .gitattributes | 3 +-- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ .github/workflows/close-pull-request.yml | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/close-pull-request.yml diff --git a/.gitattributes b/.gitattributes index 84c7add..14c3c35 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4689c4d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000..e55b478 --- /dev/null +++ b/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy