Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 /*
|
Chris@0
|
4 * This file is part of the Symfony package.
|
Chris@0
|
5 *
|
Chris@0
|
6 * (c) Fabien Potencier <fabien@symfony.com>
|
Chris@0
|
7 *
|
Chris@0
|
8 * For the full copyright and license information, please view the LICENSE
|
Chris@0
|
9 * file that was distributed with this source code.
|
Chris@0
|
10 */
|
Chris@0
|
11
|
Chris@0
|
12 namespace Symfony\Component\CssSelector\XPath;
|
Chris@0
|
13
|
Chris@0
|
14 use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
|
Chris@0
|
15 use Symfony\Component\CssSelector\Node\FunctionNode;
|
Chris@0
|
16 use Symfony\Component\CssSelector\Node\NodeInterface;
|
Chris@0
|
17 use Symfony\Component\CssSelector\Node\SelectorNode;
|
Chris@0
|
18 use Symfony\Component\CssSelector\Parser\Parser;
|
Chris@0
|
19 use Symfony\Component\CssSelector\Parser\ParserInterface;
|
Chris@0
|
20
|
Chris@0
|
21 /**
|
Chris@0
|
22 * XPath expression translator interface.
|
Chris@0
|
23 *
|
Chris@0
|
24 * This component is a port of the Python cssselect library,
|
Chris@0
|
25 * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
Chris@0
|
26 *
|
Chris@0
|
27 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
Chris@0
|
28 *
|
Chris@0
|
29 * @internal
|
Chris@0
|
30 */
|
Chris@0
|
31 class Translator implements TranslatorInterface
|
Chris@0
|
32 {
|
Chris@0
|
33 /**
|
Chris@0
|
34 * @var ParserInterface
|
Chris@0
|
35 */
|
Chris@0
|
36 private $mainParser;
|
Chris@0
|
37
|
Chris@0
|
38 /**
|
Chris@0
|
39 * @var ParserInterface[]
|
Chris@0
|
40 */
|
Chris@0
|
41 private $shortcutParsers = array();
|
Chris@0
|
42
|
Chris@0
|
43 /**
|
Chris@0
|
44 * @var Extension\ExtensionInterface
|
Chris@0
|
45 */
|
Chris@0
|
46 private $extensions = array();
|
Chris@0
|
47
|
Chris@0
|
48 /**
|
Chris@0
|
49 * @var array
|
Chris@0
|
50 */
|
Chris@0
|
51 private $nodeTranslators = array();
|
Chris@0
|
52
|
Chris@0
|
53 /**
|
Chris@0
|
54 * @var array
|
Chris@0
|
55 */
|
Chris@0
|
56 private $combinationTranslators = array();
|
Chris@0
|
57
|
Chris@0
|
58 /**
|
Chris@0
|
59 * @var array
|
Chris@0
|
60 */
|
Chris@0
|
61 private $functionTranslators = array();
|
Chris@0
|
62
|
Chris@0
|
63 /**
|
Chris@0
|
64 * @var array
|
Chris@0
|
65 */
|
Chris@0
|
66 private $pseudoClassTranslators = array();
|
Chris@0
|
67
|
Chris@0
|
68 /**
|
Chris@0
|
69 * @var array
|
Chris@0
|
70 */
|
Chris@0
|
71 private $attributeMatchingTranslators = array();
|
Chris@0
|
72
|
Chris@0
|
73 public function __construct(ParserInterface $parser = null)
|
Chris@0
|
74 {
|
Chris@0
|
75 $this->mainParser = $parser ?: new Parser();
|
Chris@0
|
76
|
Chris@0
|
77 $this
|
Chris@0
|
78 ->registerExtension(new Extension\NodeExtension())
|
Chris@0
|
79 ->registerExtension(new Extension\CombinationExtension())
|
Chris@0
|
80 ->registerExtension(new Extension\FunctionExtension())
|
Chris@0
|
81 ->registerExtension(new Extension\PseudoClassExtension())
|
Chris@0
|
82 ->registerExtension(new Extension\AttributeMatchingExtension())
|
Chris@0
|
83 ;
|
Chris@0
|
84 }
|
Chris@0
|
85
|
Chris@0
|
86 /**
|
Chris@0
|
87 * @param string $element
|
Chris@0
|
88 *
|
Chris@0
|
89 * @return string
|
Chris@0
|
90 */
|
Chris@0
|
91 public static function getXpathLiteral($element)
|
Chris@0
|
92 {
|
Chris@0
|
93 if (false === strpos($element, "'")) {
|
Chris@0
|
94 return "'".$element."'";
|
Chris@0
|
95 }
|
Chris@0
|
96
|
Chris@0
|
97 if (false === strpos($element, '"')) {
|
Chris@0
|
98 return '"'.$element.'"';
|
Chris@0
|
99 }
|
Chris@0
|
100
|
Chris@0
|
101 $string = $element;
|
Chris@0
|
102 $parts = array();
|
Chris@0
|
103 while (true) {
|
Chris@0
|
104 if (false !== $pos = strpos($string, "'")) {
|
Chris@0
|
105 $parts[] = sprintf("'%s'", substr($string, 0, $pos));
|
Chris@0
|
106 $parts[] = "\"'\"";
|
Chris@0
|
107 $string = substr($string, $pos + 1);
|
Chris@0
|
108 } else {
|
Chris@0
|
109 $parts[] = "'$string'";
|
Chris@0
|
110 break;
|
Chris@0
|
111 }
|
Chris@0
|
112 }
|
Chris@0
|
113
|
Chris@0
|
114 return sprintf('concat(%s)', implode($parts, ', '));
|
Chris@0
|
115 }
|
Chris@0
|
116
|
Chris@0
|
117 /**
|
Chris@0
|
118 * {@inheritdoc}
|
Chris@0
|
119 */
|
Chris@0
|
120 public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::')
|
Chris@0
|
121 {
|
Chris@0
|
122 $selectors = $this->parseSelectors($cssExpr);
|
Chris@0
|
123
|
Chris@0
|
124 /** @var SelectorNode $selector */
|
Chris@0
|
125 foreach ($selectors as $index => $selector) {
|
Chris@0
|
126 if (null !== $selector->getPseudoElement()) {
|
Chris@0
|
127 throw new ExpressionErrorException('Pseudo-elements are not supported.');
|
Chris@0
|
128 }
|
Chris@0
|
129
|
Chris@0
|
130 $selectors[$index] = $this->selectorToXPath($selector, $prefix);
|
Chris@0
|
131 }
|
Chris@0
|
132
|
Chris@0
|
133 return implode(' | ', $selectors);
|
Chris@0
|
134 }
|
Chris@0
|
135
|
Chris@0
|
136 /**
|
Chris@0
|
137 * {@inheritdoc}
|
Chris@0
|
138 */
|
Chris@0
|
139 public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::')
|
Chris@0
|
140 {
|
Chris@0
|
141 return ($prefix ?: '').$this->nodeToXPath($selector);
|
Chris@0
|
142 }
|
Chris@0
|
143
|
Chris@0
|
144 /**
|
Chris@0
|
145 * Registers an extension.
|
Chris@0
|
146 *
|
Chris@0
|
147 * @param Extension\ExtensionInterface $extension
|
Chris@0
|
148 *
|
Chris@0
|
149 * @return $this
|
Chris@0
|
150 */
|
Chris@0
|
151 public function registerExtension(Extension\ExtensionInterface $extension)
|
Chris@0
|
152 {
|
Chris@0
|
153 $this->extensions[$extension->getName()] = $extension;
|
Chris@0
|
154
|
Chris@0
|
155 $this->nodeTranslators = array_merge($this->nodeTranslators, $extension->getNodeTranslators());
|
Chris@0
|
156 $this->combinationTranslators = array_merge($this->combinationTranslators, $extension->getCombinationTranslators());
|
Chris@0
|
157 $this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators());
|
Chris@0
|
158 $this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators());
|
Chris@0
|
159 $this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators());
|
Chris@0
|
160
|
Chris@0
|
161 return $this;
|
Chris@0
|
162 }
|
Chris@0
|
163
|
Chris@0
|
164 /**
|
Chris@0
|
165 * @param string $name
|
Chris@0
|
166 *
|
Chris@0
|
167 * @return Extension\ExtensionInterface
|
Chris@0
|
168 *
|
Chris@0
|
169 * @throws ExpressionErrorException
|
Chris@0
|
170 */
|
Chris@0
|
171 public function getExtension($name)
|
Chris@0
|
172 {
|
Chris@0
|
173 if (!isset($this->extensions[$name])) {
|
Chris@0
|
174 throw new ExpressionErrorException(sprintf('Extension "%s" not registered.', $name));
|
Chris@0
|
175 }
|
Chris@0
|
176
|
Chris@0
|
177 return $this->extensions[$name];
|
Chris@0
|
178 }
|
Chris@0
|
179
|
Chris@0
|
180 /**
|
Chris@0
|
181 * Registers a shortcut parser.
|
Chris@0
|
182 *
|
Chris@0
|
183 * @param ParserInterface $shortcut
|
Chris@0
|
184 *
|
Chris@0
|
185 * @return $this
|
Chris@0
|
186 */
|
Chris@0
|
187 public function registerParserShortcut(ParserInterface $shortcut)
|
Chris@0
|
188 {
|
Chris@0
|
189 $this->shortcutParsers[] = $shortcut;
|
Chris@0
|
190
|
Chris@0
|
191 return $this;
|
Chris@0
|
192 }
|
Chris@0
|
193
|
Chris@0
|
194 /**
|
Chris@0
|
195 * @param NodeInterface $node
|
Chris@0
|
196 *
|
Chris@0
|
197 * @return XPathExpr
|
Chris@0
|
198 *
|
Chris@0
|
199 * @throws ExpressionErrorException
|
Chris@0
|
200 */
|
Chris@0
|
201 public function nodeToXPath(NodeInterface $node)
|
Chris@0
|
202 {
|
Chris@0
|
203 if (!isset($this->nodeTranslators[$node->getNodeName()])) {
|
Chris@0
|
204 throw new ExpressionErrorException(sprintf('Node "%s" not supported.', $node->getNodeName()));
|
Chris@0
|
205 }
|
Chris@0
|
206
|
Chris@0
|
207 return call_user_func($this->nodeTranslators[$node->getNodeName()], $node, $this);
|
Chris@0
|
208 }
|
Chris@0
|
209
|
Chris@0
|
210 /**
|
Chris@0
|
211 * @param string $combiner
|
Chris@0
|
212 * @param NodeInterface $xpath
|
Chris@0
|
213 * @param NodeInterface $combinedXpath
|
Chris@0
|
214 *
|
Chris@0
|
215 * @return XPathExpr
|
Chris@0
|
216 *
|
Chris@0
|
217 * @throws ExpressionErrorException
|
Chris@0
|
218 */
|
Chris@0
|
219 public function addCombination($combiner, NodeInterface $xpath, NodeInterface $combinedXpath)
|
Chris@0
|
220 {
|
Chris@0
|
221 if (!isset($this->combinationTranslators[$combiner])) {
|
Chris@0
|
222 throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner));
|
Chris@0
|
223 }
|
Chris@0
|
224
|
Chris@0
|
225 return call_user_func($this->combinationTranslators[$combiner], $this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
|
Chris@0
|
226 }
|
Chris@0
|
227
|
Chris@0
|
228 /**
|
Chris@0
|
229 * @param XPathExpr $xpath
|
Chris@0
|
230 * @param FunctionNode $function
|
Chris@0
|
231 *
|
Chris@0
|
232 * @return XPathExpr
|
Chris@0
|
233 *
|
Chris@0
|
234 * @throws ExpressionErrorException
|
Chris@0
|
235 */
|
Chris@0
|
236 public function addFunction(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
237 {
|
Chris@0
|
238 if (!isset($this->functionTranslators[$function->getName()])) {
|
Chris@0
|
239 throw new ExpressionErrorException(sprintf('Function "%s" not supported.', $function->getName()));
|
Chris@0
|
240 }
|
Chris@0
|
241
|
Chris@0
|
242 return call_user_func($this->functionTranslators[$function->getName()], $xpath, $function);
|
Chris@0
|
243 }
|
Chris@0
|
244
|
Chris@0
|
245 /**
|
Chris@0
|
246 * @param XPathExpr $xpath
|
Chris@0
|
247 * @param string $pseudoClass
|
Chris@0
|
248 *
|
Chris@0
|
249 * @return XPathExpr
|
Chris@0
|
250 *
|
Chris@0
|
251 * @throws ExpressionErrorException
|
Chris@0
|
252 */
|
Chris@0
|
253 public function addPseudoClass(XPathExpr $xpath, $pseudoClass)
|
Chris@0
|
254 {
|
Chris@0
|
255 if (!isset($this->pseudoClassTranslators[$pseudoClass])) {
|
Chris@0
|
256 throw new ExpressionErrorException(sprintf('Pseudo-class "%s" not supported.', $pseudoClass));
|
Chris@0
|
257 }
|
Chris@0
|
258
|
Chris@0
|
259 return call_user_func($this->pseudoClassTranslators[$pseudoClass], $xpath);
|
Chris@0
|
260 }
|
Chris@0
|
261
|
Chris@0
|
262 /**
|
Chris@0
|
263 * @param XPathExpr $xpath
|
Chris@0
|
264 * @param string $operator
|
Chris@0
|
265 * @param string $attribute
|
Chris@0
|
266 * @param string $value
|
Chris@0
|
267 *
|
Chris@0
|
268 * @return XPathExpr
|
Chris@0
|
269 *
|
Chris@0
|
270 * @throws ExpressionErrorException
|
Chris@0
|
271 */
|
Chris@0
|
272 public function addAttributeMatching(XPathExpr $xpath, $operator, $attribute, $value)
|
Chris@0
|
273 {
|
Chris@0
|
274 if (!isset($this->attributeMatchingTranslators[$operator])) {
|
Chris@0
|
275 throw new ExpressionErrorException(sprintf('Attribute matcher operator "%s" not supported.', $operator));
|
Chris@0
|
276 }
|
Chris@0
|
277
|
Chris@0
|
278 return call_user_func($this->attributeMatchingTranslators[$operator], $xpath, $attribute, $value);
|
Chris@0
|
279 }
|
Chris@0
|
280
|
Chris@0
|
281 /**
|
Chris@0
|
282 * @param string $css
|
Chris@0
|
283 *
|
Chris@0
|
284 * @return SelectorNode[]
|
Chris@0
|
285 */
|
Chris@0
|
286 private function parseSelectors($css)
|
Chris@0
|
287 {
|
Chris@0
|
288 foreach ($this->shortcutParsers as $shortcut) {
|
Chris@0
|
289 $tokens = $shortcut->parse($css);
|
Chris@0
|
290
|
Chris@0
|
291 if (!empty($tokens)) {
|
Chris@0
|
292 return $tokens;
|
Chris@0
|
293 }
|
Chris@0
|
294 }
|
Chris@0
|
295
|
Chris@0
|
296 return $this->mainParser->parse($css);
|
Chris@0
|
297 }
|
Chris@0
|
298 }
|