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\Exception\ExpressionErrorException;
|
Chris@0
|
15 use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
|
Chris@0
|
16 use Symfony\Component\CssSelector\Node\FunctionNode;
|
Chris@0
|
17 use Symfony\Component\CssSelector\Parser\Parser;
|
Chris@0
|
18 use Symfony\Component\CssSelector\XPath\Translator;
|
Chris@0
|
19 use Symfony\Component\CssSelector\XPath\XPathExpr;
|
Chris@0
|
20
|
Chris@0
|
21 /**
|
Chris@0
|
22 * XPath expression translator function extension.
|
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 FunctionExtension extends AbstractExtension
|
Chris@0
|
32 {
|
Chris@0
|
33 /**
|
Chris@0
|
34 * {@inheritdoc}
|
Chris@0
|
35 */
|
Chris@0
|
36 public function getFunctionTranslators()
|
Chris@0
|
37 {
|
Chris@0
|
38 return array(
|
Chris@0
|
39 'nth-child' => array($this, 'translateNthChild'),
|
Chris@0
|
40 'nth-last-child' => array($this, 'translateNthLastChild'),
|
Chris@0
|
41 'nth-of-type' => array($this, 'translateNthOfType'),
|
Chris@0
|
42 'nth-last-of-type' => array($this, 'translateNthLastOfType'),
|
Chris@0
|
43 'contains' => array($this, 'translateContains'),
|
Chris@0
|
44 'lang' => array($this, 'translateLang'),
|
Chris@0
|
45 );
|
Chris@0
|
46 }
|
Chris@0
|
47
|
Chris@0
|
48 /**
|
Chris@0
|
49 * @param XPathExpr $xpath
|
Chris@0
|
50 * @param FunctionNode $function
|
Chris@0
|
51 * @param bool $last
|
Chris@0
|
52 * @param bool $addNameTest
|
Chris@0
|
53 *
|
Chris@0
|
54 * @return XPathExpr
|
Chris@0
|
55 *
|
Chris@0
|
56 * @throws ExpressionErrorException
|
Chris@0
|
57 */
|
Chris@0
|
58 public function translateNthChild(XPathExpr $xpath, FunctionNode $function, $last = false, $addNameTest = true)
|
Chris@0
|
59 {
|
Chris@0
|
60 try {
|
Chris@0
|
61 list($a, $b) = Parser::parseSeries($function->getArguments());
|
Chris@0
|
62 } catch (SyntaxErrorException $e) {
|
Chris@0
|
63 throw new ExpressionErrorException(sprintf('Invalid series: %s', implode(', ', $function->getArguments())), 0, $e);
|
Chris@0
|
64 }
|
Chris@0
|
65
|
Chris@0
|
66 $xpath->addStarPrefix();
|
Chris@0
|
67 if ($addNameTest) {
|
Chris@0
|
68 $xpath->addNameTest();
|
Chris@0
|
69 }
|
Chris@0
|
70
|
Chris@0
|
71 if (0 === $a) {
|
Chris@0
|
72 return $xpath->addCondition('position() = '.($last ? 'last() - '.($b - 1) : $b));
|
Chris@0
|
73 }
|
Chris@0
|
74
|
Chris@0
|
75 if ($a < 0) {
|
Chris@0
|
76 if ($b < 1) {
|
Chris@0
|
77 return $xpath->addCondition('false()');
|
Chris@0
|
78 }
|
Chris@0
|
79
|
Chris@0
|
80 $sign = '<=';
|
Chris@0
|
81 } else {
|
Chris@0
|
82 $sign = '>=';
|
Chris@0
|
83 }
|
Chris@0
|
84
|
Chris@0
|
85 $expr = 'position()';
|
Chris@0
|
86
|
Chris@0
|
87 if ($last) {
|
Chris@0
|
88 $expr = 'last() - '.$expr;
|
Chris@0
|
89 --$b;
|
Chris@0
|
90 }
|
Chris@0
|
91
|
Chris@0
|
92 if (0 !== $b) {
|
Chris@0
|
93 $expr .= ' - '.$b;
|
Chris@0
|
94 }
|
Chris@0
|
95
|
Chris@0
|
96 $conditions = array(sprintf('%s %s 0', $expr, $sign));
|
Chris@0
|
97
|
Chris@0
|
98 if (1 !== $a && -1 !== $a) {
|
Chris@0
|
99 $conditions[] = sprintf('(%s) mod %d = 0', $expr, $a);
|
Chris@0
|
100 }
|
Chris@0
|
101
|
Chris@0
|
102 return $xpath->addCondition(implode(' and ', $conditions));
|
Chris@0
|
103
|
Chris@0
|
104 // todo: handle an+b, odd, even
|
Chris@0
|
105 // an+b means every-a, plus b, e.g., 2n+1 means odd
|
Chris@0
|
106 // 0n+b means b
|
Chris@0
|
107 // n+0 means a=1, i.e., all elements
|
Chris@0
|
108 // an means every a elements, i.e., 2n means even
|
Chris@0
|
109 // -n means -1n
|
Chris@0
|
110 // -1n+6 means elements 6 and previous
|
Chris@0
|
111 }
|
Chris@0
|
112
|
Chris@0
|
113 /**
|
Chris@0
|
114 * @param XPathExpr $xpath
|
Chris@0
|
115 * @param FunctionNode $function
|
Chris@0
|
116 *
|
Chris@0
|
117 * @return XPathExpr
|
Chris@0
|
118 */
|
Chris@0
|
119 public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
120 {
|
Chris@0
|
121 return $this->translateNthChild($xpath, $function, true);
|
Chris@0
|
122 }
|
Chris@0
|
123
|
Chris@0
|
124 /**
|
Chris@0
|
125 * @param XPathExpr $xpath
|
Chris@0
|
126 * @param FunctionNode $function
|
Chris@0
|
127 *
|
Chris@0
|
128 * @return XPathExpr
|
Chris@0
|
129 */
|
Chris@0
|
130 public function translateNthOfType(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
131 {
|
Chris@0
|
132 return $this->translateNthChild($xpath, $function, false, false);
|
Chris@0
|
133 }
|
Chris@0
|
134
|
Chris@0
|
135 /**
|
Chris@0
|
136 * @param XPathExpr $xpath
|
Chris@0
|
137 * @param FunctionNode $function
|
Chris@0
|
138 *
|
Chris@0
|
139 * @return XPathExpr
|
Chris@0
|
140 *
|
Chris@0
|
141 * @throws ExpressionErrorException
|
Chris@0
|
142 */
|
Chris@0
|
143 public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
144 {
|
Chris@0
|
145 if ('*' === $xpath->getElement()) {
|
Chris@0
|
146 throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.');
|
Chris@0
|
147 }
|
Chris@0
|
148
|
Chris@0
|
149 return $this->translateNthChild($xpath, $function, true, false);
|
Chris@0
|
150 }
|
Chris@0
|
151
|
Chris@0
|
152 /**
|
Chris@0
|
153 * @param XPathExpr $xpath
|
Chris@0
|
154 * @param FunctionNode $function
|
Chris@0
|
155 *
|
Chris@0
|
156 * @return XPathExpr
|
Chris@0
|
157 *
|
Chris@0
|
158 * @throws ExpressionErrorException
|
Chris@0
|
159 */
|
Chris@0
|
160 public function translateContains(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
161 {
|
Chris@0
|
162 $arguments = $function->getArguments();
|
Chris@0
|
163 foreach ($arguments as $token) {
|
Chris@0
|
164 if (!($token->isString() || $token->isIdentifier())) {
|
Chris@0
|
165 throw new ExpressionErrorException(
|
Chris@0
|
166 'Expected a single string or identifier for :contains(), got '
|
Chris@0
|
167 .implode(', ', $arguments)
|
Chris@0
|
168 );
|
Chris@0
|
169 }
|
Chris@0
|
170 }
|
Chris@0
|
171
|
Chris@0
|
172 return $xpath->addCondition(sprintf(
|
Chris@0
|
173 'contains(string(.), %s)',
|
Chris@0
|
174 Translator::getXpathLiteral($arguments[0]->getValue())
|
Chris@0
|
175 ));
|
Chris@0
|
176 }
|
Chris@0
|
177
|
Chris@0
|
178 /**
|
Chris@0
|
179 * @param XPathExpr $xpath
|
Chris@0
|
180 * @param FunctionNode $function
|
Chris@0
|
181 *
|
Chris@0
|
182 * @return XPathExpr
|
Chris@0
|
183 *
|
Chris@0
|
184 * @throws ExpressionErrorException
|
Chris@0
|
185 */
|
Chris@0
|
186 public function translateLang(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
187 {
|
Chris@0
|
188 $arguments = $function->getArguments();
|
Chris@0
|
189 foreach ($arguments as $token) {
|
Chris@0
|
190 if (!($token->isString() || $token->isIdentifier())) {
|
Chris@0
|
191 throw new ExpressionErrorException(
|
Chris@0
|
192 'Expected a single string or identifier for :lang(), got '
|
Chris@0
|
193 .implode(', ', $arguments)
|
Chris@0
|
194 );
|
Chris@0
|
195 }
|
Chris@0
|
196 }
|
Chris@0
|
197
|
Chris@0
|
198 return $xpath->addCondition(sprintf(
|
Chris@0
|
199 'lang(%s)',
|
Chris@0
|
200 Translator::getXpathLiteral($arguments[0]->getValue())
|
Chris@0
|
201 ));
|
Chris@0
|
202 }
|
Chris@0
|
203
|
Chris@0
|
204 /**
|
Chris@0
|
205 * {@inheritdoc}
|
Chris@0
|
206 */
|
Chris@0
|
207 public function getName()
|
Chris@0
|
208 {
|
Chris@0
|
209 return 'function';
|
Chris@0
|
210 }
|
Chris@0
|
211 }
|