friendship ended with social-app. php is my new best friend
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
12namespace Symfony\Component\CssSelector\XPath\Extension;
13
14use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
15use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
16use Symfony\Component\CssSelector\Node\FunctionNode;
17use Symfony\Component\CssSelector\Parser\Parser;
18use Symfony\Component\CssSelector\XPath\Translator;
19use Symfony\Component\CssSelector\XPath\XPathExpr;
20
21/**
22 * XPath expression translator function extension.
23 *
24 * This component is a port of the Python cssselect library,
25 * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
26 *
27 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
28 *
29 * @internal
30 */
31class FunctionExtension extends AbstractExtension
32{
33 public function getFunctionTranslators(): array
34 {
35 return [
36 'nth-child' => $this->translateNthChild(...),
37 'nth-last-child' => $this->translateNthLastChild(...),
38 'nth-of-type' => $this->translateNthOfType(...),
39 'nth-last-of-type' => $this->translateNthLastOfType(...),
40 'contains' => $this->translateContains(...),
41 'lang' => $this->translateLang(...),
42 ];
43 }
44
45 /**
46 * @throws ExpressionErrorException
47 */
48 public function translateNthChild(XPathExpr $xpath, FunctionNode $function, bool $last = false, bool $addNameTest = true): XPathExpr
49 {
50 try {
51 [$a, $b] = Parser::parseSeries($function->getArguments());
52 } catch (SyntaxErrorException $e) {
53 throw new ExpressionErrorException(\sprintf('Invalid series: "%s".', implode('", "', $function->getArguments())), 0, $e);
54 }
55
56 $xpath->addStarPrefix();
57 if ($addNameTest) {
58 $xpath->addNameTest();
59 }
60
61 if (0 === $a) {
62 return $xpath->addCondition('position() = '.($last ? 'last() - '.($b - 1) : $b));
63 }
64
65 if ($a < 0) {
66 if ($b < 1) {
67 return $xpath->addCondition('false()');
68 }
69
70 $sign = '<=';
71 } else {
72 $sign = '>=';
73 }
74
75 $expr = 'position()';
76
77 if ($last) {
78 $expr = 'last() - '.$expr;
79 --$b;
80 }
81
82 if (0 !== $b) {
83 $expr .= ' - '.$b;
84 }
85
86 $conditions = [\sprintf('%s %s 0', $expr, $sign)];
87
88 if (1 !== $a && -1 !== $a) {
89 $conditions[] = \sprintf('(%s) mod %d = 0', $expr, $a);
90 }
91
92 return $xpath->addCondition(implode(' and ', $conditions));
93
94 // todo: handle an+b, odd, even
95 // an+b means every-a, plus b, e.g., 2n+1 means odd
96 // 0n+b means b
97 // n+0 means a=1, i.e., all elements
98 // an means every a elements, i.e., 2n means even
99 // -n means -1n
100 // -1n+6 means elements 6 and previous
101 }
102
103 public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function): XPathExpr
104 {
105 return $this->translateNthChild($xpath, $function, true);
106 }
107
108 public function translateNthOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
109 {
110 return $this->translateNthChild($xpath, $function, false, false);
111 }
112
113 /**
114 * @throws ExpressionErrorException
115 */
116 public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
117 {
118 if ('*' === $xpath->getElement()) {
119 throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.');
120 }
121
122 return $this->translateNthChild($xpath, $function, true, false);
123 }
124
125 /**
126 * @throws ExpressionErrorException
127 */
128 public function translateContains(XPathExpr $xpath, FunctionNode $function): XPathExpr
129 {
130 $arguments = $function->getArguments();
131 foreach ($arguments as $token) {
132 if (!($token->isString() || $token->isIdentifier())) {
133 throw new ExpressionErrorException('Expected a single string or identifier for :contains(), got '.implode(', ', $arguments));
134 }
135 }
136
137 return $xpath->addCondition(\sprintf(
138 'contains(string(.), %s)',
139 Translator::getXpathLiteral($arguments[0]->getValue())
140 ));
141 }
142
143 /**
144 * @throws ExpressionErrorException
145 */
146 public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr
147 {
148 $arguments = $function->getArguments();
149 foreach ($arguments as $token) {
150 if (!($token->isString() || $token->isIdentifier())) {
151 throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments));
152 }
153 }
154
155 return $xpath->addCondition(\sprintf(
156 'lang(%s)',
157 Translator::getXpathLiteral($arguments[0]->getValue())
158 ));
159 }
160
161 public function getName(): string
162 {
163 return 'function';
164 }
165}