friendship ended with social-app. php is my new best friend
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 public function __construct(
35 private int $flags = 0,
36 ) {
37 }
38
39 /**
40 * @return $this
41 */
42 public function setFlag(int $flag, bool $on): static
43 {
44 if ($on && !$this->hasFlag($flag)) {
45 $this->flags += $flag;
46 }
47
48 if (!$on && $this->hasFlag($flag)) {
49 $this->flags -= $flag;
50 }
51
52 return $this;
53 }
54
55 public function hasFlag(int $flag): bool
56 {
57 return (bool) ($this->flags & $flag);
58 }
59
60 public function getNodeTranslators(): array
61 {
62 return [
63 'Selector' => $this->translateSelector(...),
64 'CombinedSelector' => $this->translateCombinedSelector(...),
65 'Negation' => $this->translateNegation(...),
66 'Matching' => $this->translateMatching(...),
67 'SpecificityAdjustment' => $this->translateSpecificityAdjustment(...),
68 'Function' => $this->translateFunction(...),
69 'Pseudo' => $this->translatePseudo(...),
70 'Attribute' => $this->translateAttribute(...),
71 'Class' => $this->translateClass(...),
72 'Hash' => $this->translateHash(...),
73 'Element' => $this->translateElement(...),
74 ];
75 }
76
77 public function translateSelector(Node\SelectorNode $node, Translator $translator): XPathExpr
78 {
79 return $translator->nodeToXPath($node->getTree());
80 }
81
82 public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator): XPathExpr
83 {
84 return $translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector());
85 }
86
87 public function translateNegation(Node\NegationNode $node, Translator $translator): XPathExpr
88 {
89 $xpath = $translator->nodeToXPath($node->getSelector());
90 $subXpath = $translator->nodeToXPath($node->getSubSelector());
91 $subXpath->addNameTest();
92
93 if ($subXpath->getCondition()) {
94 return $xpath->addCondition(\sprintf('not(%s)', $subXpath->getCondition()));
95 }
96
97 return $xpath->addCondition('0');
98 }
99
100 public function translateMatching(Node\MatchingNode $node, Translator $translator): XPathExpr
101 {
102 $xpath = $translator->nodeToXPath($node->selector);
103
104 foreach ($node->arguments as $argument) {
105 $expr = $translator->nodeToXPath($argument);
106 $expr->addNameTest();
107 if ($condition = $expr->getCondition()) {
108 $xpath->addCondition($condition, 'or');
109 }
110 }
111
112 return $xpath;
113 }
114
115 public function translateSpecificityAdjustment(Node\SpecificityAdjustmentNode $node, Translator $translator): XPathExpr
116 {
117 $xpath = $translator->nodeToXPath($node->selector);
118
119 foreach ($node->arguments as $argument) {
120 $expr = $translator->nodeToXPath($argument);
121 $expr->addNameTest();
122 if ($condition = $expr->getCondition()) {
123 $xpath->addCondition($condition, 'or');
124 }
125 }
126
127 return $xpath;
128 }
129
130 public function translateFunction(Node\FunctionNode $node, Translator $translator): XPathExpr
131 {
132 $xpath = $translator->nodeToXPath($node->getSelector());
133
134 return $translator->addFunction($xpath, $node);
135 }
136
137 public function translatePseudo(Node\PseudoNode $node, Translator $translator): XPathExpr
138 {
139 $xpath = $translator->nodeToXPath($node->getSelector());
140
141 return $translator->addPseudoClass($xpath, $node->getIdentifier());
142 }
143
144 public function translateAttribute(Node\AttributeNode $node, Translator $translator): XPathExpr
145 {
146 $name = $node->getAttribute();
147 $safe = $this->isSafeName($name);
148
149 if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) {
150 $name = strtolower($name);
151 }
152
153 if ($node->getNamespace()) {
154 $name = \sprintf('%s:%s', $node->getNamespace(), $name);
155 $safe = $safe && $this->isSafeName($node->getNamespace());
156 }
157
158 $attribute = $safe ? '@'.$name : \sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name));
159 $value = $node->getValue();
160 $xpath = $translator->nodeToXPath($node->getSelector());
161
162 if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) {
163 $value = strtolower($value);
164 }
165
166 return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value);
167 }
168
169 public function translateClass(Node\ClassNode $node, Translator $translator): XPathExpr
170 {
171 $xpath = $translator->nodeToXPath($node->getSelector());
172
173 return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName());
174 }
175
176 public function translateHash(Node\HashNode $node, Translator $translator): XPathExpr
177 {
178 $xpath = $translator->nodeToXPath($node->getSelector());
179
180 return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId());
181 }
182
183 public function translateElement(Node\ElementNode $node): XPathExpr
184 {
185 $element = $node->getElement();
186
187 if ($element && $this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) {
188 $element = strtolower($element);
189 }
190
191 if ($element) {
192 $safe = $this->isSafeName($element);
193 } else {
194 $element = '*';
195 $safe = true;
196 }
197
198 if ($node->getNamespace()) {
199 $element = \sprintf('%s:%s', $node->getNamespace(), $element);
200 $safe = $safe && $this->isSafeName($node->getNamespace());
201 }
202
203 $xpath = new XPathExpr('', $element);
204
205 if (!$safe) {
206 $xpath->addNameTest();
207 }
208
209 return $xpath;
210 }
211
212 public function getName(): string
213 {
214 return 'node';
215 }
216
217 private function isSafeName(string $name): bool
218 {
219 return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name);
220 }
221}