Skip to content

Commit

Permalink
[CssSelector] Add suport for :scope
Browse files Browse the repository at this point in the history
  • Loading branch information
franckranaivo authored and nicolas-grekas committed Mar 20, 2023
1 parent efc0747 commit 88453e6
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 2 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

6.3
-----

* Add support for `:scope`

4.4.0
-----

Expand Down
5 changes: 5 additions & 0 deletions Exception/SyntaxErrorException.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public static function nestedNot(): self
return new self('Got nested ::not().');
}

public static function notAtTheStartOfASelector(string $pseudoElement): self
{
return new self(sprintf('Got immediate child pseudo-element ":%s" not at the start of a selector', $pseudoElement));
}

public static function stringAsFunctionArgument(): self
{
return new self('String not allowed as function argument.');
Expand Down
15 changes: 13 additions & 2 deletions Parser/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* CSS selector parser.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
* which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
Expand Down Expand Up @@ -192,7 +192,18 @@ private function parseSimpleSelector(TokenStream $stream, bool $insideNegation =

if (!$stream->getPeek()->isDelimiter(['('])) {
$result = new Node\PseudoNode($result, $identifier);

if ('Pseudo[Element[*]:scope]' === $result->__toString()) {
$used = \count($stream->getUsed());
if (!(2 === $used
|| 3 === $used && $stream->getUsed()[0]->isWhiteSpace()
|| $used >= 3 && $stream->getUsed()[$used - 3]->isDelimiter([','])
|| $used >= 4
&& $stream->getUsed()[$used - 3]->isWhiteSpace()
&& $stream->getUsed()[$used - 4]->isDelimiter([','])
)) {
throw SyntaxErrorException::notAtTheStartOfASelector('scope');
}
}
continue;
}

Expand Down
7 changes: 7 additions & 0 deletions Tests/Parser/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ public static function getParserTestData()
// unicode escape: \20 == (space)
['*[aval="\'\20 \'"]', ['Attribute[Element[*][aval = \'\' \'\']]']],
["*[aval=\"'\\20\r\n '\"]", ['Attribute[Element[*][aval = \'\' \'\']]']],
[':scope > foo', ['CombinedSelector[Pseudo[Element[*]:scope] > Element[foo]]']],
[':scope > foo bar > div', ['CombinedSelector[CombinedSelector[CombinedSelector[Pseudo[Element[*]:scope] > Element[foo]] <followed> Element[bar]] > Element[div]]']],
[':scope > #foo #bar', ['CombinedSelector[CombinedSelector[Pseudo[Element[*]:scope] > Hash[Element[*]#foo]] <followed> Hash[Element[*]#bar]]']],
[':scope', ['Pseudo[Element[*]:scope]']],
['foo bar, :scope > div', ['CombinedSelector[Element[foo] <followed> Element[bar]]', 'CombinedSelector[Pseudo[Element[*]:scope] > Element[div]]']],
['foo bar,:scope > div', ['CombinedSelector[Element[foo] <followed> Element[bar]]', 'CombinedSelector[Pseudo[Element[*]:scope] > Element[div]]']],
];
}

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

Expand Down
5 changes: 5 additions & 0 deletions Tests/XPath/TranslatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ public static function getCssToXPathTestData()
['e + f', "e/following-sibling::*[(name() = 'f') and (position() = 1)]"],
['e ~ f', 'e/following-sibling::f'],
['div#container p', "div[@id = 'container']/descendant-or-self::*/p"],
[':scope > div[dataimg="<testmessage>"]', "*[1]/div[@dataimg = '<testmessage>']"],
[':scope', '*[1]'],
];
}

Expand Down Expand Up @@ -411,6 +413,9 @@ public static function getHtmlShakespearTestData()
['div[class|=dialog]', 50], // ? Seems right
['div[class!=madeup]', 243], // ? Seems right
['div[class~=dialog]', 51], // ? Seems right
[':scope > div', 1],
[':scope > div > div[class=dialog]', 1],
[':scope > div div', 242],
];
}
}
6 changes: 6 additions & 0 deletions XPath/Extension/PseudoClassExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function getPseudoClassTranslators(): array
{
return [
'root' => $this->translateRoot(...),
'scope' => $this->translateScopePseudo(...),
'first-child' => $this->translateFirstChild(...),
'last-child' => $this->translateLastChild(...),
'first-of-type' => $this->translateFirstOfType(...),
Expand All @@ -45,6 +46,11 @@ public function translateRoot(XPathExpr $xpath): XPathExpr
return $xpath->addCondition('not(parent::*)');
}

public function translateScopePseudo(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition('1');
}

public function translateFirstChild(XPathExpr $xpath): XPathExpr
{
return $xpath
Expand Down

0 comments on commit 88453e6

Please sign in to comment.