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 private $mainParser;
|
Chris@0
|
34
|
Chris@0
|
35 /**
|
Chris@0
|
36 * @var ParserInterface[]
|
Chris@0
|
37 */
|
Chris@17
|
38 private $shortcutParsers = [];
|
Chris@0
|
39
|
Chris@0
|
40 /**
|
Chris@14
|
41 * @var Extension\ExtensionInterface[]
|
Chris@0
|
42 */
|
Chris@17
|
43 private $extensions = [];
|
Chris@0
|
44
|
Chris@17
|
45 private $nodeTranslators = [];
|
Chris@17
|
46 private $combinationTranslators = [];
|
Chris@17
|
47 private $functionTranslators = [];
|
Chris@17
|
48 private $pseudoClassTranslators = [];
|
Chris@17
|
49 private $attributeMatchingTranslators = [];
|
Chris@0
|
50
|
Chris@0
|
51 public function __construct(ParserInterface $parser = null)
|
Chris@0
|
52 {
|
Chris@0
|
53 $this->mainParser = $parser ?: new Parser();
|
Chris@0
|
54
|
Chris@0
|
55 $this
|
Chris@0
|
56 ->registerExtension(new Extension\NodeExtension())
|
Chris@0
|
57 ->registerExtension(new Extension\CombinationExtension())
|
Chris@0
|
58 ->registerExtension(new Extension\FunctionExtension())
|
Chris@0
|
59 ->registerExtension(new Extension\PseudoClassExtension())
|
Chris@0
|
60 ->registerExtension(new Extension\AttributeMatchingExtension())
|
Chris@0
|
61 ;
|
Chris@0
|
62 }
|
Chris@0
|
63
|
Chris@0
|
64 /**
|
Chris@0
|
65 * @param string $element
|
Chris@0
|
66 *
|
Chris@0
|
67 * @return string
|
Chris@0
|
68 */
|
Chris@0
|
69 public static function getXpathLiteral($element)
|
Chris@0
|
70 {
|
Chris@0
|
71 if (false === strpos($element, "'")) {
|
Chris@0
|
72 return "'".$element."'";
|
Chris@0
|
73 }
|
Chris@0
|
74
|
Chris@0
|
75 if (false === strpos($element, '"')) {
|
Chris@0
|
76 return '"'.$element.'"';
|
Chris@0
|
77 }
|
Chris@0
|
78
|
Chris@0
|
79 $string = $element;
|
Chris@17
|
80 $parts = [];
|
Chris@0
|
81 while (true) {
|
Chris@0
|
82 if (false !== $pos = strpos($string, "'")) {
|
Chris@0
|
83 $parts[] = sprintf("'%s'", substr($string, 0, $pos));
|
Chris@0
|
84 $parts[] = "\"'\"";
|
Chris@0
|
85 $string = substr($string, $pos + 1);
|
Chris@0
|
86 } else {
|
Chris@0
|
87 $parts[] = "'$string'";
|
Chris@0
|
88 break;
|
Chris@0
|
89 }
|
Chris@0
|
90 }
|
Chris@0
|
91
|
Chris@17
|
92 return sprintf('concat(%s)', implode(', ', $parts));
|
Chris@0
|
93 }
|
Chris@0
|
94
|
Chris@0
|
95 /**
|
Chris@0
|
96 * {@inheritdoc}
|
Chris@0
|
97 */
|
Chris@0
|
98 public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::')
|
Chris@0
|
99 {
|
Chris@0
|
100 $selectors = $this->parseSelectors($cssExpr);
|
Chris@0
|
101
|
Chris@0
|
102 /** @var SelectorNode $selector */
|
Chris@0
|
103 foreach ($selectors as $index => $selector) {
|
Chris@0
|
104 if (null !== $selector->getPseudoElement()) {
|
Chris@0
|
105 throw new ExpressionErrorException('Pseudo-elements are not supported.');
|
Chris@0
|
106 }
|
Chris@0
|
107
|
Chris@0
|
108 $selectors[$index] = $this->selectorToXPath($selector, $prefix);
|
Chris@0
|
109 }
|
Chris@0
|
110
|
Chris@0
|
111 return implode(' | ', $selectors);
|
Chris@0
|
112 }
|
Chris@0
|
113
|
Chris@0
|
114 /**
|
Chris@0
|
115 * {@inheritdoc}
|
Chris@0
|
116 */
|
Chris@0
|
117 public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::')
|
Chris@0
|
118 {
|
Chris@0
|
119 return ($prefix ?: '').$this->nodeToXPath($selector);
|
Chris@0
|
120 }
|
Chris@0
|
121
|
Chris@0
|
122 /**
|
Chris@0
|
123 * Registers an extension.
|
Chris@0
|
124 *
|
Chris@0
|
125 * @return $this
|
Chris@0
|
126 */
|
Chris@0
|
127 public function registerExtension(Extension\ExtensionInterface $extension)
|
Chris@0
|
128 {
|
Chris@0
|
129 $this->extensions[$extension->getName()] = $extension;
|
Chris@0
|
130
|
Chris@0
|
131 $this->nodeTranslators = array_merge($this->nodeTranslators, $extension->getNodeTranslators());
|
Chris@0
|
132 $this->combinationTranslators = array_merge($this->combinationTranslators, $extension->getCombinationTranslators());
|
Chris@0
|
133 $this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators());
|
Chris@0
|
134 $this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators());
|
Chris@0
|
135 $this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators());
|
Chris@0
|
136
|
Chris@0
|
137 return $this;
|
Chris@0
|
138 }
|
Chris@0
|
139
|
Chris@0
|
140 /**
|
Chris@0
|
141 * @param string $name
|
Chris@0
|
142 *
|
Chris@0
|
143 * @return Extension\ExtensionInterface
|
Chris@0
|
144 *
|
Chris@0
|
145 * @throws ExpressionErrorException
|
Chris@0
|
146 */
|
Chris@0
|
147 public function getExtension($name)
|
Chris@0
|
148 {
|
Chris@0
|
149 if (!isset($this->extensions[$name])) {
|
Chris@0
|
150 throw new ExpressionErrorException(sprintf('Extension "%s" not registered.', $name));
|
Chris@0
|
151 }
|
Chris@0
|
152
|
Chris@0
|
153 return $this->extensions[$name];
|
Chris@0
|
154 }
|
Chris@0
|
155
|
Chris@0
|
156 /**
|
Chris@0
|
157 * Registers a shortcut parser.
|
Chris@0
|
158 *
|
Chris@0
|
159 * @return $this
|
Chris@0
|
160 */
|
Chris@0
|
161 public function registerParserShortcut(ParserInterface $shortcut)
|
Chris@0
|
162 {
|
Chris@0
|
163 $this->shortcutParsers[] = $shortcut;
|
Chris@0
|
164
|
Chris@0
|
165 return $this;
|
Chris@0
|
166 }
|
Chris@0
|
167
|
Chris@0
|
168 /**
|
Chris@0
|
169 * @return XPathExpr
|
Chris@0
|
170 *
|
Chris@0
|
171 * @throws ExpressionErrorException
|
Chris@0
|
172 */
|
Chris@0
|
173 public function nodeToXPath(NodeInterface $node)
|
Chris@0
|
174 {
|
Chris@0
|
175 if (!isset($this->nodeTranslators[$node->getNodeName()])) {
|
Chris@0
|
176 throw new ExpressionErrorException(sprintf('Node "%s" not supported.', $node->getNodeName()));
|
Chris@0
|
177 }
|
Chris@0
|
178
|
Chris@17
|
179 return \call_user_func($this->nodeTranslators[$node->getNodeName()], $node, $this);
|
Chris@0
|
180 }
|
Chris@0
|
181
|
Chris@0
|
182 /**
|
Chris@0
|
183 * @param string $combiner
|
Chris@0
|
184 * @param NodeInterface $xpath
|
Chris@0
|
185 * @param NodeInterface $combinedXpath
|
Chris@0
|
186 *
|
Chris@0
|
187 * @return XPathExpr
|
Chris@0
|
188 *
|
Chris@0
|
189 * @throws ExpressionErrorException
|
Chris@0
|
190 */
|
Chris@0
|
191 public function addCombination($combiner, NodeInterface $xpath, NodeInterface $combinedXpath)
|
Chris@0
|
192 {
|
Chris@0
|
193 if (!isset($this->combinationTranslators[$combiner])) {
|
Chris@0
|
194 throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner));
|
Chris@0
|
195 }
|
Chris@0
|
196
|
Chris@17
|
197 return \call_user_func($this->combinationTranslators[$combiner], $this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
|
Chris@0
|
198 }
|
Chris@0
|
199
|
Chris@0
|
200 /**
|
Chris@0
|
201 * @return XPathExpr
|
Chris@0
|
202 *
|
Chris@0
|
203 * @throws ExpressionErrorException
|
Chris@0
|
204 */
|
Chris@0
|
205 public function addFunction(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
206 {
|
Chris@0
|
207 if (!isset($this->functionTranslators[$function->getName()])) {
|
Chris@0
|
208 throw new ExpressionErrorException(sprintf('Function "%s" not supported.', $function->getName()));
|
Chris@0
|
209 }
|
Chris@0
|
210
|
Chris@17
|
211 return \call_user_func($this->functionTranslators[$function->getName()], $xpath, $function);
|
Chris@0
|
212 }
|
Chris@0
|
213
|
Chris@0
|
214 /**
|
Chris@0
|
215 * @param XPathExpr $xpath
|
Chris@0
|
216 * @param string $pseudoClass
|
Chris@0
|
217 *
|
Chris@0
|
218 * @return XPathExpr
|
Chris@0
|
219 *
|
Chris@0
|
220 * @throws ExpressionErrorException
|
Chris@0
|
221 */
|
Chris@0
|
222 public function addPseudoClass(XPathExpr $xpath, $pseudoClass)
|
Chris@0
|
223 {
|
Chris@0
|
224 if (!isset($this->pseudoClassTranslators[$pseudoClass])) {
|
Chris@0
|
225 throw new ExpressionErrorException(sprintf('Pseudo-class "%s" not supported.', $pseudoClass));
|
Chris@0
|
226 }
|
Chris@0
|
227
|
Chris@17
|
228 return \call_user_func($this->pseudoClassTranslators[$pseudoClass], $xpath);
|
Chris@0
|
229 }
|
Chris@0
|
230
|
Chris@0
|
231 /**
|
Chris@0
|
232 * @param XPathExpr $xpath
|
Chris@0
|
233 * @param string $operator
|
Chris@0
|
234 * @param string $attribute
|
Chris@0
|
235 * @param string $value
|
Chris@0
|
236 *
|
Chris@0
|
237 * @return XPathExpr
|
Chris@0
|
238 *
|
Chris@0
|
239 * @throws ExpressionErrorException
|
Chris@0
|
240 */
|
Chris@0
|
241 public function addAttributeMatching(XPathExpr $xpath, $operator, $attribute, $value)
|
Chris@0
|
242 {
|
Chris@0
|
243 if (!isset($this->attributeMatchingTranslators[$operator])) {
|
Chris@0
|
244 throw new ExpressionErrorException(sprintf('Attribute matcher operator "%s" not supported.', $operator));
|
Chris@0
|
245 }
|
Chris@0
|
246
|
Chris@17
|
247 return \call_user_func($this->attributeMatchingTranslators[$operator], $xpath, $attribute, $value);
|
Chris@0
|
248 }
|
Chris@0
|
249
|
Chris@0
|
250 /**
|
Chris@0
|
251 * @param string $css
|
Chris@0
|
252 *
|
Chris@0
|
253 * @return SelectorNode[]
|
Chris@0
|
254 */
|
Chris@0
|
255 private function parseSelectors($css)
|
Chris@0
|
256 {
|
Chris@0
|
257 foreach ($this->shortcutParsers as $shortcut) {
|
Chris@0
|
258 $tokens = $shortcut->parse($css);
|
Chris@0
|
259
|
Chris@0
|
260 if (!empty($tokens)) {
|
Chris@0
|
261 return $tokens;
|
Chris@0
|
262 }
|
Chris@0
|
263 }
|
Chris@0
|
264
|
Chris@0
|
265 return $this->mainParser->parse($css);
|
Chris@0
|
266 }
|
Chris@0
|
267 }
|