1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\CssSelector\XPath\Extension;
13
14use Symfony\Component\CssSelector\Node;
15use Symfony\Component\CssSelector\XPath\Translator;
16use Symfony\Component\CssSelector\XPath\XPathExpr;
17
18/**
19 * XPath expression translator node extension.
20 *
21 * This component is a port of the Python cssselect library,
22 * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
23 *
24 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
25 *
26 * @internal
27 */
28class NodeExtension extends AbstractExtension
29{
30    public const ELEMENT_NAME_IN_LOWER_CASE = 1;
31    public const ATTRIBUTE_NAME_IN_LOWER_CASE = 2;
32    public const ATTRIBUTE_VALUE_IN_LOWER_CASE = 4;
33
34    private $flags;
35
36    public function __construct(int $flags = 0)
37    {
38        $this->flags = $flags;
39    }
40
41    /**
42     * @return $this
43     */
44    public function setFlag(int $flag, bool $on): self
45    {
46        if ($on && !$this->hasFlag($flag)) {
47            $this->flags += $flag;
48        }
49
50        if (!$on && $this->hasFlag($flag)) {
51            $this->flags -= $flag;
52        }
53
54        return $this;
55    }
56
57    public function hasFlag(int $flag): bool
58    {
59        return (bool) ($this->flags & $flag);
60    }
61
62    /**
63     * {@inheritdoc}
64     */
65    public function getNodeTranslators(): array
66    {
67        return [
68            'Selector' => [$this, 'translateSelector'],
69            'CombinedSelector' => [$this, 'translateCombinedSelector'],
70            'Negation' => [$this, 'translateNegation'],
71            'Function' => [$this, 'translateFunction'],
72            'Pseudo' => [$this, 'translatePseudo'],
73            'Attribute' => [$this, 'translateAttribute'],
74            'Class' => [$this, 'translateClass'],
75            'Hash' => [$this, 'translateHash'],
76            'Element' => [$this, 'translateElement'],
77        ];
78    }
79
80    public function translateSelector(Node\SelectorNode $node, Translator $translator): XPathExpr
81    {
82        return $translator->nodeToXPath($node->getTree());
83    }
84
85    public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator): XPathExpr
86    {
87        return $translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector());
88    }
89
90    public function translateNegation(Node\NegationNode $node, Translator $translator): XPathExpr
91    {
92        $xpath = $translator->nodeToXPath($node->getSelector());
93        $subXpath = $translator->nodeToXPath($node->getSubSelector());
94        $subXpath->addNameTest();
95
96        if ($subXpath->getCondition()) {
97            return $xpath->addCondition(sprintf('not(%s)', $subXpath->getCondition()));
98        }
99
100        return $xpath->addCondition('0');
101    }
102
103    public function translateFunction(Node\FunctionNode $node, Translator $translator): XPathExpr
104    {
105        $xpath = $translator->nodeToXPath($node->getSelector());
106
107        return $translator->addFunction($xpath, $node);
108    }
109
110    public function translatePseudo(Node\PseudoNode $node, Translator $translator): XPathExpr
111    {
112        $xpath = $translator->nodeToXPath($node->getSelector());
113
114        return $translator->addPseudoClass($xpath, $node->getIdentifier());
115    }
116
117    public function translateAttribute(Node\AttributeNode $node, Translator $translator): XPathExpr
118    {
119        $name = $node->getAttribute();
120        $safe = $this->isSafeName($name);
121
122        if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) {
123            $name = strtolower($name);
124        }
125
126        if ($node->getNamespace()) {
127            $name = sprintf('%s:%s', $node->getNamespace(), $name);
128            $safe = $safe && $this->isSafeName($node->getNamespace());
129        }
130
131        $attribute = $safe ? '@'.$name : sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name));
132        $value = $node->getValue();
133        $xpath = $translator->nodeToXPath($node->getSelector());
134
135        if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) {
136            $value = strtolower($value);
137        }
138
139        return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value);
140    }
141
142    public function translateClass(Node\ClassNode $node, Translator $translator): XPathExpr
143    {
144        $xpath = $translator->nodeToXPath($node->getSelector());
145
146        return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName());
147    }
148
149    public function translateHash(Node\HashNode $node, Translator $translator): XPathExpr
150    {
151        $xpath = $translator->nodeToXPath($node->getSelector());
152
153        return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId());
154    }
155
156    public function translateElement(Node\ElementNode $node): XPathExpr
157    {
158        $element = $node->getElement();
159
160        if ($element && $this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) {
161            $element = strtolower($element);
162        }
163
164        if ($element) {
165            $safe = $this->isSafeName($element);
166        } else {
167            $element = '*';
168            $safe = true;
169        }
170
171        if ($node->getNamespace()) {
172            $element = sprintf('%s:%s', $node->getNamespace(), $element);
173            $safe = $safe && $this->isSafeName($node->getNamespace());
174        }
175
176        $xpath = new XPathExpr('', $element);
177
178        if (!$safe) {
179            $xpath->addNameTest();
180        }
181
182        return $xpath;
183    }
184
185    /**
186     * {@inheritdoc}
187     */
188    public function getName(): string
189    {
190        return 'node';
191    }
192
193    private function isSafeName(string $name): bool
194    {
195        return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name);
196    }
197}
198