From 18ad9bbfca8892a55682ad793a34aaf12b204d4e Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Wed, 15 Feb 2023 12:01:06 +0100 Subject: [PATCH 01/18] [CssSelector] Add has relation support --- .../Exception/SyntaxErrorException.php | 7 ++- .../CssSelector/Node/RelationNode.php | 54 +++++++++++++++++++ .../XPath/Extension/NodeExtension.php | 14 +++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/CssSelector/Node/RelationNode.php diff --git a/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php b/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php index 52d8259b86789..ba611dd7fbe17 100644 --- a/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php +++ b/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php @@ -17,7 +17,7 @@ * ParseException is thrown when a CSS selector syntax is not valid. * * This component is a port of the Python cssselect library, - * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect. * * @author Jean-François Simon */ @@ -48,6 +48,11 @@ 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)); } + public static function nestedHas(): self + { + return new self('Got nested :has().'); + } + public static function stringAsFunctionArgument(): self { return new self('String not allowed as function argument.'); diff --git a/src/Symfony/Component/CssSelector/Node/RelationNode.php b/src/Symfony/Component/CssSelector/Node/RelationNode.php new file mode 100644 index 0000000000000..165ce58f8973c --- /dev/null +++ b/src/Symfony/Component/CssSelector/Node/RelationNode.php @@ -0,0 +1,54 @@ + + * + * 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 ":has()" node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class RelationNode extends AbstractNode +{ + private NodeInterface $selector; + private NodeInterface $subSelector; + + public function __construct(NodeInterface $selector, NodeInterface $subSelector) + { + $this->selector = $selector; + $this->subSelector = $subSelector; + } + + public function getSelector(): NodeInterface + { + return $this->selector; + } + + public function getSubSelector(): NodeInterface + { + return $this->subSelector; + } + + public function getSpecificity(): Specificity + { + return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity()); + } + + public function __toString(): string + { + return sprintf('%s[%s:has(%s)]', $this->getNodeName(), $this->selector, $this->subSelector); + } +} diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php index 4cd46fa1fc783..e81da12b6f2f2 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php @@ -71,6 +71,7 @@ public function getNodeTranslators(): array 'Class' => $this->translateClass(...), 'Hash' => $this->translateHash(...), 'Element' => $this->translateElement(...), + 'Relation' => $this->translateRelation(...) ]; } @@ -209,6 +210,19 @@ public function translateElement(Node\ElementNode $node): XPathExpr return $xpath; } + public function translateRelation(Node\RelationNode $node, Translator $translator): XPathExpr + { + $xpath = $translator->nodeToXPath($node->getSelector()); + $subXpath = $translator->nodeToXPath($node->getSubSelector()); + $subXpath->addNameTest(); + + if ($subXpath->getCondition()) { + return $xpath->addCondition(sprintf('count(descendant-or-self::*[%s]) > 0', $subXpath->getCondition())); + } + + return $xpath->addCondition('0'); + } + public function getName(): string { return 'node'; From fcd700f4b5ce4d645f88846879a25d0bf70a6552 Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Wed, 15 Feb 2023 12:15:24 +0100 Subject: [PATCH 02/18] [CssSelector] Add test and changelog --- src/Symfony/Component/CssSelector/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/CssSelector/CHANGELOG.md b/src/Symfony/Component/CssSelector/CHANGELOG.md index d2b7fb1d62acf..56fd54de7b27f 100644 --- a/src/Symfony/Component/CssSelector/CHANGELOG.md +++ b/src/Symfony/Component/CssSelector/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG --- * Add support for `:scope` + * Added support for `*:has` 4.4.0 ----- From 9358c39071ca98ed89498ea2e0444ef94778f08b Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Mon, 20 Feb 2023 16:08:57 +0100 Subject: [PATCH 03/18] Add more complex selector inside has node --- .../CssSelector/Node/RelationNode.php | 20 +++++++++++------- .../XPath/Extension/NodeExtension.php | 21 ++++++++++++++----- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/CssSelector/Node/RelationNode.php b/src/Symfony/Component/CssSelector/Node/RelationNode.php index 165ce58f8973c..2f826b439c6ae 100644 --- a/src/Symfony/Component/CssSelector/Node/RelationNode.php +++ b/src/Symfony/Component/CssSelector/Node/RelationNode.php @@ -11,8 +11,10 @@ namespace Symfony\Component\CssSelector\Node; +use Symfony\Component\CssSelector\Parser\Token; + /** - * Represents a ":has()" node. + * Represents a ":has()" node. * * This component is a port of the Python cssselect library, * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. @@ -24,12 +26,12 @@ class RelationNode extends AbstractNode { private NodeInterface $selector; - private NodeInterface $subSelector; + private array $arguments; - public function __construct(NodeInterface $selector, NodeInterface $subSelector) + public function __construct(NodeInterface $selector, array $arguments) { $this->selector = $selector; - $this->subSelector = $subSelector; + $this->arguments = $arguments; } public function getSelector(): NodeInterface @@ -37,18 +39,20 @@ public function getSelector(): NodeInterface return $this->selector; } - public function getSubSelector(): NodeInterface + public function getArguments(): array { - return $this->subSelector; + return $this->arguments; } public function getSpecificity(): Specificity { - return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity()); + return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0)); } public function __toString(): string { - return sprintf('%s[%s:has(%s)]', $this->getNodeName(), $this->selector, $this->subSelector); + $arguments = implode(', ', array_map(fn (Token $token) => "'".$token->getValue()."'", $this->arguments)); + + return sprintf('%s[%s:has(%s)]', $this->getNodeName(), $this->selector, $arguments ? '['.$arguments.']' : ''); } } diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php index e81da12b6f2f2..b467a5a0c4d4e 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php @@ -11,7 +11,9 @@ namespace Symfony\Component\CssSelector\XPath\Extension; +use Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Symfony\Component\CssSelector\Node; +use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\XPath\Translator; use Symfony\Component\CssSelector\XPath\XPathExpr; @@ -71,7 +73,7 @@ public function getNodeTranslators(): array 'Class' => $this->translateClass(...), 'Hash' => $this->translateHash(...), 'Element' => $this->translateElement(...), - 'Relation' => $this->translateRelation(...) + 'Relation' => $this->translateRelation(...), ]; } @@ -213,11 +215,20 @@ public function translateElement(Node\ElementNode $node): XPathExpr public function translateRelation(Node\RelationNode $node, Translator $translator): XPathExpr { $xpath = $translator->nodeToXPath($node->getSelector()); - $subXpath = $translator->nodeToXPath($node->getSubSelector()); - $subXpath->addNameTest(); + $selectors = $node->getArguments(); - if ($subXpath->getCondition()) { - return $xpath->addCondition(sprintf('count(descendant-or-self::*[%s]) > 0', $subXpath->getCondition())); + foreach ($selectors as $index => $selector) { + if (null !== $selector->getPseudoElement()) { + throw new ExpressionErrorException('Pseudo-elements are not supported.'); + } + + $selectors[$index] = $translator->selectorToXPath($selector); + } + + $subSelectors = implode(' | ', $selectors); + + if ($subSelectors) { + return $xpath->addCondition(sprintf('count(%s) > 0', $subSelectors)); } return $xpath->addCondition('0'); From 16550a4053bb870ba2d98425d1b90e6310783aae Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Mon, 20 Feb 2023 16:36:32 +0100 Subject: [PATCH 04/18] Fix syntax standards errors --- .../Component/CssSelector/XPath/Extension/NodeExtension.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php index b467a5a0c4d4e..a95c7997e1265 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php @@ -13,7 +13,6 @@ use Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Symfony\Component\CssSelector\Node; -use Symfony\Component\CssSelector\Parser\Token; use Symfony\Component\CssSelector\XPath\Translator; use Symfony\Component\CssSelector\XPath\XPathExpr; @@ -225,7 +224,7 @@ public function translateRelation(Node\RelationNode $node, Translator $translato $selectors[$index] = $translator->selectorToXPath($selector); } - $subSelectors = implode(' | ', $selectors); + $subSelectors = implode(' | ', $selectors); if ($subSelectors) { return $xpath->addCondition(sprintf('count(%s) > 0', $subSelectors)); From 0d5497c918f3d7c4d411c1ae867a5ea00d60525e Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Fri, 24 Feb 2023 18:44:23 +0100 Subject: [PATCH 05/18] Replace node parser for a specific parser --- .../CssSelector/Node/RelationNode.php | 29 +++++++++++-------- .../XPath/Extension/AbstractExtension.php | 5 ++++ .../XPath/Extension/CombinationExtension.php | 22 ++++++++++++++ .../XPath/Extension/ExtensionInterface.php | 7 +++++ .../XPath/Extension/NodeExtension.php | 18 ++---------- .../CssSelector/XPath/Translator.php | 15 ++++++++++ .../Component/CssSelector/XPath/XPathExpr.php | 17 +++++++++-- 7 files changed, 82 insertions(+), 31 deletions(-) diff --git a/src/Symfony/Component/CssSelector/Node/RelationNode.php b/src/Symfony/Component/CssSelector/Node/RelationNode.php index 2f826b439c6ae..7d9f1f84fac28 100644 --- a/src/Symfony/Component/CssSelector/Node/RelationNode.php +++ b/src/Symfony/Component/CssSelector/Node/RelationNode.php @@ -14,24 +14,26 @@ use Symfony\Component\CssSelector\Parser\Token; /** - * Represents a ":has()" node. + * Represents a ":has()" node. * * This component is a port of the Python cssselect library, - * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect. * - * @author Jean-François Simon + * @author Franck Ranaivo-Harisoa * * @internal */ class RelationNode extends AbstractNode { private NodeInterface $selector; - private array $arguments; + private NodeInterface $subSelector; + private string $combinator; - public function __construct(NodeInterface $selector, array $arguments) + public function __construct(NodeInterface $selector, string $combinator, NodeInterface $subSelector) { $this->selector = $selector; - $this->arguments = $arguments; + $this->combinator = $combinator; + $this->subSelector = $subSelector; } public function getSelector(): NodeInterface @@ -39,20 +41,23 @@ public function getSelector(): NodeInterface return $this->selector; } - public function getArguments(): array + public function getCombinator(): string { - return $this->arguments; + return $this->combinator; + } + + public function getSubSelector(): NodeInterface + { + return $this->subSelector; } public function getSpecificity(): Specificity { - return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0)); + return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity()); } public function __toString(): string { - $arguments = implode(', ', array_map(fn (Token $token) => "'".$token->getValue()."'", $this->arguments)); - - return sprintf('%s[%s:has(%s)]', $this->getNodeName(), $this->selector, $arguments ? '['.$arguments.']' : ''); + return sprintf('%s[%s:has(%s)]', $this->getNodeName(), $this->selector, $this->subSelector); } } diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php index 495f882910d5a..fed93e433460d 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php @@ -47,4 +47,9 @@ public function getAttributeMatchingTranslators(): array { return []; } + + public function getRelativeCombinationTranslators(): array + { + return []; + } } diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php index f78d48883760e..e393e5e66ca7f 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php @@ -58,6 +58,28 @@ public function translateIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedX return $xpath->join('/following-sibling::', $combinedXpath); } + + public function translateRelationDescendant(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath->join('/descendant-or-self::*/', $combinedXpath, ']', true); + } + + public function translateRelationChild(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath->join('/', $combinedXpath,']'); + } + + public function translateRelationDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath + ->addCondition(sprintf('/following-sibling::*[(name() = \'%s\') and (position() = 1)]',$combinedXpath->getElement())); + } + + public function translateRelationIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath->join('[following-sibling::', $combinedXpath,']'); + } + public function getName(): string { return 'combination'; diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php b/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php index 1a74b90acc6c3..757265a13299d 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php @@ -60,6 +60,13 @@ public function getPseudoClassTranslators(): array; */ public function getAttributeMatchingTranslators(): array; + /** + * Returns relative combinators translators. + * + * @return callable[] + */ + public function getRelativeCombinationTranslators(): array; + /** * Returns extension name. */ diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php index a95c7997e1265..b11e7fc3fb7fc 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php @@ -214,23 +214,9 @@ public function translateElement(Node\ElementNode $node): XPathExpr public function translateRelation(Node\RelationNode $node, Translator $translator): XPathExpr { $xpath = $translator->nodeToXPath($node->getSelector()); - $selectors = $node->getArguments(); + $combinator = $node->getCombinator(); - foreach ($selectors as $index => $selector) { - if (null !== $selector->getPseudoElement()) { - throw new ExpressionErrorException('Pseudo-elements are not supported.'); - } - - $selectors[$index] = $translator->selectorToXPath($selector); - } - - $subSelectors = implode(' | ', $selectors); - - if ($subSelectors) { - return $xpath->addCondition(sprintf('count(%s) > 0', $subSelectors)); - } - - return $xpath->addCondition('0'); + return $xpath->addCondition(sprintf('count(%s) > 0', $translator->addRelativeCombination($combinator,$node->getSelector(),$node->getSubSelector()))); } public function getName(): string diff --git a/src/Symfony/Component/CssSelector/XPath/Translator.php b/src/Symfony/Component/CssSelector/XPath/Translator.php index b2623e5067ed4..d2670a35c7589 100644 --- a/src/Symfony/Component/CssSelector/XPath/Translator.php +++ b/src/Symfony/Component/CssSelector/XPath/Translator.php @@ -44,6 +44,7 @@ class Translator implements TranslatorInterface private array $nodeTranslators = []; private array $combinationTranslators = []; + private array $relativeCombinationTranslators = []; private array $functionTranslators = []; private array $pseudoClassTranslators = []; private array $attributeMatchingTranslators = []; @@ -58,6 +59,7 @@ public function __construct(?ParserInterface $parser = null) ->registerExtension(new Extension\FunctionExtension()) ->registerExtension(new Extension\PseudoClassExtension()) ->registerExtension(new Extension\AttributeMatchingExtension()) + ->registerExtension(new Extension\RelationExtension()) ; } @@ -120,6 +122,7 @@ public function registerExtension(Extension\ExtensionInterface $extension): stat $this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators()); $this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators()); $this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators()); + $this->relativeCombinationTranslators = array_merge($this->relativeCombinationTranslators, $extension->getRelativeCombinationTranslators()); return $this; } @@ -170,6 +173,18 @@ public function addCombination(string $combiner, NodeInterface $xpath, NodeInter return $this->combinationTranslators[$combiner]($this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath)); } + /** + * @throws ExpressionErrorException + */ + public function addRelativeCombination(string $combiner, NodeInterface $xpath, NodeInterface $combinedXpath): XPathExpr + { + if (!isset($this->relativeCombinationTranslators[$combiner])) { + throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner)); + } + + return $this->relativeCombinationTranslators[$combiner]($this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath)); + } + /** * @throws ExpressionErrorException */ diff --git a/src/Symfony/Component/CssSelector/XPath/XPathExpr.php b/src/Symfony/Component/CssSelector/XPath/XPathExpr.php index a148febc53e0d..29aad86ef4aed 100644 --- a/src/Symfony/Component/CssSelector/XPath/XPathExpr.php +++ b/src/Symfony/Component/CssSelector/XPath/XPathExpr.php @@ -82,7 +82,7 @@ public function addStarPrefix(): static * * @return $this */ - public function join(string $combiner, self $expr): static + public function join(string $combiner, self $expr, string $closingCombiner = null, bool $hasInnerConditions = false): static { $path = $this->__toString().$combiner; @@ -91,8 +91,19 @@ public function join(string $combiner, self $expr): static } $this->path = $path; - $this->element = $expr->element; - $this->condition = $expr->condition; + + if(!$hasInnerConditions) { + $this->element = $expr->element . ($closingCombiner ?? ''); + $this->condition = $expr->condition; + } else { + $this->element = $expr->element; + if($expr->condition) { + $this->element .= "[" . $expr->condition."]"; + } + if($closingCombiner) { + $this->element .= $closingCombiner; + } + } return $this; } From 9aa83110158e36bc8e8192573c8c64fd729dfd4a Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Sun, 26 Feb 2023 00:53:43 +0100 Subject: [PATCH 06/18] Fix parser issue always returning direct descendant --- .../XPath/Extension/NodeExtension.php | 3 +- .../XPath/Extension/RelationExtension.php | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php index b11e7fc3fb7fc..c394b8f2cdbbf 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php @@ -213,10 +213,9 @@ public function translateElement(Node\ElementNode $node): XPathExpr public function translateRelation(Node\RelationNode $node, Translator $translator): XPathExpr { - $xpath = $translator->nodeToXPath($node->getSelector()); $combinator = $node->getCombinator(); - return $xpath->addCondition(sprintf('count(%s) > 0', $translator->addRelativeCombination($combinator,$node->getSelector(),$node->getSubSelector()))); + return $translator->addRelativeCombination($combinator,$node->getSelector(),$node->getSubSelector()); } public function getName(): string diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php new file mode 100644 index 0000000000000..3aded5fabbcc4 --- /dev/null +++ b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath\Extension; + +use Symfony\Component\CssSelector\XPath\XPathExpr; + +/** + * XPath expression translator combination extension. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Franck Ranaivo-Harisoa + * + * @internal + */ +class RelationExtension extends AbstractExtension +{ + public function getRelativeCombinationTranslators(): array + { + return [ + ' ' => $this->translateRelationDescendant(...), + '>' => $this->translateRelationChild(...), + '+' => $this->translateRelationDirectAdjacent(...), + '~' => $this->translateRelationIndirectAdjacent(...), + ]; + } + + public function translateRelationDescendant(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath->join('[descendant-or-self::', $combinedXpath, ']', true); + } + + public function translateRelationChild(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath->join('[./', $combinedXpath,']'); + } + + public function translateRelationDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath + ->addCondition(sprintf('following-sibling::*[(name() = \'%s\') and (position() = 1)]',$combinedXpath->getElement())); + } + + public function translateRelationIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath->join('[following-sibling::', $combinedXpath,']'); + } + + public function getName(): string + { + return 'relation'; + } +} From 06e0d4766357e86716aa94c0710d9def0e24b4d3 Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Sun, 26 Feb 2023 16:01:43 +0100 Subject: [PATCH 07/18] Add translator test --- src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php b/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php index 82de5ab6b8562..c309484b15115 100644 --- a/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php +++ b/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php @@ -134,6 +134,7 @@ public static function getParserTestData() ['div:contains("foo")', ["Function[Element[div]:contains(['foo'])]"]], ['div#foobar', ['Hash[Element[div]#foobar]']], ['div:not(div.foo)', ['Negation[Element[div]:not(Class[Element[div].foo])]']], + ['div:has(div.foo)', ['Relation[Element[div]:has(Selector[Class[Element[div].foo]])]']], ['td ~ th', ['CombinedSelector[Element[td] ~ Element[th]]']], ['.foo[data-bar][data-baz=0]', ["Attribute[Attribute[Class[Element[*].foo][data-bar]][data-baz = '0']]"]], ['div#foo\.bar', ['Hash[Element[div]#foo.bar]']], From a9e89ba781285579700ebc2e40790fc7faf5f91a Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Sun, 26 Feb 2023 16:30:20 +0100 Subject: [PATCH 08/18] [CssSelector] Remove unused methods --- .../XPath/Extension/CombinationExtension.php | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php index e393e5e66ca7f..f78d48883760e 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php @@ -58,28 +58,6 @@ public function translateIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedX return $xpath->join('/following-sibling::', $combinedXpath); } - - public function translateRelationDescendant(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr - { - return $xpath->join('/descendant-or-self::*/', $combinedXpath, ']', true); - } - - public function translateRelationChild(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr - { - return $xpath->join('/', $combinedXpath,']'); - } - - public function translateRelationDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr - { - return $xpath - ->addCondition(sprintf('/following-sibling::*[(name() = \'%s\') and (position() = 1)]',$combinedXpath->getElement())); - } - - public function translateRelationIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr - { - return $xpath->join('[following-sibling::', $combinedXpath,']'); - } - public function getName(): string { return 'combination'; From b30ef12b895088d29ef4d5bca3bc75bf4915036f Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Sun, 26 Feb 2023 16:35:36 +0100 Subject: [PATCH 09/18] [CssSelector] Remove unused imports --- .../Component/CssSelector/XPath/Extension/NodeExtension.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php index c394b8f2cdbbf..4bfbeb8c0e537 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php @@ -11,7 +11,6 @@ namespace Symfony\Component\CssSelector\XPath\Extension; -use Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\XPath\Translator; use Symfony\Component\CssSelector\XPath\XPathExpr; @@ -20,7 +19,7 @@ * XPath expression translator node extension. * * This component is a port of the Python cssselect library, - * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect. * * @author Jean-François Simon * From a6cd7daa8dff50de24d9fb9e71d1798bafe4c04a Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Sun, 26 Feb 2023 16:38:07 +0100 Subject: [PATCH 10/18] [CssSelector] Fix python original url --- .../Component/CssSelector/XPath/Extension/RelationExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php index 3aded5fabbcc4..8b845ccab6d7d 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php @@ -17,7 +17,7 @@ * XPath expression translator combination extension. * * This component is a port of the Python cssselect library, - * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect. * * @author Franck Ranaivo-Harisoa * From 0b93e0fdad581f0c9fc5a9cf15a3bc5872ca3d51 Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Mon, 27 Feb 2023 09:21:48 +0100 Subject: [PATCH 11/18] [CssSelector] Fix coding standards issue --- .../Component/CssSelector/Node/RelationNode.php | 2 -- .../CssSelector/XPath/Extension/NodeExtension.php | 2 +- .../CssSelector/XPath/Extension/RelationExtension.php | 8 ++++---- src/Symfony/Component/CssSelector/XPath/XPathExpr.php | 10 +++++----- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/CssSelector/Node/RelationNode.php b/src/Symfony/Component/CssSelector/Node/RelationNode.php index 7d9f1f84fac28..7f50501d17472 100644 --- a/src/Symfony/Component/CssSelector/Node/RelationNode.php +++ b/src/Symfony/Component/CssSelector/Node/RelationNode.php @@ -11,8 +11,6 @@ namespace Symfony\Component\CssSelector\Node; -use Symfony\Component\CssSelector\Parser\Token; - /** * Represents a ":has()" node. * diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php index 4bfbeb8c0e537..4f6f961cad9c0 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php @@ -214,7 +214,7 @@ public function translateRelation(Node\RelationNode $node, Translator $translato { $combinator = $node->getCombinator(); - return $translator->addRelativeCombination($combinator,$node->getSelector(),$node->getSubSelector()); + return $translator->addRelativeCombination($combinator, $node->getSelector(), $node->getSubSelector()); } public function getName(): string diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php index 8b845ccab6d7d..ab37f5c83a81d 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php @@ -42,19 +42,19 @@ public function translateRelationDescendant(XPathExpr $xpath, XPathExpr $combine public function translateRelationChild(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr { - return $xpath->join('[./', $combinedXpath,']'); + return $xpath->join('[./', $combinedXpath, ']'); } public function translateRelationDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr { return $xpath - ->addCondition(sprintf('following-sibling::*[(name() = \'%s\') and (position() = 1)]',$combinedXpath->getElement())); + ->addCondition(sprintf('following-sibling::*[(name() = \'%s\') and (position() = 1)]', $combinedXpath->getElement())); } public function translateRelationIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr { - return $xpath->join('[following-sibling::', $combinedXpath,']'); - } + return $xpath->join('[following-sibling::', $combinedXpath, ']'); + } public function getName(): string { diff --git a/src/Symfony/Component/CssSelector/XPath/XPathExpr.php b/src/Symfony/Component/CssSelector/XPath/XPathExpr.php index 29aad86ef4aed..c3144b3f98ebd 100644 --- a/src/Symfony/Component/CssSelector/XPath/XPathExpr.php +++ b/src/Symfony/Component/CssSelector/XPath/XPathExpr.php @@ -92,15 +92,15 @@ public function join(string $combiner, self $expr, string $closingCombiner = nul $this->path = $path; - if(!$hasInnerConditions) { - $this->element = $expr->element . ($closingCombiner ?? ''); + if (!$hasInnerConditions) { + $this->element = $expr->element.($closingCombiner ?? ''); $this->condition = $expr->condition; } else { $this->element = $expr->element; - if($expr->condition) { - $this->element .= "[" . $expr->condition."]"; + if ($expr->condition) { + $this->element .= '['.$expr->condition.']'; } - if($closingCombiner) { + if ($closingCombiner) { $this->element .= $closingCombiner; } } From fbe8e386c807b30fcc10fcce3ffbf2ff22df2555 Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Thu, 2 Mar 2023 11:30:51 +0100 Subject: [PATCH 12/18] [CssSelector] Fix BC break by adding @method instead of directly adding it --- .../XPath/Extension/ExtensionInterface.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php b/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php index 757265a13299d..1adfa479cb3fe 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php @@ -15,10 +15,12 @@ * XPath expression translator extension interface. * * This component is a port of the Python cssselect library, - * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect. * * @author Jean-François Simon - * + * + * @method array getRelativeCombinationTranslators() Returns combination translators found inside ":has()" relation. + * * @internal */ interface ExtensionInterface @@ -60,13 +62,6 @@ public function getPseudoClassTranslators(): array; */ public function getAttributeMatchingTranslators(): array; - /** - * Returns relative combinators translators. - * - * @return callable[] - */ - public function getRelativeCombinationTranslators(): array; - /** * Returns extension name. */ From d35086b0b2ff555d8b110895f03e71fb2c7ec26d Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Thu, 2 Mar 2023 11:36:39 +0100 Subject: [PATCH 13/18] [CssSelector] Fix coding standard issue --- .../CssSelector/XPath/Extension/ExtensionInterface.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php b/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php index 1adfa479cb3fe..1d53eba527f23 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php @@ -18,9 +18,9 @@ * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect. * * @author Jean-François Simon - * + * * @method array getRelativeCombinationTranslators() Returns combination translators found inside ":has()" relation. - * + * * @internal */ interface ExtensionInterface From bebe4c38ca6087455a04d46cb4a27494c7d8b25d Mon Sep 17 00:00:00 2001 From: franckranaivo Date: Thu, 2 Mar 2023 17:07:39 +0100 Subject: [PATCH 14/18] [CssSelector] Imperative formulation for changelog --- src/Symfony/Component/CssSelector/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/CssSelector/CHANGELOG.md b/src/Symfony/Component/CssSelector/CHANGELOG.md index 56fd54de7b27f..2e28394fdf7b9 100644 --- a/src/Symfony/Component/CssSelector/CHANGELOG.md +++ b/src/Symfony/Component/CssSelector/CHANGELOG.md @@ -11,7 +11,7 @@ CHANGELOG --- * Add support for `:scope` - * Added support for `*:has` + * Add support for `*:has` 4.4.0 ----- From c099d95cd3d1dfc3d29b27113bc717df26c146c6 Mon Sep 17 00:00:00 2001 From: Franck RANAIVO-HARISOA Date: Wed, 21 Jun 2023 17:10:26 +0200 Subject: [PATCH 15/18] Fix Direct adjacent Relation --- .../CssSelector/XPath/Extension/RelationExtension.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php index ab37f5c83a81d..658a321fe94a2 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php @@ -47,8 +47,12 @@ public function translateRelationChild(XPathExpr $xpath, XPathExpr $combinedXpat public function translateRelationDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr { + $combinedXpath + ->addNameTest() + ->addCondition('position() = 1'); return $xpath - ->addCondition(sprintf('following-sibling::*[(name() = \'%s\') and (position() = 1)]', $combinedXpath->getElement())); + ->join('[following-sibling::', $combinedXpath, ']', true) + ; } public function translateRelationIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr From 31771f1cbc3b3ac45052b74543b4cefb9b66c3f6 Mon Sep 17 00:00:00 2001 From: Franck RANAIVO-HARISOA Date: Tue, 27 Jun 2023 13:59:16 +0200 Subject: [PATCH 16/18] Add relevant test: shakespeare and htmlIds Fix incompatible test from bug fix --- .../CssSelector/XPath/Extension/RelationExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php index 658a321fe94a2..2ea79bf996612 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php @@ -42,7 +42,7 @@ public function translateRelationDescendant(XPathExpr $xpath, XPathExpr $combine public function translateRelationChild(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr { - return $xpath->join('[./', $combinedXpath, ']'); + return $xpath->join('[./', $combinedXpath, ']', true); } public function translateRelationDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr @@ -57,7 +57,7 @@ public function translateRelationDirectAdjacent(XPathExpr $xpath, XPathExpr $com public function translateRelationIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr { - return $xpath->join('[following-sibling::', $combinedXpath, ']'); + return $xpath->join('[following-sibling::', $combinedXpath, ']', true); } public function getName(): string From 0605acafad554b2b5dc54f42eabd15f104c59277 Mon Sep 17 00:00:00 2001 From: Franck RANAIVO-HARISOA Date: Tue, 27 Jun 2023 14:13:27 +0200 Subject: [PATCH 17/18] Fix coding standard issues --- .../CssSelector/XPath/Extension/RelationExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php index 2ea79bf996612..d85ec21dfa069 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php @@ -50,9 +50,9 @@ public function translateRelationDirectAdjacent(XPathExpr $xpath, XPathExpr $com $combinedXpath ->addNameTest() ->addCondition('position() = 1'); + return $xpath - ->join('[following-sibling::', $combinedXpath, ']', true) - ; + ->join('[following-sibling::', $combinedXpath, ']', true); } public function translateRelationIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr From 9dc472a2227edfdc1ead164b8f3eb6984b5e733e Mon Sep 17 00:00:00 2001 From: Franck RANAIVO-HARISOA Date: Mon, 4 Sep 2023 10:31:21 +0200 Subject: [PATCH 18/18] Fix: remove unused method --- .../Exception/SyntaxErrorException.php | 5 --- .../Component/CssSelector/Parser/Parser.php | 38 ++++++++++++++++++- .../Tests/XPath/TranslatorTest.php | 4 ++ .../XPath/Extension/AbstractExtension.php | 2 +- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php b/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php index ba611dd7fbe17..d54f82ebeb5f3 100644 --- a/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php +++ b/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php @@ -48,11 +48,6 @@ 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)); } - public static function nestedHas(): self - { - return new self('Got nested :has().'); - } - public static function stringAsFunctionArgument(): self { return new self('String not allowed as function argument.'); diff --git a/src/Symfony/Component/CssSelector/Parser/Parser.php b/src/Symfony/Component/CssSelector/Parser/Parser.php index f7eea2f828fc3..7c4eff8c5f0c4 100644 --- a/src/Symfony/Component/CssSelector/Parser/Parser.php +++ b/src/Symfony/Component/CssSelector/Parser/Parser.php @@ -11,6 +11,7 @@ namespace Symfony\Component\CssSelector\Parser; +use Symfony\Component\CssSelector\Exception\InternalErrorException; use Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Symfony\Component\CssSelector\Node; use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer; @@ -144,10 +145,42 @@ private function parserSelectorNode(TokenStream $stream, bool $isArgument = fals return new Node\SelectorNode($result, $pseudoElement); } + /** + * @throws InternalErrorException + * @throws SyntaxErrorException + */ + function parseRelativeSelector(TokenStream $stream): array + { + $stream->skipWhitespace(); + $subSelector = ''; + $next = $stream->getNext(); + + if ($next->isDelimiter(['-', '+', '>', '~'])) { + $combinator = $next->getValue(); + $stream->skipWhitespace(); + $next = $stream->getNext(); + } else { + $combinator = new Token(Token::TYPE_DELIMITER, ' ', 0); + } + + while(true){ + if ($next->isString() || $next->isIdentifier() || $next->isNumber() + || $next->isDelimiter(['.', '*'])) { + $subSelector .= $next->getValue(); + } elseif ($next->isDelimiter([')'])) { + $result = $this->parse($subSelector); + return [$combinator, $result[0]]; + } else { + throw SyntaxErrorException::unexpectedToken('an argument', $next); + } + $next = $stream->getNext(); + } + } + /** * Parses next simple node (hash, class, pseudo, negation). * - * @throws SyntaxErrorException + * @throws SyntaxErrorException|InternalErrorException */ private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = false, bool $isArgument = false): array { @@ -253,6 +286,9 @@ private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = } $result = new Node\SpecificityAdjustmentNode($result, $selectors); + } elseif('has' === strtolower($identifier)) { + [$combinator, $arguments] = $this->parseRelativeSelector($stream); + $result = new Node\RelationNode($result, $combinator ,$arguments); } else { $arguments = []; $next = null; diff --git a/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php b/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php index f521a94708423..8b255db90a205 100644 --- a/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php +++ b/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php @@ -235,6 +235,10 @@ public static function getCssToXPathTestData() [':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"], + ['div:has(> .foo)', "div[./*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]]"], + ['div:has(~ .foo)', "div[following-sibling::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]]"], + ['div:has(+ .foo)', "div[following-sibling::*[(@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')) and (position() = 1)]]"], + ['div:has(+ .foo)', "div[following-sibling::*[(@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')) and (position() = 1)]]"], ]; } diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php index fed93e433460d..30eedb8921eb5 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php @@ -15,7 +15,7 @@ * XPath expression translator abstract extension. * * This component is a port of the Python cssselect library, - * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect. * * @author Jean-François Simon * 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