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\Extension;
|
Chris@0
|
13
|
Chris@0
|
14 use Symfony\Component\CssSelector\Node;
|
Chris@0
|
15 use Symfony\Component\CssSelector\XPath\Translator;
|
Chris@0
|
16 use Symfony\Component\CssSelector\XPath\XPathExpr;
|
Chris@0
|
17
|
Chris@0
|
18 /**
|
Chris@0
|
19 * XPath expression translator node extension.
|
Chris@0
|
20 *
|
Chris@0
|
21 * This component is a port of the Python cssselect library,
|
Chris@0
|
22 * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
Chris@0
|
23 *
|
Chris@0
|
24 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
Chris@0
|
25 *
|
Chris@0
|
26 * @internal
|
Chris@0
|
27 */
|
Chris@0
|
28 class NodeExtension extends AbstractExtension
|
Chris@0
|
29 {
|
Chris@0
|
30 const ELEMENT_NAME_IN_LOWER_CASE = 1;
|
Chris@0
|
31 const ATTRIBUTE_NAME_IN_LOWER_CASE = 2;
|
Chris@0
|
32 const ATTRIBUTE_VALUE_IN_LOWER_CASE = 4;
|
Chris@0
|
33
|
Chris@0
|
34 /**
|
Chris@0
|
35 * @var int
|
Chris@0
|
36 */
|
Chris@0
|
37 private $flags;
|
Chris@0
|
38
|
Chris@0
|
39 /**
|
Chris@0
|
40 * Constructor.
|
Chris@0
|
41 *
|
Chris@0
|
42 * @param int $flags
|
Chris@0
|
43 */
|
Chris@0
|
44 public function __construct($flags = 0)
|
Chris@0
|
45 {
|
Chris@0
|
46 $this->flags = $flags;
|
Chris@0
|
47 }
|
Chris@0
|
48
|
Chris@0
|
49 /**
|
Chris@0
|
50 * @param int $flag
|
Chris@0
|
51 * @param bool $on
|
Chris@0
|
52 *
|
Chris@0
|
53 * @return $this
|
Chris@0
|
54 */
|
Chris@0
|
55 public function setFlag($flag, $on)
|
Chris@0
|
56 {
|
Chris@0
|
57 if ($on && !$this->hasFlag($flag)) {
|
Chris@0
|
58 $this->flags += $flag;
|
Chris@0
|
59 }
|
Chris@0
|
60
|
Chris@0
|
61 if (!$on && $this->hasFlag($flag)) {
|
Chris@0
|
62 $this->flags -= $flag;
|
Chris@0
|
63 }
|
Chris@0
|
64
|
Chris@0
|
65 return $this;
|
Chris@0
|
66 }
|
Chris@0
|
67
|
Chris@0
|
68 /**
|
Chris@0
|
69 * @param int $flag
|
Chris@0
|
70 *
|
Chris@0
|
71 * @return bool
|
Chris@0
|
72 */
|
Chris@0
|
73 public function hasFlag($flag)
|
Chris@0
|
74 {
|
Chris@0
|
75 return (bool) ($this->flags & $flag);
|
Chris@0
|
76 }
|
Chris@0
|
77
|
Chris@0
|
78 /**
|
Chris@0
|
79 * {@inheritdoc}
|
Chris@0
|
80 */
|
Chris@0
|
81 public function getNodeTranslators()
|
Chris@0
|
82 {
|
Chris@0
|
83 return array(
|
Chris@0
|
84 'Selector' => array($this, 'translateSelector'),
|
Chris@0
|
85 'CombinedSelector' => array($this, 'translateCombinedSelector'),
|
Chris@0
|
86 'Negation' => array($this, 'translateNegation'),
|
Chris@0
|
87 'Function' => array($this, 'translateFunction'),
|
Chris@0
|
88 'Pseudo' => array($this, 'translatePseudo'),
|
Chris@0
|
89 'Attribute' => array($this, 'translateAttribute'),
|
Chris@0
|
90 'Class' => array($this, 'translateClass'),
|
Chris@0
|
91 'Hash' => array($this, 'translateHash'),
|
Chris@0
|
92 'Element' => array($this, 'translateElement'),
|
Chris@0
|
93 );
|
Chris@0
|
94 }
|
Chris@0
|
95
|
Chris@0
|
96 /**
|
Chris@0
|
97 * @param Node\SelectorNode $node
|
Chris@0
|
98 * @param Translator $translator
|
Chris@0
|
99 *
|
Chris@0
|
100 * @return XPathExpr
|
Chris@0
|
101 */
|
Chris@0
|
102 public function translateSelector(Node\SelectorNode $node, Translator $translator)
|
Chris@0
|
103 {
|
Chris@0
|
104 return $translator->nodeToXPath($node->getTree());
|
Chris@0
|
105 }
|
Chris@0
|
106
|
Chris@0
|
107 /**
|
Chris@0
|
108 * @param Node\CombinedSelectorNode $node
|
Chris@0
|
109 * @param Translator $translator
|
Chris@0
|
110 *
|
Chris@0
|
111 * @return XPathExpr
|
Chris@0
|
112 */
|
Chris@0
|
113 public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator)
|
Chris@0
|
114 {
|
Chris@0
|
115 return $translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector());
|
Chris@0
|
116 }
|
Chris@0
|
117
|
Chris@0
|
118 /**
|
Chris@0
|
119 * @param Node\NegationNode $node
|
Chris@0
|
120 * @param Translator $translator
|
Chris@0
|
121 *
|
Chris@0
|
122 * @return XPathExpr
|
Chris@0
|
123 */
|
Chris@0
|
124 public function translateNegation(Node\NegationNode $node, Translator $translator)
|
Chris@0
|
125 {
|
Chris@0
|
126 $xpath = $translator->nodeToXPath($node->getSelector());
|
Chris@0
|
127 $subXpath = $translator->nodeToXPath($node->getSubSelector());
|
Chris@0
|
128 $subXpath->addNameTest();
|
Chris@0
|
129
|
Chris@0
|
130 if ($subXpath->getCondition()) {
|
Chris@0
|
131 return $xpath->addCondition(sprintf('not(%s)', $subXpath->getCondition()));
|
Chris@0
|
132 }
|
Chris@0
|
133
|
Chris@0
|
134 return $xpath->addCondition('0');
|
Chris@0
|
135 }
|
Chris@0
|
136
|
Chris@0
|
137 /**
|
Chris@0
|
138 * @param Node\FunctionNode $node
|
Chris@0
|
139 * @param Translator $translator
|
Chris@0
|
140 *
|
Chris@0
|
141 * @return XPathExpr
|
Chris@0
|
142 */
|
Chris@0
|
143 public function translateFunction(Node\FunctionNode $node, Translator $translator)
|
Chris@0
|
144 {
|
Chris@0
|
145 $xpath = $translator->nodeToXPath($node->getSelector());
|
Chris@0
|
146
|
Chris@0
|
147 return $translator->addFunction($xpath, $node);
|
Chris@0
|
148 }
|
Chris@0
|
149
|
Chris@0
|
150 /**
|
Chris@0
|
151 * @param Node\PseudoNode $node
|
Chris@0
|
152 * @param Translator $translator
|
Chris@0
|
153 *
|
Chris@0
|
154 * @return XPathExpr
|
Chris@0
|
155 */
|
Chris@0
|
156 public function translatePseudo(Node\PseudoNode $node, Translator $translator)
|
Chris@0
|
157 {
|
Chris@0
|
158 $xpath = $translator->nodeToXPath($node->getSelector());
|
Chris@0
|
159
|
Chris@0
|
160 return $translator->addPseudoClass($xpath, $node->getIdentifier());
|
Chris@0
|
161 }
|
Chris@0
|
162
|
Chris@0
|
163 /**
|
Chris@0
|
164 * @param Node\AttributeNode $node
|
Chris@0
|
165 * @param Translator $translator
|
Chris@0
|
166 *
|
Chris@0
|
167 * @return XPathExpr
|
Chris@0
|
168 */
|
Chris@0
|
169 public function translateAttribute(Node\AttributeNode $node, Translator $translator)
|
Chris@0
|
170 {
|
Chris@0
|
171 $name = $node->getAttribute();
|
Chris@0
|
172 $safe = $this->isSafeName($name);
|
Chris@0
|
173
|
Chris@0
|
174 if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) {
|
Chris@0
|
175 $name = strtolower($name);
|
Chris@0
|
176 }
|
Chris@0
|
177
|
Chris@0
|
178 if ($node->getNamespace()) {
|
Chris@0
|
179 $name = sprintf('%s:%s', $node->getNamespace(), $name);
|
Chris@0
|
180 $safe = $safe && $this->isSafeName($node->getNamespace());
|
Chris@0
|
181 }
|
Chris@0
|
182
|
Chris@0
|
183 $attribute = $safe ? '@'.$name : sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name));
|
Chris@0
|
184 $value = $node->getValue();
|
Chris@0
|
185 $xpath = $translator->nodeToXPath($node->getSelector());
|
Chris@0
|
186
|
Chris@0
|
187 if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) {
|
Chris@0
|
188 $value = strtolower($value);
|
Chris@0
|
189 }
|
Chris@0
|
190
|
Chris@0
|
191 return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value);
|
Chris@0
|
192 }
|
Chris@0
|
193
|
Chris@0
|
194 /**
|
Chris@0
|
195 * @param Node\ClassNode $node
|
Chris@0
|
196 * @param Translator $translator
|
Chris@0
|
197 *
|
Chris@0
|
198 * @return XPathExpr
|
Chris@0
|
199 */
|
Chris@0
|
200 public function translateClass(Node\ClassNode $node, Translator $translator)
|
Chris@0
|
201 {
|
Chris@0
|
202 $xpath = $translator->nodeToXPath($node->getSelector());
|
Chris@0
|
203
|
Chris@0
|
204 return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName());
|
Chris@0
|
205 }
|
Chris@0
|
206
|
Chris@0
|
207 /**
|
Chris@0
|
208 * @param Node\HashNode $node
|
Chris@0
|
209 * @param Translator $translator
|
Chris@0
|
210 *
|
Chris@0
|
211 * @return XPathExpr
|
Chris@0
|
212 */
|
Chris@0
|
213 public function translateHash(Node\HashNode $node, Translator $translator)
|
Chris@0
|
214 {
|
Chris@0
|
215 $xpath = $translator->nodeToXPath($node->getSelector());
|
Chris@0
|
216
|
Chris@0
|
217 return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId());
|
Chris@0
|
218 }
|
Chris@0
|
219
|
Chris@0
|
220 /**
|
Chris@0
|
221 * @param Node\ElementNode $node
|
Chris@0
|
222 *
|
Chris@0
|
223 * @return XPathExpr
|
Chris@0
|
224 */
|
Chris@0
|
225 public function translateElement(Node\ElementNode $node)
|
Chris@0
|
226 {
|
Chris@0
|
227 $element = $node->getElement();
|
Chris@0
|
228
|
Chris@0
|
229 if ($this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) {
|
Chris@0
|
230 $element = strtolower($element);
|
Chris@0
|
231 }
|
Chris@0
|
232
|
Chris@0
|
233 if ($element) {
|
Chris@0
|
234 $safe = $this->isSafeName($element);
|
Chris@0
|
235 } else {
|
Chris@0
|
236 $element = '*';
|
Chris@0
|
237 $safe = true;
|
Chris@0
|
238 }
|
Chris@0
|
239
|
Chris@0
|
240 if ($node->getNamespace()) {
|
Chris@0
|
241 $element = sprintf('%s:%s', $node->getNamespace(), $element);
|
Chris@0
|
242 $safe = $safe && $this->isSafeName($node->getNamespace());
|
Chris@0
|
243 }
|
Chris@0
|
244
|
Chris@0
|
245 $xpath = new XPathExpr('', $element);
|
Chris@0
|
246
|
Chris@0
|
247 if (!$safe) {
|
Chris@0
|
248 $xpath->addNameTest();
|
Chris@0
|
249 }
|
Chris@0
|
250
|
Chris@0
|
251 return $xpath;
|
Chris@0
|
252 }
|
Chris@0
|
253
|
Chris@0
|
254 /**
|
Chris@0
|
255 * {@inheritdoc}
|
Chris@0
|
256 */
|
Chris@0
|
257 public function getName()
|
Chris@0
|
258 {
|
Chris@0
|
259 return 'node';
|
Chris@0
|
260 }
|
Chris@0
|
261
|
Chris@0
|
262 /**
|
Chris@0
|
263 * Tests if given name is safe.
|
Chris@0
|
264 *
|
Chris@0
|
265 * @param string $name
|
Chris@0
|
266 *
|
Chris@0
|
267 * @return bool
|
Chris@0
|
268 */
|
Chris@0
|
269 private function isSafeName($name)
|
Chris@0
|
270 {
|
Chris@0
|
271 return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name);
|
Chris@0
|
272 }
|
Chris@0
|
273 }
|