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\Node\FunctionNode;
16use Symfony\Component\CssSelector\XPath\Translator;
17use Symfony\Component\CssSelector\XPath\XPathExpr;
18
19/**
20 * XPath expression translator HTML extension.
21 *
22 * This component is a port of the Python cssselect library,
23 * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
24 *
25 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
26 *
27 * @internal
28 */
29class HtmlExtension extends AbstractExtension
30{
31    public function __construct(Translator $translator)
32    {
33        $translator
34            ->getExtension('node')
35            ->setFlag(NodeExtension::ELEMENT_NAME_IN_LOWER_CASE, true)
36            ->setFlag(NodeExtension::ATTRIBUTE_NAME_IN_LOWER_CASE, true);
37    }
38
39    /**
40     * {@inheritdoc}
41     */
42    public function getPseudoClassTranslators(): array
43    {
44        return [
45            'checked' => [$this, 'translateChecked'],
46            'link' => [$this, 'translateLink'],
47            'disabled' => [$this, 'translateDisabled'],
48            'enabled' => [$this, 'translateEnabled'],
49            'selected' => [$this, 'translateSelected'],
50            'invalid' => [$this, 'translateInvalid'],
51            'hover' => [$this, 'translateHover'],
52            'visited' => [$this, 'translateVisited'],
53        ];
54    }
55
56    /**
57     * {@inheritdoc}
58     */
59    public function getFunctionTranslators(): array
60    {
61        return [
62            'lang' => [$this, 'translateLang'],
63        ];
64    }
65
66    public function translateChecked(XPathExpr $xpath): XPathExpr
67    {
68        return $xpath->addCondition(
69            '(@checked '
70            ."and (name(.) = 'input' or name(.) = 'command')"
71            ."and (@type = 'checkbox' or @type = 'radio'))"
72        );
73    }
74
75    public function translateLink(XPathExpr $xpath): XPathExpr
76    {
77        return $xpath->addCondition("@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')");
78    }
79
80    public function translateDisabled(XPathExpr $xpath): XPathExpr
81    {
82        return $xpath->addCondition(
83            '('
84                .'@disabled and'
85                .'('
86                    ."(name(.) = 'input' and @type != 'hidden')"
87                    ." or name(.) = 'button'"
88                    ." or name(.) = 'select'"
89                    ." or name(.) = 'textarea'"
90                    ." or name(.) = 'command'"
91                    ." or name(.) = 'fieldset'"
92                    ." or name(.) = 'optgroup'"
93                    ." or name(.) = 'option'"
94                .')'
95            .') or ('
96                ."(name(.) = 'input' and @type != 'hidden')"
97                ." or name(.) = 'button'"
98                ." or name(.) = 'select'"
99                ." or name(.) = 'textarea'"
100            .')'
101            .' and ancestor::fieldset[@disabled]'
102        );
103        // todo: in the second half, add "and is not a descendant of that fieldset element's first legend element child, if any."
104    }
105
106    public function translateEnabled(XPathExpr $xpath): XPathExpr
107    {
108        return $xpath->addCondition(
109            '('
110                .'@href and ('
111                    ."name(.) = 'a'"
112                    ." or name(.) = 'link'"
113                    ." or name(.) = 'area'"
114                .')'
115            .') or ('
116                .'('
117                    ."name(.) = 'command'"
118                    ." or name(.) = 'fieldset'"
119                    ." or name(.) = 'optgroup'"
120                .')'
121                .' and not(@disabled)'
122            .') or ('
123                .'('
124                    ."(name(.) = 'input' and @type != 'hidden')"
125                    ." or name(.) = 'button'"
126                    ." or name(.) = 'select'"
127                    ." or name(.) = 'textarea'"
128                    ." or name(.) = 'keygen'"
129                .')'
130                .' and not (@disabled or ancestor::fieldset[@disabled])'
131            .') or ('
132                ."name(.) = 'option' and not("
133                    .'@disabled or ancestor::optgroup[@disabled]'
134                .')'
135            .')'
136        );
137    }
138
139    /**
140     * @throws ExpressionErrorException
141     */
142    public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr
143    {
144        $arguments = $function->getArguments();
145        foreach ($arguments as $token) {
146            if (!($token->isString() || $token->isIdentifier())) {
147                throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments));
148            }
149        }
150
151        return $xpath->addCondition(sprintf(
152            'ancestor-or-self::*[@lang][1][starts-with(concat('
153            ."translate(@%s, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '-')"
154            .', %s)]',
155            'lang',
156            Translator::getXpathLiteral(strtolower($arguments[0]->getValue()).'-')
157        ));
158    }
159
160    public function translateSelected(XPathExpr $xpath): XPathExpr
161    {
162        return $xpath->addCondition("(@selected and name(.) = 'option')");
163    }
164
165    public function translateInvalid(XPathExpr $xpath): XPathExpr
166    {
167        return $xpath->addCondition('0');
168    }
169
170    public function translateHover(XPathExpr $xpath): XPathExpr
171    {
172        return $xpath->addCondition('0');
173    }
174
175    public function translateVisited(XPathExpr $xpath): XPathExpr
176    {
177        return $xpath->addCondition('0');
178    }
179
180    /**
181     * {@inheritdoc}
182     */
183    public function getName(): string
184    {
185        return 'html';
186    }
187}
188