|
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 |
|
12 namespace Symfony\Component\CssSelector\Node; |
|
13 |
|
14 use Symfony\Component\CssSelector\Exception\ParseException; |
|
15 use Symfony\Component\CssSelector\XPathExpr; |
|
16 |
|
17 /** |
|
18 * FunctionNode represents a "selector:name(expr)" node. |
|
19 * |
|
20 * This component is a port of the Python lxml library, |
|
21 * which is copyright Infrae and distributed under the BSD license. |
|
22 * |
|
23 * @author Fabien Potencier <fabien@symfony.com> |
|
24 */ |
|
25 class FunctionNode implements NodeInterface |
|
26 { |
|
27 static protected $unsupported = array('target', 'lang', 'enabled', 'disabled'); |
|
28 |
|
29 protected $selector; |
|
30 protected $type; |
|
31 protected $name; |
|
32 protected $expr; |
|
33 |
|
34 /** |
|
35 * Constructor. |
|
36 * |
|
37 * @param NodeInterface $selector The XPath expression |
|
38 * @param string $type |
|
39 * @param string $name |
|
40 * @param XPathExpr $expr |
|
41 */ |
|
42 public function __construct($selector, $type, $name, $expr) |
|
43 { |
|
44 $this->selector = $selector; |
|
45 $this->type = $type; |
|
46 $this->name = $name; |
|
47 $this->expr = $expr; |
|
48 } |
|
49 |
|
50 /** |
|
51 * {@inheritDoc} |
|
52 */ |
|
53 public function __toString() |
|
54 { |
|
55 return sprintf('%s[%s%s%s(%s)]', __CLASS__, $this->selector, $this->type, $this->name, $this->expr); |
|
56 } |
|
57 |
|
58 /** |
|
59 * {@inheritDoc} |
|
60 * @throws ParseException When unsupported or unknown pseudo-class is found |
|
61 */ |
|
62 public function toXpath() |
|
63 { |
|
64 $selPath = $this->selector->toXpath(); |
|
65 if (in_array($this->name, self::$unsupported)) { |
|
66 throw new ParseException(sprintf('The pseudo-class %s is not supported', $this->name)); |
|
67 } |
|
68 $method = '_xpath_'.str_replace('-', '_', $this->name); |
|
69 if (!method_exists($this, $method)) { |
|
70 throw new ParseException(sprintf('The pseudo-class %s is unknown', $this->name)); |
|
71 } |
|
72 |
|
73 return $this->$method($selPath, $this->expr); |
|
74 } |
|
75 |
|
76 /** |
|
77 * undocumented function |
|
78 * |
|
79 * @param XPathExpr $xpath |
|
80 * @param mixed $expr |
|
81 * @param Boolean $last |
|
82 * @param Boolean $addNameTest |
|
83 * @return XPathExpr |
|
84 */ |
|
85 protected function _xpath_nth_child($xpath, $expr, $last = false, $addNameTest = true) |
|
86 { |
|
87 list($a, $b) = $this->parseSeries($expr); |
|
88 if (!$a && !$b && !$last) { |
|
89 // a=0 means nothing is returned... |
|
90 $xpath->addCondition('false() and position() = 0'); |
|
91 |
|
92 return $xpath; |
|
93 } |
|
94 |
|
95 if ($addNameTest) { |
|
96 $xpath->addNameTest(); |
|
97 } |
|
98 |
|
99 $xpath->addStarPrefix(); |
|
100 if ($a == 0) { |
|
101 if ($last) { |
|
102 $b = sprintf('last() - %s', $b); |
|
103 } |
|
104 $xpath->addCondition(sprintf('position() = %s', $b)); |
|
105 |
|
106 return $xpath; |
|
107 } |
|
108 |
|
109 if ($last) { |
|
110 // FIXME: I'm not sure if this is right |
|
111 $a = -$a; |
|
112 $b = -$b; |
|
113 } |
|
114 |
|
115 if ($b > 0) { |
|
116 $bNeg = -$b; |
|
117 } else { |
|
118 $bNeg = sprintf('+%s', -$b); |
|
119 } |
|
120 |
|
121 if ($a != 1) { |
|
122 $expr = array(sprintf('(position() %s) mod %s = 0', $bNeg, $a)); |
|
123 } else { |
|
124 $expr = array(); |
|
125 } |
|
126 |
|
127 if ($b >= 0) { |
|
128 $expr[] = sprintf('position() >= %s', $b); |
|
129 } elseif ($b < 0 && $last) { |
|
130 $expr[] = sprintf('position() < (last() %s)', $b); |
|
131 } |
|
132 $expr = implode($expr, ' and '); |
|
133 |
|
134 if ($expr) { |
|
135 $xpath->addCondition($expr); |
|
136 } |
|
137 |
|
138 return $xpath; |
|
139 /* FIXME: handle an+b, odd, even |
|
140 an+b means every-a, plus b, e.g., 2n+1 means odd |
|
141 0n+b means b |
|
142 n+0 means a=1, i.e., all elements |
|
143 an means every a elements, i.e., 2n means even |
|
144 -n means -1n |
|
145 -1n+6 means elements 6 and previous */ |
|
146 } |
|
147 |
|
148 /** |
|
149 * undocumented function |
|
150 * |
|
151 * @param XPathExpr $xpath |
|
152 * @param XPathExpr $expr |
|
153 * @return XPathExpr |
|
154 */ |
|
155 protected function _xpath_nth_last_child($xpath, $expr) |
|
156 { |
|
157 return $this->_xpath_nth_child($xpath, $expr, true); |
|
158 } |
|
159 |
|
160 /** |
|
161 * undocumented function |
|
162 * |
|
163 * @param XPathExpr $xpath |
|
164 * @param XPathExpr $expr |
|
165 * @return XPathExpr |
|
166 */ |
|
167 protected function _xpath_nth_of_type($xpath, $expr) |
|
168 { |
|
169 if ($xpath->getElement() == '*') { |
|
170 throw new ParseException('*:nth-of-type() is not implemented'); |
|
171 } |
|
172 |
|
173 return $this->_xpath_nth_child($xpath, $expr, false, false); |
|
174 } |
|
175 |
|
176 /** |
|
177 * undocumented function |
|
178 * |
|
179 * @param XPathExpr $xpath |
|
180 * @param XPathExpr $expr |
|
181 * @return XPathExpr |
|
182 */ |
|
183 protected function _xpath_nth_last_of_type($xpath, $expr) |
|
184 { |
|
185 return $this->_xpath_nth_child($xpath, $expr, true, false); |
|
186 } |
|
187 |
|
188 /** |
|
189 * undocumented function |
|
190 * |
|
191 * @param XPathExpr $xpath |
|
192 * @param XPathExpr $expr |
|
193 * @return XPathExpr |
|
194 */ |
|
195 protected function _xpath_contains($xpath, $expr) |
|
196 { |
|
197 // text content, minus tags, must contain expr |
|
198 if ($expr instanceof ElementNode) { |
|
199 $expr = $expr->formatElement(); |
|
200 } |
|
201 |
|
202 // FIXME: lower-case is only available with XPath 2 |
|
203 //$xpath->addCondition(sprintf('contains(lower-case(string(.)), %s)', XPathExpr::xpathLiteral(strtolower($expr)))); |
|
204 $xpath->addCondition(sprintf('contains(string(.), %s)', XPathExpr::xpathLiteral($expr))); |
|
205 |
|
206 // FIXME: Currently case insensitive matching doesn't seem to be happening |
|
207 |
|
208 return $xpath; |
|
209 } |
|
210 |
|
211 /** |
|
212 * undocumented function |
|
213 * |
|
214 * @param XPathExpr $xpath |
|
215 * @param XPathExpr $expr |
|
216 * @return XPathExpr |
|
217 */ |
|
218 protected function _xpath_not($xpath, $expr) |
|
219 { |
|
220 // everything for which not expr applies |
|
221 $expr = $expr->toXpath(); |
|
222 $cond = $expr->getCondition(); |
|
223 // FIXME: should I do something about element_path? |
|
224 $xpath->addCondition(sprintf('not(%s)', $cond)); |
|
225 |
|
226 return $xpath; |
|
227 } |
|
228 |
|
229 /** |
|
230 * Parses things like '1n+2', or 'an+b' generally, returning (a, b) |
|
231 * |
|
232 * @param mixed $s |
|
233 * @return array |
|
234 */ |
|
235 protected function parseSeries($s) |
|
236 { |
|
237 if ($s instanceof ElementNode) { |
|
238 $s = $s->formatElement(); |
|
239 } |
|
240 |
|
241 if (!$s || '*' == $s) { |
|
242 // Happens when there's nothing, which the CSS parser thinks of as * |
|
243 return array(0, 0); |
|
244 } |
|
245 |
|
246 if (is_string($s)) { |
|
247 // Happens when you just get a number |
|
248 return array(0, $s); |
|
249 } |
|
250 |
|
251 if ('odd' == $s) { |
|
252 return array(2, 1); |
|
253 } |
|
254 |
|
255 if ('even' == $s) { |
|
256 return array(2, 0); |
|
257 } |
|
258 |
|
259 if ('n' == $s) { |
|
260 return array(1, 0); |
|
261 } |
|
262 |
|
263 if (false === strpos($s, 'n')) { |
|
264 // Just a b |
|
265 |
|
266 return array(0, intval((string) $s)); |
|
267 } |
|
268 |
|
269 list($a, $b) = explode('n', $s); |
|
270 if (!$a) { |
|
271 $a = 1; |
|
272 } elseif ('-' == $a || '+' == $a) { |
|
273 $a = intval($a.'1'); |
|
274 } else { |
|
275 $a = intval($a); |
|
276 } |
|
277 |
|
278 if (!$b) { |
|
279 $b = 0; |
|
280 } elseif ('-' == $b || '+' == $b) { |
|
281 $b = intval($b.'1'); |
|
282 } else { |
|
283 $b = intval($b); |
|
284 } |
|
285 |
|
286 return array($a, $b); |
|
287 } |
|
288 } |