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;
13
14use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
15use Symfony\Component\CssSelector\Node\FunctionNode;
16use Symfony\Component\CssSelector\Node\NodeInterface;
17use Symfony\Component\CssSelector\Node\SelectorNode;
18use Symfony\Component\CssSelector\Parser\Parser;
19use Symfony\Component\CssSelector\Parser\ParserInterface;
20
21/**
22 * XPath expression translator interface.
23 *
24 * This component is a port of the Python cssselect library,
25 * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
26 *
27 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
28 *
29 * @internal
30 */
31class Translator implements TranslatorInterface
32{
33 private ParserInterface $mainParser;
34
35 /**
36 * @var ParserInterface[]
37 */
38 private array $shortcutParsers = [];
39
40 /**
41 * @var Extension\ExtensionInterface[]
42 */
43 private array $extensions = [];
44
45 private array $nodeTranslators = [];
46 private array $combinationTranslators = [];
47 private array $functionTranslators = [];
48 private array $pseudoClassTranslators = [];
49 private array $attributeMatchingTranslators = [];
50
51 public function __construct(?ParserInterface $parser = null)
52 {
53 $this->mainParser = $parser ?? new Parser();
54
55 $this
56 ->registerExtension(new Extension\NodeExtension())
57 ->registerExtension(new Extension\CombinationExtension())
58 ->registerExtension(new Extension\FunctionExtension())
59 ->registerExtension(new Extension\PseudoClassExtension())
60 ->registerExtension(new Extension\AttributeMatchingExtension())
61 ;
62 }
63
64 public static function getXpathLiteral(string $element): string
65 {
66 if (!str_contains($element, "'")) {
67 return "'".$element."'";
68 }
69
70 if (!str_contains($element, '"')) {
71 return '"'.$element.'"';
72 }
73
74 $string = $element;
75 $parts = [];
76 while (true) {
77 if (false !== $pos = strpos($string, "'")) {
78 $parts[] = \sprintf("'%s'", substr($string, 0, $pos));
79 $parts[] = "\"'\"";
80 $string = substr($string, $pos + 1);
81 } else {
82 $parts[] = "'$string'";
83 break;
84 }
85 }
86
87 return \sprintf('concat(%s)', implode(', ', $parts));
88 }
89
90 public function cssToXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string
91 {
92 $selectors = $this->parseSelectors($cssExpr);
93
94 /** @var SelectorNode $selector */
95 foreach ($selectors as $index => $selector) {
96 if (null !== $selector->getPseudoElement()) {
97 throw new ExpressionErrorException('Pseudo-elements are not supported.');
98 }
99
100 $selectors[$index] = $this->selectorToXPath($selector, $prefix);
101 }
102
103 return implode(' | ', $selectors);
104 }
105
106 public function selectorToXPath(SelectorNode $selector, string $prefix = 'descendant-or-self::'): string
107 {
108 return ($prefix ?: '').$this->nodeToXPath($selector);
109 }
110
111 /**
112 * @return $this
113 */
114 public function registerExtension(Extension\ExtensionInterface $extension): static
115 {
116 $this->extensions[$extension->getName()] = $extension;
117
118 $this->nodeTranslators = array_merge($this->nodeTranslators, $extension->getNodeTranslators());
119 $this->combinationTranslators = array_merge($this->combinationTranslators, $extension->getCombinationTranslators());
120 $this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators());
121 $this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators());
122 $this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators());
123
124 return $this;
125 }
126
127 /**
128 * @throws ExpressionErrorException
129 */
130 public function getExtension(string $name): Extension\ExtensionInterface
131 {
132 if (!isset($this->extensions[$name])) {
133 throw new ExpressionErrorException(\sprintf('Extension "%s" not registered.', $name));
134 }
135
136 return $this->extensions[$name];
137 }
138
139 /**
140 * @return $this
141 */
142 public function registerParserShortcut(ParserInterface $shortcut): static
143 {
144 $this->shortcutParsers[] = $shortcut;
145
146 return $this;
147 }
148
149 /**
150 * @throws ExpressionErrorException
151 */
152 public function nodeToXPath(NodeInterface $node): XPathExpr
153 {
154 if (!isset($this->nodeTranslators[$node->getNodeName()])) {
155 throw new ExpressionErrorException(\sprintf('Node "%s" not supported.', $node->getNodeName()));
156 }
157
158 return $this->nodeTranslators[$node->getNodeName()]($node, $this);
159 }
160
161 /**
162 * @throws ExpressionErrorException
163 */
164 public function addCombination(string $combiner, NodeInterface $xpath, NodeInterface $combinedXpath): XPathExpr
165 {
166 if (!isset($this->combinationTranslators[$combiner])) {
167 throw new ExpressionErrorException(\sprintf('Combiner "%s" not supported.', $combiner));
168 }
169
170 return $this->combinationTranslators[$combiner]($this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
171 }
172
173 /**
174 * @throws ExpressionErrorException
175 */
176 public function addFunction(XPathExpr $xpath, FunctionNode $function): XPathExpr
177 {
178 if (!isset($this->functionTranslators[$function->getName()])) {
179 throw new ExpressionErrorException(\sprintf('Function "%s" not supported.', $function->getName()));
180 }
181
182 return $this->functionTranslators[$function->getName()]($xpath, $function);
183 }
184
185 /**
186 * @throws ExpressionErrorException
187 */
188 public function addPseudoClass(XPathExpr $xpath, string $pseudoClass): XPathExpr
189 {
190 if (!isset($this->pseudoClassTranslators[$pseudoClass])) {
191 throw new ExpressionErrorException(\sprintf('Pseudo-class "%s" not supported.', $pseudoClass));
192 }
193
194 return $this->pseudoClassTranslators[$pseudoClass]($xpath);
195 }
196
197 /**
198 * @throws ExpressionErrorException
199 */
200 public function addAttributeMatching(XPathExpr $xpath, string $operator, string $attribute, ?string $value): XPathExpr
201 {
202 if (!isset($this->attributeMatchingTranslators[$operator])) {
203 throw new ExpressionErrorException(\sprintf('Attribute matcher operator "%s" not supported.', $operator));
204 }
205
206 return $this->attributeMatchingTranslators[$operator]($xpath, $attribute, $value);
207 }
208
209 /**
210 * @return SelectorNode[]
211 */
212 private function parseSelectors(string $css): array
213 {
214 foreach ($this->shortcutParsers as $shortcut) {
215 $tokens = $shortcut->parse($css);
216
217 if ($tokens) {
218 return $tokens;
219 }
220 }
221
222 return $this->mainParser->parse($css);
223 }
224}