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