Showing preview only (234K chars total). Download the full file or copy to clipboard to get everything.
Repository: symfony/css-selector
Branch: 8.1
Commit: f7c24bbef3be
Files: 93
Total size: 213.2 KB
Directory structure:
gitextract_8h3d2omr/
├── .gitattributes
├── .github/
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── close-pull-request.yml
├── .gitignore
├── CHANGELOG.md
├── CssSelectorConverter.php
├── Exception/
│ ├── ExceptionInterface.php
│ ├── ExpressionErrorException.php
│ ├── InternalErrorException.php
│ ├── ParseException.php
│ └── SyntaxErrorException.php
├── LICENSE
├── Node/
│ ├── AbstractNode.php
│ ├── AttributeNode.php
│ ├── ClassNode.php
│ ├── CombinedSelectorNode.php
│ ├── ElementNode.php
│ ├── FunctionNode.php
│ ├── HashNode.php
│ ├── MatchingNode.php
│ ├── NegationNode.php
│ ├── NodeInterface.php
│ ├── PseudoNode.php
│ ├── RelationNode.php
│ ├── SelectorNode.php
│ ├── Specificity.php
│ └── SpecificityAdjustmentNode.php
├── Parser/
│ ├── Handler/
│ │ ├── CommentHandler.php
│ │ ├── HandlerInterface.php
│ │ ├── HashHandler.php
│ │ ├── IdentifierHandler.php
│ │ ├── NumberHandler.php
│ │ ├── StringHandler.php
│ │ └── WhitespaceHandler.php
│ ├── Parser.php
│ ├── ParserInterface.php
│ ├── Reader.php
│ ├── Shortcut/
│ │ ├── ClassParser.php
│ │ ├── ElementParser.php
│ │ ├── EmptyStringParser.php
│ │ └── HashParser.php
│ ├── Token.php
│ ├── TokenStream.php
│ └── Tokenizer/
│ ├── Tokenizer.php
│ ├── TokenizerEscaping.php
│ └── TokenizerPatterns.php
├── README.md
├── Tests/
│ ├── CssSelectorConverterTest.php
│ ├── Node/
│ │ ├── AbstractNodeTestCase.php
│ │ ├── AttributeNodeTest.php
│ │ ├── ClassNodeTest.php
│ │ ├── CombinedSelectorNodeTest.php
│ │ ├── ElementNodeTest.php
│ │ ├── FunctionNodeTest.php
│ │ ├── HashNodeTest.php
│ │ ├── MatchingNodeTest.php
│ │ ├── NegationNodeTest.php
│ │ ├── PseudoNodeTest.php
│ │ ├── SelectorNodeTest.php
│ │ ├── SpecificityAdjustmentNodeTest.php
│ │ └── SpecificityTest.php
│ ├── Parser/
│ │ ├── Handler/
│ │ │ ├── AbstractHandlerTestCase.php
│ │ │ ├── CommentHandlerTest.php
│ │ │ ├── HashHandlerTest.php
│ │ │ ├── IdentifierHandlerTest.php
│ │ │ ├── NumberHandlerTest.php
│ │ │ ├── StringHandlerTest.php
│ │ │ └── WhitespaceHandlerTest.php
│ │ ├── ParserTest.php
│ │ ├── ReaderTest.php
│ │ ├── Shortcut/
│ │ │ ├── ClassParserTest.php
│ │ │ ├── ElementParserTest.php
│ │ │ ├── EmptyStringParserTest.php
│ │ │ └── HashParserTest.php
│ │ └── TokenStreamTest.php
│ └── XPath/
│ ├── Fixtures/
│ │ ├── ids.html
│ │ ├── lang.xml
│ │ └── shakespear.html
│ └── TranslatorTest.php
├── XPath/
│ ├── Extension/
│ │ ├── AbstractExtension.php
│ │ ├── AttributeMatchingExtension.php
│ │ ├── CombinationExtension.php
│ │ ├── ExtensionInterface.php
│ │ ├── FunctionExtension.php
│ │ ├── HtmlExtension.php
│ │ ├── NodeExtension.php
│ │ ├── PseudoClassExtension.php
│ │ └── RelationExtension.php
│ ├── Translator.php
│ ├── TranslatorInterface.php
│ └── XPathExpr.php
├── composer.json
└── phpunit.xml.dist
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
/Tests export-ignore
/phpunit.xml.dist export-ignore
/.git* export-ignore
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Please do not submit any Pull Requests here. They will be closed.
---
Please submit your PR here instead:
https://github.com/symfony/symfony
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!
================================================
FILE: .github/workflows/close-pull-request.yml
================================================
name: Close Pull Request
on:
pull_request_target:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thanks for your Pull Request! We love contributions.
However, you should instead open your PR on the main repository:
https://github.com/symfony/symfony
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!
================================================
FILE: .gitignore
================================================
vendor/
composer.lock
phpunit.xml
================================================
FILE: CHANGELOG.md
================================================
CHANGELOG
=========
8.1
---
* Add support for `:has()`
7.1
---
* Add support for `:is()`
* Add support for `:where()`
6.3
---
* Add support for `:scope`
4.4.0
-----
* Added support for `*:only-of-type`
2.8.0
-----
* Added the `CssSelectorConverter` class as a non-static API for the component.
* Deprecated the `CssSelector` static API of the component.
2.1.0
-----
* none
================================================
FILE: CssSelectorConverter.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector;
use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser;
use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser;
use Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser;
use Symfony\Component\CssSelector\Parser\Shortcut\HashParser;
use Symfony\Component\CssSelector\XPath\Extension\HtmlExtension;
use Symfony\Component\CssSelector\XPath\Translator;
/**
* CssSelectorConverter is the main entry point of the component and can convert CSS
* selectors to XPath expressions.
*
* @author Christophe Coevoet <stof@notk.org>
*/
class CssSelectorConverter
{
public static int $maxCachedItems = 1024;
private Translator $translator;
private array $cache;
private static array $xmlCache = [];
private static array $htmlCache = [];
/**
* @param bool $html Whether HTML support should be enabled. Disable it for XML documents
*/
public function __construct(bool $html = true)
{
$this->translator = new Translator();
if ($html) {
$this->translator->registerExtension(new HtmlExtension($this->translator));
$this->cache = &self::$htmlCache;
} else {
$this->cache = &self::$xmlCache;
}
$this->translator
->registerParserShortcut(new EmptyStringParser())
->registerParserShortcut(new ElementParser())
->registerParserShortcut(new ClassParser())
->registerParserShortcut(new HashParser())
;
}
/**
* Translates a CSS expression to its XPath equivalent.
*
* Optionally, a prefix can be added to the resulting XPath
* expression with the $prefix parameter.
*/
public function toXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string
{
$cacheKey = $prefix."\0".$cssExpr;
if (isset($this->cache[$cacheKey])) {
// Move the item last in cache (LRU)
$value = $this->cache[$cacheKey];
unset($this->cache[$cacheKey]);
return $this->cache[$cacheKey] = $value;
}
if (\count($this->cache) >= self::$maxCachedItems) {
// Evict the oldest entry
unset($this->cache[array_key_first($this->cache)]);
}
return $this->cache[$cacheKey] = $this->translator->cssToXPath($cssExpr, $prefix);
}
}
================================================
FILE: Exception/ExceptionInterface.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Exception;
/**
* Interface for exceptions.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
interface ExceptionInterface extends \Throwable
{
}
================================================
FILE: Exception/ExpressionErrorException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Exception;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class ExpressionErrorException extends ParseException
{
}
================================================
FILE: Exception/InternalErrorException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Exception;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class InternalErrorException extends ParseException
{
}
================================================
FILE: Exception/ParseException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Exception;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ParseException extends \Exception implements ExceptionInterface
{
}
================================================
FILE: Exception/SyntaxErrorException.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Exception;
use Symfony\Component\CssSelector\Parser\Token;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class SyntaxErrorException extends ParseException
{
public static function unexpectedToken(string $expectedValue, Token $foundToken): self
{
return new self(\sprintf('Expected %s, but %s found.', $expectedValue, $foundToken));
}
public static function pseudoElementFound(string $pseudoElement, string $unexpectedLocation): self
{
return new self(\sprintf('Unexpected pseudo-element "::%s" found %s.', $pseudoElement, $unexpectedLocation));
}
public static function unclosedString(int $position): self
{
return new self(\sprintf('Unclosed/invalid string at %s.', $position));
}
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.');
}
}
================================================
FILE: LICENSE
================================================
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: Node/AbstractNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Abstract base node class.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
abstract class AbstractNode implements NodeInterface
{
private string $nodeName;
public function getNodeName(): string
{
return $this->nodeName ??= preg_replace('~.*\\\\([^\\\\]+)Node$~', '$1', static::class);
}
}
================================================
FILE: Node/AttributeNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>[<namespace>|<attribute> <operator> <value>]" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class AttributeNode extends AbstractNode
{
public function __construct(
private NodeInterface $selector,
private ?string $namespace,
private string $attribute,
private string $operator,
private ?string $value,
) {
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getNamespace(): ?string
{
return $this->namespace;
}
public function getAttribute(): string
{
return $this->attribute;
}
public function getOperator(): string
{
return $this->operator;
}
public function getValue(): ?string
{
return $this->value;
}
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
public function __toString(): string
{
$attribute = $this->namespace ? $this->namespace.'|'.$this->attribute : $this->attribute;
return 'exists' === $this->operator
? \sprintf('%s[%s[%s]]', $this->getNodeName(), $this->selector, $attribute)
: \sprintf("%s[%s[%s %s '%s']]", $this->getNodeName(), $this->selector, $attribute, $this->operator, $this->value);
}
}
================================================
FILE: Node/ClassNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>.<name>" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class ClassNode extends AbstractNode
{
public function __construct(
private NodeInterface $selector,
private string $name,
) {
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getName(): string
{
return $this->name;
}
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
public function __toString(): string
{
return \sprintf('%s[%s.%s]', $this->getNodeName(), $this->selector, $this->name);
}
}
================================================
FILE: Node/CombinedSelectorNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a combined node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class CombinedSelectorNode extends AbstractNode
{
public function __construct(
private NodeInterface $selector,
private string $combinator,
private NodeInterface $subSelector,
) {
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getCombinator(): string
{
return $this->combinator;
}
public function getSubSelector(): NodeInterface
{
return $this->subSelector;
}
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
}
public function __toString(): string
{
$combinator = ' ' === $this->combinator ? '<followed>' : $this->combinator;
return \sprintf('%s[%s %s %s]', $this->getNodeName(), $this->selector, $combinator, $this->subSelector);
}
}
================================================
FILE: Node/ElementNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<namespace>|<element>" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class ElementNode extends AbstractNode
{
public function __construct(
private ?string $namespace = null,
private ?string $element = null,
) {
}
public function getNamespace(): ?string
{
return $this->namespace;
}
public function getElement(): ?string
{
return $this->element;
}
public function getSpecificity(): Specificity
{
return new Specificity(0, 0, $this->element ? 1 : 0);
}
public function __toString(): string
{
$element = $this->element ?: '*';
return \sprintf('%s[%s]', $this->getNodeName(), $this->namespace ? $this->namespace.'|'.$element : $element);
}
}
================================================
FILE: Node/FunctionNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
use Symfony\Component\CssSelector\Parser\Token;
/**
* Represents a "<selector>:<name>(<arguments>)" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class FunctionNode extends AbstractNode
{
private string $name;
/**
* @param Token[] $arguments
*/
public function __construct(
private NodeInterface $selector,
string $name,
private array $arguments = [],
) {
$this->name = strtolower($name);
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getName(): string
{
return $this->name;
}
/**
* @return Token[]
*/
public function getArguments(): array
{
return $this->arguments;
}
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
public function __toString(): string
{
$arguments = implode(', ', array_map(static fn (Token $token) => "'".$token->getValue()."'", $this->arguments));
return \sprintf('%s[%s:%s(%s)]', $this->getNodeName(), $this->selector, $this->name, $arguments ? '['.$arguments.']' : '');
}
}
================================================
FILE: Node/HashNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>#<id>" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class HashNode extends AbstractNode
{
public function __construct(
private NodeInterface $selector,
private string $id,
) {
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getId(): string
{
return $this->id;
}
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(1, 0, 0));
}
public function __toString(): string
{
return \sprintf('%s[%s#%s]', $this->getNodeName(), $this->selector, $this->id);
}
}
================================================
FILE: Node/MatchingNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>:is(<subSelectorList>)" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Hubert Lenoir <lenoir.hubert@gmail.com>
*
* @internal
*/
class MatchingNode extends AbstractNode
{
/**
* @param array<NodeInterface> $arguments
*/
public function __construct(
public readonly NodeInterface $selector,
public readonly array $arguments = [],
) {
}
public function getSpecificity(): Specificity
{
$argumentsSpecificity = array_reduce(
$this->arguments,
static fn ($c, $n) => 1 === $n->getSpecificity()->compareTo($c) ? $n->getSpecificity() : $c,
new Specificity(0, 0, 0),
);
return $this->selector->getSpecificity()->plus($argumentsSpecificity);
}
public function __toString(): string
{
$selectorArguments = array_map(
static fn ($n): string => ltrim((string) $n, '*'),
$this->arguments,
);
return \sprintf('%s[%s:is(%s)]', $this->getNodeName(), $this->selector, implode(', ', $selectorArguments));
}
}
================================================
FILE: Node/NegationNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>:not(<identifier>)" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class NegationNode extends AbstractNode
{
public function __construct(
private NodeInterface $selector,
private NodeInterface $subSelector,
) {
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getSubSelector(): NodeInterface
{
return $this->subSelector;
}
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
}
public function __toString(): string
{
return \sprintf('%s[%s:not(%s)]', $this->getNodeName(), $this->selector, $this->subSelector);
}
}
================================================
FILE: Node/NodeInterface.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Interface for nodes.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
interface NodeInterface extends \Stringable
{
public function getNodeName(): string;
public function getSpecificity(): Specificity;
}
================================================
FILE: Node/PseudoNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>:<identifier>" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class PseudoNode extends AbstractNode
{
private string $identifier;
public function __construct(
private NodeInterface $selector,
string $identifier,
) {
$this->identifier = strtolower($identifier);
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
public function __toString(): string
{
return \sprintf('%s[%s:%s]', $this->getNodeName(), $this->selector, $this->identifier);
}
}
================================================
FILE: Node/RelationNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>:has(<subselector>)" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
*
* @author Franck Ranaivo-Harisoa <franckranaivo@gmail.com>
*
* @internal
*/
class RelationNode extends AbstractNode
{
public function __construct(
private NodeInterface $selector,
private string $combinator,
private NodeInterface $subSelector,
) {
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getCombinator(): string
{
return $this->combinator;
}
public function getSubSelector(): NodeInterface
{
return $this->subSelector;
}
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
}
public function __toString(): string
{
return \sprintf('%s[%s:has(%s)]', $this->getNodeName(), $this->selector, $this->subSelector);
}
}
================================================
FILE: Node/SelectorNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>(::|:)<pseudoElement>" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class SelectorNode extends AbstractNode
{
private ?string $pseudoElement;
public function __construct(
private NodeInterface $tree,
?string $pseudoElement = null,
) {
$this->pseudoElement = $pseudoElement ? strtolower($pseudoElement) : null;
}
public function getTree(): NodeInterface
{
return $this->tree;
}
public function getPseudoElement(): ?string
{
return $this->pseudoElement;
}
public function getSpecificity(): Specificity
{
return $this->tree->getSpecificity()->plus(new Specificity(0, 0, $this->pseudoElement ? 1 : 0));
}
public function __toString(): string
{
return \sprintf('%s[%s%s]', $this->getNodeName(), $this->tree, $this->pseudoElement ? '::'.$this->pseudoElement : '');
}
}
================================================
FILE: Node/Specificity.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a node specificity.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @see http://www.w3.org/TR/selectors/#specificity
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Specificity
{
public const A_FACTOR = 100;
public const B_FACTOR = 10;
public const C_FACTOR = 1;
public function __construct(
private int $a,
private int $b,
private int $c,
) {
}
public function plus(self $specificity): self
{
return new self($this->a + $specificity->a, $this->b + $specificity->b, $this->c + $specificity->c);
}
public function getValue(): int
{
return $this->a * self::A_FACTOR + $this->b * self::B_FACTOR + $this->c * self::C_FACTOR;
}
/**
* Returns -1 if the object specificity is lower than the argument,
* 0 if they are equal, and 1 if the argument is lower.
*/
public function compareTo(self $specificity): int
{
if ($this->a !== $specificity->a) {
return $this->a > $specificity->a ? 1 : -1;
}
if ($this->b !== $specificity->b) {
return $this->b > $specificity->b ? 1 : -1;
}
if ($this->c !== $specificity->c) {
return $this->c > $specificity->c ? 1 : -1;
}
return 0;
}
}
================================================
FILE: Node/SpecificityAdjustmentNode.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>:where(<subSelectorList>)" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Hubert Lenoir <lenoir.hubert@gmail.com>
*
* @internal
*/
class SpecificityAdjustmentNode extends AbstractNode
{
/**
* @param array<NodeInterface> $arguments
*/
public function __construct(
public readonly NodeInterface $selector,
public readonly array $arguments = [],
) {
}
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity();
}
public function __toString(): string
{
$selectorArguments = array_map(
static fn ($n) => ltrim((string) $n, '*'),
$this->arguments,
);
return \sprintf('%s[%s:where(%s)]', $this->getNodeName(), $this->selector, implode(', ', $selectorArguments));
}
}
================================================
FILE: Parser/Handler/CommentHandler.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class CommentHandler implements HandlerInterface
{
public function handle(Reader $reader, TokenStream $stream): bool
{
if ('/*' !== $reader->getSubstring(2)) {
return false;
}
$offset = $reader->getOffset('*/');
if (false === $offset) {
$reader->moveToEnd();
} else {
$reader->moveForward($offset + 2);
}
return true;
}
}
================================================
FILE: Parser/Handler/HandlerInterface.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector handler interface.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
interface HandlerInterface
{
public function handle(Reader $reader, TokenStream $stream): bool;
}
================================================
FILE: Parser/Handler/HashHandler.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class HashHandler implements HandlerInterface
{
public function __construct(
private TokenizerPatterns $patterns,
private TokenizerEscaping $escaping,
) {
}
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern($this->patterns->getHashPattern());
if (!$match) {
return false;
}
$value = $this->escaping->escapeUnicode($match[1]);
$stream->push(new Token(Token::TYPE_HASH, $value, $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}
================================================
FILE: Parser/Handler/IdentifierHandler.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class IdentifierHandler implements HandlerInterface
{
public function __construct(
private TokenizerPatterns $patterns,
private TokenizerEscaping $escaping,
) {
}
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern($this->patterns->getIdentifierPattern());
if (!$match) {
return false;
}
$value = $this->escaping->escapeUnicode($match[0]);
$stream->push(new Token(Token::TYPE_IDENTIFIER, $value, $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}
================================================
FILE: Parser/Handler/NumberHandler.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class NumberHandler implements HandlerInterface
{
public function __construct(
private TokenizerPatterns $patterns,
) {
}
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern($this->patterns->getNumberPattern());
if (!$match) {
return false;
}
$stream->push(new Token(Token::TYPE_NUMBER, $match[0], $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}
================================================
FILE: Parser/Handler/StringHandler.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Exception\InternalErrorException;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class StringHandler implements HandlerInterface
{
public function __construct(
private TokenizerPatterns $patterns,
private TokenizerEscaping $escaping,
) {
}
public function handle(Reader $reader, TokenStream $stream): bool
{
$quote = $reader->getSubstring(1);
if (!\in_array($quote, ["'", '"'], true)) {
return false;
}
$reader->moveForward(1);
$match = $reader->findPattern($this->patterns->getQuotedStringPattern($quote));
if (!$match) {
throw new InternalErrorException(\sprintf('Should have found at least an empty match at %d.', $reader->getPosition()));
}
// check unclosed strings
if (\strlen($match[0]) === $reader->getRemainingLength()) {
throw SyntaxErrorException::unclosedString($reader->getPosition() - 1);
}
// check quotes pairs validity
if ($quote !== $reader->getSubstring(1, \strlen($match[0]))) {
throw SyntaxErrorException::unclosedString($reader->getPosition() - 1);
}
$string = $this->escaping->escapeUnicodeAndNewLine($match[0]);
$stream->push(new Token(Token::TYPE_STRING, $string, $reader->getPosition()));
$reader->moveForward(\strlen($match[0]) + 1);
return true;
}
}
================================================
FILE: Parser/Handler/WhitespaceHandler.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector whitespace handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class WhitespaceHandler implements HandlerInterface
{
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern('~^[ \t\r\n\f]+~');
if (false === $match) {
return false;
}
$stream->push(new Token(Token::TYPE_WHITESPACE, $match[0], $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}
================================================
FILE: Parser/Parser.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
use Symfony\Component\CssSelector\Exception\InternalErrorException;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Node;
use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer;
/**
* CSS selector parser.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Parser implements ParserInterface
{
private Tokenizer $tokenizer;
public function __construct(?Tokenizer $tokenizer = null)
{
$this->tokenizer = $tokenizer ?? new Tokenizer();
}
public function parse(string $source): array
{
$reader = new Reader($source);
$stream = $this->tokenizer->tokenize($reader);
return $this->parseSelectorList($stream);
}
/**
* Parses the arguments for ":nth-child()" and friends.
*
* @param Token[] $tokens
*
* @throws SyntaxErrorException
*/
public static function parseSeries(array $tokens): array
{
foreach ($tokens as $token) {
if ($token->isString()) {
throw SyntaxErrorException::stringAsFunctionArgument();
}
}
$joined = trim(implode('', array_map(static fn (Token $token) => $token->getValue(), $tokens)));
$int = static function ($string) {
if (!is_numeric($string)) {
throw SyntaxErrorException::stringAsFunctionArgument();
}
return (int) $string;
};
switch (true) {
case 'odd' === $joined:
return [2, 1];
case 'even' === $joined:
return [2, 0];
case 'n' === $joined:
return [1, 0];
case !str_contains($joined, 'n'):
return [0, $int($joined)];
}
$split = explode('n', $joined);
$first = $split[0] ?? null;
return [
$first ? ('-' === $first || '+' === $first ? $int($first.'1') : $int($first)) : 1,
isset($split[1]) && $split[1] ? $int($split[1]) : 0,
];
}
private function parseSelectorList(TokenStream $stream, bool $isArgument = false): array
{
$stream->skipWhitespace();
$selectors = [];
while (true) {
if ($isArgument && $stream->getPeek()->isDelimiter([')'])) {
break;
}
$selectors[] = $this->parserSelectorNode($stream, $isArgument);
if ($stream->getPeek()->isDelimiter([','])) {
$stream->getNext();
$stream->skipWhitespace();
} else {
break;
}
}
return $selectors;
}
private function parserSelectorNode(TokenStream $stream, bool $isArgument = false): Node\SelectorNode
{
[$result, $pseudoElement] = $this->parseSimpleSelector($stream, false, $isArgument);
while (true) {
$stream->skipWhitespace();
$peek = $stream->getPeek();
if (
$peek->isFileEnd()
|| $peek->isDelimiter([','])
|| ($isArgument && $peek->isDelimiter([')']))
) {
break;
}
if (null !== $pseudoElement) {
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
}
if ($peek->isDelimiter(['+', '>', '~'])) {
$combinator = $stream->getNext()->getValue();
$stream->skipWhitespace();
} else {
$combinator = ' ';
}
[$nextSelector, $pseudoElement] = $this->parseSimpleSelector($stream, false, $isArgument);
$result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
}
return new Node\SelectorNode($result, $pseudoElement);
}
/**
* @throws SyntaxErrorException
* @throws InternalErrorException
*/
private function parseRelativeSelector(TokenStream $stream): array
{
$stream->skipWhitespace();
$subSelector = '';
$next = $stream->getNext();
if ($next->isDelimiter(['+', '>', '~'])) {
$combinator = $next->getValue();
$stream->skipWhitespace();
$next = $stream->getNext();
} else {
$combinator = ' ';
}
while (true) {
if ($next->isString() || $next->isIdentifier() || $next->isNumber() || $next->isDelimiter(['.', '*'])) {
$subSelector .= $next->getValue();
} elseif ($next->isHash()) {
$subSelector .= '#'.$next->getValue();
} elseif ($next->isDelimiter([')'])) {
$result = $this->parse($subSelector);
return [$combinator, $result[0]];
} else {
throw SyntaxErrorException::unexpectedToken('an argument', $next);
}
$next = $stream->getNext();
}
}
/**
* Parses next simple node (hash, class, pseudo, negation).
*
* @throws SyntaxErrorException
* @throws InternalErrorException
*/
private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = false, bool $isArgument = false): array
{
$stream->skipWhitespace();
$selectorStart = \count($stream->getUsed());
$result = $this->parseElementNode($stream);
$pseudoElement = null;
while (true) {
$peek = $stream->getPeek();
if ($peek->isWhitespace()
|| $peek->isFileEnd()
|| $peek->isDelimiter([',', '+', '>', '~'])
|| ($isArgument && $peek->isDelimiter([')']))
) {
break;
}
if (null !== $pseudoElement) {
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
}
if ($peek->isHash()) {
$result = new Node\HashNode($result, $stream->getNext()->getValue());
} elseif ($peek->isDelimiter(['.'])) {
$stream->getNext();
$result = new Node\ClassNode($result, $stream->getNextIdentifier());
} elseif ($peek->isDelimiter(['['])) {
$stream->getNext();
$result = $this->parseAttributeNode($result, $stream);
} elseif ($peek->isDelimiter([':'])) {
$stream->getNext();
if ($stream->getPeek()->isDelimiter([':'])) {
$stream->getNext();
$pseudoElement = $stream->getNextIdentifier();
continue;
}
$identifier = $stream->getNextIdentifier();
if (\in_array(strtolower($identifier), ['first-line', 'first-letter', 'before', 'after'], true)) {
// Special case: CSS 2.1 pseudo-elements can have a single ':'.
// Any new pseudo-element must have two.
$pseudoElement = $identifier;
continue;
}
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;
}
$stream->getNext();
$stream->skipWhitespace();
if ('not' === strtolower($identifier)) {
if ($insideNegation) {
throw SyntaxErrorException::nestedNot();
}
[$argument, $argumentPseudoElement] = $this->parseSimpleSelector($stream, true, true);
$next = $stream->getNext();
if (null !== $argumentPseudoElement) {
throw SyntaxErrorException::pseudoElementFound($argumentPseudoElement, 'inside ::not()');
}
if (!$next->isDelimiter([')'])) {
throw SyntaxErrorException::unexpectedToken('")"', $next);
}
$result = new Node\NegationNode($result, $argument);
} elseif ('is' === strtolower($identifier)) {
$selectors = $this->parseSelectorList($stream, true);
$next = $stream->getNext();
if (!$next->isDelimiter([')'])) {
throw SyntaxErrorException::unexpectedToken('")"', $next);
}
$result = new Node\MatchingNode($result, $selectors);
} elseif ('where' === strtolower($identifier)) {
$selectors = $this->parseSelectorList($stream, true);
$next = $stream->getNext();
if (!$next->isDelimiter([')'])) {
throw SyntaxErrorException::unexpectedToken('")"', $next);
}
$result = new Node\SpecificityAdjustmentNode($result, $selectors);
} elseif ('has' === strtolower($identifier)) {
[$combinator, $arguments] = $this->parseRelativeSelector($stream);
$result = new Node\RelationNode($result, $combinator, $arguments);
} else {
$arguments = [];
$next = null;
while (true) {
$stream->skipWhitespace();
$next = $stream->getNext();
if ($next->isIdentifier()
|| $next->isString()
|| $next->isNumber()
|| $next->isDelimiter(['+', '-'])
) {
$arguments[] = $next;
} elseif ($next->isDelimiter([')'])) {
break;
} else {
throw SyntaxErrorException::unexpectedToken('an argument', $next);
}
}
if (!$arguments) {
throw SyntaxErrorException::unexpectedToken('at least one argument', $next);
}
$result = new Node\FunctionNode($result, $identifier, $arguments);
}
} else {
throw SyntaxErrorException::unexpectedToken('selector', $peek);
}
}
if (\count($stream->getUsed()) === $selectorStart) {
throw SyntaxErrorException::unexpectedToken('selector', $stream->getPeek());
}
return [$result, $pseudoElement];
}
private function parseElementNode(TokenStream $stream): Node\ElementNode
{
$peek = $stream->getPeek();
if ($peek->isIdentifier() || $peek->isDelimiter(['*'])) {
if ($peek->isIdentifier()) {
$namespace = $stream->getNext()->getValue();
} else {
$stream->getNext();
$namespace = null;
}
if ($stream->getPeek()->isDelimiter(['|'])) {
$stream->getNext();
$element = $stream->getNextIdentifierOrStar();
} else {
$element = $namespace;
$namespace = null;
}
} else {
$element = $namespace = null;
}
return new Node\ElementNode($namespace, $element);
}
private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream): Node\AttributeNode
{
$stream->skipWhitespace();
$attribute = $stream->getNextIdentifierOrStar();
if (null === $attribute && !$stream->getPeek()->isDelimiter(['|'])) {
throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek());
}
if ($stream->getPeek()->isDelimiter(['|'])) {
$stream->getNext();
if ($stream->getPeek()->isDelimiter(['='])) {
$namespace = null;
$stream->getNext();
$operator = '|=';
} else {
$namespace = $attribute;
$attribute = $stream->getNextIdentifier();
$operator = null;
}
} else {
$namespace = $operator = null;
}
if (null === $operator) {
$stream->skipWhitespace();
$next = $stream->getNext();
if ($next->isDelimiter([']'])) {
return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null);
} elseif ($next->isDelimiter(['='])) {
$operator = '=';
} elseif ($next->isDelimiter(['^', '$', '*', '~', '|', '!'])
&& $stream->getPeek()->isDelimiter(['='])
) {
$operator = $next->getValue().'=';
$stream->getNext();
} else {
throw SyntaxErrorException::unexpectedToken('operator', $next);
}
}
$stream->skipWhitespace();
$value = $stream->getNext();
if ($value->isNumber()) {
// if the value is a number, it's casted into a string
$value = new Token(Token::TYPE_STRING, (string) $value->getValue(), $value->getPosition());
}
if (!($value->isIdentifier() || $value->isString())) {
throw SyntaxErrorException::unexpectedToken('string or identifier', $value);
}
$stream->skipWhitespace();
$next = $stream->getNext();
if (!$next->isDelimiter([']'])) {
throw SyntaxErrorException::unexpectedToken('"]"', $next);
}
return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue());
}
}
================================================
FILE: Parser/ParserInterface.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
use Symfony\Component\CssSelector\Node\SelectorNode;
/**
* CSS selector parser interface.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
interface ParserInterface
{
/**
* Parses given selector source into an array of tokens.
*
* @return SelectorNode[]
*/
public function parse(string $source): array;
}
================================================
FILE: Parser/Reader.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
/**
* CSS selector reader.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Reader
{
private int $length;
private int $position = 0;
public function __construct(
private string $source,
) {
$this->length = \strlen($source);
}
public function isEOF(): bool
{
return $this->position >= $this->length;
}
public function getPosition(): int
{
return $this->position;
}
public function getRemainingLength(): int
{
return $this->length - $this->position;
}
public function getSubstring(int $length, int $offset = 0): string
{
return substr($this->source, $this->position + $offset, $length);
}
public function getOffset(string $string): int|false
{
$position = strpos($this->source, $string, $this->position);
return false === $position ? false : $position - $this->position;
}
public function findPattern(string $pattern): array|false
{
$source = substr($this->source, $this->position);
if (preg_match($pattern, $source, $matches)) {
return $matches;
}
return false;
}
public function moveForward(int $length): void
{
$this->position += $length;
}
public function moveToEnd(): void
{
$this->position = $this->length;
}
}
================================================
FILE: Parser/Shortcut/ClassParser.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Shortcut;
use Symfony\Component\CssSelector\Node\ClassNode;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector class parser shortcut.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class ClassParser implements ParserInterface
{
public function parse(string $source): array
{
// Matches an optional namespace, optional element, and required class
// $source = 'test|input.ab6bd_field';
// $matches = array (size=4)
// 0 => string 'test|input.ab6bd_field' (length=22)
// 1 => string 'test' (length=4)
// 2 => string 'input' (length=5)
// 3 => string 'ab6bd_field' (length=11)
if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+\.([\w-]++)$/i', trim($source), $matches)) {
return [
new SelectorNode(new ClassNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])),
];
}
return [];
}
}
================================================
FILE: Parser/Shortcut/ElementParser.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Shortcut;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector element parser shortcut.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class ElementParser implements ParserInterface
{
public function parse(string $source): array
{
// Matches an optional namespace, required element or `*`
// $source = 'testns|testel';
// $matches = array (size=3)
// 0 => string 'testns|testel' (length=13)
// 1 => string 'testns' (length=6)
// 2 => string 'testel' (length=6)
if (preg_match('/^(?:([a-z]++)\|)?([\w-]++|\*)$/i', trim($source), $matches)) {
return [new SelectorNode(new ElementNode($matches[1] ?: null, $matches[2]))];
}
return [];
}
}
================================================
FILE: Parser/Shortcut/EmptyStringParser.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Shortcut;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector class parser shortcut.
*
* This shortcut ensure compatibility with previous version.
* - The parser fails to parse an empty string.
* - In the previous version, an empty string matches each tags.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class EmptyStringParser implements ParserInterface
{
public function parse(string $source): array
{
// Matches an empty string
if ('' == $source) {
return [new SelectorNode(new ElementNode(null, '*'))];
}
return [];
}
}
================================================
FILE: Parser/Shortcut/HashParser.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Shortcut;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\HashNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector hash parser shortcut.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class HashParser implements ParserInterface
{
public function parse(string $source): array
{
// Matches an optional namespace, optional element, and required id
// $source = 'test|input#ab6bd_field';
// $matches = array (size=4)
// 0 => string 'test|input#ab6bd_field' (length=22)
// 1 => string 'test' (length=4)
// 2 => string 'input' (length=5)
// 3 => string 'ab6bd_field' (length=11)
if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+#([\w-]++)$/i', trim($source), $matches)) {
return [
new SelectorNode(new HashNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])),
];
}
return [];
}
}
================================================
FILE: Parser/Token.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
/**
* CSS selector token.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Token
{
public const TYPE_FILE_END = 'eof';
public const TYPE_DELIMITER = 'delimiter';
public const TYPE_WHITESPACE = 'whitespace';
public const TYPE_IDENTIFIER = 'identifier';
public const TYPE_HASH = 'hash';
public const TYPE_NUMBER = 'number';
public const TYPE_STRING = 'string';
/**
* @param self::TYPE_*|null $type
*/
public function __construct(
private ?string $type,
private ?string $value,
private ?int $position,
) {
}
/**
* @return self::TYPE_*|null
*/
public function getType(): ?string
{
return $this->type;
}
public function getValue(): ?string
{
return $this->value;
}
public function getPosition(): ?int
{
return $this->position;
}
public function isFileEnd(): bool
{
return self::TYPE_FILE_END === $this->type;
}
public function isDelimiter(array $values = []): bool
{
if (self::TYPE_DELIMITER !== $this->type) {
return false;
}
if (!$values) {
return true;
}
return \in_array($this->value, $values, true);
}
public function isWhitespace(): bool
{
return self::TYPE_WHITESPACE === $this->type;
}
public function isIdentifier(): bool
{
return self::TYPE_IDENTIFIER === $this->type;
}
public function isHash(): bool
{
return self::TYPE_HASH === $this->type;
}
public function isNumber(): bool
{
return self::TYPE_NUMBER === $this->type;
}
public function isString(): bool
{
return self::TYPE_STRING === $this->type;
}
public function __toString(): string
{
if ($this->value) {
return \sprintf('<%s "%s" at %s>', $this->type, $this->value, $this->position);
}
return \sprintf('<%s at %s>', $this->type, $this->position);
}
}
================================================
FILE: Parser/TokenStream.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
use Symfony\Component\CssSelector\Exception\InternalErrorException;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
/**
* CSS selector token stream.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class TokenStream
{
/**
* @var Token[]
*/
private array $tokens = [];
/**
* @var Token[]
*/
private array $used = [];
private int $cursor = 0;
private ?Token $peeked;
private bool $peeking = false;
/**
* Pushes a token.
*
* @return $this
*/
public function push(Token $token): static
{
$this->tokens[] = $token;
return $this;
}
/**
* Freezes stream.
*
* @return $this
*/
public function freeze(): static
{
return $this;
}
/**
* Returns next token.
*
* @throws InternalErrorException If there is no more token
*/
public function getNext(): Token
{
if ($this->peeking) {
$this->peeking = false;
$this->used[] = $this->peeked;
return $this->peeked;
}
if (!isset($this->tokens[$this->cursor])) {
throw new InternalErrorException('Unexpected token stream end.');
}
return $this->tokens[$this->cursor++];
}
/**
* Returns peeked token.
*/
public function getPeek(): Token
{
if (!$this->peeking) {
$this->peeked = $this->getNext();
$this->peeking = true;
}
return $this->peeked;
}
/**
* Returns used tokens.
*
* @return Token[]
*/
public function getUsed(): array
{
return $this->used;
}
/**
* Returns next identifier token.
*
* @throws SyntaxErrorException If next token is not an identifier
*/
public function getNextIdentifier(): string
{
$next = $this->getNext();
if (!$next->isIdentifier()) {
throw SyntaxErrorException::unexpectedToken('identifier', $next);
}
return $next->getValue();
}
/**
* Returns next identifier or null if star delimiter token is found.
*
* @throws SyntaxErrorException If next token is not an identifier or a star delimiter
*/
public function getNextIdentifierOrStar(): ?string
{
$next = $this->getNext();
if ($next->isIdentifier()) {
return $next->getValue();
}
if ($next->isDelimiter(['*'])) {
return null;
}
throw SyntaxErrorException::unexpectedToken('identifier or "*"', $next);
}
/**
* Skips next whitespace if any.
*/
public function skipWhitespace(): void
{
$peek = $this->getPeek();
if ($peek->isWhitespace()) {
$this->getNext();
}
}
}
================================================
FILE: Parser/Tokenizer/Tokenizer.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Tokenizer;
use Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector tokenizer.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Tokenizer
{
/**
* @var Handler\HandlerInterface[]
*/
private array $handlers;
public function __construct()
{
$patterns = new TokenizerPatterns();
$escaping = new TokenizerEscaping($patterns);
$this->handlers = [
new Handler\WhitespaceHandler(),
new Handler\IdentifierHandler($patterns, $escaping),
new Handler\HashHandler($patterns, $escaping),
new Handler\StringHandler($patterns, $escaping),
new Handler\NumberHandler($patterns),
new Handler\CommentHandler(),
];
}
/**
* Tokenize selector source code.
*/
public function tokenize(Reader $reader): TokenStream
{
$stream = new TokenStream();
while (!$reader->isEOF()) {
foreach ($this->handlers as $handler) {
if ($handler->handle($reader, $stream)) {
continue 2;
}
}
$stream->push(new Token(Token::TYPE_DELIMITER, $reader->getSubstring(1), $reader->getPosition()));
$reader->moveForward(1);
}
return $stream
->push(new Token(Token::TYPE_FILE_END, null, $reader->getPosition()))
->freeze();
}
}
================================================
FILE: Parser/Tokenizer/TokenizerEscaping.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Tokenizer;
/**
* CSS selector tokenizer escaping applier.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class TokenizerEscaping
{
public function __construct(
private TokenizerPatterns $patterns,
) {
}
public function escapeUnicode(string $value): string
{
$value = $this->replaceUnicodeSequences($value);
return preg_replace($this->patterns->getSimpleEscapePattern(), '$1', $value);
}
public function escapeUnicodeAndNewLine(string $value): string
{
$value = preg_replace($this->patterns->getNewLineEscapePattern(), '', $value);
return $this->escapeUnicode($value);
}
private function replaceUnicodeSequences(string $value): string
{
return preg_replace_callback($this->patterns->getUnicodeEscapePattern(), static function ($match) {
$c = hexdec($match[1]);
if (0x80 > $c %= 0x200000) {
return \chr($c);
}
if (0x800 > $c) {
return \chr(0xC0 | $c >> 6).\chr(0x80 | $c & 0x3F);
}
if (0x10000 > $c) {
return \chr(0xE0 | $c >> 12).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F);
}
return '';
}, $value);
}
}
================================================
FILE: Parser/Tokenizer/TokenizerPatterns.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Tokenizer;
/**
* CSS selector tokenizer patterns builder.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class TokenizerPatterns
{
private string $unicodeEscapePattern;
private string $simpleEscapePattern;
private string $newLineEscapePattern;
private string $escapePattern;
private string $stringEscapePattern;
private string $nonAsciiPattern;
private string $nmCharPattern;
private string $nmStartPattern;
private string $identifierPattern;
private string $hashPattern;
private string $numberPattern;
private string $quotedStringPattern;
public function __construct()
{
$this->unicodeEscapePattern = '\\\\([0-9a-f]{1,6})(?:\r\n|[ \n\r\t\f])?';
$this->simpleEscapePattern = '\\\\(.)';
$this->newLineEscapePattern = '\\\\(?:\n|\r\n|\r|\f)';
$this->escapePattern = $this->unicodeEscapePattern.'|\\\\[^\n\r\f0-9a-f]';
$this->stringEscapePattern = $this->newLineEscapePattern.'|'.$this->escapePattern;
$this->nonAsciiPattern = '[^\x00-\x7F]';
$this->nmCharPattern = '[_a-z0-9-]|'.$this->escapePattern.'|'.$this->nonAsciiPattern;
$this->nmStartPattern = '[_a-z]|'.$this->escapePattern.'|'.$this->nonAsciiPattern;
$this->identifierPattern = '-?(?:'.$this->nmStartPattern.')(?:'.$this->nmCharPattern.')*';
$this->hashPattern = '#((?:'.$this->nmCharPattern.')+)';
$this->numberPattern = '[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)';
$this->quotedStringPattern = '([^\n\r\f\\\\%s]|'.$this->stringEscapePattern.')*';
}
public function getNewLineEscapePattern(): string
{
return '~'.$this->newLineEscapePattern.'~';
}
public function getSimpleEscapePattern(): string
{
return '~'.$this->simpleEscapePattern.'~';
}
public function getUnicodeEscapePattern(): string
{
return '~'.$this->unicodeEscapePattern.'~i';
}
public function getIdentifierPattern(): string
{
return '~^'.$this->identifierPattern.'~i';
}
public function getHashPattern(): string
{
return '~^'.$this->hashPattern.'~i';
}
public function getNumberPattern(): string
{
return '~^'.$this->numberPattern.'~';
}
public function getQuotedStringPattern(string $quote): string
{
return '~^'.\sprintf($this->quotedStringPattern, $quote).'~i';
}
}
================================================
FILE: README.md
================================================
CssSelector Component
=====================
The CssSelector component converts CSS selectors to XPath expressions.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/css_selector.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
Credits
-------
This component is a port of the Python cssselect library
[v0.7.1](https://github.com/SimonSapin/cssselect/releases/tag/v0.7.1),
which is distributed under the BSD license.
================================================
FILE: Tests/CssSelectorConverterTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\CssSelectorConverter;
use Symfony\Component\CssSelector\Exception\ParseException;
class CssSelectorConverterTest extends TestCase
{
public function testCssToXPath()
{
$converter = new CssSelectorConverter();
$this->assertEquals('descendant-or-self::*', $converter->toXPath(''));
$this->assertEquals('descendant-or-self::h1', $converter->toXPath('h1'));
$this->assertEquals("descendant-or-self::h1[@id = 'foo']", $converter->toXPath('h1#foo'));
$this->assertEquals("descendant-or-self::h1[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", $converter->toXPath('h1.foo'));
$this->assertEquals('descendant-or-self::foo:h1', $converter->toXPath('foo|h1'));
$this->assertEquals('descendant-or-self::h1', $converter->toXPath('H1'));
// Test the cache layer
$converter = new CssSelectorConverter();
$this->assertEquals('descendant-or-self::h1', $converter->toXPath('H1'));
}
public function testCssToXPathXml()
{
$converter = new CssSelectorConverter(false);
$this->assertEquals('descendant-or-self::H1', $converter->toXPath('H1'));
$converter = new CssSelectorConverter(false);
// Test the cache layer
$this->assertEquals('descendant-or-self::H1', $converter->toXPath('H1'));
}
public function testParseExceptions()
{
$this->expectException(ParseException::class);
$this->expectExceptionMessage('Expected identifier, but <eof at 3> found.');
(new CssSelectorConverter())->toXPath('h1:');
}
public function testLruCacheMovesRecentlyUsedToEnd()
{
CssSelectorConverter::$maxCachedItems = 5;
$htmlCacheProperty = new \ReflectionProperty(CssSelectorConverter::class, 'htmlCache');
$htmlCacheProperty->setValue(null, []);
$converter = new CssSelectorConverter(true);
// Fill cache with 5 entries (h0-h4)
for ($i = 0; $i < 5; ++$i) {
$converter->toXPath("h$i");
}
// Access h0 to move it to end (most recently used)
$converter->toXPath('h0');
// Trigger eviction
$converter->toXPath('h5');
$cache = $htmlCacheProperty->getValue();
// h0 was accessed recently (moved to end), so it survives eviction
$this->assertArrayHasKey("descendant-or-self::\0h0", $cache);
// h5 is the newest entry
$this->assertArrayHasKey("descendant-or-self::\0h5", $cache);
// h1 was the oldest untouched entry, should be evicted
$this->assertArrayNotHasKey("descendant-or-self::\0h1", $cache);
CssSelectorConverter::$maxCachedItems = 1024;
}
#[DataProvider('getCssToXPathWithoutPrefixTestData')]
public function testCssToXPathWithoutPrefix($css, $xpath)
{
$converter = new CssSelectorConverter();
$this->assertEquals($xpath, $converter->toXPath($css, ''), '->parse() parses an input string and returns a node');
}
public static function getCssToXPathWithoutPrefixTestData(): array
{
return [
['h1', 'h1'],
['foo|h1', 'foo:h1'],
['h1, h2, h3', 'h1 | h2 | h3'],
['h1:nth-child(3n+1)', "*/*[(name() = 'h1') and (position() - 1 >= 0 and (position() - 1) mod 3 = 0)]"],
['h1 > p', 'h1/p'],
['h1#foo', "h1[@id = 'foo']"],
['h1.foo', "h1[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"],
['h1[class*="foo bar"]', "h1[@class and contains(@class, 'foo bar')]"],
['h1[foo|class*="foo bar"]', "h1[@foo:class and contains(@foo:class, 'foo bar')]"],
['h1[class]', 'h1[@class]'],
['h1 .foo', "h1/descendant-or-self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"],
['h1 #foo', "h1/descendant-or-self::*/*[@id = 'foo']"],
['h1 [class*=foo]', "h1/descendant-or-self::*/*[@class and contains(@class, 'foo')]"],
['div>.foo', "div/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"],
['div > .foo', "div/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"],
];
}
}
================================================
FILE: Tests/Node/AbstractNodeTestCase.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Node\NodeInterface;
abstract class AbstractNodeTestCase extends TestCase
{
#[DataProvider('getToStringConversionTestData')]
public function testToStringConversion(NodeInterface $node, $representation)
{
$this->assertEquals($representation, (string) $node);
}
#[DataProvider('getSpecificityValueTestData')]
public function testSpecificityValue(NodeInterface $node, $value)
{
$this->assertEquals($value, $node->getSpecificity()->getValue());
}
abstract public static function getToStringConversionTestData();
abstract public static function getSpecificityValueTestData();
}
================================================
FILE: Tests/Node/AttributeNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\AttributeNode;
use Symfony\Component\CssSelector\Node\ElementNode;
class AttributeNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new AttributeNode(new ElementNode(), null, 'attribute', 'exists', null), 'Attribute[Element[*][attribute]]'],
[new AttributeNode(new ElementNode(), null, 'attribute', '$=', 'value'), "Attribute[Element[*][attribute $= 'value']]"],
[new AttributeNode(new ElementNode(), 'namespace', 'attribute', '$=', 'value'), "Attribute[Element[*][namespace|attribute $= 'value']]"],
];
}
public static function getSpecificityValueTestData()
{
return [
[new AttributeNode(new ElementNode(), null, 'attribute', 'exists', null), 10],
[new AttributeNode(new ElementNode(null, 'element'), null, 'attribute', 'exists', null), 11],
[new AttributeNode(new ElementNode(), null, 'attribute', '$=', 'value'), 10],
[new AttributeNode(new ElementNode(), 'namespace', 'attribute', '$=', 'value'), 10],
];
}
}
================================================
FILE: Tests/Node/ClassNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\ClassNode;
use Symfony\Component\CssSelector\Node\ElementNode;
class ClassNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new ClassNode(new ElementNode(), 'class'), 'Class[Element[*].class]'],
];
}
public static function getSpecificityValueTestData()
{
return [
[new ClassNode(new ElementNode(), 'class'), 10],
[new ClassNode(new ElementNode(null, 'element'), 'class'), 11],
];
}
}
================================================
FILE: Tests/Node/CombinedSelectorNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\CombinedSelectorNode;
use Symfony\Component\CssSelector\Node\ElementNode;
class CombinedSelectorNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new CombinedSelectorNode(new ElementNode(), '>', new ElementNode()), 'CombinedSelector[Element[*] > Element[*]]'],
[new CombinedSelectorNode(new ElementNode(), ' ', new ElementNode()), 'CombinedSelector[Element[*] <followed> Element[*]]'],
];
}
public static function getSpecificityValueTestData()
{
return [
[new CombinedSelectorNode(new ElementNode(), '>', new ElementNode()), 0],
[new CombinedSelectorNode(new ElementNode(null, 'element'), '>', new ElementNode()), 1],
[new CombinedSelectorNode(new ElementNode(null, 'element'), '>', new ElementNode(null, 'element')), 2],
];
}
}
================================================
FILE: Tests/Node/ElementNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\ElementNode;
class ElementNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new ElementNode(), 'Element[*]'],
[new ElementNode(null, 'element'), 'Element[element]'],
[new ElementNode('namespace', 'element'), 'Element[namespace|element]'],
];
}
public static function getSpecificityValueTestData()
{
return [
[new ElementNode(), 0],
[new ElementNode(null, 'element'), 1],
[new ElementNode('namespace', 'element'), 1],
];
}
}
================================================
FILE: Tests/Node/FunctionNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\Parser\Token;
class FunctionNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new FunctionNode(new ElementNode(), 'function'), 'Function[Element[*]:function()]'],
[new FunctionNode(new ElementNode(), 'function', [
new Token(Token::TYPE_IDENTIFIER, 'value', 0),
]), "Function[Element[*]:function(['value'])]"],
[new FunctionNode(new ElementNode(), 'function', [
new Token(Token::TYPE_STRING, 'value1', 0),
new Token(Token::TYPE_NUMBER, 'value2', 0),
]), "Function[Element[*]:function(['value1', 'value2'])]"],
];
}
public static function getSpecificityValueTestData()
{
return [
[new FunctionNode(new ElementNode(), 'function'), 10],
[new FunctionNode(new ElementNode(), 'function', [
new Token(Token::TYPE_IDENTIFIER, 'value', 0),
]), 10],
[new FunctionNode(new ElementNode(), 'function', [
new Token(Token::TYPE_STRING, 'value1', 0),
new Token(Token::TYPE_NUMBER, 'value2', 0),
]), 10],
];
}
}
================================================
FILE: Tests/Node/HashNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\HashNode;
class HashNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new HashNode(new ElementNode(), 'id'), 'Hash[Element[*]#id]'],
];
}
public static function getSpecificityValueTestData()
{
return [
[new HashNode(new ElementNode(), 'id'), 100],
[new HashNode(new ElementNode(null, 'id'), 'class'), 101],
];
}
}
================================================
FILE: Tests/Node/MatchingNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\ClassNode;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\HashNode;
use Symfony\Component\CssSelector\Node\MatchingNode;
class MatchingNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new MatchingNode(new ElementNode(), [
new ClassNode(new ElementNode(), 'class'),
new HashNode(new ElementNode(), 'id'),
]), 'Matching[Element[*]:is(Class[Element[*].class], Hash[Element[*]#id])]'],
];
}
public static function getSpecificityValueTestData()
{
return [
[new MatchingNode(new ElementNode(), [
new ClassNode(new ElementNode(), 'class'),
new HashNode(new ElementNode(), 'id'),
]), 100],
[new MatchingNode(new ClassNode(new ElementNode(), 'class'), [
new ClassNode(new ElementNode(), 'class'),
new HashNode(new ElementNode(), 'id'),
]), 110],
[new MatchingNode(new HashNode(new ElementNode(), 'id'), [
new ClassNode(new ElementNode(), 'class'),
new HashNode(new ElementNode(), 'id'),
]), 200],
];
}
}
================================================
FILE: Tests/Node/NegationNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\ClassNode;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\NegationNode;
class NegationNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new NegationNode(new ElementNode(), new ClassNode(new ElementNode(), 'class')), 'Negation[Element[*]:not(Class[Element[*].class])]'],
];
}
public static function getSpecificityValueTestData()
{
return [
[new NegationNode(new ElementNode(), new ClassNode(new ElementNode(), 'class')), 10],
];
}
}
================================================
FILE: Tests/Node/PseudoNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\PseudoNode;
class PseudoNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new PseudoNode(new ElementNode(), 'pseudo'), 'Pseudo[Element[*]:pseudo]'],
];
}
public static function getSpecificityValueTestData()
{
return [
[new PseudoNode(new ElementNode(), 'pseudo'), 10],
];
}
}
================================================
FILE: Tests/Node/SelectorNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
class SelectorNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new SelectorNode(new ElementNode()), 'Selector[Element[*]]'],
[new SelectorNode(new ElementNode(), 'pseudo'), 'Selector[Element[*]::pseudo]'],
];
}
public static function getSpecificityValueTestData()
{
return [
[new SelectorNode(new ElementNode()), 0],
[new SelectorNode(new ElementNode(), 'pseudo'), 1],
];
}
}
================================================
FILE: Tests/Node/SpecificityAdjustmentNodeTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use Symfony\Component\CssSelector\Node\ClassNode;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\HashNode;
use Symfony\Component\CssSelector\Node\SpecificityAdjustmentNode;
class SpecificityAdjustmentNodeTest extends AbstractNodeTestCase
{
public static function getToStringConversionTestData()
{
return [
[new SpecificityAdjustmentNode(new ElementNode(), [
new ClassNode(new ElementNode(), 'class'),
new HashNode(new ElementNode(), 'id'),
]), 'SpecificityAdjustment[Element[*]:where(Class[Element[*].class], Hash[Element[*]#id])]'],
];
}
public static function getSpecificityValueTestData()
{
return [
[new SpecificityAdjustmentNode(new ElementNode(), [
new ClassNode(new ElementNode(), 'class'),
new HashNode(new ElementNode(), 'id'),
]), 0],
[new SpecificityAdjustmentNode(new ClassNode(new ElementNode(), 'class'), [
new ClassNode(new ElementNode(), 'class'),
new HashNode(new ElementNode(), 'id'),
]), 10],
];
}
}
================================================
FILE: Tests/Node/SpecificityTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Node;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Node\Specificity;
class SpecificityTest extends TestCase
{
#[DataProvider('getValueTestData')]
public function testValue(Specificity $specificity, $value)
{
$this->assertEquals($value, $specificity->getValue());
}
#[DataProvider('getValueTestData')]
public function testPlusValue(Specificity $specificity, $value)
{
$this->assertEquals($value + 123, $specificity->plus(new Specificity(1, 2, 3))->getValue());
}
public static function getValueTestData()
{
return [
[new Specificity(0, 0, 0), 0],
[new Specificity(0, 0, 2), 2],
[new Specificity(0, 3, 0), 30],
[new Specificity(4, 0, 0), 400],
[new Specificity(4, 3, 2), 432],
];
}
#[DataProvider('getCompareTestData')]
public function testCompareTo(Specificity $a, Specificity $b, $result)
{
$this->assertEquals($result, $a->compareTo($b));
}
public static function getCompareTestData()
{
return [
[new Specificity(0, 0, 0), new Specificity(0, 0, 0), 0],
[new Specificity(0, 0, 1), new Specificity(0, 0, 1), 0],
[new Specificity(0, 0, 2), new Specificity(0, 0, 1), 1],
[new Specificity(0, 0, 2), new Specificity(0, 0, 3), -1],
[new Specificity(0, 4, 0), new Specificity(0, 4, 0), 0],
[new Specificity(0, 6, 0), new Specificity(0, 5, 11), 1],
[new Specificity(0, 7, 0), new Specificity(0, 8, 0), -1],
[new Specificity(9, 0, 0), new Specificity(9, 0, 0), 0],
[new Specificity(11, 0, 0), new Specificity(10, 11, 0), 1],
[new Specificity(12, 11, 0), new Specificity(13, 0, 0), -1],
];
}
}
================================================
FILE: Tests/Parser/Handler/AbstractHandlerTestCase.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Handler;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* @author Jean-François Simon <contact@jfsimon.fr>
*/
abstract class AbstractHandlerTestCase extends TestCase
{
#[DataProvider('getHandleValueTestData')]
public function testHandleValue($value, Token $expectedToken, $remainingContent)
{
$reader = new Reader($value);
$stream = new TokenStream();
$this->assertTrue($this->generateHandler()->handle($reader, $stream));
$this->assertEquals($expectedToken, $stream->getNext());
$this->assertRemainingContent($reader, $remainingContent);
}
#[DataProvider('getDontHandleValueTestData')]
public function testDontHandleValue($value)
{
$reader = new Reader($value);
$stream = new TokenStream();
$this->assertFalse($this->generateHandler()->handle($reader, $stream));
$this->assertStreamEmpty($stream);
$this->assertRemainingContent($reader, $value);
}
abstract public static function getHandleValueTestData();
abstract public static function getDontHandleValueTestData();
abstract protected function generateHandler();
protected function assertStreamEmpty(TokenStream $stream)
{
$property = new \ReflectionProperty($stream, 'tokens');
$this->assertEquals([], $property->getValue($stream));
}
protected function assertRemainingContent(Reader $reader, $remainingContent)
{
if ('' === $remainingContent) {
$this->assertEquals(0, $reader->getRemainingLength());
$this->assertTrue($reader->isEOF());
} else {
$this->assertEquals(\strlen($remainingContent), $reader->getRemainingLength());
$this->assertEquals(0, $reader->getOffset($remainingContent));
}
}
}
================================================
FILE: Tests/Parser/Handler/CommentHandlerTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Handler;
use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Component\CssSelector\Parser\Handler\CommentHandler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
class CommentHandlerTest extends AbstractHandlerTestCase
{
#[DataProvider('getHandleValueTestData')]
public function testHandleValue($value, Token $unusedArgument, $remainingContent)
{
$reader = new Reader($value);
$stream = new TokenStream();
$this->assertTrue($this->generateHandler()->handle($reader, $stream));
// comments are ignored (not pushed as token in stream)
$this->assertStreamEmpty($stream);
$this->assertRemainingContent($reader, $remainingContent);
}
public static function getHandleValueTestData()
{
return [
// 2nd argument only exists for inherited method compatibility
['/* comment */', new Token(null, null, null), ''],
['/* comment */foo', new Token(null, null, null), 'foo'],
];
}
public static function getDontHandleValueTestData()
{
return [
['>'],
['+'],
[' '],
];
}
protected function generateHandler()
{
return new CommentHandler();
}
}
================================================
FILE: Tests/Parser/Handler/HashHandlerTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Handler\HashHandler;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
class HashHandlerTest extends AbstractHandlerTestCase
{
public static function getHandleValueTestData()
{
return [
['#id', new Token(Token::TYPE_HASH, 'id', 0), ''],
['#123', new Token(Token::TYPE_HASH, '123', 0), ''],
['#id.class', new Token(Token::TYPE_HASH, 'id', 0), '.class'],
['#id element', new Token(Token::TYPE_HASH, 'id', 0), ' element'],
];
}
public static function getDontHandleValueTestData()
{
return [
['id'],
['123'],
['<'],
['<'],
['#'],
];
}
protected function generateHandler()
{
$patterns = new TokenizerPatterns();
return new HashHandler($patterns, new TokenizerEscaping($patterns));
}
}
================================================
FILE: Tests/Parser/Handler/IdentifierHandlerTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Handler\IdentifierHandler;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
class IdentifierHandlerTest extends AbstractHandlerTestCase
{
public static function getHandleValueTestData()
{
return [
['foo', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), ''],
['foo|bar', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '|bar'],
['foo.class', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '.class'],
['foo[attr]', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '[attr]'],
['foo bar', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), ' bar'],
];
}
public static function getDontHandleValueTestData()
{
return [
['>'],
['+'],
[' '],
['*|foo'],
['/* comment */'],
];
}
protected function generateHandler()
{
$patterns = new TokenizerPatterns();
return new IdentifierHandler($patterns, new TokenizerEscaping($patterns));
}
}
================================================
FILE: Tests/Parser/Handler/NumberHandlerTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Handler\NumberHandler;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
class NumberHandlerTest extends AbstractHandlerTestCase
{
public static function getHandleValueTestData()
{
return [
['12', new Token(Token::TYPE_NUMBER, '12', 0), ''],
['12.34', new Token(Token::TYPE_NUMBER, '12.34', 0), ''],
['+12.34', new Token(Token::TYPE_NUMBER, '+12.34', 0), ''],
['-12.34', new Token(Token::TYPE_NUMBER, '-12.34', 0), ''],
['12 arg', new Token(Token::TYPE_NUMBER, '12', 0), ' arg'],
['12]', new Token(Token::TYPE_NUMBER, '12', 0), ']'],
];
}
public static function getDontHandleValueTestData()
{
return [
['hello'],
['>'],
['+'],
[' '],
['/* comment */'],
];
}
protected function generateHandler()
{
$patterns = new TokenizerPatterns();
return new NumberHandler($patterns);
}
}
================================================
FILE: Tests/Parser/Handler/StringHandlerTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Handler\StringHandler;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
class StringHandlerTest extends AbstractHandlerTestCase
{
public static function getHandleValueTestData()
{
return [
['"hello"', new Token(Token::TYPE_STRING, 'hello', 1), ''],
['"1"', new Token(Token::TYPE_STRING, '1', 1), ''],
['" "', new Token(Token::TYPE_STRING, ' ', 1), ''],
['""', new Token(Token::TYPE_STRING, '', 1), ''],
["'hello'", new Token(Token::TYPE_STRING, 'hello', 1), ''],
["'foo'bar", new Token(Token::TYPE_STRING, 'foo', 1), 'bar'],
];
}
public static function getDontHandleValueTestData()
{
return [
['hello'],
['>'],
['1'],
[' '],
];
}
protected function generateHandler()
{
$patterns = new TokenizerPatterns();
return new StringHandler($patterns, new TokenizerEscaping($patterns));
}
}
================================================
FILE: Tests/Parser/Handler/WhitespaceHandlerTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Handler\WhitespaceHandler;
use Symfony\Component\CssSelector\Parser\Token;
class WhitespaceHandlerTest extends AbstractHandlerTestCase
{
public static function getHandleValueTestData()
{
return [
[' ', new Token(Token::TYPE_WHITESPACE, ' ', 0), ''],
["\n", new Token(Token::TYPE_WHITESPACE, "\n", 0), ''],
["\t", new Token(Token::TYPE_WHITESPACE, "\t", 0), ''],
[' foo', new Token(Token::TYPE_WHITESPACE, ' ', 0), 'foo'],
[' .foo', new Token(Token::TYPE_WHITESPACE, ' ', 0), '.foo'],
];
}
public static function getDontHandleValueTestData()
{
return [
['>'],
['1'],
['a'],
];
}
protected function generateHandler()
{
return new WhitespaceHandler();
}
}
================================================
FILE: Tests/Parser/ParserTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\Parser;
use Symfony\Component\CssSelector\Parser\Token;
class ParserTest extends TestCase
{
#[DataProvider('getParserTestData')]
public function testParser($source, $representation)
{
$parser = new Parser();
$this->assertEquals($representation, array_map(static fn (SelectorNode $node) => (string) $node->getTree(), $parser->parse($source)));
}
#[DataProvider('getParserExceptionTestData')]
public function testParserException($source, $message)
{
$parser = new Parser();
try {
$parser->parse($source);
$this->fail('Parser should throw a SyntaxErrorException.');
} catch (SyntaxErrorException $e) {
$this->assertEquals($message, $e->getMessage());
}
}
#[DataProvider('getPseudoElementsTestData')]
public function testPseudoElements($source, $element, $pseudo)
{
$parser = new Parser();
$selectors = $parser->parse($source);
$this->assertCount(1, $selectors);
/** @var SelectorNode $selector */
$selector = $selectors[0];
$this->assertEquals($element, (string) $selector->getTree());
$this->assertEquals($pseudo, (string) $selector->getPseudoElement());
}
#[DataProvider('getSpecificityTestData')]
public function testSpecificity($source, $value)
{
$parser = new Parser();
$selectors = $parser->parse($source);
$this->assertCount(1, $selectors);
/** @var SelectorNode $selector */
$selector = $selectors[0];
$this->assertEquals($value, $selector->getSpecificity()->getValue());
}
#[DataProvider('getParseSeriesTestData')]
public function testParseSeries($series, $a, $b)
{
$parser = new Parser();
$selectors = $parser->parse(\sprintf(':nth-child(%s)', $series));
$this->assertCount(1, $selectors);
/** @var FunctionNode $function */
$function = $selectors[0]->getTree();
$this->assertEquals([$a, $b], Parser::parseSeries($function->getArguments()));
}
#[DataProvider('getParseSeriesExceptionTestData')]
public function testParseSeriesException($series)
{
$parser = new Parser();
$selectors = $parser->parse(\sprintf(':nth-child(%s)', $series));
$this->assertCount(1, $selectors);
/** @var FunctionNode $function */
$function = $selectors[0]->getTree();
$this->expectException(SyntaxErrorException::class);
Parser::parseSeries($function->getArguments());
}
public static function getParserTestData()
{
return [
['*', ['Element[*]']],
['*|*', ['Element[*]']],
['*|foo', ['Element[foo]']],
['foo|*', ['Element[foo|*]']],
['foo|bar', ['Element[foo|bar]']],
['#foo#bar', ['Hash[Hash[Element[*]#foo]#bar]']],
['div>.foo', ['CombinedSelector[Element[div] > Class[Element[*].foo]]']],
['div> .foo', ['CombinedSelector[Element[div] > Class[Element[*].foo]]']],
['div >.foo', ['CombinedSelector[Element[div] > Class[Element[*].foo]]']],
['div > .foo', ['CombinedSelector[Element[div] > Class[Element[*].foo]]']],
["div \n> \t \t .foo", ['CombinedSelector[Element[div] > Class[Element[*].foo]]']],
['td.foo,.bar', ['Class[Element[td].foo]', 'Class[Element[*].bar]']],
['td.foo, .bar', ['Class[Element[td].foo]', 'Class[Element[*].bar]']],
["td.foo\t\r\n\f ,\t\r\n\f .bar", ['Class[Element[td].foo]', 'Class[Element[*].bar]']],
['td.foo,.bar', ['Class[Element[td].foo]', 'Class[Element[*].bar]']],
['td.foo, .bar', ['Class[Element[td].foo]', 'Class[Element[*].bar]']],
["td.foo\t\r\n\f ,\t\r\n\f .bar", ['Class[Element[td].foo]', 'Class[Element[*].bar]']],
['div, td.foo, div.bar span', ['Element[div]', 'Class[Element[td].foo]', 'CombinedSelector[Class[Element[div].bar] <followed> Element[span]]']],
['div > p', ['CombinedSelector[Element[div] > Element[p]]']],
['td:first', ['Pseudo[Element[td]:first]']],
['td :first', ['CombinedSelector[Element[td] <followed> Pseudo[Element[*]:first]]']],
['a[name]', ['Attribute[Element[a][name]]']],
["a[ name\t]", ['Attribute[Element[a][name]]']],
['a [name]', ['CombinedSelector[Element[a] <followed> Attribute[Element[*][name]]]']],
['[name="foo"]', ["Attribute[Element[*][name = 'foo']]"]],
["[name='foo[1]']", ["Attribute[Element[*][name = 'foo[1]']]"]],
["[name='foo[0][bar]']", ["Attribute[Element[*][name = 'foo[0][bar]']]"]],
['a[rel="include"]', ["Attribute[Element[a][rel = 'include']]"]],
['a[rel = include]', ["Attribute[Element[a][rel = 'include']]"]],
["a[hreflang |= 'en']", ["Attribute[Element[a][hreflang |= 'en']]"]],
['a[hreflang|=en]', ["Attribute[Element[a][hreflang |= 'en']]"]],
['div:nth-child(10)', ["Function[Element[div]:nth-child(['10'])]"]],
[':nth-child(2n+2)', ["Function[Element[*]:nth-child(['2', 'n', '+2'])]"]],
['div:nth-of-type(10)', ["Function[Element[div]:nth-of-type(['10'])]"]],
['div div:nth-of-type(10) .aclass', ["CombinedSelector[CombinedSelector[Element[div] <followed> Function[Element[div]:nth-of-type(['10'])]] <followed> Class[Element[*].aclass]]"]],
['label:only', ['Pseudo[Element[label]:only]']],
['a:lang(fr)', ["Function[Element[a]:lang(['fr'])]"]],
['div:contains("foo")', ["Function[Element[div]:contains(['foo'])]"]],
['div#foobar', ['Hash[Element[div]#foobar]']],
['div:not(div.foo)', ['Negation[Element[div]:not(Class[Element[div].foo])]']],
['div:has(div.foo)', ['Relation[Element[div]:has(Selector[Class[Element[div].foo]])]']],
['td ~ th', ['CombinedSelector[Element[td] ~ Element[th]]']],
['.foo[data-bar][data-baz=0]', ["Attribute[Attribute[Class[Element[*].foo][data-bar]][data-baz = '0']]"]],
['div#foo\.bar', ['Hash[Element[div]#foo.bar]']],
['div.w-1\/3', ['Class[Element[div].w-1/3]']],
['#test\:colon', ['Hash[Element[*]#test:colon]']],
[".a\xc1b", ["Class[Element[*].a\xc1b]"]],
// unicode escape: \22 == "
['*[aval="\'\22\'"]', ['Attribute[Element[*][aval = \'\'"\'\']]']],
['*[aval="\'\22 2\'"]', ['Attribute[Element[*][aval = \'\'"2\'\']]']],
// 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]]']],
['div:is(.foo, #bar)', ['Matching[Element[div]:is(Selector[Class[Element[*].foo]], Selector[Hash[Element[*]#bar]])]']],
[':is(:hover, :visited)', ['Matching[Element[*]:is(Selector[Pseudo[Element[*]:hover]], Selector[Pseudo[Element[*]:visited]])]']],
['div:where(.foo, #bar)', ['SpecificityAdjustment[Element[div]:where(Selector[Class[Element[*].foo]], Selector[Hash[Element[*]#bar]])]']],
[':where(:hover, :visited)', ['SpecificityAdjustment[Element[*]:where(Selector[Pseudo[Element[*]:hover]], Selector[Pseudo[Element[*]:visited]])]']],
];
}
public static function getParserExceptionTestData()
{
return [
['attributes(href)/html/body/a', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '(', 10))->getMessage()],
['attributes(href)', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '(', 10))->getMessage()],
['html/body/a', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '/', 4))->getMessage()],
[' ', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_FILE_END, '', 1))->getMessage()],
['div, ', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_FILE_END, '', 5))->getMessage()],
[' , div', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, ',', 1))->getMessage()],
['p, , div', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, ',', 3))->getMessage()],
['div > ', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_FILE_END, '', 6))->getMessage()],
[' > div', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '>', 2))->getMessage()],
['foo|#bar', SyntaxErrorException::unexpectedToken('identifier or "*"', new Token(Token::TYPE_HASH, 'bar', 4))->getMessage()],
['#.foo', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '#', 0))->getMessage()],
['.#foo', SyntaxErrorException::unexpectedToken('identifier', new Token(Token::TYPE_HASH, 'foo', 1))->getMessage()],
[':#foo', SyntaxErrorException::unexpectedToken('identifier', new Token(Token::TYPE_HASH, 'foo', 1))->getMessage()],
['[*]', SyntaxErrorException::unexpectedToken('"|"', new Token(Token::TYPE_DELIMITER, ']', 2))->getMessage()],
['[foo|]', SyntaxErrorException::unexpectedToken('identifier', new Token(Token::TYPE_DELIMITER, ']', 5))->getMessage()],
['[#]', SyntaxErrorException::unexpectedToken('identifier or "*"', new Token(Token::TYPE_DELIMITER, '#', 1))->getMessage()],
['[foo=#]', SyntaxErrorException::unexpectedToken('string or identifier', new Token(Token::TYPE_DELIMITER, '#', 5))->getMessage()],
[':nth-child()', SyntaxErrorException::unexpectedToken('at least one argument', new Token(Token::TYPE_DELIMITER, ')', 11))->getMessage()],
['[href]a', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_IDENTIFIER, 'a', 6))->getMessage()],
['[rel:stylesheet]', SyntaxErrorException::unexpectedToken('operator', new Token(Token::TYPE_DELIMITER, ':', 4))->getMessage()],
['[rel=stylesheet', SyntaxErrorException::unexpectedToken('"]"', new Token(Token::TYPE_FILE_END, '', 15))->getMessage()],
[':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()],
[':not(:not(a))', SyntaxErrorException::nestedNot()->getMessage()],
];
}
public static function getPseudoElementsTestData()
{
return [
['foo', 'Element[foo]', ''],
['*', 'Element[*]', ''],
[':empty', 'Pseudo[Element[*]:empty]', ''],
[':BEfore', 'Element[*]', 'before'],
[':aftER', 'Element[*]', 'after'],
[':First-Line', 'Element[*]', 'first-line'],
[':First-Letter', 'Element[*]', 'first-letter'],
['::befoRE', 'Element[*]', 'before'],
['::AFter', 'Element[*]', 'after'],
['::firsT-linE', 'Element[*]', 'first-line'],
['::firsT-letteR', 'Element[*]', 'first-letter'],
['::Selection', 'Element[*]', 'selection'],
['foo:after', 'Element[foo]', 'after'],
['foo::selection', 'Element[foo]', 'selection'],
['lorem#ipsum ~ a#b.c[href]:empty::selection', 'CombinedSelector[Hash[Element[lorem]#ipsum] ~ Pseudo[Attribute[Class[Hash[Element[a]#b].c][href]]:empty]]', 'selection'],
['video::-webkit-media-controls', 'Element[video]', '-webkit-media-controls'],
];
}
public static function getSpecificityTestData()
{
return [
['*', 0],
[' foo', 1],
[':empty ', 10],
[':before', 1],
['*:before', 1],
[':nth-child(2)', 10],
['.bar', 10],
['[baz]', 10],
['[baz="4"]', 10],
['[baz^="4"]', 10],
['#lipsum', 100],
[':not(*)', 0],
[':not(foo)', 1],
[':not(.foo)', 10],
[':not([foo])', 10],
[':not(:empty)', 10],
[':not(#foo)', 100],
['foo:empty', 11],
['foo:before', 2],
['foo::before', 2],
['foo:empty::before', 12],
['#lorem + foo#ipsum:first-child > bar:first-line', 213],
[':is(*)', 0],
[':is(foo)', 1],
[':is(.foo)', 10],
[':is(#foo)', 100],
[':is(#foo, :empty, foo)', 100],
['#foo:is(#bar:empty)', 210],
[':where(*)', 0],
[':where(foo)', 0],
[':where(.foo)', 0],
[':where(#foo)', 0],
[':where(#foo, :empty, foo)', 0],
['#foo:where(#bar:empty)', 100],
];
}
public static function getParseSeriesTestData()
{
return [
['1n+3', 1, 3],
['1n +3', 1, 3],
['1n + 3', 1, 3],
['1n+ 3', 1, 3],
['1n-3', 1, -3],
['1n -3', 1, -3],
['1n - 3', 1, -3],
['1n- 3', 1, -3],
['n-5', 1, -5],
['odd', 2, 1],
['even', 2, 0],
['3n', 3, 0],
['n', 1, 0],
['+n', 1, 0],
['-n', -1, 0],
['5', 0, 5],
];
}
public static function getParseSeriesExceptionTestData()
{
return [
['foo'],
['n+'],
];
}
}
================================================
FILE: Tests/Parser/ReaderTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Parser\Reader;
class ReaderTest extends TestCase
{
public function testIsEOF()
{
$reader = new Reader('');
$this->assertTrue($reader->isEOF());
$reader = new Reader('hello');
$this->assertFalse($reader->isEOF());
$this->assignPosition($reader, 2);
$this->assertFalse($reader->isEOF());
$this->assignPosition($reader, 5);
$this->assertTrue($reader->isEOF());
}
public function testGetRemainingLength()
{
$reader = new Reader('hello');
$this->assertEquals(5, $reader->getRemainingLength());
$this->assignPosition($reader, 2);
$this->assertEquals(3, $reader->getRemainingLength());
$this->assignPosition($reader, 5);
$this->assertEquals(0, $reader->getRemainingLength());
}
public function testGetSubstring()
{
$reader = new Reader('hello');
$this->assertEquals('he', $reader->getSubstring(2));
$this->assertEquals('el', $reader->getSubstring(2, 1));
$this->assignPosition($reader, 2);
$this->assertEquals('ll', $reader->getSubstring(2));
$this->assertEquals('lo', $reader->getSubstring(2, 1));
}
public function testGetOffset()
{
$reader = new Reader('hello');
$this->assertEquals(2, $reader->getOffset('ll'));
$this->assertFalse($reader->getOffset('w'));
$this->assignPosition($reader, 2);
$this->assertEquals(0, $reader->getOffset('ll'));
$this->assertFalse($reader->getOffset('he'));
}
public function testFindPattern()
{
$reader = new Reader('hello');
$this->assertFalse($reader->findPattern('/world/'));
$this->assertEquals(['hello', 'h'], $reader->findPattern('/^([a-z]).*/'));
$this->assignPosition($reader, 2);
$this->assertFalse($reader->findPattern('/^h.*/'));
$this->assertEquals(['llo'], $reader->findPattern('/^llo$/'));
}
public function testMoveForward()
{
$reader = new Reader('hello');
$this->assertEquals(0, $reader->getPosition());
$reader->moveForward(2);
$this->assertEquals(2, $reader->getPosition());
}
public function testToEnd()
{
$reader = new Reader('hello');
$reader->moveToEnd();
$this->assertTrue($reader->isEOF());
}
private function assignPosition(Reader $reader, int $value)
{
$position = new \ReflectionProperty($reader, 'position');
$position->setValue($reader, $value);
}
}
================================================
FILE: Tests/Parser/Shortcut/ClassParserTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Shortcut;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class ClassParserTest extends TestCase
{
#[DataProvider('getParseTestData')]
public function testParse($source, $representation)
{
$parser = new ClassParser();
$selectors = $parser->parse($source);
$this->assertCount(1, $selectors);
/** @var SelectorNode $selector */
$selector = $selectors[0];
$this->assertEquals($representation, (string) $selector->getTree());
}
public static function getParseTestData()
{
return [
['.testclass', 'Class[Element[*].testclass]'],
['testel.testclass', 'Class[Element[testel].testclass]'],
['testns|.testclass', 'Class[Element[testns|*].testclass]'],
['testns|*.testclass', 'Class[Element[testns|*].testclass]'],
['testns|testel.testclass', 'Class[Element[testns|testel].testclass]'],
];
}
}
================================================
FILE: Tests/Parser/Shortcut/ElementParserTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Shortcut;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class ElementParserTest extends TestCase
{
#[DataProvider('getParseTestData')]
public function testParse($source, $representation)
{
$parser = new ElementParser();
$selectors = $parser->parse($source);
$this->assertCount(1, $selectors);
/** @var SelectorNode $selector */
$selector = $selectors[0];
$this->assertEquals($representation, (string) $selector->getTree());
}
public static function getParseTestData()
{
return [
['*', 'Element[*]'],
['testel', 'Element[testel]'],
['testns|*', 'Element[testns|*]'],
['testns|testel', 'Element[testns|testel]'],
];
}
}
================================================
FILE: Tests/Parser/Shortcut/EmptyStringParserTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Shortcut;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class EmptyStringParserTest extends TestCase
{
public function testParse()
{
$parser = new EmptyStringParser();
$selectors = $parser->parse('');
$this->assertCount(1, $selectors);
/** @var SelectorNode $selector */
$selector = $selectors[0];
$this->assertEquals('Element[*]', (string) $selector->getTree());
$selectors = $parser->parse('this will produce an empty array');
$this->assertCount(0, $selectors);
}
}
================================================
FILE: Tests/Parser/Shortcut/HashParserTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser\Shortcut;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\Shortcut\HashParser;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class HashParserTest extends TestCase
{
#[DataProvider('getParseTestData')]
public function testParse($source, $representation)
{
$parser = new HashParser();
$selectors = $parser->parse($source);
$this->assertCount(1, $selectors);
/** @var SelectorNode $selector */
$selector = $selectors[0];
$this->assertEquals($representation, (string) $selector->getTree());
}
public static function getParseTestData()
{
return [
['#testid', 'Hash[Element[*]#testid]'],
['testel#testid', 'Hash[Element[testel]#testid]'],
['testns|#testid', 'Hash[Element[testns|*]#testid]'],
['testns|*#testid', 'Hash[Element[testns|*]#testid]'],
['testns|testel#testid', 'Hash[Element[testns|testel]#testid]'],
];
}
}
================================================
FILE: Tests/Parser/TokenStreamTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\Parser;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
class TokenStreamTest extends TestCase
{
public function testGetNext()
{
$stream = new TokenStream();
$stream->push($t1 = new Token(Token::TYPE_IDENTIFIER, 'h1', 0));
$stream->push($t2 = new Token(Token::TYPE_DELIMITER, '.', 2));
$stream->push($t3 = new Token(Token::TYPE_IDENTIFIER, 'title', 3));
$this->assertSame($t1, $stream->getNext());
$this->assertSame($t2, $stream->getNext());
$this->assertSame($t3, $stream->getNext());
}
public function testGetPeek()
{
$stream = new TokenStream();
$stream->push($t1 = new Token(Token::TYPE_IDENTIFIER, 'h1', 0));
$stream->push($t2 = new Token(Token::TYPE_DELIMITER, '.', 2));
$stream->push($t3 = new Token(Token::TYPE_IDENTIFIER, 'title', 3));
$this->assertSame($t1, $stream->getPeek());
$this->assertSame($t1, $stream->getNext());
$this->assertSame($t2, $stream->getPeek());
$this->assertSame($t2, $stream->getPeek());
$this->assertSame($t2, $stream->getNext());
}
public function testGetNextIdentifier()
{
$stream = new TokenStream();
$stream->push(new Token(Token::TYPE_IDENTIFIER, 'h1', 0));
$this->assertEquals('h1', $stream->getNextIdentifier());
}
public function testFailToGetNextIdentifier()
{
$stream = new TokenStream();
$stream->push(new Token(Token::TYPE_DELIMITER, '.', 2));
$this->expectException(SyntaxErrorException::class);
$stream->getNextIdentifier();
}
public function testGetNextIdentifierOrStar()
{
$stream = new TokenStream();
$stream->push(new Token(Token::TYPE_IDENTIFIER, 'h1', 0));
$this->assertEquals('h1', $stream->getNextIdentifierOrStar());
$stream->push(new Token(Token::TYPE_DELIMITER, '*', 0));
$this->assertNull($stream->getNextIdentifierOrStar());
}
public function testFailToGetNextIdentifierOrStar()
{
$stream = new TokenStream();
$stream->push(new Token(Token::TYPE_DELIMITER, '.', 2));
$this->expectException(SyntaxErrorException::class);
$stream->getNextIdentifierOrStar();
}
public function testSkipWhitespace()
{
$stream = new TokenStream();
$stream->push($t1 = new Token(Token::TYPE_IDENTIFIER, 'h1', 0));
$stream->push($t2 = new Token(Token::TYPE_WHITESPACE, ' ', 2));
$stream->push($t3 = new Token(Token::TYPE_IDENTIFIER, 'h1', 3));
$stream->skipWhitespace();
$this->assertSame($t1, $stream->getNext());
$stream->skipWhitespace();
$this->assertSame($t3, $stream->getNext());
}
}
================================================
FILE: Tests/XPath/Fixtures/ids.html
================================================
<html id="html"><head>
<link id="link-href" href="foo" />
<link id="link-nohref" />
</head><body>
<div id="outer-div">
<a id="name-anchor" name="foo"></a>
<a id="tag-anchor" rel="tag" href="http://localhost/foo">link</a>
<a id="nofollow-anchor" rel="nofollow" href="https://example.org">
link</a>
<ol id="first-ol" class="a b c">
<li id="first-li">content</li>
<li id="second-li" lang="En-us">
<div id="li-div">
</div>
</li>
<li id="third-li" class="ab c"></li>
<li id="fourth-li" class="ab
c"></li>
<li id="fifth-li"></li>
<li id="sixth-li"></li>
<li id="seventh-li"> </li>
</ol>
<p id="paragraph">
<b id="p-b">hi</b> <em id="p-em">there</em>
<b id="p-b2">guy</b>
<input type="checkbox" id="checkbox-unchecked" />
<input type="checkbox" id="checkbox-disabled" disabled="" />
<input type="text" id="text-checked" checked="checked" />
<input type="hidden" />
<input type="hidden" disabled="disabled" />
<input type="checkbox" id="checkbox-checked" checked="checked" />
<input type="checkbox" id="checkbox-disabled-checked"
disabled="disabled" checked="checked" />
<fieldset id="fieldset" disabled="disabled">
<input type="checkbox" id="checkbox-fieldset-disabled" />
<input type="hidden" />
</fieldset>
</p>
<ol id="second-ol">
</ol>
<map name="dummymap">
<area shape="circle" coords="200,250,25" href="foo.html" id="area-href" />
<area shape="default" id="area-nohref" />
</map>
</div>
<div id="foobar-div" foobar="ab bc
cde"><span id="foobar-span"></span></div>
<section>
<span id="no-siblings-of-any-type"></span>
</section>
</body>
</html>
================================================
FILE: Tests/XPath/Fixtures/lang.xml
================================================
<test>
<a id="first" xml:lang="en">a</a>
<b id="second" xml:lang="en-US">b</b>
<c id="third" xml:lang="en-Nz">c</c>
<d id="fourth" xml:lang="En-us">d</d>
<e id="fifth" xml:lang="fr">e</e>
<f id="sixth" xml:lang="ru">f</f>
<g id="seventh" xml:lang="de">
<h id="eighth" xml:lang="zh"/>
</g>
</test>
================================================
FILE: Tests/XPath/Fixtures/shakespear.html
================================================
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" debug="true">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
<div id="test">
<div class="dialog">
<h2>As You Like It</h2>
<div id="playwright">
by William Shakespeare
</div>
<div class="dialog scene thirdClass" id="scene1">
<h3>ACT I, SCENE III. A room in the palace.</h3>
<div class="dialog">
<div class="direction">Enter CELIA and ROSALIND</div>
</div>
<div id="speech1" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.1">Why, cousin! why, Rosalind! Cupid have mercy! not a word?</div>
</div>
<div id="speech2" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.2">Not one to throw at a dog.</div>
</div>
<div id="speech3" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.3">No, thy words are too precious to be cast away upon</div>
<div id="scene1.3.4">curs; throw some of them at me; come, lame me with reasons.</div>
</div>
<div id="speech4" class="character">ROSALIND</div>
<div id="speech5" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.8">But is all this for your father?</div>
</div>
<div class="dialog">
<div id="scene1.3.5">Then there were two cousins laid up; when the one</div>
<div id="scene1.3.6">should be lamed with reasons and the other mad</div>
<div id="scene1.3.7">without any.</div>
</div>
<div id="speech6" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.9">No, some of it is for my child's father. O, how</div>
<div id="scene1.3.10">full of briers is this working-day world!</div>
</div>
<div id="speech7" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.11">They are but burs, cousin, thrown upon thee in</div>
<div id="scene1.3.12">holiday foolery: if we walk not in the trodden</div>
<div id="scene1.3.13">paths our very petticoats will catch them.</div>
</div>
<div id="speech8" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.14">I could shake them off my coat: these burs are in my heart.</div>
</div>
<div id="speech9" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.15">Hem them away.</div>
</div>
<div id="speech10" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.16">I would try, if I could cry 'hem' and have him.</div>
</div>
<div id="speech11" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.17">Come, come, wrestle with thy affections.</div>
</div>
<div id="speech12" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.18">O, they take the part of a better wrestler than myself!</div>
</div>
<div id="speech13" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.19">O, a good wish upon you! you will try in time, in</div>
<div id="scene1.3.20">despite of a fall. But, turning these jests out of</div>
<div id="scene1.3.21">service, let us talk in good earnest: is it</div>
<div id="scene1.3.22">possible, on such a sudden, you should fall into so</div>
<div id="scene1.3.23">strong a liking with old Sir Rowland's youngest son?</div>
</div>
<div id="speech14" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.24">The duke my father loved his father dearly.</div>
</div>
<div id="speech15" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.25">Doth it therefore ensue that you should love his son</div>
<div id="scene1.3.26">dearly? By this kind of chase, I should hate him,</div>
<div id="scene1.3.27">for my father hated his father dearly; yet I hate</div>
<div id="scene1.3.28">not Orlando.</div>
</div>
<div id="speech16" class="character">ROSALIND</div>
<div title="wtf" class="dialog">
<div id="scene1.3.29">No, faith, hate him not, for my sake.</div>
</div>
<div id="speech17" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.30">Why should I not? doth he not deserve well?</div>
</div>
<div id="speech18" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.31">Let me love him for that, and do you love him</div>
<div id="scene1.3.32">because I do. Look, here comes the duke.</div>
</div>
<div id="speech19" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.33">With his eyes full of anger.</div>
<div class="direction">Enter DUKE FREDERICK, with Lords</div>
</div>
<div id="speech20" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.34">Mistress, dispatch you with your safest haste</div>
<div id="scene1.3.35">And get you from our court.</div>
</div>
<div id="speech21" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.36">Me, uncle?</div>
</div>
<div id="speech22" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.37">You, cousin</div>
<div id="scene1.3.38">Within these ten days if that thou be'st found</div>
<div id="scene1.3.39">So near our public court as twenty miles,</div>
<div id="scene1.3.40">Thou diest for it.</div>
</div>
<div id="speech23" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.41"> I do beseech your grace,</div>
<div id="scene1.3.42">Let me the knowledge of my fault bear with me:</div>
<div id="scene1.3.43">If with myself I hold intelligence</div>
<div id="scene1.3.44">Or have acquaintance with mine own desires,</div>
<div id="scene1.3.45">If that I do not dream or be not frantic,--</div>
<div id="scene1.3.46">As I do trust I am not--then, dear uncle,</div>
<div id="scene1.3.47">Never so much as in a thought unborn</div>
<div id="scene1.3.48">Did I offend your highness.</div>
</div>
<div id="speech24" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.49">Thus do all traitors:</div>
<div id="scene1.3.50">If their purgation did consist in words,</div>
<div id="scene1.3.51">They are as innocent as grace itself:</div>
<div id="scene1.3.52">Let it suffice thee that I trust thee not.</div>
</div>
<div id="speech25" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.53">Yet your mistrust cannot make me a traitor:</div>
<div id="scene1.3.54">Tell me whereon the likelihood depends.</div>
</div>
<div id="speech26" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.55">Thou art thy father's daughter; there's enough.</div>
</div>
<div id="speech27" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.56">So was I when your highness took his dukedom;</div>
<div id="scene1.3.57">So was I when your highness banish'd him:</div>
<div id="scene1.3.58">Treason is not inherited, my lord;</div>
<div id="scene1.3.59">Or, if we did derive it from our friends,</div>
<div id="scene1.3.60">What's that to me? my father was no traitor:</div>
<div id="scene1.3.61">Then, good my liege, mistake me not so much</div>
<div id="scene1.3.62">To think my poverty is treacherous.</div>
</div>
<div id="speech28" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.63">Dear sovereign, hear me speak.</div>
</div>
<div id="speech29" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.64">Ay, Celia; we stay'd her for your sake,</div>
<div id="scene1.3.65">Else had she with her father ranged along.</div>
</div>
<div id="speech30" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.66">I did not then entreat to have her stay;</div>
<div id="scene1.3.67">It was your pleasure and your own remorse:</div>
<div id="scene1.3.68">I was too young that time to value her;</div>
<div id="scene1.3.69">But now I know her: if she be a traitor,</div>
<div id="scene1.3.70">Why so am I; we still have slept together,</div>
<div id="scene1.3.71">Rose at an instant, learn'd, play'd, eat together,</div>
<div id="scene1.3.72">And wheresoever we went, like Juno's swans,</div>
<div id="scene1.3.73">Still we went coupled and inseparable.</div>
</div>
<div id="speech31" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.74">She is too subtle for thee; and her smoothness,</div>
<div id="scene1.3.75">Her very silence and her patience</div>
<div id="scene1.3.76">Speak to the people, and they pity her.</div>
<div id="scene1.3.77">Thou art a fool: she robs thee of thy name;</div>
<div id="scene1.3.78">And thou wilt show more bright and seem more virtuous</div>
<div id="scene1.3.79">When she is gone. Then open not thy lips:</div>
<div id="scene1.3.80">Firm and irrevocable is my doom</div>
<div id="scene1.3.81">Which I have pass'd upon her; she is banish'd.</div>
</div>
<div id="speech32" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.82">Pronounce that sentence then on me, my liege:</div>
<div id="scene1.3.83">I cannot live out of her company.</div>
</div>
<div id="speech33" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.84">You are a fool. You, niece, provide yourself:</div>
<div id="scene1.3.85">If you outstay the time, upon mine honour,</div>
<div id="scene1.3.86">And in the greatness of my word, you die.</div>
<div class="direction">Exeunt DUKE FREDERICK and Lords</div>
</div>
<div id="speech34" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.87">O my poor Rosalind, whither wilt thou go?</div>
<div id="scene1.3.88">Wilt thou change fathers? I will give thee mine.</div>
<div id="scene1.3.89">I charge thee, be not thou more grieved than I am.</div>
</div>
<div id="speech35" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.90">I have more cause.</div>
</div>
<div id="speech36" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.91"> Thou hast not, cousin;</div>
<div id="scene1.3.92">Prithee be cheerful: know'st thou not, the duke</div>
<div id="scene1.3.93">Hath banish'd me, his daughter?</div>
</div>
<div id="speech37" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.94">That he hath not.</div>
</div>
<div id="speech38" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.95">No, hath not? Rosalind lacks then the love</div>
<div id="scene1.3.96">Which teacheth thee that thou and I am one:</div>
<div id="scene1.3.97">Shall we be sunder'd? shall we part, sweet girl?</div>
<div id="scene1.3.98">No: let my father seek another heir.</div>
<div id="scene1.3.99">Therefore devise with me how we may fly,</div>
<div id="scene1.3.100">Whither to go and what to bear with us;</div>
<div id="scene1.3.101">And do not seek to take your change upon you,</div>
<div id="scene1.3.102">To bear your griefs yourself and leave me out;</div>
<div id="scene1.3.103">For, by this heaven, now at our sorrows pale,</div>
<div id="scene1.3.104">Say what thou canst, I'll go along with thee.</div>
</div>
<div id="speech39" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.105">Why, whither shall we go?</div>
</div>
<div id="speech40" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.106">To seek my uncle in the forest of Arden.</div>
</div>
<div id="speech41" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.107">Alas, what danger will it be to us,</div>
<div id="scene1.3.108">Maids as we are, to travel forth so far!</div>
<div id="scene1.3.109">Beauty provoketh thieves sooner than gold.</div>
</div>
<div id="speech42" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.110">I'll put myself in poor and mean attire</div>
<div id="scene1.3.111">And with a kind of umber smirch my face;</div>
<div id="scene1.3.112">The like do you: so shall we pass along</div>
<div id="scene1.3.113">And never stir assailants.</div>
</div>
<div id="speech43" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.114">Were it not better,</div>
<div id="scene1.3.115">Because that I am more than common tall,</div>
<div id="scene1.3.116">That I did suit me all points like a man?</div>
<div id="scene1.3.117">A gallant curtle-axe upon my thigh,</div>
<div id="scene1.3.118">A boar-spear in my hand; and--in my heart</div>
<div id="scene1.3.119">Lie there what hidden woman's fear there will--</div>
<div id="scene1.3.120">We'll have a swashing and a martial outside,</div>
<div id="scene1.3.121">As many other mannish cowards have</div>
<div id="scene1.3.122">That do outface it with their semblances.</div>
</div>
<div id="speech44" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.123">What shall I call thee when thou art a man?</div>
</div>
<div id="speech45" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.124">I'll have no worse a name than Jove's own page;</div>
<div id="scene1.3.125">And therefore look you call me Ganymede.</div>
<div id="scene1.3.126">But what will you be call'd?</div>
</div>
<div id="speech46" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.127">Something that hath a reference to my state</div>
<div id="scene1.3.128">No longer Celia, but Aliena.</div>
</div>
<div id="speech47" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.129">But, cousin, what if we assay'd to steal</div>
<div id="scene1.3.130">The clownish fool out of your father's court?</div>
<div id="scene1.3.131">Would he not be a comfort to our travel?</div>
</div>
<div id="speech48" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.132">He'll go along o'er the wide world with me;</div>
<div id="scene1.3.133">Leave me alone to woo him. Let's away,</div>
<div id="scene1.3.134">And get our jewels and our wealth together,</div>
<div id="scene1.3.135">Devise the fittest time and safest way</div>
<div id="scene1.3.136">To hide us from pursuit that will be made</div>
<div id="scene1.3.137">After my flight. Now go we in content</div>
<div id="scene1.3.138">To liberty and not to banishment.</div>
<div class="direction">Exeunt</div>
</div>
</div>
</div>
</div>
</body>
</html>
================================================
FILE: Tests/XPath/TranslatorTest.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Tests\XPath;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\Parser\Parser;
use Symfony\Component\CssSelector\XPath\Extension\HtmlExtension;
use Symfony\Component\CssSelector\XPath\Translator;
use Symfony\Component\CssSelector\XPath\XPathExpr;
class TranslatorTest extends TestCase
{
#[DataProvider('getXpathLiteralTestData')]
public function testXpathLiteral($value, $literal)
{
$this->assertEquals($literal, Translator::getXpathLiteral($value));
}
#[DataProvider('getCssToXPathTestData')]
public function testCssToXPath($css, $xpath)
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$this->assertEquals($xpath, $translator->cssToXPath($css, ''));
}
#[DataProvider('getUnsupportedHasSelectorTestData')]
public function testHasUnsupportedSelector(string $css)
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$this->expectException(SyntaxErrorException::class);
$translator->cssToXPath($css, '');
}
public static function getUnsupportedHasSelectorTestData(): iterable
{
yield 'attribute selector' => ['div:has([data-x])'];
yield 'descendant combinator' => ['div:has(.foo .bar)'];
yield 'selector list' => ['div:has(.foo, .bar)'];
yield 'nested pseudo-class' => ['div:has(:not(.foo))'];
yield 'chained combinator' => ['div:has(> .foo > .bar)'];
}
public function testCssToXPathPseudoElement()
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$this->expectException(ExpressionErrorException::class);
$translator->cssToXPath('e::first-line');
}
public function testGetExtensionNotExistsExtension()
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$this->expectException(ExpressionErrorException::class);
$translator->getExtension('fake');
}
public function testAddCombinationNotExistsExtension()
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$parser = new Parser();
$xpath = $parser->parse('*')[0];
$combinedXpath = $parser->parse('*')[0];
$this->expectException(ExpressionErrorException::class);
$translator->addCombination('fake', $xpath, $combinedXpath);
}
public function testAddFunctionNotExistsFunction()
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$xpath = new XPathExpr();
$function = new FunctionNode(new ElementNode(), 'fake');
$this->expectException(ExpressionErrorException::class);
$translator->addFunction($xpath, $function);
}
public function testAddPseudoClassNotExistsClass()
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$xpath = new XPathExpr();
$this->expectException(ExpressionErrorException::class);
$translator->addPseudoClass($xpath, 'fake');
}
public function testAddAttributeMatchingClassNotExistsClass()
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$xpath = new XPathExpr();
$this->expectException(ExpressionErrorException::class);
$translator->addAttributeMatching($xpath, '', '', '');
}
#[DataProvider('getXmlLangTestData')]
public function testXmlLang($css, array $elementsId)
{
$translator = new Translator();
$document = new \SimpleXMLElement(file_get_contents(__DIR__.'/Fixtures/lang.xml'));
$elements = $document->xpath($translator->cssToXPath($css));
$this->assertCount(\count($elementsId), $elements);
foreach ($elements as $element) {
$this->assertContains((string) $element->attributes()->id, $elementsId);
}
}
#[DataProvider('getHtmlIdsTestData')]
public function testHtmlIds($css, array $elementsId)
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$document = new \DOMDocument();
$document->strictErrorChecking = false;
$internalErrors = libxml_use_internal_errors(true);
$document->loadHTMLFile(__DIR__.'/Fixtures/ids.html');
$document = simplexml_import_dom($document);
$elements = $document->xpath($translator->cssToXPath($css));
$this->assertCount(\count($elementsId), $elements);
foreach ($elements as $element) {
if (null !== $element->attributes()->id) {
$this->assertContains((string) $element->attributes()->id, $elementsId);
}
}
libxml_clear_errors();
libxml_use_internal_errors($internalErrors);
}
#[DataProvider('getHtmlShakespearTestData')]
public function testHtmlShakespear($css, $count)
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$document = new \DOMDocument();
$document->strictErrorChecking = false;
$document->loadHTMLFile(__DIR__.'/Fixtures/shakespear.html');
$document = simplexml_import_dom($document);
$bodies = $document->xpath('//body');
$elements = $bodies[0]->xpath($translator->cssToXPath($css));
$this->assertCount($count, $elements);
}
public function testOnlyOfTypeFindsSingleChildrenOfGivenType()
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$document = new \DOMDocument();
$document->loadHTML(<<<'HTML'
<html>
<body>
<p>
<span>A</span>
</p>
<p>
<span>B</span>
<span>C</span>
</p>
</body>
</html>
HTML
);
$xpath = new \DOMXPath($document);
$nodeList = $xpath->query($translator->cssToXPath('span:only-of-type'));
$this->assertSame(1, $nodeList->length);
$this->assertSame('A', $nodeList->item(0)->textContent);
}
public static function getXpathLiteralTestData()
{
return [
['foo', "'foo'"],
["foo's bar", '"foo\'s bar"'],
["foo's \"middle\" bar", 'concat(\'foo\', "\'", \'s "middle" bar\')'],
["foo's 'middle' \"bar\"", 'concat(\'foo\', "\'", \'s \', "\'", \'middle\', "\'", \' "bar"\')'],
];
}
public static function getCssToXPathTestData()
{
return [
['*', '*'],
['e', 'e'],
['*|e', 'e'],
['e|f', 'e:f'],
['e[foo]', 'e[@foo]'],
['e[foo|bar]', 'e[@foo:bar]'],
['e[foo="bar"]', "e[@foo = 'bar']"],
['e[foo~="bar"]', "e[@foo and contains(concat(' ', normalize-space(@foo), ' '), ' bar ')]"],
['e[foo^="bar"]', "e[@foo and starts-with(@foo, 'bar')]"],
['e[foo$="bar"]', "e[@foo and substring(@foo, string-length(@foo)-2) = 'bar']"],
['e[foo*="bar"]', "e[@foo and contains(@foo, 'bar')]"],
['e[foo!="bar"]', "e[not(@foo) or @foo != 'bar']"],
['e[foo!="bar"][foo!="baz"]', "e[(not(@foo) or @foo != 'bar') and (not(@foo) or @foo != 'baz')]"],
['e[hreflang|="en"]', "e[@hreflang and (@hreflang = 'en' or starts-with(@hreflang, 'en-'))]"],
['e:nth-child(1)', "*/*[(name() = 'e') and (position() = 1)]"],
['e:nth-last-child(1)', "*/*[(name() = 'e') and (position() = last() - 0)]"],
['e:nth-last-child(2n+2)', "*/*[(name() = 'e') and (last() - position() - 1 >= 0 and (last() - position() - 1) mod 2 = 0)]"],
['e:nth-of-type(1)', '*/e[position() = 1]'],
['e:nth-last-of-type(1)', '*/e[position() = last() - 0]'],
['div e:nth-last-of-type(1) .aclass', "div/descendant-or-self::*/e[position() = last() - 0]/descendant-or-self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' aclass ')]"],
['e:first-child', "*/*[(name() = 'e') and (position() = 1)]"],
['e:last-child', "*/*[(name() = 'e') and (position() = last())]"],
['e:first-of-type', '*/e[position() = 1]'],
['e:last-of-type', '*/e[position() = last()]'],
['e:only-child', "*/*[(name() = 'e') and (last() = 1)]"],
['e:only-of-type', 'e[count(preceding-sibling::e)=0 and count(following-sibling::e)=0]'],
['e:empty', 'e[not(*) and not(string-length())]'],
['e:EmPTY', 'e[not(*) and not(string-length())]'],
['e:root', 'e[not(parent::*)]'],
['e:hover', 'e[0]'],
['e:contains("foo")', "e[contains(string(.), 'foo')]"],
['e:ConTains(foo)', "e[contains(string(.), 'foo')]"],
['e.warning', "e[@class and contains(concat(' ', normalize-space(@class), ' '), ' warning ')]"],
['e#myid', "e[@id = 'myid']"],
['e:not(:nth-child(odd))', 'e[not(position() - 1 >= 0 and (position() - 1) mod 2 = 0)]'],
['e:nOT(*)', 'e[0]'],
['e f', 'e/descendant-or-self::*/f'],
['e > f', 'e/f'],
['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]'],
['e:is(section, article) h1', "e[(name() = 'section') or (name() = 'article')]/descendant-or-self::*/h1"],
['e:where(section, article) h1', "e[(name() = 'section') or (name() = 'article')]/descendant-or-self::*/h1"],
['div:has(> .foo)', "div[./*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]]"],
['div:has(~ .foo)', "div[following-sibling::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]]"],
['div:has(+ .foo)', "div[following-sibling::*[(@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')) and (position() = 1)]]"],
['div:has(.foo)', "div[descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]]"],
['div:has(#bar)', "div[descendant-or-self::*[@id = 'bar']]"],
];
}
public static function getXmlLangTestData()
{
return [
[':lang("EN")', ['first', 'second', 'third', 'fourth']],
[':lang("en-us")', ['second', 'fourth']],
[':lang(en-nz)', ['third']],
[':lang(fr)', ['fifth']],
[':lang(ru)', ['sixth']],
[":lang('ZH')", ['eighth']],
[':lang(de) :lang(zh)', ['eighth']],
[':lang(en), :lang(zh)', ['first', 'second', 'third', 'fourth', 'eighth']],
[':lang(es)', []],
];
}
public static function getHtmlIdsTestData()
{
return [
['div', ['outer-div', 'li-div', 'foobar-div']],
['DIV', ['outer-div', 'li-div', 'foobar-div']], // case-insensitive in HTML
['div div', ['li-div']],
['div, div div', ['outer-div', 'li-div', 'foobar-div']],
['a[name]', ['name-anchor']],
['a[NAme]', ['name-anchor']], // case-insensitive in HTML:
['a[rel]', ['tag-anchor', 'nofollow-anchor']],
['a[rel="tag"]', ['tag-anchor']],
['a[href*="localhost"]', ['tag-anchor']],
['a[href*=""]', []],
['a[href^="http"]', ['tag-anchor', 'nofollow-anchor']],
['a[href^="http:"]', ['tag-anchor']],
['a[href^=""]', []],
['a[href$="org"]', ['nofollow-anchor']],
['a[href$=""]', []],
['div[foobar~="bc"]', ['foobar-div']],
['div[foobar~="cde"]', ['foobar-div']],
['[foobar~="ab bc"]', ['foobar-div']],
['[foobar~=""]', []],
['[foobar~=" \t"]', []],
['div[foobar~="cd"]', []],
['*[lang|="En"]', ['second-li']],
['[lang|="En-us"]', ['second-li']],
// Attribute values are case-sensitive
['*[lang|="en"]', []],
['[lang|="en-US"]', []],
['*[lang|="e"]', []],
// ... :lang() is not.
[':lang("EN")', ['second-li', 'li-div']],
['*:lang(en-US)', ['second-li', 'li-div']],
[':lang("e")', []],
['li:nth-child(3)', ['third-li']],
['li:nth-child(10)', []],
['li:nth-child(2n)', ['second-li', 'fourth-li', 'sixth-li']],
['li:nth-child(even)', ['second-li', 'fourth-li', 'sixth-li']],
['li:nth-child(2n+0)', ['second-li', 'fourth-li', 'sixth-li']],
['li:nth-child(+2n+1)', ['first-li', 'third-li', 'fifth-li', 'seventh-li']],
['li:nth-child(odd)', ['first-li', 'third-li', 'fifth-li', 'seventh-li']],
['li:nth-child(2n+4)', ['fourth-li', 'sixth-li']],
['li:nth-child(3n+1)', ['first-li', 'fourth-li', 'seventh-li']],
['li:nth-child(n)', ['first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li']],
['li:nth-child(n-1)', ['first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li']],
['li:nth-child(n+1)', ['first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li']],
['li:nth-child(n+3)', ['third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li']],
['li:nth-child(-n)', []],
['li:nth-child(-n-1)', []],
['li:nth-child(-n+1)', ['first-li']],
['li:nth-child(-n+3)', ['first-li', 'second-li', 'third-li']],
['li:nth-last-child(0)', []],
['li:nth-last-child(2n)', ['second-li', 'fourth-li', 'sixth-li']],
['li:nth-last-child(even)', ['second-li', 'fourth-li', 'sixth-li']],
['li:nth-last-child(2n+2)', ['second-li', 'fourth-li', 'sixth-li']],
['li:nth-last-child(n)', ['first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li']],
['li:nth-last-child(n-1)', ['first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li']],
['li:nth-last-child(n-3)', ['first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li']],
['li:nth-last-child(n+1)', ['first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li', 'sixth-li', 'seventh-li']],
['li:nth-last-child(n+3)', ['first-li', 'second-li', 'third-li', 'fourth-li', 'fifth-li']],
['li:nth-last-child(-n)', []],
['li:nth-last-child(-n-1)', []],
['li:nth-last-child(-n+1)', ['seventh-li']],
['li:nth-last-child(-n+3)', ['fifth-li', 'sixth-li', 'seventh-li']],
['ol:first-of-type', ['first-ol']],
['ol:nth-child(4)', ['first-ol']],
['ol:nth-of-type(2)', ['second-ol']],
['ol:nth-last-of-type(1)', ['second-ol']],
['span:only-child', ['foobar-span', 'no-siblings-of-any-type']],
['li div:only-child', ['li-div']],
['div *:only-child', ['li-div', 'foobar-span']],
['p:only-of-type', ['paragraph']],
[':only-of-type', ['html', 'li-div', 'foobar-span', 'no-siblings-of-any-type']],
['div#foobar-div :only-of-type', ['foobar-span']],
['a:empty', ['name-anchor']],
['a:EMpty', ['name-anchor']],
['li:empty', ['third-li', 'fourth-li', 'fifth-li', 'sixth-li']],
[':root', ['html']],
['html:root', ['html']],
['li:root', []],
['* :root', []],
['*:contains("link")', ['html', 'nil', 'outer-div', 'tag-anchor', 'nofollow-anchor']],
[':CONtains("link")', ['html', 'nil', 'outer-div', 'tag-anchor', 'nofollow-anchor']],
['*:contains("LInk")', []], // case-sensitive
['*:contains("e")', ['html', 'nil', 'outer-div', 'first-ol', 'first-li', 'paragraph', 'p-em']],
['*:contains("E")', []], // case-sensitive
['.a', ['first-ol']],
['.b', ['first-ol']],
['*.a', ['first-ol']],
['ol.a', ['first-ol']],
['.c', ['first-ol', 'third-li', 'fourth-li']],
['*.c', ['first-ol', 'third-li', 'fourth-li']],
['ol *.c', ['third-li', 'fourth-li']],
['ol li.c', ['third-li', 'fourth-li']],
['li ~ li.c', ['third-li', 'fourth-li']],
['ol > li.c', ['third-li', 'fourth-li']],
['#first-li', ['first-li']],
['li#first-li', ['first-li']],
['*#first-li', ['first-li']],
['li div', ['li-div']],
['li > div', ['li-div']],
['div div', ['li-div']],
['div > div', []],
['div>.c', ['first-ol']],
['div > .c', ['first-ol']],
['div + div', ['foobar-div']],
['a ~ a', ['tag-anchor', 'nofollow-anchor']],
['a[rel="tag"] ~ a', ['nofollow-anchor']],
['ol#first-ol li:last-child', ['seventh-li']],
['ol#first-ol *:last-child', ['li-div', 'seventh-li']],
['#outer-div:first-child', ['outer-div']],
['#outer-div :first-child', ['name-anchor', 'first-li', 'li-div', 'p-b', 'checkbox-fieldset-disabled', 'area-href']],
['a[href]', ['tag-anchor', 'nofollow-anchor']],
[':not(*)', []],
['a:not([href])', ['name-anchor']],
['ol :Not(li[class])', ['first-li', 'second-li', 'li-div', 'fifth-li', 'sixth-li', 'seventh-li']],
[':is(#first-li, #second-li)', ['first-li', 'second-li']],
['a:is(#name-anchor, #tag-anchor)', ['name-anchor', 'tag-anchor']],
[':is(.c)', ['first-ol', 'third-li', 'fourth-li']],
['a:is(:not(#name-anchor))', ['tag-anchor', 'nofollow-anchor']],
['a:not(:is(#name-anchor))', ['tag-anchor', 'nofollow-anchor']],
[':where(#first-li, #second-li)', ['first-li', 'second-li']],
['a:where(#name-anchor, #tag-anchor)', ['name-anchor', 'tag-anchor']],
[':where(.c)', ['first-ol', 'third-li', 'fourth-li']],
['a:where(:not(#name-anchor))', ['tag-anchor', 'nofollow-anchor']],
['a:not(:where(#name-anchor))', ['tag-anchor', 'nofollow-anchor']],
['a:where(:is(#name-anchor), :where(#tag-anchor))', ['name-anchor', 'tag-anchor']],
// HTML-specific
[':link', ['link-href', 'tag-anchor', 'nofollow-anchor', 'area-href']],
[':visited', []],
[':enabled', ['link-href', 'tag-anchor', 'nofollow-anchor', 'checkbox-unchecked', 'text-checked', 'checkbox-checked', 'area-href']],
[':disabled', ['checkbox-disabled', 'checkbox-disabled-checked', 'fieldset', 'checkbox-fieldset-disabled']],
[':checked', ['checkbox-checked', 'checkbox-disabled-checked']],
];
}
public static function getHtmlShakespearTestData()
{
return [
['*', 246],
['div:contains(CELIA)', 26],
['div:only-child', 22], // ?
['div:nth-child(even)', 106],
['div:nth-child(2n)', 106],
['div:nth-child(odd)', 137],
['div:nth-child(2n+1)', 137],
['div:nth-child(n)', 243],
['div:last-child', 53],
['div:first-child', 51],
['div > div', 242],
['div + div', 190],
['div ~ div', 190],
['body', 1],
['body div', 243],
['div', 243],
['div div', 242],
['div div div', 241],
['div, div, div', 243],
['div, a, span', 243],
['.dialog', 51],
['div.dialog', 51],
['div .dialog', 51],
['div.character, div.dialog', 99],
['div.direction.dialog', 0],
['div.dialog.direction', 0],
['div.dialog.scene', 1],
['div.scene.scene', 1],
['div.scene .scene', 0],
['div.direction .dialog ', 0],
['div .dialog .direction', 4],
['div.dialog .dialog .direction', 4],
['#speech5', 1],
['div#speech5', 1],
['div #speech5', 1],
['div.scene div.dialog', 49],
['div#scene1 div.dialog div', 142],
['#scene1 #speech1', 1],
['div[class]', 103],
['div[class=dialog]', 50],
['div[class^=dia]', 51],
['div[class$=log]', 50],
['div[class*=sce]', 1],
['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],
['div:is(div#test .dialog) .direction', 4],
];
}
}
================================================
FILE: XPath/Extension/AbstractExtension.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
/**
* XPath expression translator abstract extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
abstract class AbstractExtension implements ExtensionInterface
{
public function getNodeTranslators(): array
{
return [];
}
public function getCombinationTranslators(): array
{
return [];
}
public function getFunctionTranslators(): array
{
return [];
}
public function getPseudoClassTranslators(): array
{
return [];
}
public function getAttributeMatchingTranslators(): array
{
return [];
}
public function getRelativeCombinationTranslators(): array
{
return [];
}
}
================================================
FILE: XPath/Extension/AttributeMatchingExtension.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\XPath\Translator;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator attribute extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class AttributeMatchingExtension extends AbstractExtension
{
public function getAttributeMatchingTranslators(): array
{
return [
'exists' => $this->translateExists(...),
'=' => $this->translateEquals(...),
'~=' => $this->translateIncludes(...),
'|=' => $this->translateDashMatch(...),
'^=' => $this->translatePrefixMatch(...),
'$=' => $this->translateSuffixMatch(...),
'*=' => $this->translateSubstringMatch(...),
'!=' => $this->translateDifferent(...),
];
}
public function translateExists(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($attribute);
}
public function translateEquals(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition(\sprintf('%s = %s', $attribute, Translator::getXpathLiteral($value)));
}
public function translateIncludes(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? \sprintf(
'%1$s and contains(concat(\' \', normalize-space(%1$s), \' \'), %2$s)',
$attribute,
Translator::getXpathLiteral(' '.$value.' ')
) : '0');
}
public function translateDashMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition(\sprintf(
'%1$s and (%1$s = %2$s or starts-with(%1$s, %3$s))',
$attribute,
Translator::getXpathLiteral($value),
Translator::getXpathLiteral($value.'-')
));
}
public function translatePrefixMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? \sprintf(
'%1$s and starts-with(%1$s, %2$s)',
$attribute,
Translator::getXpathLiteral($value)
) : '0');
}
public function translateSuffixMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? \sprintf(
'%1$s and substring(%1$s, string-length(%1$s)-%2$s) = %3$s',
$attribute,
\strlen($value) - 1,
Translator::getXpathLiteral($value)
) : '0');
}
public function translateSubstringMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? \sprintf(
'%1$s and contains(%1$s, %2$s)',
$attribute,
Translator::getXpathLiteral($value)
) : '0');
}
public function translateDifferent(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition(\sprintf(
$value ? 'not(%1$s) or %1$s != %2$s' : '%s != %s',
$attribute,
Translator::getXpathLiteral($value)
));
}
public function getName(): string
{
return 'attribute-matching';
}
}
================================================
FILE: XPath/Extension/CombinationExtension.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator combination extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class CombinationExtension extends AbstractExtension
{
public function getCombinationTranslators(): array
{
return [
' ' => $this->translateDescendant(...),
'>' => $this->translateChild(...),
'+' => $this->translateDirectAdjacent(...),
'~' => $this->translateIndirectAdjacent(...),
];
}
public function translateDescendant(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
{
return $xpath->join('/descendant-or-self::*/', $combinedXpath);
}
public function translateChild(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
{
return $xpath->join('/', $combinedXpath);
}
public function translateDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
{
return $xpath
->join('/following-sibling::', $combinedXpath)
->addNameTest()
->addCondition('position() = 1');
}
public function translateIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
{
return $xpath->join('/following-sibling::', $combinedXpath);
}
public function getName(): string
{
return 'combination';
}
}
================================================
FILE: XPath/Extension/ExtensionInterface.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator extension interface.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
interface ExtensionInterface
{
/**
* Returns node translators.
*
* These callables will receive the node as first argument and the translator as second argument.
*
* @return callable[]
*/
public function getNodeTranslators(): array;
/**
* Returns combination translators.
*
* @return callable[]
*/
public function getCombinationTranslators(): array;
/**
* Returns function translators.
*
* @return callable[]
*/
public function getFunctionTranslators(): array;
/**
* Returns pseudo-class translators.
*
* @return callable[]
*/
public function getPseudoClassTranslators(): array;
/**
* Returns attribute operation translators.
*
* @return callable[]
*/
public function getAttributeMatchingTranslators(): array;
/**
* Returns combination translators found inside ":has()" relation.
*
* @return array<string, callable(XPathExpr, XPathExpr): XPathExpr>
*/
public function getRelativeCombinationTranslators(): array;
/**
* Returns extension name.
*/
public function getName(): string;
}
================================================
FILE: XPath/Extension/FunctionExtension.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\Parser\Parser;
use Symfony\Component\CssSelector\XPath\Translator;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator function extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class FunctionExtension extends AbstractExtension
{
public function getFunctionTranslators(): array
{
return [
'nth-child' => $this->translateNthChild(...),
'nth-last-child' => $this->translateNthLastChild(...),
'nth-of-type' => $this->translateNthOfType(...),
'nth-last-of-type' => $this->translateNthLastOfType(...),
'contains' => $this->translateContains(...),
'lang' => $this->translateLang(...),
];
}
/**
* @throws ExpressionErrorException
*/
public function translateNthChild(XPathExpr $xpath, FunctionNode $function, bool $last = false, bool $addNameTest = true): XPathExpr
{
try {
[$a, $b] = Parser::parseSeries($function->getArguments());
} catch (SyntaxErrorException $e) {
throw new ExpressionErrorException(\sprintf('Invalid series: "%s".', implode('", "', $function->getArguments())), 0, $e);
}
$xpath->addStarPrefix();
if ($addNameTest) {
$xpath->addNameTest();
}
if (0 === $a) {
return $xpath->addCondition('position() = '.($last ? 'last() - '.($b - 1) : $b));
}
if ($a < 0) {
if ($b < 1) {
return $xpath->addCondition('false()');
}
$sign = '<=';
} else {
$sign = '>=';
}
$expr = 'position()';
if ($last) {
$expr = 'last() - '.$expr;
--$b;
}
if (0 !== $b) {
$expr .= ' - '.$b;
}
$conditions = [\sprintf('%s %s 0', $expr, $sign)];
if (1 !== $a && -1 !== $a) {
$conditions[] = \sprintf('(%s) mod %d = 0', $expr, $a);
}
return $xpath->addCondition(implode(' and ', $conditions));
// todo: handle an+b, odd, even
// an+b means every-a, plus b, e.g., 2n+1 means odd
// 0n+b means b
// n+0 means a=1, i.e., all elements
// an means every a elements, i.e., 2n means even
// -n means -1n
// -1n+6 means elements 6 and previous
}
public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
return $this->translateNthChild($xpath, $function, true);
}
public function translateNthOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
return $this->translateNthChild($xpath, $function, false, false);
}
/**
* @throws ExpressionErrorException
*/
public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
if ('*' === $xpath->getElement()) {
throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.');
}
return $this->translateNthChild($xpath, $function, true, false);
}
/**
* @throws ExpressionErrorException
*/
public function translateContains(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
$arguments = $function->getArguments();
foreach ($arguments as $token) {
if (!($token->isString() || $token->isIdentifier())) {
throw new ExpressionErrorException('Expected a single string or identifier for :contains(), got '.implode(', ', $arguments));
}
}
return $xpath->addCondition(\sprintf(
'contains(string(.), %s)',
Translator::getXpathLiteral($arguments[0]->getValue())
));
}
/**
* @throws ExpressionErrorException
*/
public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
$arguments = $function->getArguments();
foreach ($arguments as $token) {
if (!($token->isString() || $token->isIdentifier())) {
throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments));
}
}
return $xpath->addCondition(\sprintf(
'lang(%s)',
Translator::getXpathLiteral($arguments[0]->getValue())
));
}
public function getName(): string
{
return 'function';
}
}
================================================
FILE: XPath/Extension/HtmlExtension.php
================================================
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\XPath\Translator;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator HTML extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class HtmlExtension extends AbstractExtension
{
public function __construct(Translator $translator)
{
$translator
->getExtension('node')
->setFlag(NodeExtension::ELEMENT_NAME_IN_LOWER_CASE, true)
->setFlag(NodeExtension::ATTRIBUTE_NAME_IN_LOWER_CASE, true);
}
public function getPseudoClassTranslators(): array
{
return [
'checked' => $this->translateChecked(...),
'link' => $this->translateLink(...),
'disabled' => $this->translateDisabled(...),
'enabled' => $this->translateEnabled(...),
'selected' => $this->translateSelected(...),
'invalid' => $this->translateInvalid(...),
'hover' => $this->translateHover(...),
'visited' => $this->translateVisited(...),
];
}
public function getFunctionTranslators(): array
{
return [
'lang' => $this->translateLang(...),
];
}
public function translateChecked(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition(
'(@checked '
."and (name(.) = 'input' or name(.) = 'command')"
."and (@type = 'checkbox' or @type = 'radio'))"
);
}
public function translateLink(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition("@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')");
}
public function translateD
gitextract_8h3d2omr/ ├── .gitattributes ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── close-pull-request.yml ├── .gitignore ├── CHANGELOG.md ├── CssSelectorConverter.php ├── Exception/ │ ├── ExceptionInterface.php │ ├── ExpressionErrorException.php │ ├── InternalErrorException.php │ ├── ParseException.php │ └── SyntaxErrorException.php ├── LICENSE ├── Node/ │ ├── AbstractNode.php │ ├── AttributeNode.php │ ├── ClassNode.php │ ├── CombinedSelectorNode.php │ ├── ElementNode.php │ ├── FunctionNode.php │ ├── HashNode.php │ ├── MatchingNode.php │ ├── NegationNode.php │ ├── NodeInterface.php │ ├── PseudoNode.php │ ├── RelationNode.php │ ├── SelectorNode.php │ ├── Specificity.php │ └── SpecificityAdjustmentNode.php ├── Parser/ │ ├── Handler/ │ │ ├── CommentHandler.php │ │ ├── HandlerInterface.php │ │ ├── HashHandler.php │ │ ├── IdentifierHandler.php │ │ ├── NumberHandler.php │ │ ├── StringHandler.php │ │ └── WhitespaceHandler.php │ ├── Parser.php │ ├── ParserInterface.php │ ├── Reader.php │ ├── Shortcut/ │ │ ├── ClassParser.php │ │ ├── ElementParser.php │ │ ├── EmptyStringParser.php │ │ └── HashParser.php │ ├── Token.php │ ├── TokenStream.php │ └── Tokenizer/ │ ├── Tokenizer.php │ ├── TokenizerEscaping.php │ └── TokenizerPatterns.php ├── README.md ├── Tests/ │ ├── CssSelectorConverterTest.php │ ├── Node/ │ │ ├── AbstractNodeTestCase.php │ │ ├── AttributeNodeTest.php │ │ ├── ClassNodeTest.php │ │ ├── CombinedSelectorNodeTest.php │ │ ├── ElementNodeTest.php │ │ ├── FunctionNodeTest.php │ │ ├── HashNodeTest.php │ │ ├── MatchingNodeTest.php │ │ ├── NegationNodeTest.php │ │ ├── PseudoNodeTest.php │ │ ├── SelectorNodeTest.php │ │ ├── SpecificityAdjustmentNodeTest.php │ │ └── SpecificityTest.php │ ├── Parser/ │ │ ├── Handler/ │ │ │ ├── AbstractHandlerTestCase.php │ │ │ ├── CommentHandlerTest.php │ │ │ ├── HashHandlerTest.php │ │ │ ├── IdentifierHandlerTest.php │ │ │ ├── NumberHandlerTest.php │ │ │ ├── StringHandlerTest.php │ │ │ └── WhitespaceHandlerTest.php │ │ ├── ParserTest.php │ │ ├── ReaderTest.php │ │ ├── Shortcut/ │ │ │ ├── ClassParserTest.php │ │ │ ├── ElementParserTest.php │ │ │ ├── EmptyStringParserTest.php │ │ │ └── HashParserTest.php │ │ └── TokenStreamTest.php │ └── XPath/ │ ├── Fixtures/ │ │ ├── ids.html │ │ ├── lang.xml │ │ └── shakespear.html │ └── TranslatorTest.php ├── XPath/ │ ├── Extension/ │ │ ├── AbstractExtension.php │ │ ├── AttributeMatchingExtension.php │ │ ├── CombinationExtension.php │ │ ├── ExtensionInterface.php │ │ ├── FunctionExtension.php │ │ ├── HtmlExtension.php │ │ ├── NodeExtension.php │ │ ├── PseudoClassExtension.php │ │ └── RelationExtension.php │ ├── Translator.php │ ├── TranslatorInterface.php │ └── XPathExpr.php ├── composer.json └── phpunit.xml.dist
SYMBOL INDEX (451 symbols across 81 files)
FILE: CssSelectorConverter.php
class CssSelectorConverter (line 27) | class CssSelectorConverter
method __construct (line 40) | public function __construct(bool $html = true)
method toXPath (line 65) | public function toXPath(string $cssExpr, string $prefix = 'descendant-...
FILE: Exception/ExceptionInterface.php
type ExceptionInterface (line 22) | interface ExceptionInterface extends \Throwable
FILE: Exception/ExpressionErrorException.php
class ExpressionErrorException (line 22) | class ExpressionErrorException extends ParseException
FILE: Exception/InternalErrorException.php
class InternalErrorException (line 22) | class InternalErrorException extends ParseException
FILE: Exception/ParseException.php
class ParseException (line 22) | class ParseException extends \Exception implements ExceptionInterface
FILE: Exception/SyntaxErrorException.php
class SyntaxErrorException (line 24) | class SyntaxErrorException extends ParseException
method unexpectedToken (line 26) | public static function unexpectedToken(string $expectedValue, Token $f...
method pseudoElementFound (line 31) | public static function pseudoElementFound(string $pseudoElement, strin...
method unclosedString (line 36) | public static function unclosedString(int $position): self
method nestedNot (line 41) | public static function nestedNot(): self
method notAtTheStartOfASelector (line 46) | public static function notAtTheStartOfASelector(string $pseudoElement)...
method stringAsFunctionArgument (line 51) | public static function stringAsFunctionArgument(): self
FILE: Node/AbstractNode.php
class AbstractNode (line 24) | abstract class AbstractNode implements NodeInterface
method getNodeName (line 28) | public function getNodeName(): string
FILE: Node/AttributeNode.php
class AttributeNode (line 24) | class AttributeNode extends AbstractNode
method __construct (line 26) | public function __construct(
method getSelector (line 35) | public function getSelector(): NodeInterface
method getNamespace (line 40) | public function getNamespace(): ?string
method getAttribute (line 45) | public function getAttribute(): string
method getOperator (line 50) | public function getOperator(): string
method getValue (line 55) | public function getValue(): ?string
method getSpecificity (line 60) | public function getSpecificity(): Specificity
method __toString (line 65) | public function __toString(): string
FILE: Node/ClassNode.php
class ClassNode (line 24) | class ClassNode extends AbstractNode
method __construct (line 26) | public function __construct(
method getSelector (line 32) | public function getSelector(): NodeInterface
method getName (line 37) | public function getName(): string
method getSpecificity (line 42) | public function getSpecificity(): Specificity
method __toString (line 47) | public function __toString(): string
FILE: Node/CombinedSelectorNode.php
class CombinedSelectorNode (line 24) | class CombinedSelectorNode extends AbstractNode
method __construct (line 26) | public function __construct(
method getSelector (line 33) | public function getSelector(): NodeInterface
method getCombinator (line 38) | public function getCombinator(): string
method getSubSelector (line 43) | public function getSubSelector(): NodeInterface
method getSpecificity (line 48) | public function getSpecificity(): Specificity
method __toString (line 53) | public function __toString(): string
FILE: Node/ElementNode.php
class ElementNode (line 24) | class ElementNode extends AbstractNode
method __construct (line 26) | public function __construct(
method getNamespace (line 32) | public function getNamespace(): ?string
method getElement (line 37) | public function getElement(): ?string
method getSpecificity (line 42) | public function getSpecificity(): Specificity
method __toString (line 47) | public function __toString(): string
FILE: Node/FunctionNode.php
class FunctionNode (line 26) | class FunctionNode extends AbstractNode
method __construct (line 33) | public function __construct(
method getSelector (line 41) | public function getSelector(): NodeInterface
method getName (line 46) | public function getName(): string
method getArguments (line 54) | public function getArguments(): array
method getSpecificity (line 59) | public function getSpecificity(): Specificity
method __toString (line 64) | public function __toString(): string
FILE: Node/HashNode.php
class HashNode (line 24) | class HashNode extends AbstractNode
method __construct (line 26) | public function __construct(
method getSelector (line 32) | public function getSelector(): NodeInterface
method getId (line 37) | public function getId(): string
method getSpecificity (line 42) | public function getSpecificity(): Specificity
method __toString (line 47) | public function __toString(): string
FILE: Node/MatchingNode.php
class MatchingNode (line 24) | class MatchingNode extends AbstractNode
method __construct (line 29) | public function __construct(
method getSpecificity (line 35) | public function getSpecificity(): Specificity
method __toString (line 46) | public function __toString(): string
FILE: Node/NegationNode.php
class NegationNode (line 24) | class NegationNode extends AbstractNode
method __construct (line 26) | public function __construct(
method getSelector (line 32) | public function getSelector(): NodeInterface
method getSubSelector (line 37) | public function getSubSelector(): NodeInterface
method getSpecificity (line 42) | public function getSpecificity(): Specificity
method __toString (line 47) | public function __toString(): string
FILE: Node/NodeInterface.php
type NodeInterface (line 24) | interface NodeInterface extends \Stringable
method getNodeName (line 26) | public function getNodeName(): string;
method getSpecificity (line 28) | public function getSpecificity(): Specificity;
FILE: Node/PseudoNode.php
class PseudoNode (line 24) | class PseudoNode extends AbstractNode
method __construct (line 28) | public function __construct(
method getSelector (line 35) | public function getSelector(): NodeInterface
method getIdentifier (line 40) | public function getIdentifier(): string
method getSpecificity (line 45) | public function getSpecificity(): Specificity
method __toString (line 50) | public function __toString(): string
FILE: Node/RelationNode.php
class RelationNode (line 24) | class RelationNode extends AbstractNode
method __construct (line 26) | public function __construct(
method getSelector (line 33) | public function getSelector(): NodeInterface
method getCombinator (line 38) | public function getCombinator(): string
method getSubSelector (line 43) | public function getSubSelector(): NodeInterface
method getSpecificity (line 48) | public function getSpecificity(): Specificity
method __toString (line 53) | public function __toString(): string
FILE: Node/SelectorNode.php
class SelectorNode (line 24) | class SelectorNode extends AbstractNode
method __construct (line 28) | public function __construct(
method getTree (line 35) | public function getTree(): NodeInterface
method getPseudoElement (line 40) | public function getPseudoElement(): ?string
method getSpecificity (line 45) | public function getSpecificity(): Specificity
method __toString (line 50) | public function __toString(): string
FILE: Node/Specificity.php
class Specificity (line 26) | class Specificity
method __construct (line 32) | public function __construct(
method plus (line 39) | public function plus(self $specificity): self
method getValue (line 44) | public function getValue(): int
method compareTo (line 53) | public function compareTo(self $specificity): int
FILE: Node/SpecificityAdjustmentNode.php
class SpecificityAdjustmentNode (line 24) | class SpecificityAdjustmentNode extends AbstractNode
method __construct (line 29) | public function __construct(
method getSpecificity (line 35) | public function getSpecificity(): Specificity
method __toString (line 40) | public function __toString(): string
FILE: Parser/Handler/CommentHandler.php
class CommentHandler (line 27) | class CommentHandler implements HandlerInterface
method handle (line 29) | public function handle(Reader $reader, TokenStream $stream): bool
FILE: Parser/Handler/HandlerInterface.php
type HandlerInterface (line 27) | interface HandlerInterface
method handle (line 29) | public function handle(Reader $reader, TokenStream $stream): bool;
FILE: Parser/Handler/HashHandler.php
class HashHandler (line 30) | class HashHandler implements HandlerInterface
method __construct (line 32) | public function __construct(
method handle (line 38) | public function handle(Reader $reader, TokenStream $stream): bool
FILE: Parser/Handler/IdentifierHandler.php
class IdentifierHandler (line 30) | class IdentifierHandler implements HandlerInterface
method __construct (line 32) | public function __construct(
method handle (line 38) | public function handle(Reader $reader, TokenStream $stream): bool
FILE: Parser/Handler/NumberHandler.php
class NumberHandler (line 29) | class NumberHandler implements HandlerInterface
method __construct (line 31) | public function __construct(
method handle (line 36) | public function handle(Reader $reader, TokenStream $stream): bool
FILE: Parser/Handler/StringHandler.php
class StringHandler (line 32) | class StringHandler implements HandlerInterface
method __construct (line 34) | public function __construct(
method handle (line 40) | public function handle(Reader $reader, TokenStream $stream): bool
FILE: Parser/Handler/WhitespaceHandler.php
class WhitespaceHandler (line 28) | class WhitespaceHandler implements HandlerInterface
method handle (line 30) | public function handle(Reader $reader, TokenStream $stream): bool
FILE: Parser/Parser.php
class Parser (line 29) | class Parser implements ParserInterface
method __construct (line 33) | public function __construct(?Tokenizer $tokenizer = null)
method parse (line 38) | public function parse(string $source): array
method parseSeries (line 53) | public static function parseSeries(array $tokens): array
method parseSelectorList (line 91) | private function parseSelectorList(TokenStream $stream, bool $isArgume...
method parserSelectorNode (line 114) | private function parserSelectorNode(TokenStream $stream, bool $isArgum...
method parseRelativeSelector (line 152) | private function parseRelativeSelector(TokenStream $stream): array
method parseSimpleSelector (line 189) | private function parseSimpleSelector(TokenStream $stream, bool $inside...
method parseElementNode (line 335) | private function parseElementNode(TokenStream $stream): Node\ElementNode
method parseAttributeNode (line 361) | private function parseAttributeNode(Node\NodeInterface $selector, Toke...
FILE: Parser/ParserInterface.php
type ParserInterface (line 26) | interface ParserInterface
method parse (line 33) | public function parse(string $source): array;
FILE: Parser/Reader.php
class Reader (line 24) | class Reader
method __construct (line 29) | public function __construct(
method isEOF (line 35) | public function isEOF(): bool
method getPosition (line 40) | public function getPosition(): int
method getRemainingLength (line 45) | public function getRemainingLength(): int
method getSubstring (line 50) | public function getSubstring(int $length, int $offset = 0): string
method getOffset (line 55) | public function getOffset(string $string): int|false
method findPattern (line 62) | public function findPattern(string $pattern): array|false
method moveForward (line 73) | public function moveForward(int $length): void
method moveToEnd (line 78) | public function moveToEnd(): void
FILE: Parser/Shortcut/ClassParser.php
class ClassParser (line 29) | class ClassParser implements ParserInterface
method parse (line 31) | public function parse(string $source): array
FILE: Parser/Shortcut/ElementParser.php
class ElementParser (line 28) | class ElementParser implements ParserInterface
method parse (line 30) | public function parse(string $source): array
FILE: Parser/Shortcut/EmptyStringParser.php
class EmptyStringParser (line 32) | class EmptyStringParser implements ParserInterface
method parse (line 34) | public function parse(string $source): array
FILE: Parser/Shortcut/HashParser.php
class HashParser (line 29) | class HashParser implements ParserInterface
method parse (line 31) | public function parse(string $source): array
FILE: Parser/Token.php
class Token (line 24) | class Token
method __construct (line 37) | public function __construct(
method getType (line 47) | public function getType(): ?string
method getValue (line 52) | public function getValue(): ?string
method getPosition (line 57) | public function getPosition(): ?int
method isFileEnd (line 62) | public function isFileEnd(): bool
method isDelimiter (line 67) | public function isDelimiter(array $values = []): bool
method isWhitespace (line 80) | public function isWhitespace(): bool
method isIdentifier (line 85) | public function isIdentifier(): bool
method isHash (line 90) | public function isHash(): bool
method isNumber (line 95) | public function isNumber(): bool
method isString (line 100) | public function isString(): bool
method __toString (line 105) | public function __toString(): string
FILE: Parser/TokenStream.php
class TokenStream (line 27) | class TokenStream
method push (line 48) | public function push(Token $token): static
method freeze (line 60) | public function freeze(): static
method getNext (line 70) | public function getNext(): Token
method getPeek (line 89) | public function getPeek(): Token
method getUsed (line 104) | public function getUsed(): array
method getNextIdentifier (line 114) | public function getNextIdentifier(): string
method getNextIdentifierOrStar (line 130) | public function getNextIdentifierOrStar(): ?string
method skipWhitespace (line 148) | public function skipWhitespace(): void
FILE: Parser/Tokenizer/Tokenizer.php
class Tokenizer (line 29) | class Tokenizer
method __construct (line 36) | public function __construct()
method tokenize (line 54) | public function tokenize(Reader $reader): TokenStream
FILE: Parser/Tokenizer/TokenizerEscaping.php
class TokenizerEscaping (line 24) | class TokenizerEscaping
method __construct (line 26) | public function __construct(
method escapeUnicode (line 31) | public function escapeUnicode(string $value): string
method escapeUnicodeAndNewLine (line 38) | public function escapeUnicodeAndNewLine(string $value): string
method replaceUnicodeSequences (line 45) | private function replaceUnicodeSequences(string $value): string
FILE: Parser/Tokenizer/TokenizerPatterns.php
class TokenizerPatterns (line 24) | class TokenizerPatterns
method __construct (line 39) | public function __construct()
method getNewLineEscapePattern (line 55) | public function getNewLineEscapePattern(): string
method getSimpleEscapePattern (line 60) | public function getSimpleEscapePattern(): string
method getUnicodeEscapePattern (line 65) | public function getUnicodeEscapePattern(): string
method getIdentifierPattern (line 70) | public function getIdentifierPattern(): string
method getHashPattern (line 75) | public function getHashPattern(): string
method getNumberPattern (line 80) | public function getNumberPattern(): string
method getQuotedStringPattern (line 85) | public function getQuotedStringPattern(string $quote): string
FILE: Tests/CssSelectorConverterTest.php
class CssSelectorConverterTest (line 19) | class CssSelectorConverterTest extends TestCase
method testCssToXPath (line 21) | public function testCssToXPath()
method testCssToXPathXml (line 37) | public function testCssToXPathXml()
method testParseExceptions (line 48) | public function testParseExceptions()
method testLruCacheMovesRecentlyUsedToEnd (line 55) | public function testLruCacheMovesRecentlyUsedToEnd()
method testCssToXPathWithoutPrefix (line 86) | #[DataProvider('getCssToXPathWithoutPrefixTestData')]
method getCssToXPathWithoutPrefixTestData (line 94) | public static function getCssToXPathWithoutPrefixTestData(): array
FILE: Tests/Node/AbstractNodeTestCase.php
class AbstractNodeTestCase (line 18) | abstract class AbstractNodeTestCase extends TestCase
method testToStringConversion (line 20) | #[DataProvider('getToStringConversionTestData')]
method testSpecificityValue (line 26) | #[DataProvider('getSpecificityValueTestData')]
method getToStringConversionTestData (line 32) | abstract public static function getToStringConversionTestData();
method getSpecificityValueTestData (line 34) | abstract public static function getSpecificityValueTestData();
FILE: Tests/Node/AttributeNodeTest.php
class AttributeNodeTest (line 17) | class AttributeNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 19) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 28) | public static function getSpecificityValueTestData()
FILE: Tests/Node/ClassNodeTest.php
class ClassNodeTest (line 17) | class ClassNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 19) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 26) | public static function getSpecificityValueTestData()
FILE: Tests/Node/CombinedSelectorNodeTest.php
class CombinedSelectorNodeTest (line 17) | class CombinedSelectorNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 19) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 27) | public static function getSpecificityValueTestData()
FILE: Tests/Node/ElementNodeTest.php
class ElementNodeTest (line 16) | class ElementNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 18) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 27) | public static function getSpecificityValueTestData()
FILE: Tests/Node/FunctionNodeTest.php
class FunctionNodeTest (line 18) | class FunctionNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 20) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 34) | public static function getSpecificityValueTestData()
FILE: Tests/Node/HashNodeTest.php
class HashNodeTest (line 17) | class HashNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 19) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 26) | public static function getSpecificityValueTestData()
FILE: Tests/Node/MatchingNodeTest.php
class MatchingNodeTest (line 19) | class MatchingNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 21) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 31) | public static function getSpecificityValueTestData()
FILE: Tests/Node/NegationNodeTest.php
class NegationNodeTest (line 18) | class NegationNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 20) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 27) | public static function getSpecificityValueTestData()
FILE: Tests/Node/PseudoNodeTest.php
class PseudoNodeTest (line 17) | class PseudoNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 19) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 26) | public static function getSpecificityValueTestData()
FILE: Tests/Node/SelectorNodeTest.php
class SelectorNodeTest (line 17) | class SelectorNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 19) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 27) | public static function getSpecificityValueTestData()
FILE: Tests/Node/SpecificityAdjustmentNodeTest.php
class SpecificityAdjustmentNodeTest (line 19) | class SpecificityAdjustmentNodeTest extends AbstractNodeTestCase
method getToStringConversionTestData (line 21) | public static function getToStringConversionTestData()
method getSpecificityValueTestData (line 31) | public static function getSpecificityValueTestData()
FILE: Tests/Node/SpecificityTest.php
class SpecificityTest (line 18) | class SpecificityTest extends TestCase
method testValue (line 20) | #[DataProvider('getValueTestData')]
method testPlusValue (line 26) | #[DataProvider('getValueTestData')]
method getValueTestData (line 32) | public static function getValueTestData()
method testCompareTo (line 43) | #[DataProvider('getCompareTestData')]
method getCompareTestData (line 49) | public static function getCompareTestData()
FILE: Tests/Parser/Handler/AbstractHandlerTestCase.php
class AbstractHandlerTestCase (line 23) | abstract class AbstractHandlerTestCase extends TestCase
method testHandleValue (line 25) | #[DataProvider('getHandleValueTestData')]
method testDontHandleValue (line 36) | #[DataProvider('getDontHandleValueTestData')]
method getHandleValueTestData (line 47) | abstract public static function getHandleValueTestData();
method getDontHandleValueTestData (line 49) | abstract public static function getDontHandleValueTestData();
method generateHandler (line 51) | abstract protected function generateHandler();
method assertStreamEmpty (line 53) | protected function assertStreamEmpty(TokenStream $stream)
method assertRemainingContent (line 60) | protected function assertRemainingContent(Reader $reader, $remainingCo...
FILE: Tests/Parser/Handler/CommentHandlerTest.php
class CommentHandlerTest (line 20) | class CommentHandlerTest extends AbstractHandlerTestCase
method testHandleValue (line 22) | #[DataProvider('getHandleValueTestData')]
method getHandleValueTestData (line 34) | public static function getHandleValueTestData()
method getDontHandleValueTestData (line 43) | public static function getDontHandleValueTestData()
method generateHandler (line 52) | protected function generateHandler()
FILE: Tests/Parser/Handler/HashHandlerTest.php
class HashHandlerTest (line 19) | class HashHandlerTest extends AbstractHandlerTestCase
method getHandleValueTestData (line 21) | public static function getHandleValueTestData()
method getDontHandleValueTestData (line 32) | public static function getDontHandleValueTestData()
method generateHandler (line 43) | protected function generateHandler()
FILE: Tests/Parser/Handler/IdentifierHandlerTest.php
class IdentifierHandlerTest (line 19) | class IdentifierHandlerTest extends AbstractHandlerTestCase
method getHandleValueTestData (line 21) | public static function getHandleValueTestData()
method getDontHandleValueTestData (line 32) | public static function getDontHandleValueTestData()
method generateHandler (line 43) | protected function generateHandler()
FILE: Tests/Parser/Handler/NumberHandlerTest.php
class NumberHandlerTest (line 18) | class NumberHandlerTest extends AbstractHandlerTestCase
method getHandleValueTestData (line 20) | public static function getHandleValueTestData()
method getDontHandleValueTestData (line 33) | public static function getDontHandleValueTestData()
method generateHandler (line 44) | protected function generateHandler()
FILE: Tests/Parser/Handler/StringHandlerTest.php
class StringHandlerTest (line 19) | class StringHandlerTest extends AbstractHandlerTestCase
method getHandleValueTestData (line 21) | public static function getHandleValueTestData()
method getDontHandleValueTestData (line 34) | public static function getDontHandleValueTestData()
method generateHandler (line 44) | protected function generateHandler()
FILE: Tests/Parser/Handler/WhitespaceHandlerTest.php
class WhitespaceHandlerTest (line 17) | class WhitespaceHandlerTest extends AbstractHandlerTestCase
method getHandleValueTestData (line 19) | public static function getHandleValueTestData()
method getDontHandleValueTestData (line 31) | public static function getDontHandleValueTestData()
method generateHandler (line 40) | protected function generateHandler()
FILE: Tests/Parser/ParserTest.php
class ParserTest (line 22) | class ParserTest extends TestCase
method testParser (line 24) | #[DataProvider('getParserTestData')]
method testParserException (line 32) | #[DataProvider('getParserExceptionTestData')]
method testPseudoElements (line 45) | #[DataProvider('getPseudoElementsTestData')]
method testSpecificity (line 58) | #[DataProvider('getSpecificityTestData')]
method testParseSeries (line 70) | #[DataProvider('getParseSeriesTestData')]
method testParseSeriesException (line 82) | #[DataProvider('getParseSeriesExceptionTestData')]
method getParserTestData (line 95) | public static function getParserTestData()
method getParserExceptionTestData (line 164) | public static function getParserExceptionTestData()
method getPseudoElementsTestData (line 196) | public static function getPseudoElementsTestData()
method getSpecificityTestData (line 218) | public static function getSpecificityTestData()
method getParseSeriesTestData (line 258) | public static function getParseSeriesTestData()
method getParseSeriesExceptionTestData (line 280) | public static function getParseSeriesExceptionTestData()
FILE: Tests/Parser/ReaderTest.php
class ReaderTest (line 17) | class ReaderTest extends TestCase
method testIsEOF (line 19) | public function testIsEOF()
method testGetRemainingLength (line 34) | public function testGetRemainingLength()
method testGetSubstring (line 46) | public function testGetSubstring()
method testGetOffset (line 57) | public function testGetOffset()
method testFindPattern (line 68) | public function testFindPattern()
method testMoveForward (line 80) | public function testMoveForward()
method testToEnd (line 89) | public function testToEnd()
method assignPosition (line 96) | private function assignPosition(Reader $reader, int $value)
FILE: Tests/Parser/Shortcut/ClassParserTest.php
class ClassParserTest (line 22) | class ClassParserTest extends TestCase
method testParse (line 24) | #[DataProvider('getParseTestData')]
method getParseTestData (line 36) | public static function getParseTestData()
FILE: Tests/Parser/Shortcut/ElementParserTest.php
class ElementParserTest (line 22) | class ElementParserTest extends TestCase
method testParse (line 24) | #[DataProvider('getParseTestData')]
method getParseTestData (line 36) | public static function getParseTestData()
FILE: Tests/Parser/Shortcut/EmptyStringParserTest.php
class EmptyStringParserTest (line 21) | class EmptyStringParserTest extends TestCase
method testParse (line 23) | public function testParse()
FILE: Tests/Parser/Shortcut/HashParserTest.php
class HashParserTest (line 22) | class HashParserTest extends TestCase
method testParse (line 24) | #[DataProvider('getParseTestData')]
method getParseTestData (line 36) | public static function getParseTestData()
FILE: Tests/Parser/TokenStreamTest.php
class TokenStreamTest (line 19) | class TokenStreamTest extends TestCase
method testGetNext (line 21) | public function testGetNext()
method testGetPeek (line 33) | public function testGetPeek()
method testGetNextIdentifier (line 47) | public function testGetNextIdentifier()
method testFailToGetNextIdentifier (line 55) | public function testFailToGetNextIdentifier()
method testGetNextIdentifierOrStar (line 65) | public function testGetNextIdentifierOrStar()
method testFailToGetNextIdentifierOrStar (line 76) | public function testFailToGetNextIdentifierOrStar()
method testSkipWhitespace (line 86) | public function testSkipWhitespace()
FILE: Tests/XPath/TranslatorTest.php
class TranslatorTest (line 25) | class TranslatorTest extends TestCase
method testXpathLiteral (line 27) | #[DataProvider('getXpathLiteralTestData')]
method testCssToXPath (line 33) | #[DataProvider('getCssToXPathTestData')]
method testHasUnsupportedSelector (line 41) | #[DataProvider('getUnsupportedHasSelectorTestData')]
method getUnsupportedHasSelectorTestData (line 52) | public static function getUnsupportedHasSelectorTestData(): iterable
method testCssToXPathPseudoElement (line 61) | public function testCssToXPathPseudoElement()
method testGetExtensionNotExistsExtension (line 71) | public function testGetExtensionNotExistsExtension()
method testAddCombinationNotExistsExtension (line 81) | public function testAddCombinationNotExistsExtension()
method testAddFunctionNotExistsFunction (line 94) | public function testAddFunctionNotExistsFunction()
method testAddPseudoClassNotExistsClass (line 106) | public function testAddPseudoClassNotExistsClass()
method testAddAttributeMatchingClassNotExistsClass (line 117) | public function testAddAttributeMatchingClassNotExistsClass()
method testXmlLang (line 128) | #[DataProvider('getXmlLangTestData')]
method testHtmlIds (line 140) | #[DataProvider('getHtmlIdsTestData')]
method testHtmlShakespear (line 161) | #[DataProvider('getHtmlShakespearTestData')]
method testOnlyOfTypeFindsSingleChildrenOfGivenType (line 175) | public function testOnlyOfTypeFindsSingleChildrenOfGivenType()
method getXpathLiteralTestData (line 202) | public static function getXpathLiteralTestData()
method getCssToXPathTestData (line 212) | public static function getCssToXPathTestData()
method getXmlLangTestData (line 268) | public static function getXmlLangTestData()
method getHtmlIdsTestData (line 283) | public static function getHtmlIdsTestData()
method getHtmlShakespearTestData (line 419) | public static function getHtmlShakespearTestData()
FILE: XPath/Extension/AbstractExtension.php
class AbstractExtension (line 24) | abstract class AbstractExtension implements ExtensionInterface
method getNodeTranslators (line 26) | public function getNodeTranslators(): array
method getCombinationTranslators (line 31) | public function getCombinationTranslators(): array
method getFunctionTranslators (line 36) | public function getFunctionTranslators(): array
method getPseudoClassTranslators (line 41) | public function getPseudoClassTranslators(): array
method getAttributeMatchingTranslators (line 46) | public function getAttributeMatchingTranslators(): array
method getRelativeCombinationTranslators (line 51) | public function getRelativeCombinationTranslators(): array
FILE: XPath/Extension/AttributeMatchingExtension.php
class AttributeMatchingExtension (line 27) | class AttributeMatchingExtension extends AbstractExtension
method getAttributeMatchingTranslators (line 29) | public function getAttributeMatchingTranslators(): array
method translateExists (line 43) | public function translateExists(XPathExpr $xpath, string $attribute, ?...
method translateEquals (line 48) | public function translateEquals(XPathExpr $xpath, string $attribute, ?...
method translateIncludes (line 53) | public function translateIncludes(XPathExpr $xpath, string $attribute,...
method translateDashMatch (line 62) | public function translateDashMatch(XPathExpr $xpath, string $attribute...
method translatePrefixMatch (line 72) | public function translatePrefixMatch(XPathExpr $xpath, string $attribu...
method translateSuffixMatch (line 81) | public function translateSuffixMatch(XPathExpr $xpath, string $attribu...
method translateSubstringMatch (line 91) | public function translateSubstringMatch(XPathExpr $xpath, string $attr...
method translateDifferent (line 100) | public function translateDifferent(XPathExpr $xpath, string $attribute...
method getName (line 109) | public function getName(): string
FILE: XPath/Extension/CombinationExtension.php
class CombinationExtension (line 26) | class CombinationExtension extends AbstractExtension
method getCombinationTranslators (line 28) | public function getCombinationTranslators(): array
method translateDescendant (line 38) | public function translateDescendant(XPathExpr $xpath, XPathExpr $combi...
method translateChild (line 43) | public function translateChild(XPathExpr $xpath, XPathExpr $combinedXp...
method translateDirectAdjacent (line 48) | public function translateDirectAdjacent(XPathExpr $xpath, XPathExpr $c...
method translateIndirectAdjacent (line 56) | public function translateIndirectAdjacent(XPathExpr $xpath, XPathExpr ...
method getName (line 61) | public function getName(): string
FILE: XPath/Extension/ExtensionInterface.php
type ExtensionInterface (line 26) | interface ExtensionInterface
method getNodeTranslators (line 35) | public function getNodeTranslators(): array;
method getCombinationTranslators (line 42) | public function getCombinationTranslators(): array;
method getFunctionTranslators (line 49) | public function getFunctionTranslators(): array;
method getPseudoClassTranslators (line 56) | public function getPseudoClassTranslators(): array;
method getAttributeMatchingTranslators (line 63) | public function getAttributeMatchingTranslators(): array;
method getRelativeCombinationTranslators (line 70) | public function getRelativeCombinationTranslators(): array;
method getName (line 75) | public function getName(): string;
FILE: XPath/Extension/FunctionExtension.php
class FunctionExtension (line 31) | class FunctionExtension extends AbstractExtension
method getFunctionTranslators (line 33) | public function getFunctionTranslators(): array
method translateNthChild (line 48) | public function translateNthChild(XPathExpr $xpath, FunctionNode $func...
method translateNthLastChild (line 103) | public function translateNthLastChild(XPathExpr $xpath, FunctionNode $...
method translateNthOfType (line 108) | public function translateNthOfType(XPathExpr $xpath, FunctionNode $fun...
method translateNthLastOfType (line 116) | public function translateNthLastOfType(XPathExpr $xpath, FunctionNode ...
method translateContains (line 128) | public function translateContains(XPathExpr $xpath, FunctionNode $func...
method translateLang (line 146) | public function translateLang(XPathExpr $xpath, FunctionNode $function...
method getName (line 161) | public function getName(): string
FILE: XPath/Extension/HtmlExtension.php
class HtmlExtension (line 29) | class HtmlExtension extends AbstractExtension
method __construct (line 31) | public function __construct(Translator $translator)
method getPseudoClassTranslators (line 39) | public function getPseudoClassTranslators(): array
method getFunctionTranslators (line 53) | public function getFunctionTranslators(): array
method translateChecked (line 60) | public function translateChecked(XPathExpr $xpath): XPathExpr
method translateLink (line 69) | public function translateLink(XPathExpr $xpath): XPathExpr
method translateDisabled (line 74) | public function translateDisabled(XPathExpr $xpath): XPathExpr
method translateEnabled (line 100) | public function translateEnabled(XPathExpr $xpath): XPathExpr
method translateLang (line 136) | public function translateLang(XPathExpr $xpath, FunctionNode $function...
method translateSelected (line 154) | public function translateSelected(XPathExpr $xpath): XPathExpr
method translateInvalid (line 159) | public function translateInvalid(XPathExpr $xpath): XPathExpr
method translateHover (line 164) | public function translateHover(XPathExpr $xpath): XPathExpr
method translateVisited (line 169) | public function translateVisited(XPathExpr $xpath): XPathExpr
method getName (line 174) | public function getName(): string
FILE: XPath/Extension/NodeExtension.php
class NodeExtension (line 28) | class NodeExtension extends AbstractExtension
method __construct (line 34) | public function __construct(
method setFlag (line 42) | public function setFlag(int $flag, bool $on): static
method hasFlag (line 55) | public function hasFlag(int $flag): bool
method getNodeTranslators (line 60) | public function getNodeTranslators(): array
method translateSelector (line 78) | public function translateSelector(Node\SelectorNode $node, Translator ...
method translateCombinedSelector (line 83) | public function translateCombinedSelector(Node\CombinedSelectorNode $n...
method translateNegation (line 88) | public function translateNegation(Node\NegationNode $node, Translator ...
method translateMatching (line 101) | public function translateMatching(Node\MatchingNode $node, Translator ...
method translateSpecificityAdjustment (line 116) | public function translateSpecificityAdjustment(Node\SpecificityAdjustm...
method translateFunction (line 131) | public function translateFunction(Node\FunctionNode $node, Translator ...
method translatePseudo (line 138) | public function translatePseudo(Node\PseudoNode $node, Translator $tra...
method translateAttribute (line 145) | public function translateAttribute(Node\AttributeNode $node, Translato...
method translateClass (line 170) | public function translateClass(Node\ClassNode $node, Translator $trans...
method translateHash (line 177) | public function translateHash(Node\HashNode $node, Translator $transla...
method translateElement (line 184) | public function translateElement(Node\ElementNode $node): XPathExpr
method translateRelation (line 213) | public function translateRelation(Node\RelationNode $node, Translator ...
method getName (line 220) | public function getName(): string
method isSafeName (line 225) | private function isSafeName(string $name): bool
FILE: XPath/Extension/PseudoClassExtension.php
class PseudoClassExtension (line 27) | class PseudoClassExtension extends AbstractExtension
method getPseudoClassTranslators (line 29) | public function getPseudoClassTranslators(): array
method translateRoot (line 44) | public function translateRoot(XPathExpr $xpath): XPathExpr
method translateScopePseudo (line 49) | public function translateScopePseudo(XPathExpr $xpath): XPathExpr
method translateFirstChild (line 54) | public function translateFirstChild(XPathExpr $xpath): XPathExpr
method translateLastChild (line 62) | public function translateLastChild(XPathExpr $xpath): XPathExpr
method translateFirstOfType (line 73) | public function translateFirstOfType(XPathExpr $xpath): XPathExpr
method translateLastOfType (line 87) | public function translateLastOfType(XPathExpr $xpath): XPathExpr
method translateOnlyChild (line 98) | public function translateOnlyChild(XPathExpr $xpath): XPathExpr
method translateOnlyOfType (line 106) | public function translateOnlyOfType(XPathExpr $xpath): XPathExpr
method translateEmpty (line 113) | public function translateEmpty(XPathExpr $xpath): XPathExpr
method getName (line 118) | public function getName(): string
FILE: XPath/Extension/RelationExtension.php
class RelationExtension (line 26) | class RelationExtension extends AbstractExtension
method getRelativeCombinationTranslators (line 28) | public function getRelativeCombinationTranslators(): array
method translateRelationDescendant (line 38) | public function translateRelationDescendant(XPathExpr $xpath, XPathExp...
method translateRelationChild (line 43) | public function translateRelationChild(XPathExpr $xpath, XPathExpr $co...
method translateRelationDirectAdjacent (line 48) | public function translateRelationDirectAdjacent(XPathExpr $xpath, XPat...
method translateRelationIndirectAdjacent (line 58) | public function translateRelationIndirectAdjacent(XPathExpr $xpath, XP...
method getName (line 63) | public function getName(): string
FILE: XPath/Translator.php
class Translator (line 31) | class Translator implements TranslatorInterface
method __construct (line 52) | public function __construct(?ParserInterface $parser = null)
method getXpathLiteral (line 66) | public static function getXpathLiteral(string $element): string
method cssToXPath (line 92) | public function cssToXPath(string $cssExpr, string $prefix = 'descenda...
method selectorToXPath (line 107) | public function selectorToXPath(SelectorNode $selector, string $prefix...
method registerExtension (line 115) | public function registerExtension(Extension\ExtensionInterface $extens...
method getExtension (line 132) | public function getExtension(string $name): Extension\ExtensionInterface
method registerParserShortcut (line 144) | public function registerParserShortcut(ParserInterface $shortcut): static
method nodeToXPath (line 154) | public function nodeToXPath(NodeInterface $node): XPathExpr
method addCombination (line 166) | public function addCombination(string $combiner, NodeInterface $xpath,...
method addRelativeCombination (line 178) | public function addRelativeCombination(string $combiner, NodeInterface...
method addFunction (line 190) | public function addFunction(XPathExpr $xpath, FunctionNode $function):...
method addPseudoClass (line 202) | public function addPseudoClass(XPathExpr $xpath, string $pseudoClass):...
method addAttributeMatching (line 214) | public function addAttributeMatching(XPathExpr $xpath, string $operato...
method parseSelectors (line 226) | private function parseSelectors(string $css): array
FILE: XPath/TranslatorInterface.php
type TranslatorInterface (line 26) | interface TranslatorInterface
method cssToXPath (line 31) | public function cssToXPath(string $cssExpr, string $prefix = 'descenda...
method selectorToXPath (line 36) | public function selectorToXPath(SelectorNode $selector, string $prefix...
FILE: XPath/XPathExpr.php
class XPathExpr (line 24) | class XPathExpr
method __construct (line 26) | public function __construct(
method getElement (line 37) | public function getElement(): string
method addCondition (line 45) | public function addCondition(string $condition, string $operator = 'an...
method getCondition (line 52) | public function getCondition(): string
method addNameTest (line 60) | public function addNameTest(): static
method addStarPrefix (line 73) | public function addStarPrefix(): static
method join (line 85) | public function join(string $combiner, self $expr, ?string $closingCom...
method __toString (line 111) | public function __toString(): string
Condensed preview — 93 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (234K chars).
[
{
"path": ".gitattributes",
"chars": 74,
"preview": "/Tests export-ignore\n/phpunit.xml.dist export-ignore\n/.git* export-ignore\n"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 278,
"preview": "Please do not submit any Pull Requests here. They will be closed.\n---\n\nPlease submit your PR here instead:\nhttps://githu"
},
{
"path": ".github/workflows/close-pull-request.yml",
"chars": 544,
"preview": "name: Close Pull Request\n\non:\n pull_request_target:\n types: [opened]\n\njobs:\n run:\n runs-on: ubuntu-latest\n st"
},
{
"path": ".gitignore",
"chars": 34,
"preview": "vendor/\ncomposer.lock\nphpunit.xml\n"
},
{
"path": "CHANGELOG.md",
"chars": 393,
"preview": "CHANGELOG\n=========\n\n8.1\n---\n\n * Add support for `:has()`\n\n7.1\n---\n\n * Add support for `:is()`\n * Add support for `:wher"
},
{
"path": "CssSelectorConverter.php",
"chars": 2633,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/ExceptionInterface.php",
"chars": 594,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/ExpressionErrorException.php",
"chars": 640,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/InternalErrorException.php",
"chars": 638,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/ParseException.php",
"chars": 638,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Exception/SyntaxErrorException.php",
"chars": 1736,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "Copyright (c) 2004-present Fabien Potencier\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "Node/AbstractNode.php",
"chars": 792,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/AttributeNode.php",
"chars": 1873,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/ClassNode.php",
"chars": 1187,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/CombinedSelectorNode.php",
"chars": 1469,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/ElementNode.php",
"chars": 1260,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/FunctionNode.php",
"chars": 1691,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/HashNode.php",
"chars": 1176,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/MatchingNode.php",
"chars": 1502,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/NegationNode.php",
"chars": 1260,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/NodeInterface.php",
"chars": 691,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/PseudoNode.php",
"chars": 1296,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/RelationNode.php",
"chars": 1377,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/SelectorNode.php",
"chars": 1400,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/Specificity.php",
"chars": 1736,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Node/SpecificityAdjustmentNode.php",
"chars": 1253,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Handler/CommentHandler.php",
"chars": 1100,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Handler/HandlerInterface.php",
"chars": 775,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Handler/HashHandler.php",
"chars": 1480,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Handler/IdentifierHandler.php",
"chars": 1498,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Handler/NumberHandler.php",
"chars": 1314,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Handler/StringHandler.php",
"chars": 2322,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Handler/WhitespaceHandler.php",
"chars": 1153,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Parser.php",
"chars": 14967,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/ParserInterface.php",
"chars": 808,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Reader.php",
"chars": 1847,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Shortcut/ClassParser.php",
"chars": 1558,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Shortcut/ElementParser.php",
"chars": 1350,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Shortcut/EmptyStringParser.php",
"chars": 1192,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Shortcut/HashParser.php",
"chars": 1550,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Token.php",
"chars": 2506,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/TokenStream.php",
"chars": 3315,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Tokenizer/Tokenizer.php",
"chars": 2039,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Tokenizer/TokenizerEscaping.php",
"chars": 1740,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Parser/Tokenizer/TokenizerPatterns.php",
"chars": 2862,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "README.md",
"chars": 688,
"preview": "CssSelector Component\n=====================\n\nThe CssSelector component converts CSS selectors to XPath expressions.\n\nRes"
},
{
"path": "Tests/CssSelectorConverterTest.php",
"chars": 4652,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/AbstractNodeTestCase.php",
"chars": 1036,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/AttributeNodeTest.php",
"chars": 1442,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/ClassNodeTest.php",
"chars": 865,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/CombinedSelectorNodeTest.php",
"chars": 1234,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/ElementNodeTest.php",
"chars": 941,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/FunctionNodeTest.php",
"chars": 1656,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/HashNodeTest.php",
"chars": 847,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/MatchingNodeTest.php",
"chars": 1609,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/NegationNodeTest.php",
"chars": 945,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/PseudoNodeTest.php",
"chars": 797,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/SelectorNodeTest.php",
"chars": 936,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/SpecificityAdjustmentNodeTest.php",
"chars": 1480,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Node/SpecificityTest.php",
"chars": 2150,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Handler/AbstractHandlerTestCase.php",
"chars": 2277,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Handler/CommentHandlerTest.php",
"chars": 1646,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Handler/HashHandlerTest.php",
"chars": 1357,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Handler/IdentifierHandlerTest.php",
"chars": 1495,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Handler/NumberHandlerTest.php",
"chars": 1406,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Handler/StringHandlerTest.php",
"chars": 1471,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Handler/WhitespaceHandlerTest.php",
"chars": 1171,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/ParserTest.php",
"chars": 15261,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/ReaderTest.php",
"chars": 2909,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Shortcut/ClassParserTest.php",
"chars": 1453,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Shortcut/ElementParserTest.php",
"chars": 1279,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Shortcut/EmptyStringParserTest.php",
"chars": 1022,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/Shortcut/HashParserTest.php",
"chars": 1415,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/Parser/TokenStreamTest.php",
"chars": 3186,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "Tests/XPath/Fixtures/ids.html",
"chars": 1660,
"preview": "<html id=\"html\"><head>\n <link id=\"link-href\" href=\"foo\" />\n <link id=\"link-nohref\" />\n</head><body>\n<div id=\"outer-div"
},
{
"path": "Tests/XPath/Fixtures/lang.xml",
"chars": 317,
"preview": "<test>\n <a id=\"first\" xml:lang=\"en\">a</a>\n <b id=\"second\" xml:lang=\"en-US\">b</b>\n <c id=\"third\" xml:lang=\"en-Nz\">c</c"
},
{
"path": "Tests/XPath/Fixtures/shakespear.html",
"chars": 14980,
"preview": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n\t\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<html xml"
},
{
"path": "Tests/XPath/TranslatorTest.php",
"chars": 22059,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/Extension/AbstractExtension.php",
"chars": 1174,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/Extension/AttributeMatchingExtension.php",
"chars": 3762,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/Extension/CombinationExtension.php",
"chars": 1870,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/Extension/ExtensionInterface.php",
"chars": 1819,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/Extension/FunctionExtension.php",
"chars": 5164,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/Extension/HtmlExtension.php",
"chars": 5808,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/Extension/NodeExtension.php",
"chars": 7221,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/Extension/PseudoClassExtension.php",
"chars": 3529,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/Extension/RelationExtension.php",
"chars": 1994,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/Translator.php",
"chars": 8050,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/TranslatorInterface.php",
"chars": 1018,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "XPath/XPathExpr.php",
"chars": 2813,
"preview": "<?php\n\n/*\n * This file is part of the Symfony package.\n *\n * (c) Fabien Potencier <fabien@symfony.com>\n *\n * For the ful"
},
{
"path": "composer.json",
"chars": 810,
"preview": "{\n \"name\": \"symfony/css-selector\",\n \"type\": \"library\",\n \"description\": \"Converts CSS selectors to XPath express"
},
{
"path": "phpunit.xml.dist",
"chars": 1046,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:noNa"
}
]
About this extraction
This page contains the full source code of the symfony/css-selector GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 93 files (213.2 KB), approximately 56.9k tokens, and a symbol index with 451 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.