Skip to content

Commit da0ff45

Browse files
committed
Replace node parser for a specific parser
1 parent 3c95a44 commit da0ff45

File tree

8 files changed

+116
-40
lines changed

8 files changed

+116
-40
lines changed

src/Symfony/Component/CssSelector/Node/RelationNode.php

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,50 @@
1414
use Symfony\Component\CssSelector\Parser\Token;
1515

1616
/**
17-
* Represents a "<selector>:has(<arguments>)" node.
17+
* Represents a "<selector>:has(<subselector>)" node.
1818
*
1919
* This component is a port of the Python cssselect library,
20-
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
20+
* which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
2121
*
22-
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
22+
* @author Franck Ranaivo-Harisoa <franckranaivo@gmail.com>
2323
*
2424
* @internal
2525
*/
2626
class RelationNode extends AbstractNode
2727
{
2828
private NodeInterface $selector;
29-
private array $arguments;
29+
private NodeInterface $subSelector;
30+
private string $combinator;
3031

31-
public function __construct(NodeInterface $selector, array $arguments)
32+
public function __construct(NodeInterface $selector, string $combinator, NodeInterface $subSelector)
3233
{
3334
$this->selector = $selector;
34-
$this->arguments = $arguments;
35+
$this->combinator = $combinator;
36+
$this->subSelector = $subSelector;
3537
}
3638

3739
public function getSelector(): NodeInterface
3840
{
3941
return $this->selector;
4042
}
4143

42-
public function getArguments(): array
44+
public function getCombinator(): string
4345
{
44-
return $this->arguments;
46+
return $this->combinator;
47+
}
48+
49+
public function getSubSelector(): NodeInterface
50+
{
51+
return $this->subSelector;
4552
}
4653

4754
public function getSpecificity(): Specificity
4855
{
49-
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
56+
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
5057
}
5158

5259
public function __toString(): string
5360
{
54-
$arguments = implode(', ', array_map(fn (Token $token) => "'".$token->getValue()."'", $this->arguments));
55-
56-
return sprintf('%s[%s:has(%s)]', $this->getNodeName(), $this->selector, $arguments ? '['.$arguments.']' : '');
61+
return sprintf('%s[%s:has(%s)]', $this->getNodeName(), $this->selector, $this->subSelector);
5762
}
5863
}

src/Symfony/Component/CssSelector/Parser/Parser.php

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
1515
use Symfony\Component\CssSelector\Node;
1616
use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer;
17+
use Symfony\Component\ExpressionLanguage\SyntaxError;
1718

