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@17
|
38 return [
|
Chris@17
|
39 'nth-child' => [$this, 'translateNthChild'],
|
Chris@17
|
40 'nth-last-child' => [$this, 'translateNthLastChild'],
|
Chris@17
|
41 'nth-of-type' => [$this, 'translateNthOfType'],
|
Chris@17
|
42 'nth-last-of-type' => [$this, 'translateNthLastOfType'],
|
Chris@17
|
43 'contains' => [$this, 'translateContains'],
|
Chris@17
|
44 'lang' => [$this, 'translateLang'],
|
Chris@17
|
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@17
|
96 $conditions = [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 * @return XPathExpr
|
Chris@0
|
115 */
|
Chris@0
|
116 public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
117 {
|
Chris@0
|
118 return $this->translateNthChild($xpath, $function, true);
|
Chris@0
|
119 }
|
Chris@0
|
120
|
Chris@0
|
121 /**
|
Chris@0
|
122 * @return XPathExpr
|
Chris@0
|
123 */
|
Chris@0
|
124 public function translateNthOfType(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
125 {
|
Chris@0
|
126 return $this->translateNthChild($xpath, $function, false, false);
|
Chris@0
|
127 }
|
Chris@0
|
128
|
Chris@0
|
129 /**
|
Chris@0
|
130 * @return XPathExpr
|
Chris@0
|
131 *
|
Chris@0
|
132 * @throws ExpressionErrorException
|
Chris@0
|
133 */
|
Chris@0
|
134 public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
135 {
|
Chris@0
|
136 if ('*' === $xpath->getElement()) {
|
Chris@0
|
137 throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.');
|
Chris@0
|
138 }
|
Chris@0
|
139
|
Chris@0
|
140 return $this->translateNthChild($xpath, $function, true, false);
|
Chris@0
|
141 }
|
Chris@0
|
142
|
Chris@0
|
143 /**
|
Chris@0
|
144 * @return XPathExpr
|
Chris@0
|
145 *
|
Chris@0
|
146 * @throws ExpressionErrorException
|
Chris@0
|
147 */
|
Chris@0
|
148 public function translateContains(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
149 {
|
Chris@0
|
150 $arguments = $function->getArguments();
|
Chris@0
|
151 foreach ($arguments as $token) {
|
Chris@0
|
152 if (!($token->isString() || $token->isIdentifier())) {
|
Chris@17
|
153 throw new ExpressionErrorException('Expected a single string or identifier for :contains(), got '.implode(', ', $arguments));
|
Chris@0
|
154 }
|
Chris@0
|
155 }
|
Chris@0
|
156
|
Chris@0
|
157 return $xpath->addCondition(sprintf(
|
Chris@0
|
158 'contains(string(.), %s)',
|
Chris@0
|
159 Translator::getXpathLiteral($arguments[0]->getValue())
|
Chris@0
|
160 ));
|
Chris@0
|
161 }
|
Chris@0
|
162
|
Chris@0
|
163 /**
|
Chris@0
|
164 * @return XPathExpr
|
Chris@0
|
165 *
|
Chris@0
|
166 * @throws ExpressionErrorException
|
Chris@0
|
167 */
|
Chris@0
|
168 public function translateLang(XPathExpr $xpath, FunctionNode $function)
|
Chris@0
|
169 {
|
Chris@0
|
170 $arguments = $function->getArguments();
|
Chris@0
|
171 foreach ($arguments as $token) {
|
Chris@0
|
172 if (!($token->isString() || $token->isIdentifier())) {
|
Chris@17
|
173 throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments));
|
Chris@0
|
174 }
|
Chris@0
|
175 }
|
Chris@0
|
176
|
Chris@0
|
177 return $xpath->addCondition(sprintf(
|
Chris@0
|
178 'lang(%s)',
|
Chris@0
|
179 Translator::getXpathLiteral($arguments[0]->getValue())
|
Chris@0
|
180 ));
|
Chris@0
|
181 }
|
Chris@0
|
182
|
Chris@0
|
183 /**
|
Chris@0
|
184 * {@inheritdoc}
|
Chris@0
|
185 */
|
Chris@0
|
186 public function getName()
|
Chris@0
|
187 {
|
Chris@0
|
188 return 'function';
|
Chris@0
|
189 }
|
Chris@0
|
190 }
|