From 5cd8af561c3703d6ec71120ef3a3839e59416781 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Mon, 20 Nov 2023 19:32:45 +0100 Subject: [PATCH 01/10] [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 02/10] 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 03/10] 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 04/10] [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 05/10] 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 06/10] [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 07/10] 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 84c06b5eaf5c2cefd2dd612aba4df1af05813815 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 20 Jun 2024 17:52:34 +0200 Subject: [PATCH 08/10] Prefix all sprintf() calls --- Exception/SyntaxErrorException.php | 8 ++++---- Node/AttributeNode.php | 4 ++-- Node/ClassNode.php | 2 +- Node/CombinedSelectorNode.php | 2 +- Node/ElementNode.php | 2 +- Node/FunctionNode.php | 2 +- Node/HashNode.php | 2 +- Node/MatchingNode.php | 2 +- Node/NegationNode.php | 2 +- Node/PseudoNode.php | 2 +- Node/SelectorNode.php | 2 +- Node/SpecificityAdjustmentNode.php | 2 +- Parser/Handler/StringHandler.php | 2 +- Parser/Token.php | 4 ++-- Parser/Tokenizer/TokenizerPatterns.php | 2 +- Tests/Parser/ParserTest.php | 4 ++-- XPath/Extension/AttributeMatchingExtension.php | 14 +++++++------- XPath/Extension/FunctionExtension.php | 10 +++++----- XPath/Extension/HtmlExtension.php | 2 +- XPath/Extension/NodeExtension.php | 8 ++++---- XPath/Extension/PseudoClassExtension.php | 2 +- XPath/Translator.php | 16 ++++++++-------- XPath/XPathExpr.php | 2 +- 23 files changed, 49 insertions(+), 49 deletions(-) diff --git a/Exception/SyntaxErrorException.php b/Exception/SyntaxErrorException.php index 5a9d807..52d8259 100644 --- a/Exception/SyntaxErrorException.php +++ b/Exception/SyntaxErrorException.php @@ -25,17 +25,17 @@ class SyntaxErrorException extends ParseException { public static function unexpectedToken(string $expectedValue, Token $foundToken): self { - return new self(sprintf('Expected %s, but %s found.', $expectedValue, $foundToken)); + return new self(\sprintf('Expected %s, but %s found.', $expectedValue, $foundToken)); } public static function pseudoElementFound(string $pseudoElement, string $unexpectedLocation): self { - return new self(sprintf('Unexpected pseudo-element "::%s" found %s.', $pseudoElement, $unexpectedLocation)); + return new self(\sprintf('Unexpected pseudo-element "::%s" found %s.', $pseudoElement, $unexpectedLocation)); } public static function unclosedString(int $position): self { - return new self(sprintf('Unclosed/invalid string at %s.', $position)); + return new self(\sprintf('Unclosed/invalid string at %s.', $position)); } public static function nestedNot(): self @@ -45,7 +45,7 @@ public static function nestedNot(): self public static function notAtTheStartOfASelector(string $pseudoElement): self { - return new self(sprintf('Got immediate child pseudo-element ":%s" not at the start of a selector', $pseudoElement)); + return new self(\sprintf('Got immediate child pseudo-element ":%s" not at the start of a selector', $pseudoElement)); } public static function stringAsFunctionArgument(): self diff --git a/Node/AttributeNode.php b/Node/AttributeNode.php index 41b1787..9bcb3a4 100644 --- a/Node/AttributeNode.php +++ b/Node/AttributeNode.php @@ -67,7 +67,7 @@ public function __toString(): string $attribute = $this->namespace ? $this->namespace.'|'.$this->attribute : $this->attribute; return 'exists' === $this->operator - ? sprintf('%s[%s[%s]]', $this->getNodeName(), $this->selector, $attribute) - : sprintf("%s[%s[%s %s '%s']]", $this->getNodeName(), $this->selector, $attribute, $this->operator, $this->value); + ? \sprintf('%s[%s[%s]]', $this->getNodeName(), $this->selector, $attribute) + : \sprintf("%s[%s[%s %s '%s']]", $this->getNodeName(), $this->selector, $attribute, $this->operator, $this->value); } } diff --git a/Node/ClassNode.php b/Node/ClassNode.php index 2b488c7..e9862c3 100644 --- a/Node/ClassNode.php +++ b/Node/ClassNode.php @@ -46,6 +46,6 @@ public function getSpecificity(): Specificity public function __toString(): string { - return sprintf('%s[%s.%s]', $this->getNodeName(), $this->selector, $this->name); + return \sprintf('%s[%s.%s]', $this->getNodeName(), $this->selector, $this->name); } } diff --git a/Node/CombinedSelectorNode.php b/Node/CombinedSelectorNode.php index fead5e5..78a2fd3 100644 --- a/Node/CombinedSelectorNode.php +++ b/Node/CombinedSelectorNode.php @@ -54,6 +54,6 @@ public function __toString(): string { $combinator = ' ' === $this->combinator ? '' : $this->combinator; - return sprintf('%s[%s %s %s]', $this->getNodeName(), $this->selector, $combinator, $this->subSelector); + return \sprintf('%s[%s %s %s]', $this->getNodeName(), $this->selector, $combinator, $this->subSelector); } } diff --git a/Node/ElementNode.php b/Node/ElementNode.php index 366681f..9bfbd08 100644 --- a/Node/ElementNode.php +++ b/Node/ElementNode.php @@ -48,6 +48,6 @@ public function __toString(): string { $element = $this->element ?: '*'; - return sprintf('%s[%s]', $this->getNodeName(), $this->namespace ? $this->namespace.'|'.$element : $element); + return \sprintf('%s[%s]', $this->getNodeName(), $this->namespace ? $this->namespace.'|'.$element : $element); } } diff --git a/Node/FunctionNode.php b/Node/FunctionNode.php index 5a04d63..de44600 100644 --- a/Node/FunctionNode.php +++ b/Node/FunctionNode.php @@ -65,6 +65,6 @@ public function __toString(): string { $arguments = implode(', ', array_map(fn (Token $token) => "'".$token->getValue()."'", $this->arguments)); - return sprintf('%s[%s:%s(%s)]', $this->getNodeName(), $this->selector, $this->name, $arguments ? '['.$arguments.']' : ''); + return \sprintf('%s[%s:%s(%s)]', $this->getNodeName(), $this->selector, $this->name, $arguments ? '['.$arguments.']' : ''); } } diff --git a/Node/HashNode.php b/Node/HashNode.php index 97e51d6..b3fb3c9 100644 --- a/Node/HashNode.php +++ b/Node/HashNode.php @@ -46,6 +46,6 @@ public function getSpecificity(): Specificity public function __toString(): string { - return sprintf('%s[%s#%s]', $this->getNodeName(), $this->selector, $this->id); + return \sprintf('%s[%s#%s]', $this->getNodeName(), $this->selector, $this->id); } } diff --git a/Node/MatchingNode.php b/Node/MatchingNode.php index 381ac45..9b59503 100644 --- a/Node/MatchingNode.php +++ b/Node/MatchingNode.php @@ -50,6 +50,6 @@ public function __toString(): string $this->arguments, ); - return sprintf('%s[%s:is(%s)]', $this->getNodeName(), $this->selector, implode(', ', $selectorArguments)); + return \sprintf('%s[%s:is(%s)]', $this->getNodeName(), $this->selector, implode(', ', $selectorArguments)); } } diff --git a/Node/NegationNode.php b/Node/NegationNode.php index c4c251d..c14c33d 100644 --- a/Node/NegationNode.php +++ b/Node/NegationNode.php @@ -46,6 +46,6 @@ public function getSpecificity(): Specificity public function __toString(): string { - return sprintf('%s[%s:not(%s)]', $this->getNodeName(), $this->selector, $this->subSelector); + return \sprintf('%s[%s:not(%s)]', $this->getNodeName(), $this->selector, $this->subSelector); } } diff --git a/Node/PseudoNode.php b/Node/PseudoNode.php index dbf2f85..d1082e8 100644 --- a/Node/PseudoNode.php +++ b/Node/PseudoNode.php @@ -49,6 +49,6 @@ public function getSpecificity(): Specificity public function __toString(): string { - return sprintf('%s[%s:%s]', $this->getNodeName(), $this->selector, $this->identifier); + return \sprintf('%s[%s:%s]', $this->getNodeName(), $this->selector, $this->identifier); } } diff --git a/Node/SelectorNode.php b/Node/SelectorNode.php index aebe502..f36e54c 100644 --- a/Node/SelectorNode.php +++ b/Node/SelectorNode.php @@ -49,6 +49,6 @@ public function getSpecificity(): Specificity public function __toString(): string { - return sprintf('%s[%s%s]', $this->getNodeName(), $this->tree, $this->pseudoElement ? '::'.$this->pseudoElement : ''); + return \sprintf('%s[%s%s]', $this->getNodeName(), $this->tree, $this->pseudoElement ? '::'.$this->pseudoElement : ''); } } diff --git a/Node/SpecificityAdjustmentNode.php b/Node/SpecificityAdjustmentNode.php index d49ed4c..58659cc 100644 --- a/Node/SpecificityAdjustmentNode.php +++ b/Node/SpecificityAdjustmentNode.php @@ -44,6 +44,6 @@ public function __toString(): string $this->arguments, ); - return sprintf('%s[%s:where(%s)]', $this->getNodeName(), $this->selector, implode(', ', $selectorArguments)); + return \sprintf('%s[%s:where(%s)]', $this->getNodeName(), $this->selector, implode(', ', $selectorArguments)); } } diff --git a/Parser/Handler/StringHandler.php b/Parser/Handler/StringHandler.php index 28bc457..5e00eda 100644 --- a/Parser/Handler/StringHandler.php +++ b/Parser/Handler/StringHandler.php @@ -49,7 +49,7 @@ public function handle(Reader $reader, TokenStream $stream): bool $match = $reader->findPattern($this->patterns->getQuotedStringPattern($quote)); if (!$match) { - throw new InternalErrorException(sprintf('Should have found at least an empty match at %d.', $reader->getPosition())); + throw new InternalErrorException(\sprintf('Should have found at least an empty match at %d.', $reader->getPosition())); } // check unclosed strings diff --git a/Parser/Token.php b/Parser/Token.php index 73f7234..5bfb8d4 100644 --- a/Parser/Token.php +++ b/Parser/Token.php @@ -99,9 +99,9 @@ public function isString(): bool public function __toString(): string { if ($this->value) { - return sprintf('<%s "%s" at %s>', $this->type, $this->value, $this->position); + return \sprintf('<%s "%s" at %s>', $this->type, $this->value, $this->position); } - return sprintf('<%s at %s>', $this->type, $this->position); + return \sprintf('<%s at %s>', $this->type, $this->position); } } diff --git a/Parser/Tokenizer/TokenizerPatterns.php b/Parser/Tokenizer/TokenizerPatterns.php index 3c77cf0..1825bbf 100644 --- a/Parser/Tokenizer/TokenizerPatterns.php +++ b/Parser/Tokenizer/TokenizerPatterns.php @@ -84,6 +84,6 @@ public function getNumberPattern(): string public function getQuotedStringPattern(string $quote): string { - return '~^'.sprintf($this->quotedStringPattern, $quote).'~i'; + return '~^'.\sprintf($this->quotedStringPattern, $quote).'~i'; } } diff --git a/Tests/Parser/ParserTest.php b/Tests/Parser/ParserTest.php index 509f6e3..82de5ab 100644 --- a/Tests/Parser/ParserTest.php +++ b/Tests/Parser/ParserTest.php @@ -70,7 +70,7 @@ public function testSpecificity($source, $value) public function testParseSeries($series, $a, $b) { $parser = new Parser(); - $selectors = $parser->parse(sprintf(':nth-child(%s)', $series)); + $selectors = $parser->parse(\sprintf(':nth-child(%s)', $series)); $this->assertCount(1, $selectors); /** @var FunctionNode $function */ @@ -82,7 +82,7 @@ public function testParseSeries($series, $a, $b) public function testParseSeriesException($series) { $parser = new Parser(); - $selectors = $parser->parse(sprintf(':nth-child(%s)', $series)); + $selectors = $parser->parse(\sprintf(':nth-child(%s)', $series)); $this->assertCount(1, $selectors); /** @var FunctionNode $function */ diff --git a/XPath/Extension/AttributeMatchingExtension.php b/XPath/Extension/AttributeMatchingExtension.php index 3c785e9..28a16c1 100644 --- a/XPath/Extension/AttributeMatchingExtension.php +++ b/XPath/Extension/AttributeMatchingExtension.php @@ -47,12 +47,12 @@ public function translateExists(XPathExpr $xpath, string $attribute, ?string $va public function translateEquals(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr { - return $xpath->addCondition(sprintf('%s = %s', $attribute, Translator::getXpathLiteral($value))); + return $xpath->addCondition(\sprintf('%s = %s', $attribute, Translator::getXpathLiteral($value))); } public function translateIncludes(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr { - return $xpath->addCondition($value ? sprintf( + return $xpath->addCondition($value ? \sprintf( '%1$s and contains(concat(\' \', normalize-space(%1$s), \' \'), %2$s)', $attribute, Translator::getXpathLiteral(' '.$value.' ') @@ -61,7 +61,7 @@ public function translateIncludes(XPathExpr $xpath, string $attribute, ?string $ public function translateDashMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr { - return $xpath->addCondition(sprintf( + return $xpath->addCondition(\sprintf( '%1$s and (%1$s = %2$s or starts-with(%1$s, %3$s))', $attribute, Translator::getXpathLiteral($value), @@ -71,7 +71,7 @@ public function translateDashMatch(XPathExpr $xpath, string $attribute, ?string public function translatePrefixMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr { - return $xpath->addCondition($value ? sprintf( + return $xpath->addCondition($value ? \sprintf( '%1$s and starts-with(%1$s, %2$s)', $attribute, Translator::getXpathLiteral($value) @@ -80,7 +80,7 @@ public function translatePrefixMatch(XPathExpr $xpath, string $attribute, ?strin public function translateSuffixMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr { - return $xpath->addCondition($value ? sprintf( + return $xpath->addCondition($value ? \sprintf( '%1$s and substring(%1$s, string-length(%1$s)-%2$s) = %3$s', $attribute, \strlen($value) - 1, @@ -90,7 +90,7 @@ public function translateSuffixMatch(XPathExpr $xpath, string $attribute, ?strin public function translateSubstringMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr { - return $xpath->addCondition($value ? sprintf( + return $xpath->addCondition($value ? \sprintf( '%1$s and contains(%1$s, %2$s)', $attribute, Translator::getXpathLiteral($value) @@ -99,7 +99,7 @@ public function translateSubstringMatch(XPathExpr $xpath, string $attribute, ?st public function translateDifferent(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr { - return $xpath->addCondition(sprintf( + return $xpath->addCondition(\sprintf( $value ? 'not(%1$s) or %1$s != %2$s' : '%s != %s', $attribute, Translator::getXpathLiteral($value) diff --git a/XPath/Extension/FunctionExtension.php b/XPath/Extension/FunctionExtension.php index 4b9d7bc..557e305 100644 --- a/XPath/Extension/FunctionExtension.php +++ b/XPath/Extension/FunctionExtension.php @@ -50,7 +50,7 @@ public function translateNthChild(XPathExpr $xpath, FunctionNode $function, bool try { [$a, $b] = Parser::parseSeries($function->getArguments()); } catch (SyntaxErrorException $e) { - throw new ExpressionErrorException(sprintf('Invalid series: "%s".', implode('", "', $function->getArguments())), 0, $e); + throw new ExpressionErrorException(\sprintf('Invalid series: "%s".', implode('", "', $function->getArguments())), 0, $e); } $xpath->addStarPrefix(); @@ -83,10 +83,10 @@ public function translateNthChild(XPathExpr $xpath, FunctionNode $function, bool $expr .= ' - '.$b; } - $conditions = [sprintf('%s %s 0', $expr, $sign)]; + $conditions = [\sprintf('%s %s 0', $expr, $sign)]; if (1 !== $a && -1 !== $a) { - $conditions[] = sprintf('(%s) mod %d = 0', $expr, $a); + $conditions[] = \sprintf('(%s) mod %d = 0', $expr, $a); } return $xpath->addCondition(implode(' and ', $conditions)); @@ -134,7 +134,7 @@ public function translateContains(XPathExpr $xpath, FunctionNode $function): XPa } } - return $xpath->addCondition(sprintf( + return $xpath->addCondition(\sprintf( 'contains(string(.), %s)', Translator::getXpathLiteral($arguments[0]->getValue()) )); @@ -152,7 +152,7 @@ public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathEx } } - return $xpath->addCondition(sprintf( + return $xpath->addCondition(\sprintf( 'lang(%s)', Translator::getXpathLiteral($arguments[0]->getValue()) )); diff --git a/XPath/Extension/HtmlExtension.php b/XPath/Extension/HtmlExtension.php index 4653add..b3bf132 100644 --- a/XPath/Extension/HtmlExtension.php +++ b/XPath/Extension/HtmlExtension.php @@ -142,7 +142,7 @@ public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathEx } } - return $xpath->addCondition(sprintf( + return $xpath->addCondition(\sprintf( 'ancestor-or-self::*[@lang][1][starts-with(concat(' ."translate(@%s, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '-')" .', %s)]', diff --git a/XPath/Extension/NodeExtension.php b/XPath/Extension/NodeExtension.php index 87b70df..4cd46fa 100644 --- a/XPath/Extension/NodeExtension.php +++ b/XPath/Extension/NodeExtension.php @@ -91,7 +91,7 @@ public function translateNegation(Node\NegationNode $node, Translator $translato $subXpath->addNameTest(); if ($subXpath->getCondition()) { - return $xpath->addCondition(sprintf('not(%s)', $subXpath->getCondition())); + return $xpath->addCondition(\sprintf('not(%s)', $subXpath->getCondition())); } return $xpath->addCondition('0'); @@ -151,11 +151,11 @@ public function translateAttribute(Node\AttributeNode $node, Translator $transla } if ($node->getNamespace()) { - $name = sprintf('%s:%s', $node->getNamespace(), $name); + $name = \sprintf('%s:%s', $node->getNamespace(), $name); $safe = $safe && $this->isSafeName($node->getNamespace()); } - $attribute = $safe ? '@'.$name : sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name)); + $attribute = $safe ? '@'.$name : \sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name)); $value = $node->getValue(); $xpath = $translator->nodeToXPath($node->getSelector()); @@ -196,7 +196,7 @@ public function translateElement(Node\ElementNode $node): XPathExpr } if ($node->getNamespace()) { - $element = sprintf('%s:%s', $node->getNamespace(), $element); + $element = \sprintf('%s:%s', $node->getNamespace(), $element); $safe = $safe && $this->isSafeName($node->getNamespace()); } diff --git a/XPath/Extension/PseudoClassExtension.php b/XPath/Extension/PseudoClassExtension.php index aada832..397f06f 100644 --- a/XPath/Extension/PseudoClassExtension.php +++ b/XPath/Extension/PseudoClassExtension.php @@ -107,7 +107,7 @@ public function translateOnlyOfType(XPathExpr $xpath): XPathExpr { $element = $xpath->getElement(); - return $xpath->addCondition(sprintf('count(preceding-sibling::%s)=0 and count(following-sibling::%s)=0', $element, $element)); + return $xpath->addCondition(\sprintf('count(preceding-sibling::%s)=0 and count(following-sibling::%s)=0', $element, $element)); } public function translateEmpty(XPathExpr $xpath): XPathExpr diff --git a/XPath/Translator.php b/XPath/Translator.php index 9e66ce7..b2623e5 100644 --- a/XPath/Translator.php +++ b/XPath/Translator.php @@ -75,7 +75,7 @@ public static function getXpathLiteral(string $element): string $parts = []; while (true) { if (false !== $pos = strpos($string, "'")) { - $parts[] = sprintf("'%s'", substr($string, 0, $pos)); + $parts[] = \sprintf("'%s'", substr($string, 0, $pos)); $parts[] = "\"'\""; $string = substr($string, $pos + 1); } else { @@ -84,7 +84,7 @@ public static function getXpathLiteral(string $element): string } } - return sprintf('concat(%s)', implode(', ', $parts)); + return \sprintf('concat(%s)', implode(', ', $parts)); } public function cssToXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string @@ -130,7 +130,7 @@ public function registerExtension(Extension\ExtensionInterface $extension): stat public function getExtension(string $name): Extension\ExtensionInterface { if (!isset($this->extensions[$name])) { - throw new ExpressionErrorException(sprintf('Extension "%s" not registered.', $name)); + throw new ExpressionErrorException(\sprintf('Extension "%s" not registered.', $name)); } return $this->extensions[$name]; @@ -152,7 +152,7 @@ public function registerParserShortcut(ParserInterface $shortcut): static public function nodeToXPath(NodeInterface $node): XPathExpr { if (!isset($this->nodeTranslators[$node->getNodeName()])) { - throw new ExpressionErrorException(sprintf('Node "%s" not supported.', $node->getNodeName())); + throw new ExpressionErrorException(\sprintf('Node "%s" not supported.', $node->getNodeName())); } return $this->nodeTranslators[$node->getNodeName()]($node, $this); @@ -164,7 +164,7 @@ public function nodeToXPath(NodeInterface $node): XPathExpr public function addCombination(string $combiner, NodeInterface $xpath, NodeInterface $combinedXpath): XPathExpr { if (!isset($this->combinationTranslators[$combiner])) { - throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner)); + throw new ExpressionErrorException(\sprintf('Combiner "%s" not supported.', $combiner)); } return $this->combinationTranslators[$combiner]($this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath)); @@ -176,7 +176,7 @@ public function addCombination(string $combiner, NodeInterface $xpath, NodeInter public function addFunction(XPathExpr $xpath, FunctionNode $function): XPathExpr { if (!isset($this->functionTranslators[$function->getName()])) { - throw new ExpressionErrorException(sprintf('Function "%s" not supported.', $function->getName())); + throw new ExpressionErrorException(\sprintf('Function "%s" not supported.', $function->getName())); } return $this->functionTranslators[$function->getName()]($xpath, $function); @@ -188,7 +188,7 @@ public function addFunction(XPathExpr $xpath, FunctionNode $function): XPathExpr public function addPseudoClass(XPathExpr $xpath, string $pseudoClass): XPathExpr { if (!isset($this->pseudoClassTranslators[$pseudoClass])) { - throw new ExpressionErrorException(sprintf('Pseudo-class "%s" not supported.', $pseudoClass)); + throw new ExpressionErrorException(\sprintf('Pseudo-class "%s" not supported.', $pseudoClass)); } return $this->pseudoClassTranslators[$pseudoClass]($xpath); @@ -200,7 +200,7 @@ public function addPseudoClass(XPathExpr $xpath, string $pseudoClass): XPathExpr public function addAttributeMatching(XPathExpr $xpath, string $operator, string $attribute, ?string $value): XPathExpr { if (!isset($this->attributeMatchingTranslators[$operator])) { - throw new ExpressionErrorException(sprintf('Attribute matcher operator "%s" not supported.', $operator)); + throw new ExpressionErrorException(\sprintf('Attribute matcher operator "%s" not supported.', $operator)); } return $this->attributeMatchingTranslators[$operator]($xpath, $attribute, $value); diff --git a/XPath/XPathExpr.php b/XPath/XPathExpr.php index ceccab6..a148feb 100644 --- a/XPath/XPathExpr.php +++ b/XPath/XPathExpr.php @@ -44,7 +44,7 @@ public function getElement(): string */ public function addCondition(string $condition, string $operator = 'and'): static { - $this->condition = $this->condition ? sprintf('(%s) %s (%s)', $this->condition, $operator, $condition) : $condition; + $this->condition = $this->condition ? \sprintf('(%s) %s (%s)', $this->condition, $operator, $condition) : $condition; return $this; } From 8198536854262560fce7695308daf7fb1217a535 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 6 Jul 2024 09:57:16 +0200 Subject: [PATCH 09/10] Update .gitattributes --- .gitattributes | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 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 10/10] 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!