1819
/**
1920
* CSS selector parser.
@@ -222,16 +223,9 @@ private function parseSimpleSelector(TokenStream $stream, bool $insideNegation =
222223
throw SyntaxErrorException::nestedHas();
223224
}
224225

225-
$arguments[] = $this->parserSelectorNode($stream, true);
226+
[$combinator, $subSelector] = $this->parseRelativeNode($stream);
226227

227-
$stream->skipWhitespace();
228-
$next = $stream->getNext();
229-
230-
if (!$arguments) {
231-
throw SyntaxErrorException::unexpectedToken('at least one argument', $next);
232-
}
233-
234-
$result = new Node\RelationNode($result, $arguments);
228+
$result = new Node\RelationNode($result, $combinator,$subSelector);
235229
} else {
236230
$arguments = [];
237231
$next = null;
@@ -271,6 +265,37 @@ private function parseSimpleSelector(TokenStream $stream, bool $insideNegation =
271265
return [$result, $pseudoElement];
272266
}
273267

268+
private function parseRelativeNode($stream): array {
269+
$stream->skipWhitespace();
270+
$subSelector = "";
271+
$next = $stream->getNext();
272+
273+
if($next->isDelimiter(['+','-','>','~'])) {
274+
$combinator = $stream->getNext()->getValue();
275+
$stream->skipWhitespace();
276+
$next = $stream->getNext();
277+
} else {
278+
$combinator = " ";
279+
}
280+
281+
while(true) {
282+
if($next->isIdentifier()
283+
|| $next->isString()
284+
|| $next->isNumber()
285+
|| $next->isDelimiter(['.', '*'])
286+
) {
287+
$subSelector .= $next->getValue();
288+
} elseif($next->isDelimiter([')'])) {
289+
$result = $this->parse($subSelector);
290+
return [$combinator, $result[0]];
291+
} else {
292+
throw SyntaxErrorException::unexpectedToken('selector',$stream->getPeek());
293+
}
294+
$next = $stream->getNext();
295+
}
296+
297+
}
298+
274299
private function parseElementNode(TokenStream $stream): Node\ElementNode
275300
{
276301
$peek = $stream->getPeek();

src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,9 @@ public function getAttributeMatchingTranslators(): array
4747
{
4848
return [];
4949
}
50+
51+
public function getRelativeCombinationTranslators(): array
52+
{
53+
return [];
54+
}
5055
}

src/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,28 @@ public function translateIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedX
5858
return $xpath->join('/following-sibling::', $combinedXpath);
5959
}
6060

61+
62+
public function translateRelationDescendant(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
63+
{
64+
return $xpath->join('/descendant-or-self::*/', $combinedXpath, ']', true);
65+
}
66+
67+
public function translateRelationChild(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
68+
{
69+
return $xpath->join('/', $combinedXpath,']');
70+
}
71+
72+
public function translateRelationDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
73+
{
74+
return $xpath
75+
->addCondition(sprintf('/following-sibling::*[(name() = \'%s\') and (position() = 1)]',$combinedXpath->getElement()));
76+
}
77+
78+
public function translateRelationIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
79+
{
80+
return $xpath->join('[following-sibling::', $combinedXpath,']');
81+
}
82+
6183
public function getName(): string
6284
{
6385
return 'combination';

src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ public function getPseudoClassTranslators(): array;
6060
*/
6161
public function getAttributeMatchingTranslators(): array;
6262

63+
/**
64+
* Returns relative combinators translators.
65+
*
66+
* @return callable[]
67+
*/
68+
public function getRelativeCombinationTranslators(): array;
69+
6370
/**
6471
* Returns extension name.
6572
*/

src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -184,23 +184,9 @@ public function translateElement(Node\ElementNode $node): XPathExpr
184184
public function translateRelation(Node\RelationNode $node, Translator $translator): XPathExpr
185185
{
186186
$xpath = $translator->nodeToXPath($node->getSelector());
187-
$selectors = $node->getArguments();
187+
$combinator = $node->getCombinator();
188188

189-
foreach ($selectors as $index => $selector) {
190-
if (null !== $selector->getPseudoElement()) {
191-
throw new ExpressionErrorException('Pseudo-elements are not supported.');
192-
}
193-
194-
$selectors[$index] = $translator->selectorToXPath($selector);
195-
}
196-
197-
$subSelectors = implode(' | ', $selectors);
198-
199-
if ($subSelectors) {
200-
return $xpath->addCondition(sprintf('count(%s) > 0', $subSelectors));
201-
}
202-
203-
return $xpath->addCondition('0');
189+
return $xpath->addCondition(sprintf('count(%s) > 0', $translator->addRelativeCombination($combinator,$node->getSelector(),$node->getSubSelector())));
204190
}
205191

206192
public function getName(): string

src/Symfony/Component/CssSelector/XPath/Translator.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class Translator implements TranslatorInterface
4444

4545
private array $nodeTranslators = [];
4646
private array $combinationTranslators = [];
47+
private array $relativeCombinationTranslators = [];
4748
private array $functionTranslators = [];
4849
private array $pseudoClassTranslators = [];
4950
private array $attributeMatchingTranslators = [];
@@ -58,6 +59,7 @@ public function __construct(ParserInterface $parser = null)
5859
->registerExtension(new Extension\FunctionExtension())
5960
->registerExtension(new Extension\PseudoClassExtension())
6061
->registerExtension(new Extension\AttributeMatchingExtension())
62+
->registerExtension(new Extension\RelationExtension())
6163
;
6264
}
6365

@@ -120,6 +122,7 @@ public function registerExtension(Extension\ExtensionInterface $extension): stat
120122
$this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators());
121123
$this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators());
122124
$this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators());
125+
$this->relativeCombinationTranslators = array_merge($this->relativeCombinationTranslators, $extension->getRelativeCombinationTranslators());
123126

124127
return $this;
125128
}
@@ -170,6 +173,18 @@ public function addCombination(string $combiner, NodeInterface $xpath, NodeInter
170173
return $this->combinationTranslators[$combiner]($this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
171174
}
172175

176+
/**
177+
* @throws ExpressionErrorException
178+
*/
179+
public function addRelativeCombination(string $combiner, NodeInterface $xpath, NodeInterface $combinedXpath): XPathExpr
180+
{
181+
if (!isset($this->relativeCombinationTranslators[$combiner])) {
182+
throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner));
183+
}
184+
185+
return $this->relativeCombinationTranslators[$combiner]($this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
186+
}
187+
173188
/**
174189
* @throws ExpressionErrorException
175190
*/

src/Symfony/Component/CssSelector/XPath/XPathExpr.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public function addStarPrefix(): static
8686
*
8787
* @return $this
8888
*/
89-
public function join(string $combiner, self $expr): static
89+
public function join(string $combiner, self $expr, string $closingCombiner = null, bool $hasInnerConditions = false): static
9090
{
9191
$path = $this->__toString().$combiner;
9292

@@ -95,8 +95,19 @@ public function join(string $combiner, self $expr): static
9595
}
9696

9797
$this->path = $path;
98-
$this->element = $expr->element;
99-
$this->condition = $expr->condition;
98+
99+
if(!$hasInnerConditions) {
100+
$this->element = $expr->element . ($closingCombiner ?? '');
101+
$this->condition = $expr->condition;
102+
} else {
103+
$this->element = $expr->element;
104+
if($expr->condition) {
105+
$this->element .= "[" . $expr->condition."]";
106+
}
107+
if($closingCombiner) {
108+
$this->element .= $closingCombiner;
109+
}
110+
}
100111

101112
return $this;
102113
}

0 commit comments

Comments
 (0)