Skip to content

Commit 88453e6

Browse files
franckranaivonicolas-grekas
authored andcommitted
[CssSelector] Add suport for :scope
1 parent efc0747 commit 88453e6

File tree

6 files changed

+41
-2
lines changed

6 files changed

+41
-2
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.3
5+
-----
6+
7+
* Add support for `:scope`
8+
49
4.4.0
510
-----
611

Exception/SyntaxErrorException.php

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ public static function nestedNot(): self
4343
return new self('Got nested ::not().');
4444
}
4545

46+
public static function notAtTheStartOfASelector(string $pseudoElement): self
47+
{
48+
return new self(sprintf('Got immediate child pseudo-element ":%s" not at the start of a selector', $pseudoElement));
49+
}
50+
4651
public static function stringAsFunctionArgument(): self
4752
{
4853
return new self('String not allowed as function argument.');

Parser/Parser.php

+13-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* CSS selector parser.
2020
*
2121
* This component is a port of the Python cssselect library,
22-
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
22+
* which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
2323
*
2424
* @author Jean-François Simon <[email protected]>
2525
*
@@ -192,7 +192,18 @@ private function parseSimpleSelector(TokenStream $stream, bool $insideNegation =
192192

193193
if (!$stream->getPeek()->isDelimiter(['('])) {
194194
$result = new Node\PseudoNode($result, $identifier);
195-
195+
if ('Pseudo[Element[*]:scope]' === $result->__toString()) {
196+
$used = \count($stream->getUsed());
197+
if (!(2 === $used
198+
|| 3 === $used && $stream->getUsed()[0]->isWhiteSpace()
199+
|| $used >= 3 && $stream->getUsed()[$used - 3]->isDelimiter([','])
200+
|| $used >= 4
201+
&& $stream->getUsed()[$used - 3]->isWhiteSpace()
202+
&& $stream->getUsed()[$used - 4]->isDelimiter([','])
203+
)) {
204+
throw SyntaxErrorException::notAtTheStartOfASelector('scope');
205+
}
206+
}
196207
continue;
197208
}
198209

Tests/Parser/ParserTest.php

+7
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@ public static function getParserTestData()
146146
// unicode escape: \20 == (space)
147147
['*[aval="\'\20 \'"]', ['Attribute[Element[*][aval = \'\' \'\']]']],
148148
["*[aval=\"'\\20\r\n '\"]", ['Attribute[Element[*][aval = \'\' \'\']]']],
149+
[':scope > foo', ['CombinedSelector[Pseudo[Element[*]:scope] > Element[foo]]']],
150+
[':scope > foo bar > div', ['CombinedSelector[CombinedSelector[CombinedSelector[Pseudo[Element[*]:scope] > Element[foo]] <followed> Element[bar]] > Element[div]]']],
151+
[':scope > #foo #bar', ['CombinedSelector[CombinedSelector[Pseudo[Element[*]:scope] > Hash[Element[*]#foo]] <followed> Hash[Element[*]#bar]]']],
152+
[':scope', ['Pseudo[Element[*]:scope]']],
153+
['foo bar, :scope > div', ['CombinedSelector[Element[foo] <followed> Element[bar]]', 'CombinedSelector[Pseudo[Element[*]:scope] > Element[div]]']],
154+
['foo bar,:scope > div', ['CombinedSelector[Element[foo] <followed> Element[bar]]', 'CombinedSelector[Pseudo[Element[*]:scope] > Element[div]]']],
149155
];
150156
}
151157

@@ -176,6 +182,7 @@ public static function getParserExceptionTestData()
176182
[':lang(fr', SyntaxErrorException::unexpectedToken('an argument', new Token(Token::TYPE_FILE_END, '', 8))->getMessage()],
177183
[':contains("foo', SyntaxErrorException::unclosedString(10)->getMessage()],
178184
['foo!', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '!', 3))->getMessage()],
185+
[':scope > div :scope header', SyntaxErrorException::notAtTheStartOfASelector('scope')->getMessage()],
179186
];
180187
}
181188

Tests/XPath/TranslatorTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ public static function getCssToXPathTestData()
219219
['e + f', "e/following-sibling::*[(name() = 'f') and (position() = 1)]"],
220220
['e ~ f', 'e/following-sibling::f'],
221221
['div#container p', "div[@id = 'container']/descendant-or-self::*/p"],
222+
[':scope > div[dataimg="<testmessage>"]', "*[1]/div[@dataimg = '<testmessage>']"],
223+
[':scope', '*[1]'],
222224
];
223225
}
224226

@@ -411,6 +413,9 @@ public static function getHtmlShakespearTestData()
411413
['div[class|=dialog]', 50], // ? Seems right
412414
['div[class!=madeup]', 243], // ? Seems right
413415
['div[class~=dialog]', 51], // ? Seems right
416+
[':scope > div', 1],
417+
[':scope > div > div[class=dialog]', 1],
418+
[':scope > div div', 242],
414419
];
415420
}
416421
}

XPath/Extension/PseudoClassExtension.php

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function getPseudoClassTranslators(): array
3030
{
3131
return [
3232
'root' => $this->translateRoot(...),
33+
'scope' => $this->translateScopePseudo(...),
3334
'first-child' => $this->translateFirstChild(...),
3435
'last-child' => $this->translateLastChild(...),
3536
'first-of-type' => $this->translateFirstOfType(...),
@@ -45,6 +46,11 @@ public function translateRoot(XPathExpr $xpath): XPathExpr
4546
return $xpath->addCondition('not(parent::*)');
4647
}
4748

49+
public function translateScopePseudo(XPathExpr $xpath): XPathExpr
50+
{
51+
return $xpath->addCondition('1');
52+
}
53+
4854
public function translateFirstChild(XPathExpr $xpath): XPathExpr
4955
{
5056
return $xpath

0 commit comments

Comments
 (0)