merged branch jfsimon/css-selector-rewriting (PR #7463)
This PR was merged into the master branch.
Discussion
----------
[CssSelector] fully rewritted component
The `CssSelector` component is a port of the Python https://github.com/SimonSapin/cssselect library. Previous implementation was a port of the `v0.1` tag, this implementation is a port of the `v0.7.1` tag. As Python and PHP have different philosophies, this is not a simple language-to-language translation, I needed to re-architecture the lib.
**Note about BC:** This new version introduces some changes making fail legacy tests.
New XPath should be equivalents, these changes are:
- When having a condition on an class, legacy condition is prefixed with a test of class existence. Example: `[contains(@class, 'foo')]` is transformed to `[@class and contains(@class, 'foo')]`.
- When having conditions on descendants, `/descendant::*` is transformed to `/descendant-or-self::*/*`.
I updated legacy tests (stored in `CssSelectorTest` class) accordingly.
| Q | A
| ------------- | ---
| Bug fix? | yes
| New feature? | yes
| BC breaks? | see above
| Deprecations? | no
| Tests pass? | yes
Should fix #3615 and #4271
Commits
-------
c6f87d0
[CssSelector] fully rewritted component
This commit is contained in:
commit
d855650577
@ -11,7 +11,13 @@
|
||||
|
||||
namespace Symfony\Component\CssSelector;
|
||||
|
||||
use Symfony\Component\CssSelector\Exception\ParseException;
|
||||
use Symfony\Component\CssSelector\Exception;
|
||||
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;
|
||||
|
||||
/**
|
||||
* CssSelector is the main entry point of the component and can convert CSS
|
||||
@ -19,8 +25,8 @@ use Symfony\Component\CssSelector\Exception\ParseException;
|
||||
*
|
||||
* $xpath = CssSelector::toXpath('h1.foo');
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
@ -33,290 +39,29 @@ class CssSelector
|
||||
* Optionally, a prefix can be added to the resulting XPath
|
||||
* expression with the $prefix parameter.
|
||||
*
|
||||
* @param mixed $cssExpr The CSS expression.
|
||||
* @param string $prefix An optional prefix for the XPath expression.
|
||||
* @param mixed $cssExpr The CSS expression.
|
||||
* @param string $prefix An optional prefix for the XPath expression.
|
||||
* @param boolean $html Enables HTML extension.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws ParseException When got None for xpath expression
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function toXPath($cssExpr, $prefix = 'descendant-or-self::')
|
||||
public static function toXPath($cssExpr, $prefix = 'descendant-or-self::', $html = true)
|
||||
{
|
||||
if (is_string($cssExpr)) {
|
||||
if (!$cssExpr) {
|
||||
return $prefix.'*';
|
||||
}
|
||||
$translator = new Translator();
|
||||
|
||||
if (preg_match('#^\w+\s*$#u', $cssExpr, $match)) {
|
||||
return $prefix.trim($match[0]);
|
||||
}
|
||||
|
||||
if (preg_match('~^(\w*)#(\w+)\s*$~u', $cssExpr, $match)) {
|
||||
return sprintf("%s%s[@id = '%s']", $prefix, $match[1] ? $match[1] : '*', $match[2]);
|
||||
}
|
||||
|
||||
if (preg_match('#^(\w*)\.(\w+)\s*$#u', $cssExpr, $match)) {
|
||||
return sprintf("%s%s[contains(concat(' ', normalize-space(@class), ' '), ' %s ')]", $prefix, $match[1] ? $match[1] : '*', $match[2]);
|
||||
}
|
||||
|
||||
$parser = new self();
|
||||
$cssExpr = $parser->parse($cssExpr);
|
||||
if ($html) {
|
||||
$translator->registerExtension(new HtmlExtension($translator));
|
||||
}
|
||||
|
||||
$expr = $cssExpr->toXpath();
|
||||
$translator
|
||||
->registerParserShortcut(new EmptyStringParser())
|
||||
->registerParserShortcut(new ElementParser())
|
||||
->registerParserShortcut(new ClassParser())
|
||||
->registerParserShortcut(new HashParser())
|
||||
;
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if (!$expr) {
|
||||
throw new ParseException(sprintf('Got None for xpath expression from %s.', $cssExpr));
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if ($prefix) {
|
||||
$expr->addPrefix($prefix);
|
||||
}
|
||||
|
||||
return (string) $expr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an expression and returns the Node object that represents
|
||||
* the parsed expression.
|
||||
*
|
||||
* @param string $string The expression to parse
|
||||
*
|
||||
* @return Node\NodeInterface
|
||||
*
|
||||
* @throws \Exception When tokenizer throws it while parsing
|
||||
*/
|
||||
public function parse($string)
|
||||
{
|
||||
$tokenizer = new Tokenizer();
|
||||
|
||||
$stream = new TokenStream($tokenizer->tokenize($string), $string);
|
||||
|
||||
try {
|
||||
return $this->parseSelectorGroup($stream);
|
||||
} catch (\Exception $e) {
|
||||
$class = get_class($e);
|
||||
|
||||
throw new $class(sprintf('%s at %s -> %s', $e->getMessage(), implode($stream->getUsed(), ''), $stream->peek()), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a selector group contained in $stream and returns
|
||||
* the Node object that represents the expression.
|
||||
*
|
||||
* @param TokenStream $stream The stream to parse.
|
||||
*
|
||||
* @return Node\NodeInterface
|
||||
*/
|
||||
private function parseSelectorGroup($stream)
|
||||
{
|
||||
$result = array();
|
||||
while (true) {
|
||||
$result[] = $this->parseSelector($stream);
|
||||
if ($stream->peek() == ',') {
|
||||
$stream->next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($result) == 1) {
|
||||
return $result[0];
|
||||
}
|
||||
|
||||
return new Node\OrNode($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a selector contained in $stream and returns the Node
|
||||
* object that represents it.
|
||||
*
|
||||
* @param TokenStream $stream The stream containing the selector.
|
||||
*
|
||||
* @return Node\NodeInterface
|
||||
*
|
||||
* @throws ParseException When expected selector but got something else
|
||||
*/
|
||||
private function parseSelector($stream)
|
||||
{
|
||||
$result = $this->parseSimpleSelector($stream);
|
||||
|
||||
while (true) {
|
||||
$peek = $stream->peek();
|
||||
if (',' == $peek || null === $peek) {
|
||||
return $result;
|
||||
} elseif (in_array($peek, array('+', '>', '~'))) {
|
||||
// A combinator
|
||||
$combinator = (string) $stream->next();
|
||||
|
||||
// Ignore optional whitespace after a combinator
|
||||
while (' ' == $stream->peek()) {
|
||||
$stream->next();
|
||||
}
|
||||
} else {
|
||||
$combinator = ' ';
|
||||
}
|
||||
$consumed = count($stream->getUsed());
|
||||
$nextSelector = $this->parseSimpleSelector($stream);
|
||||
if ($consumed == count($stream->getUsed())) {
|
||||
throw new ParseException(sprintf("Expected selector, got '%s'", $stream->peek()));
|
||||
}
|
||||
|
||||
$result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a simple selector (the current token) from $stream and returns
|
||||
* the resulting Node object.
|
||||
*
|
||||
* @param TokenStream $stream The stream containing the selector.
|
||||
*
|
||||
* @return Node\NodeInterface
|
||||
*
|
||||
* @throws ParseException When expected symbol but got something else
|
||||
*/
|
||||
private function parseSimpleSelector($stream)
|
||||
{
|
||||
$peek = $stream->peek();
|
||||
if ('*' != $peek && !$peek->isType('Symbol')) {
|
||||
$element = $namespace = '*';
|
||||
} else {
|
||||
$next = $stream->next();
|
||||
if ('*' != $next && !$next->isType('Symbol')) {
|
||||
throw new ParseException(sprintf("Expected symbol, got '%s'", $next));
|
||||
}
|
||||
|
||||
if ($stream->peek() == '|') {
|
||||
$namespace = $next;
|
||||
$stream->next();
|
||||
$element = $stream->next();
|
||||
if ('*' != $element && !$next->isType('Symbol')) {
|
||||
throw new ParseException(sprintf("Expected symbol, got '%s'", $next));
|
||||
}
|
||||
} else {
|
||||
$namespace = '*';
|
||||
$element = $next;
|
||||
}
|
||||
}
|
||||
|
||||
$result = new Node\ElementNode($namespace, $element);
|
||||
$hasHash = false;
|
||||
while (true) {
|
||||
$peek = $stream->peek();
|
||||
if ('#' == $peek) {
|
||||
if ($hasHash) {
|
||||
/* You can't have two hashes
|
||||
(FIXME: is there some more general rule I'm missing?) */
|
||||
// @codeCoverageIgnoreStart
|
||||
break;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
$stream->next();
|
||||
$result = new Node\HashNode($result, $stream->next());
|
||||
$hasHash = true;
|
||||
|
||||
continue;
|
||||
} elseif ('.' == $peek) {
|
||||
$stream->next();
|
||||
$result = new Node\ClassNode($result, $stream->next());
|
||||
|
||||
continue;
|
||||
} elseif ('[' == $peek) {
|
||||
$stream->next();
|
||||
$result = $this->parseAttrib($result, $stream);
|
||||
$next = $stream->next();
|
||||
if (']' != $next) {
|
||||
throw new ParseException(sprintf("] expected, got '%s'", $next));
|
||||
}
|
||||
|
||||
continue;
|
||||
} elseif (':' == $peek || '::' == $peek) {
|
||||
$type = $stream->next();
|
||||
$ident = $stream->next();
|
||||
if (!$ident || !$ident->isType('Symbol')) {
|
||||
throw new ParseException(sprintf("Expected symbol, got '%s'", $ident));
|
||||
}
|
||||
|
||||
if ($stream->peek() == '(') {
|
||||
$stream->next();
|
||||
$peek = $stream->peek();
|
||||
if ($peek->isType('String')) {
|
||||
$selector = $stream->next();
|
||||
} elseif ($peek->isType('Symbol') && is_int($peek)) {
|
||||
$selector = intval($stream->next());
|
||||
} else {
|
||||
// FIXME: parseSimpleSelector, or selector, or...?
|
||||
$selector = $this->parseSimpleSelector($stream);
|
||||
}
|
||||
$next = $stream->next();
|
||||
if (')' != $next) {
|
||||
throw new ParseException(sprintf("Expected ')', got '%s' and '%s'", $next, $selector));
|
||||
}
|
||||
|
||||
$result = new Node\FunctionNode($result, $type, $ident, $selector);
|
||||
} else {
|
||||
$result = new Node\PseudoNode($result, $type, $ident);
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
if (' ' == $peek) {
|
||||
$stream->next();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// FIXME: not sure what "negation" is
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an attribute from a selector contained in $stream and returns
|
||||
* the resulting AttribNode object.
|
||||
*
|
||||
* @param Node\NodeInterface $selector The selector object whose attribute
|
||||
* is to be parsed.
|
||||
* @param TokenStream $stream The container token stream.
|
||||
*
|
||||
* @return Node\AttribNode
|
||||
*
|
||||
* @throws ParseException When encountered unexpected selector
|
||||
*/
|
||||
private function parseAttrib($selector, $stream)
|
||||
{
|
||||
$attrib = $stream->next();
|
||||
if ($stream->peek() == '|') {
|
||||
$namespace = $attrib;
|
||||
$stream->next();
|
||||
$attrib = $stream->next();
|
||||
} else {
|
||||
$namespace = '*';
|
||||
}
|
||||
|
||||
if ($stream->peek() == ']') {
|
||||
return new Node\AttribNode($selector, $namespace, $attrib, 'exists', null);
|
||||
}
|
||||
|
||||
$op = $stream->next();
|
||||
if (!in_array($op, array('^=', '$=', '*=', '=', '~=', '|=', '!='))) {
|
||||
throw new ParseException(sprintf("Operator expected, got '%s'", $op));
|
||||
}
|
||||
|
||||
$value = $stream->next();
|
||||
if (!$value->isType('Symbol') && !$value->isType('String')) {
|
||||
throw new ParseException(sprintf("Expected string or symbol, got '%s'", $value));
|
||||
}
|
||||
|
||||
return new Node\AttribNode($selector, $namespace, $attrib, $op, $value);
|
||||
return $translator->cssToXPath($cssExpr, $prefix);
|
||||
}
|
||||
}
|
||||
|
62
src/Symfony/Component/CssSelector/CssSelectorTest.php
Normal file
62
src/Symfony/Component/CssSelector/CssSelectorTest.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?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;
|
||||
|
||||
class CssSelectorTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testCssToXPath()
|
||||
{
|
||||
$this->assertEquals('descendant-or-self::*', CssSelector::toXPath(''));
|
||||
$this->assertEquals('descendant-or-self::h1', CssSelector::toXPath('h1'));
|
||||
$this->assertEquals("descendant-or-self::h1[@id = 'foo']", CssSelector::toXPath('h1#foo'));
|
||||
$this->assertEquals("descendant-or-self::h1[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", CssSelector::toXPath('h1.foo'));
|
||||
$this->assertEquals('descendant-or-self::foo:h1', CssSelector::toXPath('foo|h1'));
|
||||
}
|
||||
|
||||
/** @dataProvider getCssToXPathWithoutPrefixTestData */
|
||||
public function testCssToXPathWithoutPrefix($css, $xpath)
|
||||
{
|
||||
$this->assertEquals($xpath, CssSelector::toXPath($css, ''), '->parse() parses an input string and returns a node');
|
||||
}
|
||||
|
||||
public function testParseExceptions()
|
||||
{
|
||||
try {
|
||||
CssSelector::toXPath('h1:');
|
||||
$this->fail('->parse() throws an Exception if the css selector is not valid');
|
||||
} catch (\Exception $e) {
|
||||
$this->assertInstanceOf('\Symfony\Component\CssSelector\Exception\ParseException', $e, '->parse() throws an Exception if the css selector is not valid');
|
||||
$this->assertEquals("Expected identifier, but <eof at 3> found.", $e->getMessage(), '->parse() throws an Exception if the css selector is not valid');
|
||||
}
|
||||
}
|
||||
|
||||
public function getCssToXPathWithoutPrefixTestData()
|
||||
{
|
||||
return array(
|
||||
array('h1', "h1"),
|
||||
array('foo|h1', "foo:h1"),
|
||||
array('h1, h2, h3', "h1 | h2 | h3"),
|
||||
array('h1:nth-child(3n+1)', "*/*[name() = 'h1' and ((position() -1) mod 3 = 0 and position() >= 1)]"),
|
||||
array('h1 > p', "h1/p"),
|
||||
array('h1#foo', "h1[@id = 'foo']"),
|
||||
array('h1.foo', "h1[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"),
|
||||
array('h1[class*="foo bar"]', "h1[@class and contains(@class, 'foo bar')]"),
|
||||
array('h1[foo|class*="foo bar"]', "h1[@foo:class and contains(@foo:class, 'foo bar')]"),
|
||||
array('h1[class]', "h1[@class]"),
|
||||
array('h1 .foo', "h1/descendant-or-self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"),
|
||||
array('h1 #foo', "h1/descendant-or-self::*/*[@id = 'foo']"),
|
||||
array('h1 [class*=foo]', "h1/descendant-or-self::*/*[@class and contains(@class, 'foo')]"),
|
||||
array('div>.foo', "div/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"),
|
||||
array('div > .foo', "div/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
interface ExceptionInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?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 cssselector 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 implements ExceptionInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?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 cssselector 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 implements ExceptionInterface
|
||||
{
|
||||
}
|
@ -14,11 +14,11 @@ namespace Symfony\Component\CssSelector\Exception;
|
||||
/**
|
||||
* ParseException is thrown when a CSS selector syntax is not valid.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class ParseException extends \Exception
|
||||
class ParseException extends \Exception implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class SyntaxErrorException extends ParseException implements ExceptionInterface
|
||||
{
|
||||
/**
|
||||
* @param string $expectedValue
|
||||
* @param Token $foundToken
|
||||
*
|
||||
* @return SyntaxErrorException
|
||||
*/
|
||||
public static function unexpectedToken($expectedValue, Token $foundToken)
|
||||
{
|
||||
return new self(sprintf('Expected %s, but %s found.', $expectedValue, $foundToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $pseudoElement
|
||||
* @param string $unexpectedLocation
|
||||
*
|
||||
* @return SyntaxErrorException
|
||||
*/
|
||||
public static function pseudoElementFound($pseudoElement, $unexpectedLocation)
|
||||
{
|
||||
return new self(sprintf('Unexpected pseudo-element "::%s" found %s.', $pseudoElement, $unexpectedLocation));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $position
|
||||
*
|
||||
* @return SyntaxErrorException
|
||||
*/
|
||||
public static function unclosedString($position)
|
||||
{
|
||||
return new self(sprintf('Unclosed/invalid string at %s.', $position));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SyntaxErrorException
|
||||
*/
|
||||
public static function nestedNot()
|
||||
{
|
||||
return new self('Got nested ::not().');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SyntaxErrorException
|
||||
*/
|
||||
public static function stringAsFunctionArgument()
|
||||
{
|
||||
return new self('String not allowed as function argument.');
|
||||
}
|
||||
}
|
40
src/Symfony/Component/CssSelector/Node/AbstractNode.php
Normal file
40
src/Symfony/Component/CssSelector/Node/AbstractNode.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
abstract class AbstractNode implements NodeInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $nodeName;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeName()
|
||||
{
|
||||
if (null === $this->nodeName) {
|
||||
$this->nodeName = preg_replace('~.*\\\\([^\\\\]+)Node$~', '$1', get_called_class());
|
||||
}
|
||||
|
||||
return $this->nodeName;
|
||||
}
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
<?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\XPathExpr;
|
||||
use Symfony\Component\CssSelector\Exception\ParseException;
|
||||
|
||||
/**
|
||||
* AttribNode represents a "selector[namespace|attrib operator value]" node.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class AttribNode implements NodeInterface
|
||||
{
|
||||
protected $selector;
|
||||
protected $namespace;
|
||||
protected $attrib;
|
||||
protected $operator;
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param NodeInterface $selector The XPath selector
|
||||
* @param string $namespace The namespace
|
||||
* @param string $attrib The attribute
|
||||
* @param string $operator The operator
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($selector, $namespace, $attrib, $operator, $value)
|
||||
{
|
||||
$this->selector = $selector;
|
||||
$this->namespace = $namespace;
|
||||
$this->attrib = $attrib;
|
||||
$this->operator = $operator;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
if ($this->operator == 'exists') {
|
||||
return sprintf('%s[%s[%s]]', __CLASS__, $this->selector, $this->formatAttrib());
|
||||
}
|
||||
|
||||
return sprintf('%s[%s[%s %s %s]]', __CLASS__, $this->selector, $this->formatAttrib(), $this->operator, $this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toXpath()
|
||||
{
|
||||
$path = $this->selector->toXpath();
|
||||
$attrib = $this->xpathAttrib();
|
||||
$value = $this->value;
|
||||
if ($this->operator == 'exists') {
|
||||
$path->addCondition($attrib);
|
||||
} elseif ($this->operator == '=') {
|
||||
$path->addCondition(sprintf('%s = %s', $attrib, XPathExpr::xpathLiteral($value)));
|
||||
} elseif ($this->operator == '!=') {
|
||||
// FIXME: this seems like a weird hack...
|
||||
if ($value) {
|
||||
$path->addCondition(sprintf('not(%s) or %s != %s', $attrib, $attrib, XPathExpr::xpathLiteral($value)));
|
||||
} else {
|
||||
$path->addCondition(sprintf('%s != %s', $attrib, XPathExpr::xpathLiteral($value)));
|
||||
}
|
||||
// path.addCondition('%s != %s' % (attrib, xpathLiteral(value)))
|
||||
} elseif ($this->operator == '~=') {
|
||||
$path->addCondition(sprintf("contains(concat(' ', normalize-space(%s), ' '), %s)", $attrib, XPathExpr::xpathLiteral(' '.$value.' ')));
|
||||
} elseif ($this->operator == '|=') {
|
||||
// Weird, but true...
|
||||
$path->addCondition(sprintf('%s = %s or starts-with(%s, %s)', $attrib, XPathExpr::xpathLiteral($value), $attrib, XPathExpr::xpathLiteral($value.'-')));
|
||||
} elseif ($this->operator == '^=') {
|
||||
$path->addCondition(sprintf('starts-with(%s, %s)', $attrib, XPathExpr::xpathLiteral($value)));
|
||||
} elseif ($this->operator == '$=') {
|
||||
// Oddly there is a starts-with in XPath 1.0, but not ends-with
|
||||
$path->addCondition(sprintf('substring(%s, string-length(%s)-%s) = %s', $attrib, $attrib, strlen($value) - 1, XPathExpr::xpathLiteral($value)));
|
||||
} elseif ($this->operator == '*=') {
|
||||
// FIXME: case sensitive?
|
||||
$path->addCondition(sprintf('contains(%s, %s)', $attrib, XPathExpr::xpathLiteral($value)));
|
||||
} else {
|
||||
throw new ParseException(sprintf('Unknown operator: %s', $this->operator));
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the XPath Attribute
|
||||
*
|
||||
* @return string The XPath attribute
|
||||
*/
|
||||
protected function xpathAttrib()
|
||||
{
|
||||
// FIXME: if attrib is *?
|
||||
if ($this->namespace == '*') {
|
||||
return '@'.$this->attrib;
|
||||
}
|
||||
|
||||
return sprintf('@%s:%s', $this->namespace, $this->attrib);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted attribute
|
||||
*
|
||||
* @return string The formatted attribute
|
||||
*/
|
||||
protected function formatAttrib()
|
||||
{
|
||||
if ($this->namespace == '*') {
|
||||
return $this->attrib;
|
||||
}
|
||||
|
||||
return sprintf('%s|%s', $this->namespace, $this->attrib);
|
||||
}
|
||||
}
|
124
src/Symfony/Component/CssSelector/Node/AttributeNode.php
Normal file
124
src/Symfony/Component/CssSelector/Node/AttributeNode.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class AttributeNode extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* @var NodeInterface
|
||||
*/
|
||||
private $selector;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $namespace;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $attribute;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $operator;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @param NodeInterface $selector
|
||||
* @param string $namespace
|
||||
* @param string $attribute
|
||||
* @param string $operator
|
||||
* @param string $value
|
||||
*/
|
||||
public function __construct(NodeInterface $selector, $namespace, $attribute, $operator, $value)
|
||||
{
|
||||
$this->selector = $selector;
|
||||
$this->namespace = $namespace;
|
||||
$this->attribute = $attribute;
|
||||
$this->operator = $operator;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NodeInterface
|
||||
*/
|
||||
public function getSelector()
|
||||
{
|
||||
return $this->selector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
return $this->namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAttribute()
|
||||
{
|
||||
return $this->attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOperator()
|
||||
{
|
||||
return $this->operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSpecificity()
|
||||
{
|
||||
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
@ -11,49 +11,65 @@
|
||||
|
||||
namespace Symfony\Component\CssSelector\Node;
|
||||
|
||||
use Symfony\Component\CssSelector\XPathExpr;
|
||||
|
||||
/**
|
||||
* ClassNode represents a "selector.className" node.
|
||||
* Represents a "<selector>.<name>" node.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class ClassNode implements NodeInterface
|
||||
class ClassNode extends AbstractNode
|
||||
{
|
||||
protected $selector;
|
||||
protected $className;
|
||||
/**
|
||||
* @var NodeInterface
|
||||
*/
|
||||
private $selector;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param NodeInterface $selector The XPath Selector
|
||||
* @param string $className The class name
|
||||
* @var string
|
||||
*/
|
||||
public function __construct($selector, $className)
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @param NodeInterface $selector
|
||||
* @param string $name
|
||||
*/
|
||||
public function __construct(NodeInterface $selector, $name)
|
||||
{
|
||||
$this->selector = $selector;
|
||||
$this->className = $className;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @return NodeInterface
|
||||
*/
|
||||
public function getSelector()
|
||||
{
|
||||
return $this->selector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSpecificity()
|
||||
{
|
||||
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('%s[%s.%s]', __CLASS__, $this->selector, $this->className);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toXpath()
|
||||
{
|
||||
$selXpath = $this->selector->toXpath();
|
||||
$selXpath->addCondition(sprintf("contains(concat(' ', normalize-space(@class), ' '), %s)", XPathExpr::xpathLiteral(' '.$this->className.' ')));
|
||||
|
||||
return $selXpath;
|
||||
return sprintf('%s[%s.%s]', $this->getNodeName(), $this->selector, $this->name);
|
||||
}
|
||||
}
|
||||
|
@ -11,132 +11,82 @@
|
||||
|
||||
namespace Symfony\Component\CssSelector\Node;
|
||||
|
||||
use Symfony\Component\CssSelector\Exception\ParseException;
|
||||
|
||||
/**
|
||||
* CombinedSelectorNode represents a combinator node.
|
||||
* Represents a combined node.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class CombinedSelectorNode implements NodeInterface
|
||||
class CombinedSelectorNode extends AbstractNode
|
||||
{
|
||||
protected static $methodMapping = array(
|
||||
' ' => 'descendant',
|
||||
'>' => 'child',
|
||||
'+' => 'direct_adjacent',
|
||||
'~' => 'indirect_adjacent',
|
||||
);
|
||||
|
||||
protected $selector;
|
||||
protected $combinator;
|
||||
protected $subselector;
|
||||
/**
|
||||
* @var NodeInterface
|
||||
*/
|
||||
private $selector;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param NodeInterface $selector The XPath selector
|
||||
* @param string $combinator The combinator
|
||||
* @param NodeInterface $subselector The sub XPath selector
|
||||
* @var string
|
||||
*/
|
||||
public function __construct($selector, $combinator, $subselector)
|
||||
private $combinator;
|
||||
|
||||
/**
|
||||
* @var NodeInterface
|
||||
*/
|
||||
private $subSelector;
|
||||
|
||||
/**
|
||||
* @param NodeInterface $selector
|
||||
* @param string $combinator
|
||||
* @param NodeInterface $subSelector
|
||||
*/
|
||||
public function __construct(NodeInterface $selector, $combinator, NodeInterface $subSelector)
|
||||
{
|
||||
$this->selector = $selector;
|
||||
$this->combinator = $combinator;
|
||||
$this->subselector = $subselector;
|
||||
$this->subSelector = $subSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @return NodeInterface
|
||||
*/
|
||||
public function getSelector()
|
||||
{
|
||||
return $this->selector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCombinator()
|
||||
{
|
||||
return $this->combinator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NodeInterface
|
||||
*/
|
||||
public function getSubSelector()
|
||||
{
|
||||
return $this->subSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSpecificity()
|
||||
{
|
||||
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$comb = $this->combinator == ' ' ? '<followed>' : $this->combinator;
|
||||
$combinator = ' ' === $this->combinator ? '<followed>' : $this->combinator;
|
||||
|
||||
return sprintf('%s[%s %s %s]', __CLASS__, $this->selector, $comb, $this->subselector);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @throws ParseException When unknown combinator is found
|
||||
*/
|
||||
public function toXpath()
|
||||
{
|
||||
if (!isset(self::$methodMapping[$this->combinator])) {
|
||||
throw new ParseException(sprintf('Unknown combinator: %s', $this->combinator));
|
||||
}
|
||||
|
||||
$method = '_xpath_'.self::$methodMapping[$this->combinator];
|
||||
$path = $this->selector->toXpath();
|
||||
|
||||
return $this->$method($path, $this->subselector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a NodeInterface into the XPath of this object.
|
||||
*
|
||||
* @param XPathExpr $xpath The XPath expression for this object
|
||||
* @param NodeInterface $sub The NodeInterface object to add
|
||||
*
|
||||
* @return XPathExpr An XPath instance
|
||||
*/
|
||||
protected function _xpath_descendant($xpath, $sub)
|
||||
{
|
||||
// when sub is a descendant in any way of xpath
|
||||
$xpath->join('/descendant::', $sub->toXpath());
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a NodeInterface as a child of this object.
|
||||
*
|
||||
* @param XPathExpr $xpath The parent XPath expression
|
||||
* @param NodeInterface $sub The NodeInterface object to add
|
||||
*
|
||||
* @return XPathExpr An XPath instance
|
||||
*/
|
||||
protected function _xpath_child($xpath, $sub)
|
||||
{
|
||||
// when sub is an immediate child of xpath
|
||||
$xpath->join('/', $sub->toXpath());
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins an XPath expression as an adjacent of another.
|
||||
*
|
||||
* @param XPathExpr $xpath The parent XPath expression
|
||||
* @param NodeInterface $sub The adjacent XPath expression
|
||||
*
|
||||
* @return XPathExpr An XPath instance
|
||||
*/
|
||||
protected function _xpath_direct_adjacent($xpath, $sub)
|
||||
{
|
||||
// when sub immediately follows xpath
|
||||
$xpath->join('/following-sibling::', $sub->toXpath());
|
||||
$xpath->addNameTest();
|
||||
$xpath->addCondition('position() = 1');
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins an XPath expression as an indirect adjacent of another.
|
||||
*
|
||||
* @param XPathExpr $xpath The parent XPath expression
|
||||
* @param NodeInterface $sub The indirect adjacent NodeInterface object
|
||||
*
|
||||
* @return XPathExpr An XPath instance
|
||||
*/
|
||||
protected function _xpath_indirect_adjacent($xpath, $sub)
|
||||
{
|
||||
// when sub comes somewhere after xpath as a sibling
|
||||
$xpath->join('/following-sibling::', $sub->toXpath());
|
||||
|
||||
return $xpath;
|
||||
return sprintf('%s[%s %s %s]', $this->getNodeName(), $this->selector, $combinator, $this->subSelector);
|
||||
}
|
||||
}
|
||||
|
@ -11,67 +11,67 @@
|
||||
|
||||
namespace Symfony\Component\CssSelector\Node;
|
||||
|
||||
use Symfony\Component\CssSelector\XPathExpr;
|
||||
|
||||
/**
|
||||
* ElementNode represents a "namespace|element" node.
|
||||
* Represents a "<namespace>|<element>" node.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class ElementNode implements NodeInterface
|
||||
class ElementNode extends AbstractNode
|
||||
{
|
||||
protected $namespace;
|
||||
protected $element;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $namespace;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $namespace Namespace
|
||||
* @param string $element Element
|
||||
* @var string|null
|
||||
*/
|
||||
public function __construct($namespace, $element)
|
||||
private $element;
|
||||
|
||||
/**
|
||||
* @param string|null $namespace
|
||||
* @param string|null $element
|
||||
*/
|
||||
public function __construct($namespace = null, $element = null)
|
||||
{
|
||||
$this->namespace = $namespace;
|
||||
$this->element = $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @return null|string
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
return $this->namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getElement()
|
||||
{
|
||||
return $this->element;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSpecificity()
|
||||
{
|
||||
return new Specificity(0, 0, $this->element ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('%s[%s]', __CLASS__, $this->formatElement());
|
||||
}
|
||||
$element = $this->element ?: '*';
|
||||
|
||||
/**
|
||||
* Formats the element into a string.
|
||||
*
|
||||
* @return string Element as an XPath string
|
||||
*/
|
||||
public function formatElement()
|
||||
{
|
||||
if ($this->namespace == '*') {
|
||||
return $this->element;
|
||||
}
|
||||
|
||||
return sprintf('%s|%s', $this->namespace, $this->element);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toXpath()
|
||||
{
|
||||
if ($this->namespace == '*') {
|
||||
$el = strtolower($this->element);
|
||||
} else {
|
||||
// FIXME: Should we lowercase here?
|
||||
$el = sprintf('%s:%s', $this->namespace, $this->element);
|
||||
}
|
||||
|
||||
return new XPathExpr(null, null, $el);
|
||||
return sprintf('%s[%s]', $this->getNodeName(), $this->namespace ? $this->namespace.'|'.$element : $element);
|
||||
}
|
||||
}
|
||||
|
@ -11,280 +11,86 @@
|
||||
|
||||
namespace Symfony\Component\CssSelector\Node;
|
||||
|
||||
use Symfony\Component\CssSelector\Exception\ParseException;
|
||||
use Symfony\Component\CssSelector\XPathExpr;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
|
||||
/**
|
||||
* FunctionNode represents a "selector:name(expr)" node.
|
||||
* Represents a "<selector>:<name>(<arguments>)" node.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class FunctionNode implements NodeInterface
|
||||
class FunctionNode extends AbstractNode
|
||||
{
|
||||
protected static $unsupported = array('target', 'lang', 'enabled', 'disabled');
|
||||
|
||||
protected $selector;
|
||||
protected $type;
|
||||
protected $name;
|
||||
protected $expr;
|
||||
/**
|
||||
* @var NodeInterface
|
||||
*/
|
||||
private $selector;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param NodeInterface $selector The XPath expression
|
||||
* @param string $type
|
||||
* @param string $name
|
||||
* @param XPathExpr $expr
|
||||
* @var string
|
||||
*/
|
||||
public function __construct($selector, $type, $name, $expr)
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var Token[]
|
||||
*/
|
||||
private $arguments;
|
||||
|
||||
/**
|
||||
* @param NodeInterface $selector
|
||||
* @param string $name
|
||||
* @param Token[] $arguments
|
||||
*/
|
||||
public function __construct(NodeInterface $selector, $name, array $arguments = array())
|
||||
{
|
||||
$this->selector = $selector;
|
||||
$this->type = $type;
|
||||
$this->name = $name;
|
||||
$this->expr = $expr;
|
||||
$this->name = strtolower($name);
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @return NodeInterface
|
||||
*/
|
||||
public function getSelector()
|
||||
{
|
||||
return $this->selector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Token[]
|
||||
*/
|
||||
public function getArguments()
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSpecificity()
|
||||
{
|
||||
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('%s[%s%s%s(%s)]', __CLASS__, $this->selector, $this->type, $this->name, $this->expr);
|
||||
}
|
||||
$arguments = implode(', ', array_map(function (Token $token) {
|
||||
return "'".$token->getValue()."'";
|
||||
}, $this->arguments));
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @throws ParseException When unsupported or unknown pseudo-class is found
|
||||
*/
|
||||
public function toXpath()
|
||||
{
|
||||
$selPath = $this->selector->toXpath();
|
||||
if (in_array($this->name, self::$unsupported)) {
|
||||
throw new ParseException(sprintf('The pseudo-class %s is not supported', $this->name));
|
||||
}
|
||||
$method = '_xpath_'.str_replace('-', '_', $this->name);
|
||||
if (!method_exists($this, $method)) {
|
||||
throw new ParseException(sprintf('The pseudo-class %s is unknown', $this->name));
|
||||
}
|
||||
|
||||
return $this->$method($selPath, $this->expr);
|
||||
}
|
||||
|
||||
/**
|
||||
* undocumented function
|
||||
*
|
||||
* @param XPathExpr $xpath
|
||||
* @param mixed $expr
|
||||
* @param Boolean $last
|
||||
* @param Boolean $addNameTest
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
protected function _xpath_nth_child($xpath, $expr, $last = false, $addNameTest = true)
|
||||
{
|
||||
list($a, $b) = $this->parseSeries($expr);
|
||||
if (!$a && !$b && !$last) {
|
||||
// a=0 means nothing is returned...
|
||||
$xpath->addCondition('false() and position() = 0');
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
if ($addNameTest) {
|
||||
$xpath->addNameTest();
|
||||
}
|
||||
|
||||
$xpath->addStarPrefix();
|
||||
if ($a == 0) {
|
||||
if ($last) {
|
||||
$b = sprintf('last() - %s', $b);
|
||||
}
|
||||
$xpath->addCondition(sprintf('position() = %s', $b));
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
if ($last) {
|
||||
// FIXME: I'm not sure if this is right
|
||||
$a = -$a;
|
||||
$b = -$b;
|
||||
}
|
||||
|
||||
if ($b > 0) {
|
||||
$bNeg = -$b;
|
||||
} else {
|
||||
$bNeg = sprintf('+%s', -$b);
|
||||
}
|
||||
|
||||
if ($a != 1) {
|
||||
$expr = array(sprintf('(position() %s) mod %s = 0', $bNeg, $a));
|
||||
} else {
|
||||
$expr = array();
|
||||
}
|
||||
|
||||
if ($b >= 0) {
|
||||
$expr[] = sprintf('position() >= %s', $b);
|
||||
} elseif ($b < 0 && $last) {
|
||||
$expr[] = sprintf('position() < (last() %s)', $b);
|
||||
}
|
||||
$expr = implode($expr, ' and ');
|
||||
|
||||
if ($expr) {
|
||||
$xpath->addCondition($expr);
|
||||
}
|
||||
|
||||
return $xpath;
|
||||
/* FIXME: 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 */
|
||||
}
|
||||
|
||||
/**
|
||||
* undocumented function
|
||||
*
|
||||
* @param XPathExpr $xpath
|
||||
* @param XPathExpr $expr
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
protected function _xpath_nth_last_child($xpath, $expr)
|
||||
{
|
||||
return $this->_xpath_nth_child($xpath, $expr, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* undocumented function
|
||||
*
|
||||
* @param XPathExpr $xpath
|
||||
* @param XPathExpr $expr
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ParseException
|
||||
*/
|
||||
protected function _xpath_nth_of_type($xpath, $expr)
|
||||
{
|
||||
if ($xpath->getElement() == '*') {
|
||||
throw new ParseException('*:nth-of-type() is not implemented');
|
||||
}
|
||||
|
||||
return $this->_xpath_nth_child($xpath, $expr, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* undocumented function
|
||||
*
|
||||
* @param XPathExpr $xpath
|
||||
* @param XPathExpr $expr
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
protected function _xpath_nth_last_of_type($xpath, $expr)
|
||||
{
|
||||
return $this->_xpath_nth_child($xpath, $expr, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* undocumented function
|
||||
*
|
||||
* @param XPathExpr $xpath
|
||||
* @param XPathExpr $expr
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
protected function _xpath_contains($xpath, $expr)
|
||||
{
|
||||
// text content, minus tags, must contain expr
|
||||
if ($expr instanceof ElementNode) {
|
||||
$expr = $expr->formatElement();
|
||||
}
|
||||
|
||||
// FIXME: lower-case is only available with XPath 2
|
||||
//$xpath->addCondition(sprintf('contains(lower-case(string(.)), %s)', XPathExpr::xpathLiteral(strtolower($expr))));
|
||||
$xpath->addCondition(sprintf('contains(string(.), %s)', XPathExpr::xpathLiteral($expr)));
|
||||
|
||||
// FIXME: Currently case insensitive matching doesn't seem to be happening
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* undocumented function
|
||||
*
|
||||
* @param XPathExpr $xpath
|
||||
* @param XPathExpr $expr
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
protected function _xpath_not($xpath, $expr)
|
||||
{
|
||||
// everything for which not expr applies
|
||||
$expr = $expr->toXpath();
|
||||
$cond = $expr->getCondition();
|
||||
// FIXME: should I do something about element_path?
|
||||
$xpath->addCondition(sprintf('not(%s)', $cond));
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses things like '1n+2', or 'an+b' generally, returning (a, b)
|
||||
*
|
||||
* @param mixed $s
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function parseSeries($s)
|
||||
{
|
||||
if ($s instanceof ElementNode) {
|
||||
$s = $s->formatElement();
|
||||
}
|
||||
|
||||
if (!$s || '*' == $s) {
|
||||
// Happens when there's nothing, which the CSS parser thinks of as *
|
||||
return array(0, 0);
|
||||
}
|
||||
|
||||
if ('odd' == $s) {
|
||||
return array(2, 1);
|
||||
}
|
||||
|
||||
if ('even' == $s) {
|
||||
return array(2, 0);
|
||||
}
|
||||
|
||||
if ('n' == $s) {
|
||||
return array(1, 0);
|
||||
}
|
||||
|
||||
if (false === strpos($s, 'n')) {
|
||||
// Just a b
|
||||
return array(0, intval((string) $s));
|
||||
}
|
||||
|
||||
list($a, $b) = explode('n', $s);
|
||||
if (!$a) {
|
||||
$a = 1;
|
||||
} elseif ('-' == $a || '+' == $a) {
|
||||
$a = intval($a.'1');
|
||||
} else {
|
||||
$a = intval($a);
|
||||
}
|
||||
|
||||
if (!$b) {
|
||||
$b = 0;
|
||||
} elseif ('-' == $b || '+' == $b) {
|
||||
$b = intval($b.'1');
|
||||
} else {
|
||||
$b = intval($b);
|
||||
}
|
||||
|
||||
return array($a, $b);
|
||||
return sprintf('%s[%s:%s(%s)]', $this->getNodeName(), $this->selector, $this->name, $arguments ? '['.$arguments.']' : '');
|
||||
}
|
||||
}
|
||||
|
@ -11,49 +11,65 @@
|
||||
|
||||
namespace Symfony\Component\CssSelector\Node;
|
||||
|
||||
use Symfony\Component\CssSelector\XPathExpr;
|
||||
|
||||
/**
|
||||
* HashNode represents a "selector#id" node.
|
||||
* Represents a "<selector>#<id>" node.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class HashNode implements NodeInterface
|
||||
class HashNode extends AbstractNode
|
||||
{
|
||||
protected $selector;
|
||||
protected $id;
|
||||
/**
|
||||
* @var NodeInterface
|
||||
*/
|
||||
private $selector;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param NodeInterface $selector The NodeInterface object
|
||||
* @param string $id The ID
|
||||
* @var string
|
||||
*/
|
||||
public function __construct($selector, $id)
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @param NodeInterface $selector
|
||||
* @param string $id
|
||||
*/
|
||||
public function __construct(NodeInterface $selector, $id)
|
||||
{
|
||||
$this->selector = $selector;
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @return NodeInterface
|
||||
*/
|
||||
public function __toString()
|
||||
public function getSelector()
|
||||
{
|
||||
return sprintf('%s[%s#%s]', __CLASS__, $this->selector, $this->id);
|
||||
return $this->selector;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @return string
|
||||
*/
|
||||
public function toXpath()
|
||||
public function getId()
|
||||
{
|
||||
$path = $this->selector->toXpath();
|
||||
$path->addCondition(sprintf('@id = %s', XPathExpr::xpathLiteral($this->id)));
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
return $path;
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSpecificity()
|
||||
{
|
||||
return $this->selector->getSpecificity()->plus(new Specificity(1, 0, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('%s[%s#%s]', $this->getNodeName(), $this->selector, $this->id);
|
||||
}
|
||||
}
|
||||
|
75
src/Symfony/Component/CssSelector/Node/NegationNode.php
Normal file
75
src/Symfony/Component/CssSelector/Node/NegationNode.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class NegationNode extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* @var NodeInterface
|
||||
*/
|
||||
private $selector;
|
||||
|
||||
/**
|
||||
* @var NodeInterface
|
||||
*/
|
||||
private $subSelector;
|
||||
|
||||
/**
|
||||
* @param NodeInterface $selector
|
||||
* @param NodeInterface $subSelector
|
||||
*/
|
||||
public function __construct(NodeInterface $selector, NodeInterface $subSelector)
|
||||
{
|
||||
$this->selector = $selector;
|
||||
$this->subSelector = $subSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NodeInterface
|
||||
*/
|
||||
public function getSelector()
|
||||
{
|
||||
return $this->selector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NodeInterface
|
||||
*/
|
||||
public function getSubSelector()
|
||||
{
|
||||
return $this->subSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSpecificity()
|
||||
{
|
||||
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('%s[%s:not(%s)]', $this->getNodeName(), $this->selector, $this->subSelector);
|
||||
}
|
||||
}
|
@ -12,26 +12,33 @@
|
||||
namespace Symfony\Component\CssSelector\Node;
|
||||
|
||||
/**
|
||||
* ClassNode represents a "selector.className" node.
|
||||
* Interface for nodes.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
interface NodeInterface
|
||||
{
|
||||
/**
|
||||
* Returns a string representation of the object.
|
||||
* Returns node's name.
|
||||
*
|
||||
* @return string The string representation
|
||||
* @return string
|
||||
*/
|
||||
public function __toString();
|
||||
public function getNodeName();
|
||||
|
||||
/**
|
||||
* @return XPathExpr The XPath expression
|
||||
* Returns node's specificity.
|
||||
*
|
||||
* @throws ParseException When unknown operator is found
|
||||
* @return Specificity
|
||||
*/
|
||||
public function toXpath();
|
||||
public function getSpecificity();
|
||||
|
||||
/**
|
||||
* Returns node's string representation.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString();
|
||||
}
|
||||
|
@ -1,61 +0,0 @@
|
||||
<?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\XPathExprOr;
|
||||
|
||||
/**
|
||||
* OrNode represents a "Or" node.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class OrNode implements NodeInterface
|
||||
{
|
||||
/**
|
||||
* @var NodeInterface[]
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param NodeInterface[] $items An array of NodeInterface objects
|
||||
*/
|
||||
public function __construct($items)
|
||||
{
|
||||
$this->items = $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('%s(%s)', __CLASS__, $this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toXpath()
|
||||
{
|
||||
$paths = array();
|
||||
foreach ($this->items as $item) {
|
||||
$paths[] = $item->toXpath();
|
||||
}
|
||||
|
||||
return new XPathExprOr($paths);
|
||||
}
|
||||
}
|
@ -11,221 +11,65 @@
|
||||
|
||||
namespace Symfony\Component\CssSelector\Node;
|
||||
|
||||
use Symfony\Component\CssSelector\Exception\ParseException;
|
||||
use Symfony\Component\CssSelector\XPathExpr;
|
||||
|
||||
/**
|
||||
* PseudoNode represents a "selector:ident" node.
|
||||
* Represents a "<selector>:<identifier>" node.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class PseudoNode implements NodeInterface
|
||||
class PseudoNode extends AbstractNode
|
||||
{
|
||||
protected static $unsupported = array(
|
||||
'indeterminate', 'first-line', 'first-letter',
|
||||
'selection', 'before', 'after', 'link', 'visited',
|
||||
'active', 'focus', 'hover',
|
||||
);
|
||||
|
||||
protected $element;
|
||||
protected $type;
|
||||
protected $ident;
|
||||
/**
|
||||
* @var NodeInterface
|
||||
*/
|
||||
private $selector;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param NodeInterface $element The NodeInterface element
|
||||
* @param string $type Node type
|
||||
* @param string $ident The ident
|
||||
*
|
||||
* @throws ParseException When incorrect PseudoNode type is given
|
||||
* @var string
|
||||
*/
|
||||
public function __construct($element, $type, $ident)
|
||||
private $identifier;
|
||||
|
||||
/**
|
||||
* @param NodeInterface $selector
|
||||
* @param string $identifier
|
||||
*/
|
||||
public function __construct(NodeInterface $selector, $identifier)
|
||||
{
|
||||
$this->element = $element;
|
||||
|
||||
if (!in_array($type, array(':', '::'))) {
|
||||
throw new ParseException(sprintf('The PseudoNode type can only be : or :: (%s given).', $type));
|
||||
}
|
||||
|
||||
$this->type = $type;
|
||||
$this->ident = $ident;
|
||||
$this->selector = $selector;
|
||||
$this->identifier = strtolower($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @return NodeInterface
|
||||
*/
|
||||
public function getSelector()
|
||||
{
|
||||
return $this->selector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIdentifier()
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSpecificity()
|
||||
{
|
||||
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('%s[%s%s%s]', __CLASS__, $this->element, $this->type, $this->ident);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @throws ParseException When unsupported or unknown pseudo-class is found
|
||||
*/
|
||||
public function toXpath()
|
||||
{
|
||||
$elXpath = $this->element->toXpath();
|
||||
|
||||
if (in_array($this->ident, self::$unsupported)) {
|
||||
throw new ParseException(sprintf('The pseudo-class %s is unsupported', $this->ident));
|
||||
}
|
||||
$method = 'xpath_'.str_replace('-', '_', $this->ident);
|
||||
if (!method_exists($this, $method)) {
|
||||
throw new ParseException(sprintf('The pseudo-class %s is unknown', $this->ident));
|
||||
}
|
||||
|
||||
return $this->$method($elXpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath The XPath expression
|
||||
*
|
||||
* @return XPathExpr The modified XPath expression
|
||||
*/
|
||||
protected function xpath_checked($xpath)
|
||||
{
|
||||
// FIXME: is this really all the elements?
|
||||
$xpath->addCondition("(@selected or @checked) and (name(.) = 'input' or name(.) = 'option')");
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath The XPath expression
|
||||
*
|
||||
* @return XPathExpr The modified XPath expression
|
||||
*
|
||||
* @throws ParseException If this element is the root element
|
||||
*/
|
||||
protected function xpath_root($xpath)
|
||||
{
|
||||
// if this element is the root element
|
||||
throw new ParseException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this XPath expression as the first child.
|
||||
*
|
||||
* @param XPathExpr $xpath The XPath expression
|
||||
*
|
||||
* @return XPathExpr The modified expression
|
||||
*/
|
||||
protected function xpath_first_child($xpath)
|
||||
{
|
||||
$xpath->addStarPrefix();
|
||||
$xpath->addNameTest();
|
||||
$xpath->addCondition('position() = 1');
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the XPath to be the last child.
|
||||
*
|
||||
* @param XPathExpr $xpath The XPath expression
|
||||
*
|
||||
* @return XPathExpr The modified expression
|
||||
*/
|
||||
protected function xpath_last_child($xpath)
|
||||
{
|
||||
$xpath->addStarPrefix();
|
||||
$xpath->addNameTest();
|
||||
$xpath->addCondition('position() = last()');
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the XPath expression to be the first of type.
|
||||
*
|
||||
* @param XPathExpr $xpath The XPath expression
|
||||
*
|
||||
* @return XPathExpr The modified expression
|
||||
*
|
||||
* @throws ParseException
|
||||
*/
|
||||
protected function xpath_first_of_type($xpath)
|
||||
{
|
||||
if ($xpath->getElement() == '*') {
|
||||
throw new ParseException('*:first-of-type is not implemented');
|
||||
}
|
||||
$xpath->addStarPrefix();
|
||||
$xpath->addCondition('position() = 1');
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the XPath expression to be the last of type.
|
||||
*
|
||||
* @param XPathExpr $xpath The XPath expression
|
||||
*
|
||||
* @return XPathExpr The modified expression
|
||||
*
|
||||
* @throws ParseException Because *:last-of-type is not implemented
|
||||
*/
|
||||
protected function xpath_last_of_type($xpath)
|
||||
{
|
||||
if ($xpath->getElement() == '*') {
|
||||
throw new ParseException('*:last-of-type is not implemented');
|
||||
}
|
||||
$xpath->addStarPrefix();
|
||||
$xpath->addCondition('position() = last()');
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the XPath expression to be the only child.
|
||||
*
|
||||
* @param XPathExpr $xpath The XPath expression
|
||||
*
|
||||
* @return XPathExpr The modified expression
|
||||
*/
|
||||
protected function xpath_only_child($xpath)
|
||||
{
|
||||
$xpath->addNameTest();
|
||||
$xpath->addStarPrefix();
|
||||
$xpath->addCondition('last() = 1');
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the XPath expression to be only of type.
|
||||
*
|
||||
* @param XPathExpr $xpath The XPath expression
|
||||
*
|
||||
* @return XPathExpr The modified expression
|
||||
*
|
||||
* @throws ParseException Because *:only-of-type is not implemented
|
||||
*/
|
||||
protected function xpath_only_of_type($xpath)
|
||||
{
|
||||
if ($xpath->getElement() == '*') {
|
||||
throw new ParseException('*:only-of-type is not implemented');
|
||||
}
|
||||
$xpath->addCondition('last() = 1');
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* undocumented function
|
||||
*
|
||||
* @param XPathExpr $xpath The XPath expression
|
||||
*
|
||||
* @return XPathExpr The modified expression
|
||||
*/
|
||||
protected function xpath_empty($xpath)
|
||||
{
|
||||
$xpath->addCondition('not(*) and not(normalize-space())');
|
||||
|
||||
return $xpath;
|
||||
return sprintf('%s[%s:%s]', $this->getNodeName(), $this->selector, $this->identifier);
|
||||
}
|
||||
}
|
||||
|
75
src/Symfony/Component/CssSelector/Node/SelectorNode.php
Normal file
75
src/Symfony/Component/CssSelector/Node/SelectorNode.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class SelectorNode extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* @var NodeInterface
|
||||
*/
|
||||
private $tree;
|
||||
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
private $pseudoElement;
|
||||
|
||||
/**
|
||||
* @param NodeInterface $tree
|
||||
* @param null|string $pseudoElement
|
||||
*/
|
||||
public function __construct(NodeInterface $tree, $pseudoElement = null)
|
||||
{
|
||||
$this->tree = $tree;
|
||||
$this->pseudoElement = $pseudoElement ? strtolower($pseudoElement) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NodeInterface
|
||||
*/
|
||||
public function getTree()
|
||||
{
|
||||
return $this->tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getPseudoElement()
|
||||
{
|
||||
return $this->pseudoElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSpecificity()
|
||||
{
|
||||
return $this->tree->getSpecificity()->plus(new Specificity(0, 0, $this->pseudoElement ? 1 : 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('%s[%s%s]', $this->getNodeName(), $this->tree, $this->pseudoElement ? '::'.$this->pseudoElement : '');
|
||||
}
|
||||
}
|
78
src/Symfony/Component/CssSelector/Node/Specificity.php
Normal file
78
src/Symfony/Component/CssSelector/Node/Specificity.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?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 cssselector 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>
|
||||
*/
|
||||
class Specificity
|
||||
{
|
||||
const A_FACTOR = 100;
|
||||
const B_FACTOR = 10;
|
||||
const C_FACTOR = 1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $a;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $b;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $c;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $a
|
||||
* @param int $b
|
||||
* @param int $c
|
||||
*/
|
||||
public function __construct($a, $b, $c)
|
||||
{
|
||||
$this->a = $a;
|
||||
$this->b = $b;
|
||||
$this->c = $c;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Specificity $specificity
|
||||
*
|
||||
* @return Specificity
|
||||
*/
|
||||
public function plus(Specificity $specificity)
|
||||
{
|
||||
return new self($this->a + $specificity->a, $this->b + $specificity->b, $this->c + $specificity->c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns global specificity value.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->a * self::A_FACTOR + $this->b * self::B_FACTOR + $this->c * self::C_FACTOR;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<?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 comment handler.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class CommentHandler implements HandlerInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(Reader $reader, TokenStream $stream)
|
||||
{
|
||||
if ('/*' !== $reader->getSubstring(2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$offset = $reader->getOffset('*/');
|
||||
|
||||
if (false === $offset) {
|
||||
$reader->moveToEnd();
|
||||
} else {
|
||||
$reader->moveForward($offset + 2);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?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 handler interface.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
interface HandlerInterface
|
||||
{
|
||||
/**
|
||||
* @param Reader $reader
|
||||
* @param TokenStream $stream
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function handle(Reader $reader, TokenStream $stream);
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
<?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;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
|
||||
/**
|
||||
* CSS selector comment handler.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class HashHandler implements HandlerInterface
|
||||
{
|
||||
/**
|
||||
* @var TokenizerPatterns
|
||||
*/
|
||||
private $patterns;
|
||||
|
||||
/**
|
||||
* @var TokenizerEscaping
|
||||
*/
|
||||
private $escaping;
|
||||
|
||||
/**
|
||||
* @param TokenizerPatterns $patterns
|
||||
* @param TokenizerEscaping $escaping
|
||||
*/
|
||||
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
$this->escaping = $escaping;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(Reader $reader, TokenStream $stream)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
<?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;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
|
||||
/**
|
||||
* CSS selector comment handler.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class IdentifierHandler implements HandlerInterface
|
||||
{
|
||||
/**
|
||||
* @var TokenizerPatterns
|
||||
*/
|
||||
private $patterns;
|
||||
|
||||
/**
|
||||
* @var TokenizerEscaping
|
||||
*/
|
||||
private $escaping;
|
||||
|
||||
/**
|
||||
* @param TokenizerPatterns $patterns
|
||||
* @param TokenizerEscaping $escaping
|
||||
*/
|
||||
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
$this->escaping = $escaping;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(Reader $reader, TokenStream $stream)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?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;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
|
||||
/**
|
||||
* CSS selector comment handler.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class NumberHandler implements HandlerInterface
|
||||
{
|
||||
/**
|
||||
* @var TokenizerPatterns
|
||||
*/
|
||||
private $patterns;
|
||||
|
||||
/**
|
||||
* @param TokenizerPatterns $patterns
|
||||
*/
|
||||
public function __construct(TokenizerPatterns $patterns)
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(Reader $reader, TokenStream $stream)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
<?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\TokenStream;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
|
||||
/**
|
||||
* CSS selector comment handler.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class StringHandler implements HandlerInterface
|
||||
{
|
||||
/**
|
||||
* @var TokenizerPatterns
|
||||
*/
|
||||
private $patterns;
|
||||
|
||||
/**
|
||||
* @var TokenizerEscaping
|
||||
*/
|
||||
private $escaping;
|
||||
|
||||
/**
|
||||
* @param TokenizerPatterns $patterns
|
||||
* @param TokenizerEscaping $escaping
|
||||
*/
|
||||
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
$this->escaping = $escaping;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(Reader $reader, TokenStream $stream)
|
||||
{
|
||||
$quote = $reader->getSubstring(1);
|
||||
|
||||
if (!in_array($quote, array("'", '"'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$reader->moveForward(1);
|
||||
$match = $reader->findPattern($this->patterns->getQuotedStringPattern($quote));
|
||||
|
||||
if (!$match) {
|
||||
throw new InternalErrorException('Should have found at least an empty match at '.$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;
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class WhitespaceHandler implements HandlerInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(Reader $reader, TokenStream $stream)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
395
src/Symfony/Component/CssSelector/Parser/Parser.php
Normal file
395
src/Symfony/Component/CssSelector/Parser/Parser.php
Normal file
@ -0,0 +1,395 @@
|
||||
<?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\SyntaxErrorException;
|
||||
use Symfony\Component\CssSelector\Node;
|
||||
use Symfony\Component\CssSelector\Parser\Shortcut;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer;
|
||||
|
||||
/**
|
||||
* CSS selector parser.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class Parser implements ParserInterface
|
||||
{
|
||||
/**
|
||||
* @var Tokenizer
|
||||
*/
|
||||
private $tokenizer;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param null|Tokenizer $tokenizer
|
||||
*/
|
||||
public function __construct(Tokenizer $tokenizer = null)
|
||||
{
|
||||
$this->tokenizer = $tokenizer ?: new Tokenizer();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parse($source)
|
||||
{
|
||||
$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
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function parseSeries(array $tokens)
|
||||
{
|
||||
foreach ($tokens as $token) {
|
||||
if ($token->isString()) {
|
||||
throw SyntaxErrorException::stringAsFunctionArgument();
|
||||
}
|
||||
}
|
||||
|
||||
$joined = trim(implode('', array_map(function (Token $token) {
|
||||
return $token->getValue();
|
||||
}, $tokens)));
|
||||
|
||||
$int = function ($string) {
|
||||
if (!is_numeric($string)) {
|
||||
throw SyntaxErrorException::stringAsFunctionArgument();
|
||||
}
|
||||
|
||||
return (int) $string;
|
||||
};
|
||||
|
||||
switch (true) {
|
||||
case 'odd' === $joined:
|
||||
return array(2, 1);
|
||||
case 'even' === $joined:
|
||||
return array(2, 0);
|
||||
case 'n' === $joined:
|
||||
return array(1, 0);
|
||||
case false === strpos($joined, 'n'):
|
||||
return array(0, $int($joined));
|
||||
}
|
||||
|
||||
$split = explode('n', $joined);
|
||||
$first = isset($split[0]) ? $split[0] : null;
|
||||
|
||||
return array(
|
||||
$first ? ('-' === $first || '+' === $first ? $int($first.'1') : $int($first)) : 1,
|
||||
isset($split[1]) && $split[1] ? $int($split[1]) : 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses selector nodes.
|
||||
*
|
||||
* @param TokenStream $stream
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function parseSelectorList(TokenStream $stream)
|
||||
{
|
||||
$stream->skipWhitespace();
|
||||
$selectors = array();
|
||||
|
||||
while (true) {
|
||||
$selectors[] = $this->parserSelectorNode($stream);
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(array(','))) {
|
||||
$stream->getNext();
|
||||
$stream->skipWhitespace();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $selectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next selector or combined node.
|
||||
*
|
||||
* @param TokenStream $stream
|
||||
*
|
||||
* @throws SyntaxErrorException
|
||||
*
|
||||
* @return Node\SelectorNode
|
||||
*/
|
||||
private function parserSelectorNode(TokenStream $stream)
|
||||
{
|
||||
list($result, $pseudoElement) = $this->parseSimpleSelector($stream);
|
||||
|
||||
while (true) {
|
||||
$stream->skipWhitespace();
|
||||
$peek = $stream->getPeek();
|
||||
|
||||
if ($peek->isFileEnd() || $peek->isDelimiter(array(','))) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (null !== $pseudoElement) {
|
||||
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
|
||||
}
|
||||
|
||||
if ($peek->isDelimiter(array('+', '>', '~'))) {
|
||||
$combinator = $stream->getNext()->getValue();
|
||||
$stream->skipWhitespace();
|
||||
} else {
|
||||
$combinator = ' ';
|
||||
}
|
||||
|
||||
list($nextSelector, $pseudoElement) = $this->parseSimpleSelector($stream);
|
||||
$result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
|
||||
}
|
||||
|
||||
return new Node\SelectorNode($result, $pseudoElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next simple node (hash, class, pseudo, negation).
|
||||
*
|
||||
* @param TokenStream $stream
|
||||
* @param boolean $insideNegation
|
||||
*
|
||||
* @throws SyntaxErrorException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function parseSimpleSelector(TokenStream $stream, $insideNegation = false)
|
||||
{
|
||||
$stream->skipWhitespace();
|
||||
|
||||
$selectorStart = count($stream->getUsed());
|
||||
$result = $this->parseElementNode($stream);
|
||||
$pseudoElement = null;
|
||||
|
||||
while (true) {
|
||||
$peek = $stream->getPeek();
|
||||
if ($peek->isWhitespace()
|
||||
|| $peek->isFileEnd()
|
||||
|| $peek->isDelimiter(array(',', '+', '>', '~'))
|
||||
|| ($insideNegation && $peek->isDelimiter(array(')')))
|
||||
) {
|
||||
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(array('.'))) {
|
||||
$stream->getNext();
|
||||
$result = new Node\ClassNode($result, $stream->getNextIdentifier());
|
||||
} elseif ($peek->isDelimiter(array('['))) {
|
||||
$stream->getNext();
|
||||
$result = $this->parseAttributeNode($result, $stream);
|
||||
} elseif ($peek->isDelimiter(array(':'))) {
|
||||
$stream->getNext();
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(array(':'))) {
|
||||
$stream->getNext();
|
||||
$pseudoElement = $stream->getNextIdentifier();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$identifier = $stream->getNextIdentifier();
|
||||
if (in_array(strtolower($identifier), array('first-line', 'first-letter', 'before', 'after'))) {
|
||||
// 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(array('('))) {
|
||||
$result = new Node\PseudoNode($result, $identifier);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$stream->getNext();
|
||||
$stream->skipWhitespace();
|
||||
|
||||
if ('not' === strtolower($identifier)) {
|
||||
if ($insideNegation) {
|
||||
throw SyntaxErrorException::nestedNot();
|
||||
}
|
||||
|
||||
list($argument, $argumentPseudoElement) = $this->parseSimpleSelector($stream, true);
|
||||
$next = $stream->getNext();
|
||||
|
||||
if (null !== $argumentPseudoElement) {
|
||||
throw SyntaxErrorException::pseudoElementFound($argumentPseudoElement, 'inside ::not()');
|
||||
}
|
||||
|
||||
if (!$next->isDelimiter(array(')'))) {
|
||||
throw SyntaxErrorException::unexpectedToken('")"', $next);
|
||||
}
|
||||
|
||||
$result = new Node\NegationNode($result, $argument);
|
||||
} else {
|
||||
$arguments = array();
|
||||
$next = null;
|
||||
|
||||
while (true) {
|
||||
$stream->skipWhitespace();
|
||||
$next = $stream->getNext();
|
||||
|
||||
if ($next->isIdentifier()
|
||||
|| $next->isString()
|
||||
|| $next->isNumber()
|
||||
|| $next->isDelimiter(array('+', '-'))
|
||||
) {
|
||||
$arguments[] = $next;
|
||||
} elseif ($next->isDelimiter(array(')'))) {
|
||||
break;
|
||||
} else {
|
||||
throw SyntaxErrorException::unexpectedToken('an argument', $next);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($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 array($result, $pseudoElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next element node.
|
||||
*
|
||||
* @param TokenStream $stream
|
||||
*
|
||||
* @return Node\ElementNode
|
||||
*/
|
||||
private function parseElementNode(TokenStream $stream)
|
||||
{
|
||||
$peek = $stream->getPeek();
|
||||
|
||||
if ($peek->isIdentifier() || $peek->isDelimiter(array('*'))) {
|
||||
if ($peek->isIdentifier()) {
|
||||
$namespace = $stream->getNext()->getValue();
|
||||
} else {
|
||||
$stream->getNext();
|
||||
$namespace = null;
|
||||
}
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(array('|'))) {
|
||||
$stream->getNext();
|
||||
$element = $stream->getNextIdentifierOrStar();
|
||||
} else {
|
||||
$element = $namespace;
|
||||
$namespace = null;
|
||||
}
|
||||
} else {
|
||||
$element = $namespace = null;
|
||||
}
|
||||
|
||||
return new Node\ElementNode($namespace, $element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next attribute node.
|
||||
*
|
||||
* @param Node\NodeInterface $selector
|
||||
* @param TokenStream $stream
|
||||
*
|
||||
* @throws SyntaxErrorException
|
||||
*
|
||||
* @return Node\AttributeNode
|
||||
*/
|
||||
private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream)
|
||||
{
|
||||
$stream->skipWhitespace();
|
||||
$attribute = $stream->getNextIdentifierOrStar();
|
||||
|
||||
if (null === $attribute && !$stream->getPeek()->isDelimiter(array('|'))) {
|
||||
throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek());
|
||||
}
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(array('|'))) {
|
||||
$stream->getNext();
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(array('='))) {
|
||||
$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(array(']'))) {
|
||||
return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null);
|
||||
} elseif ($next->isDelimiter(array('='))) {
|
||||
$operator = '=';
|
||||
} elseif ($next->isDelimiter(array('^', '$', '*', '~', '|', '!'))
|
||||
&& $stream->getPeek()->isDelimiter(array('='))
|
||||
) {
|
||||
$operator = $next->getValue().'=';
|
||||
$stream->getNext();
|
||||
} else {
|
||||
throw SyntaxErrorException::unexpectedToken('operator', $next);
|
||||
}
|
||||
}
|
||||
|
||||
$stream->skipWhitespace();
|
||||
$value = $stream->getNext();
|
||||
|
||||
if (!($value->isIdentifier() || $value->isString())) {
|
||||
throw SyntaxErrorException::unexpectedToken('string or identifier', $value);
|
||||
}
|
||||
|
||||
$stream->skipWhitespace();
|
||||
$next = $stream->getNext();
|
||||
|
||||
if (!$next->isDelimiter(array(']'))) {
|
||||
throw SyntaxErrorException::unexpectedToken('"]"', $next);
|
||||
}
|
||||
|
||||
return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue());
|
||||
}
|
||||
}
|
34
src/Symfony/Component/CssSelector/Parser/ParserInterface.php
Normal file
34
src/Symfony/Component/CssSelector/Parser/ParserInterface.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
interface ParserInterface
|
||||
{
|
||||
/**
|
||||
* Parses given selector source into an array of tokens.
|
||||
*
|
||||
* @param string $source
|
||||
*
|
||||
* @return SelectorNode[]
|
||||
*/
|
||||
public function parse($source);
|
||||
}
|
126
src/Symfony/Component/CssSelector/Parser/Reader.php
Normal file
126
src/Symfony/Component/CssSelector/Parser/Reader.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class Reader
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $length;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @param string $source
|
||||
*/
|
||||
public function __construct($source)
|
||||
{
|
||||
$this->source = $source;
|
||||
$this->length = strlen($source);
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEOF()
|
||||
{
|
||||
return $this->position >= $this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPosition()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getRemainingLength()
|
||||
{
|
||||
return $this->length - $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $length
|
||||
* @param int $offset
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSubstring($length, $offset = 0)
|
||||
{
|
||||
return substr($this->source, $this->position + $offset, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOffset($string)
|
||||
{
|
||||
$position = strpos($this->source, $string, $this->position);
|
||||
|
||||
return false === $position ? false : $position - $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $pattern
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function findPattern($pattern)
|
||||
{
|
||||
$source = substr($this->source, $this->position);
|
||||
|
||||
if (preg_match($pattern, $source, $matches)) {
|
||||
return $matches;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $length
|
||||
*/
|
||||
public function moveForward($length)
|
||||
{
|
||||
$this->position += $length;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public function moveToEnd()
|
||||
{
|
||||
$this->position = $this->length;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
<?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;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
|
||||
/**
|
||||
* CSS selector class parser shortcut.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class ClassParser implements ParserInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parse($source)
|
||||
{
|
||||
// matches "<selector>.<name>"
|
||||
if (preg_match('~^[ \t\r\n\f]*([a-zA-Z]*)\.([a-zA-Z][a-zA-Z0-9_-]*)[ \t\r\n\f]*$~', $source, $matches)) {
|
||||
return array(new SelectorNode(new ClassNode(new ElementNode($matches[1] ?: null), $matches[2])));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?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;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
|
||||
/**
|
||||
* CSS selector element parser shortcut.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class ElementParser implements ParserInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parse($source)
|
||||
{
|
||||
// matches "<element>"
|
||||
if (preg_match('~^[ \t\r\n\f]*([a-zA-Z][a-zA-Z0-9_-]*|\\*)[ \t\r\n\f]*$~', $source, $matches)) {
|
||||
return array(new SelectorNode(new ElementNode(null, $matches[1])));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?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;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
|
||||
/**
|
||||
* 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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class EmptyStringParser implements ParserInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parse($source)
|
||||
{
|
||||
// matches ""
|
||||
if (preg_match('~^$~', $source, $matches)) {
|
||||
return array(new SelectorNode(new ElementNode(null, '*')));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
<?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;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
|
||||
/**
|
||||
* CSS selector hash parser shortcut.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class HashParser implements ParserInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parse($source)
|
||||
{
|
||||
// matches "<selector>#<id>"
|
||||
if (preg_match('~^[ \t\r\n\f]*([a-zA-Z][a-zA-Z0-9_-]*|\\*)?#([a-zA-Z0-9_-]+)[ \t\r\n\f]*$~', $source, $matches)) {
|
||||
return array(new SelectorNode(new HashNode(new ElementNode(null, $matches[1] ?: null), $matches[2])));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
}
|
160
src/Symfony/Component/CssSelector/Parser/Token.php
Normal file
160
src/Symfony/Component/CssSelector/Parser/Token.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class Token
|
||||
{
|
||||
const TYPE_FILE_END = 'eof';
|
||||
const TYPE_DELIMITER = 'delimiter';
|
||||
const TYPE_WHITESPACE = 'whitespace';
|
||||
const TYPE_IDENTIFIER = 'identifier';
|
||||
const TYPE_HASH = 'hash';
|
||||
const TYPE_NUMBER = 'number';
|
||||
const TYPE_STRING = 'string';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @param int $type
|
||||
* @param string $value
|
||||
* @param int $position
|
||||
*/
|
||||
public function __construct($type, $value, $position)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->value = $value;
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPosition()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isFileEnd()
|
||||
{
|
||||
return self::TYPE_FILE_END === $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $values
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isDelimiter(array $values = array())
|
||||
{
|
||||
if (self::TYPE_DELIMITER !== $this->type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($values)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($this->value, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isWhitespace()
|
||||
{
|
||||
return self::TYPE_WHITESPACE === $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isIdentifier()
|
||||
{
|
||||
return self::TYPE_IDENTIFIER === $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isHash()
|
||||
{
|
||||
return self::TYPE_HASH === $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isNumber()
|
||||
{
|
||||
return self::TYPE_NUMBER === $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isString()
|
||||
{
|
||||
return self::TYPE_STRING === $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
if ($this->value) {
|
||||
return sprintf('<%s "%s" at %s>', $this->type, $this->value, $this->position);
|
||||
}
|
||||
|
||||
return sprintf('<%s at %s>', $this->type, $this->position);
|
||||
}
|
||||
}
|
182
src/Symfony/Component/CssSelector/Parser/TokenStream.php
Normal file
182
src/Symfony/Component/CssSelector/Parser/TokenStream.php
Normal file
@ -0,0 +1,182 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class TokenStream
|
||||
{
|
||||
/**
|
||||
* @var Token[]
|
||||
*/
|
||||
private $tokens = array();
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
private $frozen = false;
|
||||
|
||||
/**
|
||||
* @var Token[]
|
||||
*/
|
||||
private $used = array();
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $cursor = 0;
|
||||
|
||||
/**
|
||||
* @var Token|null
|
||||
*/
|
||||
private $peeked = null;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
private $peeking = false;
|
||||
|
||||
/**
|
||||
* Pushes a token.
|
||||
*
|
||||
* @param Token $token
|
||||
*
|
||||
* @return TokenStream
|
||||
*/
|
||||
public function push(Token $token)
|
||||
{
|
||||
$this->tokens[] = $token;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Freezes stream.
|
||||
*
|
||||
* @return TokenStream
|
||||
*/
|
||||
public function freeze()
|
||||
{
|
||||
$this->frozen = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token.
|
||||
*
|
||||
* @throws InternalErrorException If there is no more token
|
||||
*
|
||||
* @return Token
|
||||
*/
|
||||
public function getNext()
|
||||
{
|
||||
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.
|
||||
*
|
||||
* @return Token
|
||||
*/
|
||||
public function getPeek()
|
||||
{
|
||||
if (!$this->peeking) {
|
||||
$this->peeked = $this->getNext();
|
||||
$this->peeking = true;
|
||||
}
|
||||
|
||||
return $this->peeked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns used tokens.
|
||||
*
|
||||
* @return Token[]
|
||||
*/
|
||||
public function getUsed()
|
||||
{
|
||||
return $this->used;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns nex identifier token.
|
||||
*
|
||||
* @throws SyntaxErrorException If next token is not an identifier
|
||||
*
|
||||
* @return string The identifier token value
|
||||
*/
|
||||
public function getNextIdentifier()
|
||||
{
|
||||
$next = $this->getNext();
|
||||
|
||||
if (!$next->isIdentifier()) {
|
||||
throw SyntaxErrorException::unexpectedToken('identifier', $next);
|
||||
}
|
||||
|
||||
return $next->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns nex identifier or star delimiter token.
|
||||
*
|
||||
* @throws SyntaxErrorException If next token is not an identifier or a star delimiter
|
||||
*
|
||||
* @return null|string The identifier token value or null if star found
|
||||
*/
|
||||
public function getNextIdentifierOrStar()
|
||||
{
|
||||
$next = $this->getNext();
|
||||
|
||||
if ($next->isIdentifier()) {
|
||||
return $next->getValue();
|
||||
}
|
||||
|
||||
if ($next->isDelimiter(array('*'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw SyntaxErrorException::unexpectedToken('identifier or "*"', $next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips next whitespace if any.
|
||||
*/
|
||||
public function skipWhitespace()
|
||||
{
|
||||
$peek = $this->getPeek();
|
||||
|
||||
if ($peek->isWhitespace()) {
|
||||
$this->getNext();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class Tokenizer
|
||||
{
|
||||
/**
|
||||
* @var Handler\HandlerInterface[]
|
||||
*/
|
||||
private $handlers;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$patterns = new TokenizerPatterns();
|
||||
$escaping = new TokenizerEscaping($patterns);
|
||||
|
||||
$this->handlers = array(
|
||||
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.
|
||||
*
|
||||
* @param Reader $reader
|
||||
*
|
||||
* @return TokenStream
|
||||
*/
|
||||
public function tokenize(Reader $reader)
|
||||
{
|
||||
$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();
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class TokenizerEscaping
|
||||
{
|
||||
/**
|
||||
* @var TokenizerPatterns
|
||||
*/
|
||||
private $patterns;
|
||||
|
||||
/**
|
||||
* @param TokenizerPatterns $patterns
|
||||
*/
|
||||
public function __construct(TokenizerPatterns $patterns)
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeUnicode($value)
|
||||
{
|
||||
$value = $this->replaceUnicodeSequences($value);
|
||||
|
||||
return preg_replace($this->patterns->getSimpleEscapePattern(), '$1', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeUnicodeAndNewLine($value)
|
||||
{
|
||||
$value = preg_replace($this->patterns->getNewLineEscapePattern(), '', $value);
|
||||
|
||||
return $this->escapeUnicode($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function replaceUnicodeSequences($value)
|
||||
{
|
||||
return preg_replace_callback($this->patterns->getUnicodeEscapePattern(), function (array $match) {
|
||||
$code = $match[1];
|
||||
|
||||
if (bin2hex($code) > 0xFFFD) {
|
||||
$code = '\\FFFD';
|
||||
}
|
||||
|
||||
return mb_convert_encoding(pack('H*', $code), 'UTF-8', 'UCS-2BE');
|
||||
}, $value);
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class TokenizerPatterns
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $unicodeEscapePattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $simpleEscapePattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $newLineEscapePattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $escapePattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $stringEscapePattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $nonAsciiPattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $nmCharPattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $nmStartPattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $identifierPattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $hashPattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $numberPattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $quotedStringPattern;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
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.')*';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNewLineEscapePattern()
|
||||
{
|
||||
return '~^'.$this->newLineEscapePattern.'~';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSimpleEscapePattern()
|
||||
{
|
||||
return '~^'.$this->simpleEscapePattern.'~';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUnicodeEscapePattern()
|
||||
{
|
||||
return '~^'.$this->unicodeEscapePattern.'~i';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIdentifierPattern()
|
||||
{
|
||||
return '~^'.$this->identifierPattern.'~i';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHashPattern()
|
||||
{
|
||||
return '~^'.$this->hashPattern.'~i';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNumberPattern()
|
||||
{
|
||||
return '~^'.$this->numberPattern.'~';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $quote
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQuotedStringPattern($quote)
|
||||
{
|
||||
return '~^'.sprintf($this->quotedStringPattern, $quote).'~i';
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
<?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 Symfony\Component\CssSelector\CssSelector;
|
||||
|
||||
class CssSelectorTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testCsstoXPath()
|
||||
{
|
||||
$this->assertEquals('descendant-or-self::*', CssSelector::toXPath(''));
|
||||
$this->assertEquals('descendant-or-self::h1', CssSelector::toXPath('h1'));
|
||||
$this->assertEquals("descendant-or-self::h1[@id = 'foo']", CssSelector::toXPath('h1#foo'));
|
||||
$this->assertEquals("descendant-or-self::h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", CssSelector::toXPath('h1.foo'));
|
||||
|
||||
$this->assertEquals('descendant-or-self::foo:h1', CssSelector::toXPath('foo|h1'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getCssSelectors
|
||||
*/
|
||||
public function testParse($css, $xpath)
|
||||
{
|
||||
$parser = new CssSelector();
|
||||
|
||||
$this->assertEquals($xpath, (string) $parser->parse($css)->toXPath(), '->parse() parses an input string and returns a node');
|
||||
}
|
||||
|
||||
public function testParseExceptions()
|
||||
{
|
||||
$parser = new CssSelector();
|
||||
|
||||
try {
|
||||
$parser->parse('h1:');
|
||||
$this->fail('->parse() throws an Exception if the css selector is not valid');
|
||||
} catch (\Exception $e) {
|
||||
$this->assertInstanceOf('\Symfony\Component\CssSelector\Exception\ParseException', $e, '->parse() throws an Exception if the css selector is not valid');
|
||||
$this->assertEquals("Expected symbol, got '' at h1: -> ", $e->getMessage(), '->parse() throws an Exception if the css selector is not valid');
|
||||
}
|
||||
}
|
||||
|
||||
public function getCssSelectors()
|
||||
{
|
||||
return array(
|
||||
array('h1', "h1"),
|
||||
array('foo|h1', "foo:h1"),
|
||||
array('h1, h2, h3', "h1 | h2 | h3"),
|
||||
array('h1:nth-child(3n+1)', "*/*[name() = 'h1' and ((position() -1) mod 3 = 0 and position() >= 1)]"),
|
||||
array('h1 > p', "h1/p"),
|
||||
array('h1#foo', "h1[@id = 'foo']"),
|
||||
array('h1.foo', "h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"),
|
||||
array('h1[class*="foo bar"]', "h1[contains(@class, 'foo bar')]"),
|
||||
array('h1[foo|class*="foo bar"]', "h1[contains(@foo:class, 'foo bar')]"),
|
||||
array('h1[class]', "h1[@class]"),
|
||||
array('h1 .foo', "h1/descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"),
|
||||
array('h1 #foo', "h1/descendant::*[@id = 'foo']"),
|
||||
array('h1 [class*=foo]', "h1/descendant::*[contains(@class, 'foo')]"),
|
||||
array('div>.foo', "div/*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"),
|
||||
array('div > .foo', "div/*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?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\NodeInterface;
|
||||
|
||||
abstract class AbstractNodeTest extends \PHPUnit_Framework_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 function getToStringConversionTestData();
|
||||
abstract public function getSpecificityValueTestData();
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
<?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\AttribNode;
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
|
||||
class AttribNodeTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testToXpath()
|
||||
{
|
||||
$element = new ElementNode('*', 'h1');
|
||||
|
||||
$operators = array(
|
||||
'^=' => "h1[starts-with(@class, 'foo')]",
|
||||
'$=' => "h1[substring(@class, string-length(@class)-2) = 'foo']",
|
||||
'*=' => "h1[contains(@class, 'foo')]",
|
||||
'=' => "h1[@class = 'foo']",
|
||||
'~=' => "h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]",
|
||||
'|=' => "h1[@class = 'foo' or starts-with(@class, 'foo-')]",
|
||||
'!=' => "h1[not(@class) or @class != 'foo']",
|
||||
);
|
||||
|
||||
// h1[class??foo]
|
||||
foreach ($operators as $op => $xpath) {
|
||||
$attrib = new AttribNode($element, '*', 'class', $op, 'foo');
|
||||
$this->assertEquals($xpath, (string) $attrib->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
}
|
||||
|
||||
// h1[class]
|
||||
$attrib = new AttribNode($element, '*', 'class', 'exists', 'foo');
|
||||
$this->assertEquals('h1[@class]', (string) $attrib->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?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 AbstractNodeTest
|
||||
{
|
||||
public function getToStringConversionTestData()
|
||||
{
|
||||
return array(
|
||||
array(new AttributeNode(new ElementNode(), null, 'attribute', 'exists', null), 'Attribute[Element[*][attribute]]'),
|
||||
array(new AttributeNode(new ElementNode(), null, 'attribute', '$=', 'value'), "Attribute[Element[*][attribute $= 'value']]"),
|
||||
array(new AttributeNode(new ElementNode(), 'namespace', 'attribute', '$=', 'value'), "Attribute[Element[*][namespace|attribute $= 'value']]"),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSpecificityValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(new AttributeNode(new ElementNode(), null, 'attribute', 'exists', null), 10),
|
||||
array(new AttributeNode(new ElementNode(null, 'element'), null, 'attribute', 'exists', null), 11),
|
||||
array(new AttributeNode(new ElementNode(), null, 'attribute', '$=', 'value'), 10),
|
||||
array(new AttributeNode(new ElementNode(), 'namespace', 'attribute', '$=', 'value'), 10),
|
||||
);
|
||||
}
|
||||
}
|
@ -14,14 +14,20 @@ namespace Symfony\Component\CssSelector\Tests\Node;
|
||||
use Symfony\Component\CssSelector\Node\ClassNode;
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
|
||||
class ClassNodeTest extends \PHPUnit_Framework_TestCase
|
||||
class ClassNodeTest extends AbstractNodeTest
|
||||
{
|
||||
public function testToXpath()
|
||||
public function getToStringConversionTestData()
|
||||
{
|
||||
// h1.foo
|
||||
$element = new ElementNode('*', 'h1');
|
||||
$class = new ClassNode($element, 'foo');
|
||||
return array(
|
||||
array(new ClassNode(new ElementNode(), 'class'), 'Class[Element[*].class]'),
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertEquals("h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", (string) $class->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
public function getSpecificityValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(new ClassNode(new ElementNode(), 'class'), 10),
|
||||
array(new ClassNode(new ElementNode(null, 'element'), 'class'), 11),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,23 +14,22 @@ namespace Symfony\Component\CssSelector\Tests\Node;
|
||||
use Symfony\Component\CssSelector\Node\CombinedSelectorNode;
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
|
||||
class CombinedSelectorNodeTest extends \PHPUnit_Framework_TestCase
|
||||
class CombinedSelectorNodeTest extends AbstractNodeTest
|
||||
{
|
||||
public function testToXpath()
|
||||
public function getToStringConversionTestData()
|
||||
{
|
||||
$combinators = array(
|
||||
' ' => "h1/descendant::p",
|
||||
'>' => "h1/p",
|
||||
'+' => "h1/following-sibling::*[name() = 'p' and (position() = 1)]",
|
||||
'~' => "h1/following-sibling::p",
|
||||
return array(
|
||||
array(new CombinedSelectorNode(new ElementNode(), '>', new ElementNode()), 'CombinedSelector[Element[*] > Element[*]]'),
|
||||
array(new CombinedSelectorNode(new ElementNode(), ' ', new ElementNode()), 'CombinedSelector[Element[*] <followed> Element[*]]'),
|
||||
);
|
||||
}
|
||||
|
||||
// h1 ?? p
|
||||
$element1 = new ElementNode('*', 'h1');
|
||||
$element2 = new ElementNode('*', 'p');
|
||||
foreach ($combinators as $combinator => $xpath) {
|
||||
$combinator = new CombinedSelectorNode($element1, $combinator, $element2);
|
||||
$this->assertEquals($xpath, (string) $combinator->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
}
|
||||
public function getSpecificityValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(new CombinedSelectorNode(new ElementNode(), '>', new ElementNode()), 0),
|
||||
array(new CombinedSelectorNode(new ElementNode(null, 'element'), '>', new ElementNode()), 1),
|
||||
array(new CombinedSelectorNode(new ElementNode(null, 'element'), '>', new ElementNode(null, 'element')), 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,18 +13,23 @@ namespace Symfony\Component\CssSelector\Tests\Node;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
|
||||
class ElementNodeTest extends \PHPUnit_Framework_TestCase
|
||||
class ElementNodeTest extends AbstractNodeTest
|
||||
{
|
||||
public function testToXpath()
|
||||
public function getToStringConversionTestData()
|
||||
{
|
||||
// h1
|
||||
$element = new ElementNode('*', 'h1');
|
||||
return array(
|
||||
array(new ElementNode(), 'Element[*]'),
|
||||
array(new ElementNode(null, 'element'), 'Element[element]'),
|
||||
array(new ElementNode('namespace', 'element'), 'Element[namespace|element]'),
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertEquals('h1', (string) $element->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// foo|h1
|
||||
$element = new ElementNode('foo', 'h1');
|
||||
|
||||
$this->assertEquals('foo:h1', (string) $element->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
public function getSpecificityValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(new ElementNode(), 0),
|
||||
array(new ElementNode(null, 'element'), 1),
|
||||
array(new ElementNode('namespace', 'element'),1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,86 +11,37 @@
|
||||
|
||||
namespace Symfony\Component\CssSelector\Tests\Node;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\FunctionNode;
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
use Symfony\Component\CssSelector\Token;
|
||||
use Symfony\Component\CssSelector\Node\FunctionNode;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
|
||||
class FunctionNodeTest extends \PHPUnit_Framework_TestCase
|
||||
class FunctionNodeTest extends AbstractNodeTest
|
||||
{
|
||||
public function testToXpath()
|
||||
public function getToStringConversionTestData()
|
||||
{
|
||||
$element = new ElementNode('*', 'h1');
|
||||
return array(
|
||||
array(new FunctionNode(new ElementNode(), 'function'), 'Function[Element[*]:function()]'),
|
||||
array(new FunctionNode(new ElementNode(), 'function', array(
|
||||
new Token(Token::TYPE_IDENTIFIER, 'value', 0),
|
||||
)), "Function[Element[*]:function(['value'])]"),
|
||||
array(new FunctionNode(new ElementNode(), 'function', array(
|
||||
new Token(Token::TYPE_STRING, 'value1', 0),
|
||||
new Token(Token::TYPE_NUMBER, 'value2', 0),
|
||||
)), "Function[Element[*]:function(['value1', 'value2'])]"),
|
||||
);
|
||||
}
|
||||
|
||||
// h1:contains("foo")
|
||||
$function = new FunctionNode($element, ':', 'contains', 'foo');
|
||||
$this->assertEquals("h1[contains(string(.), 'foo')]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-child(1)
|
||||
$function = new FunctionNode($element, ':', 'nth-child', 1);
|
||||
$this->assertEquals("*/*[name() = 'h1' and (position() = 1)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-child()
|
||||
$function = new FunctionNode($element, ':', 'nth-child', '');
|
||||
$this->assertEquals("h1[false() and position() = 0]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-child(odd)
|
||||
$element2 = new ElementNode('*', new Token('Symbol', 'odd', -1));
|
||||
$function = new FunctionNode($element, ':', 'nth-child', $element2);
|
||||
$this->assertEquals("*/*[name() = 'h1' and ((position() -1) mod 2 = 0 and position() >= 1)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-child(even)
|
||||
$element2 = new ElementNode('*', new Token('Symbol', 'even', -1));
|
||||
$function = new FunctionNode($element, ':', 'nth-child', $element2);
|
||||
$this->assertEquals("*/*[name() = 'h1' and ((position() +0) mod 2 = 0 and position() >= 0)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-child(n)
|
||||
$element2 = new ElementNode('*', new Token('Symbol', 'n', -1));
|
||||
$function = new FunctionNode($element, ':', 'nth-child', $element2);
|
||||
$this->assertEquals("*/*[name() = 'h1' and (position() >= 0)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-child(3n+1)
|
||||
$element2 = new ElementNode('*', new Token('Symbol', '3n+1', -1));
|
||||
$function = new FunctionNode($element, ':', 'nth-child', $element2);
|
||||
$this->assertEquals("*/*[name() = 'h1' and ((position() -1) mod 3 = 0 and position() >= 1)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-child(n+1)
|
||||
$element2 = new ElementNode('*', new Token('Symbol', 'n+1', -1));
|
||||
$function = new FunctionNode($element, ':', 'nth-child', $element2);
|
||||
$this->assertEquals("*/*[name() = 'h1' and (position() >= 1)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-child(1)
|
||||
$element2 = new ElementNode('*', new Token('Symbol', '2', -1));
|
||||
$function = new FunctionNode($element, ':', 'nth-child', $element2);
|
||||
$this->assertEquals("*/*[name() = 'h1' and (position() = 2)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-child(2n)
|
||||
$element2 = new ElementNode('*', new Token('Symbol', '2n', -1));
|
||||
$function = new FunctionNode($element, ':', 'nth-child', $element2);
|
||||
$this->assertEquals("*/*[name() = 'h1' and ((position() +0) mod 2 = 0 and position() >= 0)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-child(-n)
|
||||
$element2 = new ElementNode('*', new Token('Symbol', '-n', -1));
|
||||
$function = new FunctionNode($element, ':', 'nth-child', $element2);
|
||||
$this->assertEquals("*/*[name() = 'h1' and ((position() +0) mod -1 = 0 and position() >= 0)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-last-child(2)
|
||||
$function = new FunctionNode($element, ':', 'nth-last-child', 2);
|
||||
$this->assertEquals("*/*[name() = 'h1' and (position() = last() - 2)]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-of-type(2)
|
||||
$function = new FunctionNode($element, ':', 'nth-of-type', 2);
|
||||
$this->assertEquals("*/h1[position() = 2]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:nth-last-of-type(2)
|
||||
$function = new FunctionNode($element, ':', 'nth-last-of-type', 2);
|
||||
$this->assertEquals("*/h1[position() = last() - 2]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
/*
|
||||
// h1:not(p)
|
||||
$element2 = new ElementNode('*', 'p');
|
||||
$function = new FunctionNode($element, ':', 'not', $element2);
|
||||
|
||||
$this->assertEquals("h1[not()]", (string) $function->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
*/
|
||||
public function getSpecificityValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(new FunctionNode(new ElementNode(), 'function'), 10),
|
||||
array(new FunctionNode(new ElementNode(), 'function', array(
|
||||
new Token(Token::TYPE_IDENTIFIER, 'value', 0),
|
||||
)), 10),
|
||||
array(new FunctionNode(new ElementNode(), 'function', array(
|
||||
new Token(Token::TYPE_STRING, 'value1', 0),
|
||||
new Token(Token::TYPE_NUMBER, 'value2', 0),
|
||||
)), 10),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,14 +14,20 @@ namespace Symfony\Component\CssSelector\Tests\Node;
|
||||
use Symfony\Component\CssSelector\Node\HashNode;
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
|
||||
class HashNodeTest extends \PHPUnit_Framework_TestCase
|
||||
class HashNodeTest extends AbstractNodeTest
|
||||
{
|
||||
public function testToXpath()
|
||||
public function getToStringConversionTestData()
|
||||
{
|
||||
// h1#foo
|
||||
$element = new ElementNode('*', 'h1');
|
||||
$hash = new HashNode($element, 'foo');
|
||||
return array(
|
||||
array(new HashNode(new ElementNode(), 'id'), 'Hash[Element[*]#id]'),
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertEquals("h1[@id = 'foo']", (string) $hash->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
public function getSpecificityValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(new HashNode(new ElementNode(), 'id'), 100),
|
||||
array(new HashNode(new ElementNode(null, 'id'), 'class'), 101),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
<?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\NegationNode;
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
|
||||
class NegationNodeTest extends AbstractNodeTest
|
||||
{
|
||||
public function getToStringConversionTestData()
|
||||
{
|
||||
return array(
|
||||
array(new NegationNode(new ElementNode(), new ClassNode(new ElementNode(), 'class')), 'Negation[Element[*]:not(Class[Element[*].class])]'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSpecificityValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(new NegationNode(new ElementNode(), new ClassNode(new ElementNode(), 'class')), 10),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
<?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\OrNode;
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
|
||||
class OrNodeTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testToXpath()
|
||||
{
|
||||
// h1, h2, h3
|
||||
$element1 = new ElementNode('*', 'h1');
|
||||
$element2 = new ElementNode('*', 'h2');
|
||||
$element3 = new ElementNode('*', 'h3');
|
||||
$or = new OrNode(array($element1, $element2, $element3));
|
||||
|
||||
$this->assertEquals("h1 | h2 | h3", (string) $or->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
}
|
||||
|
||||
public function testIssueMissingPrefix()
|
||||
{
|
||||
// h1, h2, h3
|
||||
$element1 = new ElementNode('*', 'h1');
|
||||
$element2 = new ElementNode('*', 'h2');
|
||||
$element3 = new ElementNode('*', 'h3');
|
||||
$or = new OrNode(array($element1, $element2, $element3));
|
||||
|
||||
$xPath = $or->toXPath();
|
||||
$xPath->addPrefix('descendant-or-self::');
|
||||
|
||||
$this->assertEquals("descendant-or-self::h1 | descendant-or-self::h2 | descendant-or-self::h3", (string) $xPath);
|
||||
}
|
||||
}
|
@ -11,45 +11,22 @@
|
||||
|
||||
namespace Symfony\Component\CssSelector\Tests\Node;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\PseudoNode;
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
use Symfony\Component\CssSelector\Node\PseudoNode;
|
||||
|
||||
class PseudoNodeTest extends \PHPUnit_Framework_TestCase
|
||||
class PseudoNodeTest extends AbstractNodeTest
|
||||
{
|
||||
public function testToXpath()
|
||||
public function getToStringConversionTestData()
|
||||
{
|
||||
$element = new ElementNode('*', 'h1');
|
||||
return array(
|
||||
array(new PseudoNode(new ElementNode(), 'pseudo'), 'Pseudo[Element[*]:pseudo]'),
|
||||
);
|
||||
}
|
||||
|
||||
// h1:checked
|
||||
$pseudo = new PseudoNode($element, ':', 'checked');
|
||||
$this->assertEquals("h1[(@selected or @checked) and (name(.) = 'input' or name(.) = 'option')]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:first-child
|
||||
$pseudo = new PseudoNode($element, ':', 'first-child');
|
||||
$this->assertEquals("*/*[name() = 'h1' and (position() = 1)]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:last-child
|
||||
$pseudo = new PseudoNode($element, ':', 'last-child');
|
||||
$this->assertEquals("*/*[name() = 'h1' and (position() = last())]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:first-of-type
|
||||
$pseudo = new PseudoNode($element, ':', 'first-of-type');
|
||||
$this->assertEquals("*/h1[position() = 1]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:last-of-type
|
||||
$pseudo = new PseudoNode($element, ':', 'last-of-type');
|
||||
$this->assertEquals("*/h1[position() = last()]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:only-child
|
||||
$pseudo = new PseudoNode($element, ':', 'only-child');
|
||||
$this->assertEquals("*/*[name() = 'h1' and (last() = 1)]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:only-of-type
|
||||
$pseudo = new PseudoNode($element, ':', 'only-of-type');
|
||||
$this->assertEquals("h1[last() = 1]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
|
||||
// h1:empty
|
||||
$pseudo = new PseudoNode($element, ':', 'empty');
|
||||
$this->assertEquals("h1[not(*) and not(normalize-space())]", (string) $pseudo->toXpath(), '->toXpath() returns the xpath representation of the node');
|
||||
public function getSpecificityValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(new PseudoNode(new ElementNode(), 'pseudo'), 10),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
<?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 AbstractNodeTest
|
||||
{
|
||||
public function getToStringConversionTestData()
|
||||
{
|
||||
return array(
|
||||
array(new SelectorNode(new ElementNode()), 'Selector[Element[*]]'),
|
||||
array(new SelectorNode(new ElementNode(), 'pseudo'), 'Selector[Element[*]::pseudo]'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSpecificityValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(new SelectorNode(new ElementNode()), 0),
|
||||
array(new SelectorNode(new ElementNode(), 'pseudo'), 1),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<?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\Specificity;
|
||||
|
||||
class SpecificityTest extends \PHPUnit_Framework_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 function getValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(new Specificity(0, 0, 0), 0),
|
||||
array(new Specificity(0, 0, 2), 2),
|
||||
array(new Specificity(0, 3, 0), 30),
|
||||
array(new Specificity(4, 0, 0), 400),
|
||||
array(new Specificity(4, 3, 2), 432),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
<?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\Handler;
|
||||
|
||||
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 AbstractHandlerTest extends \PHPUnit_Framework_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 function getHandleValueTestData();
|
||||
abstract public function getDontHandleValueTestData();
|
||||
abstract protected function generateHandler();
|
||||
|
||||
protected function assertStreamEmpty(TokenStream $stream)
|
||||
{
|
||||
$property = new \ReflectionProperty($stream, 'tokens');
|
||||
$property->setAccessible(true);
|
||||
|
||||
$this->assertEquals(array(), $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));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?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\Handler;
|
||||
|
||||
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 AbstractHandlerTest
|
||||
{
|
||||
/** @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 function getHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
// 2nd argument only exists for inherited method compatibility
|
||||
array('/* comment */', new Token(null, null, null), ''),
|
||||
array('/* comment */foo', new Token(null, null, null), 'foo'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getDontHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array('>'),
|
||||
array('+'),
|
||||
array(' '),
|
||||
);
|
||||
}
|
||||
|
||||
protected function generateHandler()
|
||||
{
|
||||
return new CommentHandler();
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?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\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Handler\HashHandler;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
|
||||
|
||||
class HashHandlerTest extends AbstractHandlerTest
|
||||
{
|
||||
public function getHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array('#id', new Token(Token::TYPE_HASH, 'id', 0), ''),
|
||||
array('#123', new Token(Token::TYPE_HASH, '123', 0), ''),
|
||||
|
||||
array('#id.class', new Token(Token::TYPE_HASH, 'id', 0), '.class'),
|
||||
array('#id element', new Token(Token::TYPE_HASH, 'id', 0), ' element'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getDontHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array('id'),
|
||||
array('123'),
|
||||
array('<'),
|
||||
array('<'),
|
||||
array('#'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function generateHandler()
|
||||
{
|
||||
$patterns = new TokenizerPatterns();
|
||||
|
||||
return new HashHandler($patterns, new TokenizerEscaping($patterns));
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?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\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Handler\IdentifierHandler;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
|
||||
|
||||
class IdentifierHandlerTest extends AbstractHandlerTest
|
||||
{
|
||||
public function getHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array('foo', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), ''),
|
||||
array('foo|bar', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '|bar'),
|
||||
array('foo.class', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '.class'),
|
||||
array('foo[attr]', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '[attr]'),
|
||||
array('foo bar', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), ' bar'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getDontHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array('>'),
|
||||
array('+'),
|
||||
array(' '),
|
||||
array('*|foo'),
|
||||
array('/* comment */'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function generateHandler()
|
||||
{
|
||||
$patterns = new TokenizerPatterns();
|
||||
|
||||
return new IdentifierHandler($patterns, new TokenizerEscaping($patterns));
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
<?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\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Handler\NumberHandler;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
|
||||
|
||||
class NumberHandlerTest extends AbstractHandlerTest
|
||||
{
|
||||
public function getHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array('12', new Token(Token::TYPE_NUMBER, '12', 0), ''),
|
||||
array('12.34', new Token(Token::TYPE_NUMBER, '12.34', 0), ''),
|
||||
array('+12.34', new Token(Token::TYPE_NUMBER, '+12.34', 0), ''),
|
||||
array('-12.34', new Token(Token::TYPE_NUMBER, '-12.34', 0), ''),
|
||||
|
||||
array('12 arg', new Token(Token::TYPE_NUMBER, '12', 0), ' arg'),
|
||||
array('12]', new Token(Token::TYPE_NUMBER, '12', 0), ']'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getDontHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array('hello'),
|
||||
array('>'),
|
||||
array('+'),
|
||||
array(' '),
|
||||
array('/* comment */'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function generateHandler()
|
||||
{
|
||||
$patterns = new TokenizerPatterns();
|
||||
|
||||
return new NumberHandler($patterns, new TokenizerEscaping($patterns));
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?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\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Handler\StringHandler;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
|
||||
|
||||
class StringHandlerTest extends AbstractHandlerTest
|
||||
{
|
||||
public function getHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array('"hello"', new Token(Token::TYPE_STRING, 'hello', 1), ''),
|
||||
array('"1"', new Token(Token::TYPE_STRING, '1', 1), ''),
|
||||
array('" "', new Token(Token::TYPE_STRING, ' ', 1), ''),
|
||||
array('""', new Token(Token::TYPE_STRING, '', 1), ''),
|
||||
array("'hello'", new Token(Token::TYPE_STRING, 'hello', 1), ''),
|
||||
|
||||
array("'foo'bar", new Token(Token::TYPE_STRING, 'foo', 1), 'bar'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getDontHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array('hello'),
|
||||
array('>'),
|
||||
array('1'),
|
||||
array(' '),
|
||||
);
|
||||
}
|
||||
|
||||
protected function generateHandler()
|
||||
{
|
||||
$patterns = new TokenizerPatterns();
|
||||
|
||||
return new StringHandler($patterns, new TokenizerEscaping($patterns));
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
<?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\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Handler\WhitespaceHandler;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
|
||||
class WhitespaceHandlerTest extends AbstractHandlerTest
|
||||
{
|
||||
public function getHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array(' ', new Token(Token::TYPE_WHITESPACE, ' ', 0), ''),
|
||||
array("\n", new Token(Token::TYPE_WHITESPACE, "\n", 0), ''),
|
||||
array("\t", new Token(Token::TYPE_WHITESPACE, "\t", 0), ''),
|
||||
|
||||
array(' foo', new Token(Token::TYPE_WHITESPACE, ' ', 0), 'foo'),
|
||||
array(' .foo', new Token(Token::TYPE_WHITESPACE, ' ', 0), '.foo'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getDontHandleValueTestData()
|
||||
{
|
||||
return array(
|
||||
array('>'),
|
||||
array('1'),
|
||||
array('a'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function generateHandler()
|
||||
{
|
||||
return new WhitespaceHandler();
|
||||
}
|
||||
}
|
247
src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php
Normal file
247
src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php
Normal file
@ -0,0 +1,247 @@
|
||||
<?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 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 \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/** @dataProvider getParserTestData */
|
||||
public function testParser($source, $representation)
|
||||
{
|
||||
$parser = new Parser();
|
||||
|
||||
$this->assertEquals($representation, array_map(function (SelectorNode $node) {
|
||||
return (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->assertEquals(1, count($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->assertEquals(1, count($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->assertEquals(1, count($selectors));
|
||||
|
||||
/** @var FunctionNode $function */
|
||||
$function = $selectors[0]->getTree();
|
||||
$this->assertEquals(array($a, $b), Parser::parseSeries($function->getArguments()));
|
||||
}
|
||||
|
||||
/** @dataProvider getParseSeriesExceptionTestData */
|
||||
public function testParseSeriesException($series)
|
||||
{
|
||||
$parser = new Parser();
|
||||
$selectors = $parser->parse(sprintf(':nth-child(%s)', $series));
|
||||
$this->assertEquals(1, count($selectors));
|
||||
|
||||
/** @var FunctionNode $function */
|
||||
$function = $selectors[0]->getTree();
|
||||
$this->setExpectedException('Symfony\Component\CssSelector\Exception\SyntaxErrorException');
|
||||
Parser::parseSeries($function->getArguments());
|
||||
}
|
||||
|
||||
public function getParserTestData()
|
||||
{
|
||||
return array(
|
||||
array('*', array('Element[*]')),
|
||||
array('*|*', array('Element[*]')),
|
||||
array('*|foo', array('Element[foo]')),
|
||||
array('foo|*', array('Element[foo|*]')),
|
||||
array('foo|bar', array('Element[foo|bar]')),
|
||||
array('#foo#bar', array('Hash[Hash[Element[*]#foo]#bar]')),
|
||||
array('div>.foo', array('CombinedSelector[Element[div] > Class[Element[*].foo]]')),
|
||||
array('div> .foo', array('CombinedSelector[Element[div] > Class[Element[*].foo]]')),
|
||||
array('div >.foo', array('CombinedSelector[Element[div] > Class[Element[*].foo]]')),
|
||||
array('div > .foo', array('CombinedSelector[Element[div] > Class[Element[*].foo]]')),
|
||||
array("div \n> \t \t .foo", array('CombinedSelector[Element[div] > Class[Element[*].foo]]')),
|
||||
array('td.foo,.bar', array('Class[Element[td].foo]', 'Class[Element[*].bar]')),
|
||||
array('td.foo, .bar', array('Class[Element[td].foo]', 'Class[Element[*].bar]')),
|
||||
array("td.foo\t\r\n\f ,\t\r\n\f .bar", array('Class[Element[td].foo]', 'Class[Element[*].bar]')),
|
||||
array('td.foo,.bar', array('Class[Element[td].foo]', 'Class[Element[*].bar]')),
|
||||
array('td.foo, .bar', array('Class[Element[td].foo]', 'Class[Element[*].bar]')),
|
||||
array("td.foo\t\r\n\f ,\t\r\n\f .bar", array('Class[Element[td].foo]', 'Class[Element[*].bar]')),
|
||||
array('div, td.foo, div.bar span', array('Element[div]', 'Class[Element[td].foo]', 'CombinedSelector[Class[Element[div].bar] <followed> Element[span]]')),
|
||||
array('div > p', array('CombinedSelector[Element[div] > Element[p]]')),
|
||||
array('td:first', array('Pseudo[Element[td]:first]')),
|
||||
array('td :first', array('CombinedSelector[Element[td] <followed> Pseudo[Element[*]:first]]')),
|
||||
array('a[name]', array('Attribute[Element[a][name]]')),
|
||||
array("a[ name\t]", array('Attribute[Element[a][name]]')),
|
||||
array('a [name]', array('CombinedSelector[Element[a] <followed> Attribute[Element[*][name]]]')),
|
||||
array('a[rel="include"]', array("Attribute[Element[a][rel = 'include']]")),
|
||||
array('a[rel = include]', array("Attribute[Element[a][rel = 'include']]")),
|
||||
array("a[hreflang |= 'en']", array("Attribute[Element[a][hreflang |= 'en']]")),
|
||||
array('a[hreflang|=en]', array("Attribute[Element[a][hreflang |= 'en']]")),
|
||||
array('div:nth-child(10)', array("Function[Element[div]:nth-child(['10'])]")),
|
||||
array(':nth-child(2n+2)', array("Function[Element[*]:nth-child(['2', 'n', '+2'])]")),
|
||||
array('div:nth-of-type(10)', array("Function[Element[div]:nth-of-type(['10'])]")),
|
||||
array('div div:nth-of-type(10) .aclass', array("CombinedSelector[CombinedSelector[Element[div] <followed> Function[Element[div]:nth-of-type(['10'])]] <followed> Class[Element[*].aclass]]")),
|
||||
array('label:only', array('Pseudo[Element[label]:only]')),
|
||||
array('a:lang(fr)', array("Function[Element[a]:lang(['fr'])]")),
|
||||
array('div:contains("foo")', array("Function[Element[div]:contains(['foo'])]")),
|
||||
array('div#foobar', array('Hash[Element[div]#foobar]')),
|
||||
array('div:not(div.foo)', array('Negation[Element[div]:not(Class[Element[div].foo])]')),
|
||||
array('td ~ th', array('CombinedSelector[Element[td] ~ Element[th]]')),
|
||||
);
|
||||
}
|
||||
|
||||
public function getParserExceptionTestData()
|
||||
{
|
||||
return array(
|
||||
array('attributes(href)/html/body/a', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '(', 10))->getMessage()),
|
||||
array('attributes(href)', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '(', 10))->getMessage()),
|
||||
array('html/body/a', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '/', 4))->getMessage()),
|
||||
array(' ', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_FILE_END, '', 1))->getMessage()),
|
||||
array('div, ', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_FILE_END, '', 5))->getMessage()),
|
||||
array(' , div', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, ',', 1))->getMessage()),
|
||||
array('p, , div', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, ',', 3))->getMessage()),
|
||||
array('div > ', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_FILE_END, '', 6))->getMessage()),
|
||||
array(' > div', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '>', 2))->getMessage()),
|
||||
array('foo|#bar', SyntaxErrorException::unexpectedToken('identifier or "*"', new Token(Token::TYPE_HASH, 'bar', 4))->getMessage()),
|
||||
array('#.foo', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '#', 0))->getMessage()),
|
||||
array('.#foo', SyntaxErrorException::unexpectedToken('identifier', new Token(Token::TYPE_HASH, 'foo', 1))->getMessage()),
|
||||
array(':#foo', SyntaxErrorException::unexpectedToken('identifier', new Token(Token::TYPE_HASH, 'foo', 1))->getMessage()),
|
||||
array('[*]', SyntaxErrorException::unexpectedToken('"|"', new Token(Token::TYPE_DELIMITER, ']', 2))->getMessage()),
|
||||
array('[foo|]', SyntaxErrorException::unexpectedToken('identifier', new Token(Token::TYPE_DELIMITER, ']', 5))->getMessage()),
|
||||
array('[#]', SyntaxErrorException::unexpectedToken('identifier or "*"', new Token(Token::TYPE_DELIMITER, '#', 1))->getMessage()),
|
||||
array('[foo=#]', SyntaxErrorException::unexpectedToken('string or identifier', new Token(Token::TYPE_DELIMITER, '#', 5))->getMessage()),
|
||||
array(':nth-child()', SyntaxErrorException::unexpectedToken('at least one argument', new Token(Token::TYPE_DELIMITER, ')', 11))->getMessage()),
|
||||
array('[href]a', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_IDENTIFIER, 'a', 6))->getMessage()),
|
||||
array('[rel:stylesheet]', SyntaxErrorException::unexpectedToken('operator', new Token(Token::TYPE_DELIMITER, ':', 4))->getMessage()),
|
||||
array('[rel=stylesheet', SyntaxErrorException::unexpectedToken('"]"', new Token(Token::TYPE_FILE_END, '', 15))->getMessage()),
|
||||
array(':lang(fr', SyntaxErrorException::unexpectedToken('an argument', new Token(Token::TYPE_FILE_END, '', 8))->getMessage()),
|
||||
array(':contains("foo', SyntaxErrorException::unclosedString(10)->getMessage()),
|
||||
array('foo!', SyntaxErrorException::unexpectedToken('selector', new Token(Token::TYPE_DELIMITER, '!', 3))->getMessage()),
|
||||
);
|
||||
}
|
||||
|
||||
public function getPseudoElementsTestData()
|
||||
{
|
||||
return array(
|
||||
array('foo', 'Element[foo]', ''),
|
||||
array('*', 'Element[*]', ''),
|
||||
array(':empty', 'Pseudo[Element[*]:empty]', ''),
|
||||
array(':BEfore', 'Element[*]', 'before'),
|
||||
array(':aftER', 'Element[*]', 'after'),
|
||||
array(':First-Line', 'Element[*]', 'first-line'),
|
||||
array(':First-Letter', 'Element[*]', 'first-letter'),
|
||||
array('::befoRE', 'Element[*]', 'before'),
|
||||
array('::AFter', 'Element[*]', 'after'),
|
||||
array('::firsT-linE', 'Element[*]', 'first-line'),
|
||||
array('::firsT-letteR', 'Element[*]', 'first-letter'),
|
||||
array('::Selection', 'Element[*]', 'selection'),
|
||||
array('foo:after', 'Element[foo]', 'after'),
|
||||
array('foo::selection', 'Element[foo]', 'selection'),
|
||||
array('lorem#ipsum ~ a#b.c[href]:empty::selection', 'CombinedSelector[Hash[Element[lorem]#ipsum] ~ Pseudo[Attribute[Class[Hash[Element[a]#b].c][href]]:empty]]', 'selection'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSpecificityTestData()
|
||||
{
|
||||
return array(
|
||||
array('*', 0),
|
||||
array(' foo', 1),
|
||||
array(':empty ', 10),
|
||||
array(':before', 1),
|
||||
array('*:before', 1),
|
||||
array(':nth-child(2)', 10),
|
||||
array('.bar', 10),
|
||||
array('[baz]', 10),
|
||||
array('[baz="4"]', 10),
|
||||
array('[baz^="4"]', 10),
|
||||
array('#lipsum', 100),
|
||||
array(':not(*)', 0),
|
||||
array(':not(foo)', 1),
|
||||
array(':not(.foo)', 10),
|
||||
array(':not([foo])', 10),
|
||||
array(':not(:empty)', 10),
|
||||
array(':not(#foo)', 100),
|
||||
array('foo:empty', 11),
|
||||
array('foo:before', 2),
|
||||
array('foo::before', 2),
|
||||
array('foo:empty::before', 12),
|
||||
array('#lorem + foo#ipsum:first-child > bar:first-line', 213),
|
||||
);
|
||||
}
|
||||
|
||||
public function getParseSeriesTestData()
|
||||
{
|
||||
return array(
|
||||
array('1n+3', 1, 3),
|
||||
array('1n +3', 1, 3),
|
||||
array('1n + 3', 1, 3),
|
||||
array('1n+ 3', 1, 3),
|
||||
array('1n-3', 1, -3),
|
||||
array('1n -3', 1, -3),
|
||||
array('1n - 3', 1, -3),
|
||||
array('1n- 3', 1, -3),
|
||||
array('n-5', 1, -5),
|
||||
array('odd', 2, 1),
|
||||
array('even', 2, 0),
|
||||
array('3n', 3, 0),
|
||||
array('n', 1, 0),
|
||||
array('+n', 1, 0),
|
||||
array('-n', -1, 0),
|
||||
array('5', 0, 5),
|
||||
);
|
||||
}
|
||||
|
||||
public function getParseSeriesExceptionTestData()
|
||||
{
|
||||
return array(
|
||||
array('foo'),
|
||||
array('n+'),
|
||||
);
|
||||
}
|
||||
}
|
101
src/Symfony/Component/CssSelector/Tests/Parser/ReaderTest.php
Normal file
101
src/Symfony/Component/CssSelector/Tests/Parser/ReaderTest.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?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 Symfony\Component\CssSelector\Parser\Reader;
|
||||
|
||||
class ReaderTest extends \PHPUnit_Framework_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(array('hello', 'h'), $reader->findPattern('/^([a-z]).*/'));
|
||||
|
||||
$this->assignPosition($reader, 2);
|
||||
$this->assertFalse($reader->findPattern('/^h.*/'));
|
||||
$this->assertEquals(array('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, $value)
|
||||
{
|
||||
$position = new \ReflectionProperty($reader, 'position');
|
||||
$position->setAccessible(true);
|
||||
$position->setValue($reader, $value);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<?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 Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser;
|
||||
|
||||
/**
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class ClassParserTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/** @dataProvider getParseTestData */
|
||||
public function testParse($source, $representation)
|
||||
{
|
||||
$parser = new ClassParser();
|
||||
$selectors = $parser->parse($source);
|
||||
$this->assertEquals(1, count($selectors));
|
||||
|
||||
/** @var SelectorNode $selector */
|
||||
$selector = $selectors[0];
|
||||
$this->assertEquals($representation, (string) $selector->getTree());
|
||||
}
|
||||
|
||||
public function getParseTestData()
|
||||
{
|
||||
return array(
|
||||
array('.class', 'Class[Element[*].class]'),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
<?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 Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser;
|
||||
|
||||
/**
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class ElementParserTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/** @dataProvider getParseTestData */
|
||||
public function testParse($source, $representation)
|
||||
{
|
||||
$parser = new ElementParser();
|
||||
$selectors = $parser->parse($source);
|
||||
$this->assertEquals(1, count($selectors));
|
||||
|
||||
/** @var SelectorNode $selector */
|
||||
$selector = $selectors[0];
|
||||
$this->assertEquals($representation, (string) $selector->getTree());
|
||||
}
|
||||
|
||||
public function getParseTestData()
|
||||
{
|
||||
return array(
|
||||
array('p', 'Element[p]'),
|
||||
array('*', 'Element[*]'),
|
||||
array('h1', 'Element[h1]'),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?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 Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
use Symfony\Component\CssSelector\Parser\Shortcut\HashParser;
|
||||
|
||||
/**
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class HashParserTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/** @dataProvider getParseTestData */
|
||||
public function testParse($source, $representation)
|
||||
{
|
||||
$parser = new HashParser();
|
||||
$selectors = $parser->parse($source);
|
||||
$this->assertEquals(1, count($selectors));
|
||||
|
||||
/** @var SelectorNode $selector */
|
||||
$selector = $selectors[0];
|
||||
$this->assertEquals($representation, (string) $selector->getTree());
|
||||
}
|
||||
|
||||
public function getParseTestData()
|
||||
{
|
||||
return array(
|
||||
array('#id', 'Hash[Element[*]#id]'),
|
||||
array('h1#main', 'Hash[Element[h1]#main]'),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
<?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 Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\TokenStream;
|
||||
|
||||
class TokenStreamTest extends \PHPUnit_Framework_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()
|
||||
{
|
||||
$this->setExpectedException('Symfony\Component\CssSelector\Exception\SyntaxErrorException');
|
||||
|
||||
$stream = new TokenStream();
|
||||
$stream->push(new Token(Token::TYPE_DELIMITER, '.', 2));
|
||||
$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()
|
||||
{
|
||||
$this->setExpectedException('Symfony\Component\CssSelector\Exception\SyntaxErrorException');
|
||||
|
||||
$stream = new TokenStream();
|
||||
$stream->push(new Token(Token::TYPE_DELIMITER, '.', 2));
|
||||
$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());
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
<?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 Symfony\Component\CssSelector\Tokenizer;
|
||||
|
||||
class TokenizerTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
protected $tokenizer;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$this->tokenizer = new Tokenizer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getCssSelectors
|
||||
*/
|
||||
public function testTokenize($css)
|
||||
{
|
||||
$this->assertEquals($css, $this->tokensToString($this->tokenizer->tokenize($css)), '->tokenize() lexes an input string and returns an array of tokens');
|
||||
}
|
||||
|
||||
public function testTokenizeWithQuotedStrings()
|
||||
{
|
||||
$this->assertEquals('foo[class=foo bar ]', $this->tokensToString($this->tokenizer->tokenize('foo[class="foo bar"]')), '->tokenize() lexes an input string and returns an array of tokens');
|
||||
$this->assertEquals("foo[class=foo Abar ]", $this->tokensToString($this->tokenizer->tokenize('foo[class="foo \\65 bar"]')), '->tokenize() lexes an input string and returns an array of tokens');
|
||||
$this->assertEquals("img[alt= ]", $this->tokensToString($this->tokenizer->tokenize('img[alt=""]')), '->tokenize() lexes an input string and returns an array of tokens');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\CssSelector\Exception\ParseException
|
||||
*/
|
||||
public function testTokenizeInvalidString()
|
||||
{
|
||||
$this->tokensToString($this->tokenizer->tokenize('/invalid'));
|
||||
}
|
||||
|
||||
public function getCssSelectors()
|
||||
{
|
||||
return array(
|
||||
array('h1'),
|
||||
array('h1:nth-child(3n+1)'),
|
||||
array('h1 > p'),
|
||||
array('h1#foo'),
|
||||
array('h1.foo'),
|
||||
array('h1[class*=foo]'),
|
||||
array('h1 .foo'),
|
||||
array('h1 #foo'),
|
||||
array('h1 [class*=foo]'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function tokensToString($tokens)
|
||||
{
|
||||
$str = '';
|
||||
foreach ($tokens as $token) {
|
||||
$str .= str_repeat(' ', $token->getPosition() - strlen($str)).$token;
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<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>
|
||||
</body></html>
|
@ -0,0 +1,11 @@
|
||||
<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>
|
@ -0,0 +1,308 @@
|
||||
<!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>
|
308
src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php
Normal file
308
src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php
Normal file
@ -0,0 +1,308 @@
|
||||
<?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 Symfony\Component\CssSelector\XPath\Extension\HtmlExtension;
|
||||
use Symfony\Component\CssSelector\XPath\Translator;
|
||||
|
||||
class TranslatorTest extends \PHPUnit_Framework_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 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->assertEquals(count($elementsId), count($elements));
|
||||
foreach ($elements as $element) {
|
||||
$this->assertTrue(in_array($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;
|
||||
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), $elementsId);
|
||||
foreach ($elements as $element) {
|
||||
if (null !== $element->attributes()->id) {
|
||||
$this->assertTrue(in_array($element->attributes()->id, $elementsId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @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->assertEquals($count, count($elements));
|
||||
}
|
||||
|
||||
public function getXpathLiteralTestData()
|
||||
{
|
||||
return array(
|
||||
array('foo', "'foo'"),
|
||||
array("foo's bar", '"foo\'s bar"'),
|
||||
array("foo's \"middle\" bar", 'concat(\'foo\', "\'", \'s "middle" bar\')'),
|
||||
array("foo's 'middle' \"bar\"", 'concat(\'foo\', "\'", \'s \', "\'", \'middle\', "\'", \' "bar"\')'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getCssToXPathTestData()
|
||||
{
|
||||
return array(
|
||||
array('*', "*"),
|
||||
array('e', "e"),
|
||||
array('*|e', "e"),
|
||||
array('e|f', "e:f"),
|
||||
array('e[foo]', "e[@foo]"),
|
||||
array('e[foo|bar]', "e[@foo:bar]"),
|
||||
array('e[foo="bar"]', "e[@foo = 'bar']"),
|
||||
array('e[foo~="bar"]', "e[@foo and contains(concat(' ', normalize-space(@foo), ' '), ' bar ')]"),
|
||||
array('e[foo^="bar"]', "e[@foo and starts-with(@foo, 'bar')]"),
|
||||
array('e[foo$="bar"]', "e[@foo and substring(@foo, string-length(@foo)-2) = 'bar']"),
|
||||
array('e[foo*="bar"]', "e[@foo and contains(@foo, 'bar')]"),
|
||||
array('e[hreflang|="en"]', "e[@hreflang and (@hreflang = 'en' or starts-with(@hreflang, 'en-'))]"),
|
||||
array('e:nth-child(1)', "*/*[name() = 'e' and (position() = 1)]"),
|
||||
array('e:nth-last-child(1)', "*/*[name() = 'e' and (position() = last() - 1)]"),
|
||||
array('e:nth-last-child(2n+2)', "*/*[name() = 'e' and ((position() +2) mod -2 = 0 and position() < (last() -2))]"),
|
||||
array('e:nth-of-type(1)', "*/e[position() = 1]"),
|
||||
array('e:nth-last-of-type(1)', "*/e[position() = last() - 1]"),
|
||||
array('e:nth-last-of-type(1)', "*/e[position() = last() - 1]"),
|
||||
array('div e:nth-last-of-type(1) .aclass', "div/descendant-or-self::*/e[position() = last() - 1]/descendant-or-self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' aclass ')]"),
|
||||
array('e:first-child', "*/*[name() = 'e' and (position() = 1)]"),
|
||||
array('e:last-child', "*/*[name() = 'e' and (position() = last())]"),
|
||||
array('e:first-of-type', "*/e[position() = 1]"),
|
||||
array('e:last-of-type', "*/e[position() = last()]"),
|
||||
array('e:only-child', "*/*[name() = 'e' and (last() = 1)]"),
|
||||
array('e:only-of-type', "e[last() = 1]"),
|
||||
array('e:empty', "e[not(*) and not(string-length())]"),
|
||||
array('e:EmPTY', "e[not(*) and not(string-length())]"),
|
||||
array('e:root', "e[not(parent::*)]"),
|
||||
array('e:hover', "e[0]"),
|
||||
array('e:contains("foo")', "e[contains(string(.), 'foo')]"),
|
||||
array('e:ConTains(foo)', "e[contains(string(.), 'foo')]"),
|
||||
array('e.warning', "e[@class and contains(concat(' ', normalize-space(@class), ' '), ' warning ')]"),
|
||||
array('e#myid', "e[@id = 'myid']"),
|
||||
array('e:not(:nth-child(odd))', "e[not((position() -1) mod 2 = 0 and position() >= 1)]"),
|
||||
array('e:nOT(*)', "e[0]"),
|
||||
array('e f', "e/descendant-or-self::*/f"),
|
||||
array('e > f', "e/f"),
|
||||
array('e + f', "e/following-sibling::*[name() = 'f' and (position() = 1)]"),
|
||||
array('e ~ f', "e/following-sibling::f"),
|
||||
array('div#container p', "div[@id = 'container']/descendant-or-self::*/p"),
|
||||
);
|
||||
}
|
||||
|
||||
public function getXmlLangTestData()
|
||||
{
|
||||
return array(
|
||||
array(':lang("EN")', array('first', 'second', 'third', 'fourth')),
|
||||
array(':lang("en-us")', array('second', 'fourth')),
|
||||
array(':lang(en-nz)', array('third')),
|
||||
array(':lang(fr)', array('fifth')),
|
||||
array(':lang(ru)', array('sixth')),
|
||||
array(":lang('ZH')", array('eighth')),
|
||||
array(':lang(de) :lang(zh)', array('eighth')),
|
||||
array(':lang(en), :lang(zh)', array('first', 'second', 'third', 'fourth', 'eighth')),
|
||||
array(':lang(es)', array()),
|
||||
);
|
||||
}
|
||||
|
||||
public function getHtmlIdsTestData()
|
||||
{
|
||||
return array(
|
||||
array('div', array('outer-div', 'li-div', 'foobar-div')),
|
||||
array('DIV', array('outer-div', 'li-div', 'foobar-div')), // case-insensitive in HTML
|
||||
array('div div', array('li-div')),
|
||||
array('div, div div', array('outer-div', 'li-div', 'foobar-div')),
|
||||
array('a[name]', array('name-anchor')),
|
||||
array('a[NAme]', array('name-anchor')), // case-insensitive in HTML:
|
||||
array('a[rel]', array('tag-anchor', 'nofollow-anchor')),
|
||||
array('a[rel="tag"]', array('tag-anchor')),
|
||||
array('a[href*="localhost"]', array('tag-anchor')),
|
||||
array('a[href*=""]', array()),
|
||||
array('a[href^="http"]', array('tag-anchor', 'nofollow-anchor')),
|
||||
array('a[href^="http:"]', array('tag-anchor')),
|
||||
array('a[href^=""]', array()),
|
||||
array('a[href$="org"]', array('nofollow-anchor')),
|
||||
array('a[href$=""]', array()),
|
||||
array('div[foobar~="bc"]', array('foobar-div')),
|
||||
array('div[foobar~="cde"]', array('foobar-div')),
|
||||
array('[foobar~="ab bc"]', array('foobar-div')),
|
||||
array('[foobar~=""]', array()),
|
||||
array('[foobar~=" \t"]', array()),
|
||||
array('div[foobar~="cd"]', array()),
|
||||
array('*[lang|="En"]', array('second-li')),
|
||||
array('[lang|="En-us"]', array('second-li')),
|
||||
// Attribute values are case sensitive
|
||||
array('*[lang|="en"]', array()),
|
||||
array('[lang|="en-US"]', array()),
|
||||
array('*[lang|="e"]', array()),
|
||||
// ... :lang() is not.
|
||||
array(':lang("EN")', array('second-li', 'li-div')),
|
||||
array('*:lang(en-US)', array('second-li', 'li-div')),
|
||||
array(':lang("e")', array()),
|
||||
array('li:nth-child(3)', array('third-li')),
|
||||
array('li:nth-child(10)', array()),
|
||||
array('li:nth-child(2n)', array('second-li', 'fourth-li', 'sixth-li')),
|
||||
array('li:nth-child(even)', array('second-li', 'fourth-li', 'sixth-li')),
|
||||
array('li:nth-child(2n+0)', array('second-li', 'fourth-li', 'sixth-li')),
|
||||
array('li:nth-child(+2n+1)', array('first-li', 'third-li', 'fifth-li', 'seventh-li')),
|
||||
array('li:nth-child(odd)', array('first-li', 'third-li', 'fifth-li', 'seventh-li')),
|
||||
array('li:nth-child(2n+4)', array('fourth-li', 'sixth-li')),
|
||||
// FIXME: I'm not 100% sure this is right:
|
||||
array('li:nth-child(3n+1)', array('first-li', 'fourth-li', 'seventh-li')),
|
||||
array('li:nth-last-child(0)', array('seventh-li')),
|
||||
array('li:nth-last-child(2n)', array('second-li', 'fourth-li', 'sixth-li')),
|
||||
array('li:nth-last-child(even)', array('second-li', 'fourth-li', 'sixth-li')),
|
||||
array('li:nth-last-child(2n+2)', array('second-li', 'fourth-li')),
|
||||
array('ol:first-of-type', array('first-ol')),
|
||||
array('ol:nth-child(1)', array()),
|
||||
array('ol:nth-of-type(2)', array('second-ol')),
|
||||
// FIXME: like above (1) or (2)?
|
||||
array('ol:nth-last-of-type(1)', array('first-ol')),
|
||||
array('span:only-child', array('foobar-span')),
|
||||
array('li div:only-child', array('li-div')),
|
||||
array('div *:only-child', array('li-div', 'foobar-span')),
|
||||
array('p:only-of-type', array('paragraph')),
|
||||
array('a:empty', array('name-anchor')),
|
||||
array('a:EMpty', array('name-anchor')),
|
||||
array('li:empty', array('third-li', 'fourth-li', 'fifth-li', 'sixth-li')),
|
||||
array(':root', array('html')),
|
||||
array('html:root', array('html')),
|
||||
array('li:root', array()),
|
||||
array('* :root', array()),
|
||||
array('*:contains("link")', array('html', 'outer-div', 'tag-anchor', 'nofollow-anchor')),
|
||||
array(':CONtains("link")', array('html', 'outer-div', 'tag-anchor', 'nofollow-anchor')),
|
||||
array('*:contains("LInk")', array()), // case sensitive
|
||||
array('*:contains("e")', array('html', 'nil', 'outer-div', 'first-ol', 'first-li', 'paragraph', 'p-em')),
|
||||
array('*:contains("E")', array()), // case-sensitive
|
||||
array('.a', array('first-ol')),
|
||||
array('.b', array('first-ol')),
|
||||
array('*.a', array('first-ol')),
|
||||
array('ol.a', array('first-ol')),
|
||||
array('.c', array('first-ol', 'third-li', 'fourth-li')),
|
||||
array('*.c', array('first-ol', 'third-li', 'fourth-li')),
|
||||
array('ol *.c', array('third-li', 'fourth-li')),
|
||||
array('ol li.c', array('third-li', 'fourth-li')),
|
||||
array('li ~ li.c', array('third-li', 'fourth-li')),
|
||||
array('ol > li.c', array('third-li', 'fourth-li')),
|
||||
array('#first-li', array('first-li')),
|
||||
array('li#first-li', array('first-li')),
|
||||
array('*#first-li', array('first-li')),
|
||||
array('li div', array('li-div')),
|
||||
array('li > div', array('li-div')),
|
||||
array('div div', array('li-div')),
|
||||
array('div > div', array()),
|
||||
array('div>.c', array('first-ol')),
|
||||
array('div > .c', array('first-ol')),
|
||||
array('div + div', array('foobar-div')),
|
||||
array('a ~ a', array('tag-anchor', 'nofollow-anchor')),
|
||||
array('a[rel="tag"] ~ a', array('nofollow-anchor')),
|
||||
array('ol#first-ol li:last-child', array('seventh-li')),
|
||||
array('ol#first-ol *:last-child', array('li-div', 'seventh-li')),
|
||||
array('#outer-div:first-child', array('outer-div')),
|
||||
array('#outer-div :first-child', array('name-anchor', 'first-li', 'li-div', 'p-b', 'checkbox-fieldset-disabled', 'area-href')),
|
||||
array('a[href]', array('tag-anchor', 'nofollow-anchor')),
|
||||
array(':not(*)', array()),
|
||||
array('a:not([href])', array('name-anchor')),
|
||||
array('ol :Not(li[class])', array('first-li', 'second-li', 'li-div', 'fifth-li', 'sixth-li', 'seventh-li')),
|
||||
// HTML-specific
|
||||
array(':link', array('link-href', 'tag-anchor', 'nofollow-anchor', 'area-href')),
|
||||
array(':visited', array()),
|
||||
array(':enabled', array('link-href', 'tag-anchor', 'nofollow-anchor', 'checkbox-unchecked', 'text-checked', 'checkbox-checked', 'area-href')),
|
||||
array(':disabled', array('checkbox-disabled', 'checkbox-disabled-checked', 'fieldset', 'checkbox-fieldset-disabled')),
|
||||
array(':checked', array('checkbox-checked', 'checkbox-disabled-checked')),
|
||||
);
|
||||
}
|
||||
|
||||
public function getHtmlShakespearTestData()
|
||||
{
|
||||
return array(
|
||||
array('*', 246),
|
||||
array('div:contains(CELIA)', 26),
|
||||
array('div:only-child', 22), // ?
|
||||
array('div:nth-child(even)', 106),
|
||||
array('div:nth-child(2n)', 106),
|
||||
array('div:nth-child(odd)', 137),
|
||||
array('div:nth-child(2n+1)', 137),
|
||||
array('div:nth-child(n)', 243),
|
||||
array('div:last-child', 53),
|
||||
array('div:first-child', 51),
|
||||
array('div > div', 242),
|
||||
array('div + div', 190),
|
||||
array('div ~ div', 190),
|
||||
array('body', 1),
|
||||
array('body div', 243),
|
||||
array('div', 243),
|
||||
array('div div', 242),
|
||||
array('div div div', 241),
|
||||
array('div, div, div', 243),
|
||||
array('div, a, span', 243),
|
||||
array('.dialog', 51),
|
||||
array('div.dialog', 51),
|
||||
array('div .dialog', 51),
|
||||
array('div.character, div.dialog', 99),
|
||||
array('div.direction.dialog', 0),
|
||||
array('div.dialog.direction', 0),
|
||||
array('div.dialog.scene', 1),
|
||||
array('div.scene.scene', 1),
|
||||
array('div.scene .scene', 0),
|
||||
array('div.direction .dialog ', 0),
|
||||
array('div .dialog .direction', 4),
|
||||
array('div.dialog .dialog .direction', 4),
|
||||
array('#speech5', 1),
|
||||
array('div#speech5', 1),
|
||||
array('div #speech5', 1),
|
||||
array('div.scene div.dialog', 49),
|
||||
array('div#scene1 div.dialog div', 142),
|
||||
array('#scene1 #speech1', 1),
|
||||
array('div[class]', 103),
|
||||
array('div[class=dialog]', 50),
|
||||
array('div[class^=dia]', 51),
|
||||
array('div[class$=log]', 50),
|
||||
array('div[class*=sce]', 1),
|
||||
array('div[class|=dialog]', 50), // ? Seems right
|
||||
array('div[class!=madeup]', 243), // ? Seems right
|
||||
array('div[class~=dialog]', 51), // ? Seems right
|
||||
);
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
<?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 Symfony\Component\CssSelector\XPathExpr;
|
||||
|
||||
class XPathExprTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider getXPathLiteralValues
|
||||
*/
|
||||
public function testXpathLiteral($value, $literal)
|
||||
{
|
||||
$this->assertEquals($literal, XPathExpr::xpathLiteral($value));
|
||||
}
|
||||
|
||||
public function getXPathLiteralValues()
|
||||
{
|
||||
return array(
|
||||
array('foo', "'foo'"),
|
||||
array("foo's bar", '"foo\'s bar"'),
|
||||
array("foo's \"middle\" bar", 'concat(\'foo\', "\'", \'s "middle" bar\')'),
|
||||
array("foo's 'middle' \"bar\"", 'concat(\'foo\', "\'", \'s \', "\'", \'middle\', "\'", \' "bar"\')'),
|
||||
);
|
||||
}
|
||||
}
|
31
src/Symfony/Component/CssSelector/Tests/bootstrap.php
Normal file
31
src/Symfony/Component/CssSelector/Tests/bootstrap.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
spl_autoload_register(function ($class) {
|
||||
if (0 === strpos(ltrim($class, '/'), 'Symfony\Component\CssSelector')) {
|
||||
if (file_exists($file = __DIR__.'/../'.substr(str_replace('\\', '/', $class), strlen('Symfony\Component\CssSelector')).'.php')) {
|
||||
require_once $file;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (file_exists($loader = __DIR__.'/../vendor/autoload.php')) {
|
||||
require_once $loader;
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Token represents a CSS Selector token.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Token
|
||||
{
|
||||
private $type;
|
||||
private $value;
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $type The type of this token.
|
||||
* @param mixed $value The value of this token.
|
||||
* @param integer $position The order of this token.
|
||||
*/
|
||||
public function __construct($type, $value, $position)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->value = $value;
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string representation of this token.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return (string) $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers whether this token's type equals to $type.
|
||||
*
|
||||
* @param string $type The type to test against this token's one.
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
public function isType($type)
|
||||
{
|
||||
return $this->type == $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the position of this token.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getPosition()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* TokenStream represents a stream of CSS Selector tokens.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class TokenStream
|
||||
{
|
||||
private $used;
|
||||
private $tokens;
|
||||
private $source;
|
||||
private $peeked;
|
||||
private $peeking;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $tokens The tokens that make the stream.
|
||||
* @param mixed $source The source of the stream.
|
||||
*/
|
||||
public function __construct($tokens, $source = null)
|
||||
{
|
||||
$this->used = array();
|
||||
$this->tokens = $tokens;
|
||||
$this->source = $source;
|
||||
$this->peeked = null;
|
||||
$this->peeking = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tokens that have already been visited in this stream.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUsed()
|
||||
{
|
||||
return $this->used;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next token in the stream or null if there is none.
|
||||
* Note that if this stream was set to be peeking its behavior
|
||||
* will be restored to not peeking after this operation.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
if ($this->peeking) {
|
||||
$this->peeking = false;
|
||||
$this->used[] = $this->peeked;
|
||||
|
||||
return $this->peeked;
|
||||
}
|
||||
|
||||
if (!count($this->tokens)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$next = array_shift($this->tokens);
|
||||
$this->used[] = $next;
|
||||
|
||||
return $next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peeks for the next token in this stream. This means that the next token
|
||||
* will be returned but it won't be considered as used (visited) until the
|
||||
* next() method is invoked.
|
||||
* If there are no remaining tokens null will be returned.
|
||||
*
|
||||
* @see next()
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function peek()
|
||||
{
|
||||
if (!$this->peeking) {
|
||||
if (!count($this->tokens)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->peeked = array_shift($this->tokens);
|
||||
|
||||
$this->peeking = true;
|
||||
}
|
||||
|
||||
return $this->peeked;
|
||||
}
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
<?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\Exception\ParseException;
|
||||
|
||||
/**
|
||||
* Tokenizer lexes a CSS Selector to tokens.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Tokenizer
|
||||
{
|
||||
/**
|
||||
* Takes a CSS selector and returns an array holding the Tokens
|
||||
* it contains.
|
||||
*
|
||||
* @param string $s The selector to lex.
|
||||
*
|
||||
* @return array Token[]
|
||||
*/
|
||||
public function tokenize($s)
|
||||
{
|
||||
if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) {
|
||||
$mbEncoding = mb_internal_encoding();
|
||||
mb_internal_encoding('ASCII');
|
||||
}
|
||||
|
||||
$tokens = array();
|
||||
$pos = 0;
|
||||
$s = preg_replace('#/\*.*?\*/#s', '', $s);
|
||||
|
||||
while (true) {
|
||||
if (preg_match('#\s+#A', $s, $match, 0, $pos)) {
|
||||
$precedingWhitespacePos = $pos;
|
||||
$pos += strlen($match[0]);
|
||||
} else {
|
||||
$precedingWhitespacePos = 0;
|
||||
}
|
||||
|
||||
if ($pos >= strlen($s)) {
|
||||
if (isset($mbEncoding)) {
|
||||
mb_internal_encoding($mbEncoding);
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
if (preg_match('#[+-]?\d*n(?:[+-]\d+)?#A', $s, $match, 0, $pos) && 'n' !== $match[0]) {
|
||||
$sym = substr($s, $pos, strlen($match[0]));
|
||||
$tokens[] = new Token('Symbol', $sym, $pos);
|
||||
$pos += strlen($match[0]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$c = $s[$pos];
|
||||
$c2 = substr($s, $pos, 2);
|
||||
if (in_array($c2, array('~=', '|=', '^=', '$=', '*=', '::', '!='))) {
|
||||
$tokens[] = new Token('Token', $c2, $pos);
|
||||
$pos += 2;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($c, array('>', '+', '~', ',', '.', '*', '=', '[', ']', '(', ')', '|', ':', '#'))) {
|
||||
if (in_array($c, array('.', '#', '[')) && $precedingWhitespacePos > 0) {
|
||||
$tokens[] = new Token('Token', ' ', $precedingWhitespacePos);
|
||||
}
|
||||
$tokens[] = new Token('Token', $c, $pos);
|
||||
++$pos;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('"' === $c || "'" === $c) {
|
||||
// Quoted string
|
||||
$oldPos = $pos;
|
||||
list($sym, $pos) = $this->tokenizeEscapedString($s, $pos);
|
||||
|
||||
$tokens[] = new Token('String', $sym, $oldPos);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldPos = $pos;
|
||||
list($sym, $pos) = $this->tokenizeSymbol($s, $pos);
|
||||
|
||||
$tokens[] = new Token('Symbol', $sym, $oldPos);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenizes a quoted string (i.e. 'A string quoted with \' characters'),
|
||||
* and returns an array holding the unquoted string contained by $s and
|
||||
* the new position from which tokenizing should take over.
|
||||
*
|
||||
* @param string $s The selector string containing the quoted string.
|
||||
* @param integer $pos The starting position for the quoted string.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws ParseException When expected closing is not found
|
||||
*/
|
||||
private function tokenizeEscapedString($s, $pos)
|
||||
{
|
||||
$quote = $s[$pos];
|
||||
|
||||
$pos = $pos + 1;
|
||||
$start = $pos;
|
||||
while (true) {
|
||||
$next = strpos($s, $quote, $pos);
|
||||
if (false === $next) {
|
||||
throw new ParseException(sprintf('Expected closing %s for string in: %s', $quote, substr($s, $start)));
|
||||
}
|
||||
|
||||
$result = substr($s, $start, $next - $start);
|
||||
if (strlen($result) > 0 && '\\' === $result[strlen($result) - 1]) {
|
||||
// next quote character is escaped
|
||||
$pos = $next + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false !== strpos($result, '\\')) {
|
||||
$result = $this->unescapeStringLiteral($result);
|
||||
}
|
||||
|
||||
return array($result, $next + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes a string literal and returns the unescaped string.
|
||||
*
|
||||
* @param string $literal The string literal to unescape.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws ParseException When invalid escape sequence is found
|
||||
*/
|
||||
private function unescapeStringLiteral($literal)
|
||||
{
|
||||
return preg_replace_callback('#(\\\\(?:[A-Fa-f0-9]{1,6}(?:\r\n|\s)?|[^A-Fa-f0-9]))#', function ($matches) use ($literal) {
|
||||
if ($matches[0][0] == '\\' && strlen($matches[0]) > 1) {
|
||||
$matches[0] = substr($matches[0], 1);
|
||||
if (in_array($matches[0][0], array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b', 'c', 'd', 'e', 'f'))) {
|
||||
return chr(trim($matches[0]));
|
||||
}
|
||||
} else {
|
||||
throw new ParseException(sprintf('Invalid escape sequence %s in string %s', $matches[0], $literal));
|
||||
}
|
||||
}, $literal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexes selector $s and returns an array holding the name of the symbol
|
||||
* contained in it and the new position from which tokenizing should take
|
||||
* over.
|
||||
*
|
||||
* @param string $s The selector string.
|
||||
* @param integer $pos The position in $s at which the symbol starts.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws ParseException When Unexpected symbol is found
|
||||
*/
|
||||
private function tokenizeSymbol($s, $pos)
|
||||
{
|
||||
$start = $pos;
|
||||
|
||||
if (!preg_match('#[^\w\-]#', $s, $match, PREG_OFFSET_CAPTURE, $pos)) {
|
||||
// Goes to end of s
|
||||
return array(substr($s, $start), strlen($s));
|
||||
}
|
||||
|
||||
$matchStart = $match[0][1];
|
||||
|
||||
if ($matchStart == $pos) {
|
||||
throw new ParseException(sprintf('Unexpected symbol: %s at %s', $s[$pos], $pos));
|
||||
}
|
||||
|
||||
$result = substr($s, $start, $matchStart - $start);
|
||||
$pos = $matchStart;
|
||||
|
||||
return array($result, $pos);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
abstract class AbstractExtension implements ExtensionInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNodeTranslators()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCombinationTranslators()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctionTranslators()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPseudoClassTranslators()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAttributeMatchingTranslators()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class AttributeMatchingExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAttributeMatchingTranslators()
|
||||
{
|
||||
return array(
|
||||
'exists' => array($this, 'translateExists'),
|
||||
'=' => array($this, 'translateEquals'),
|
||||
'~=' => array($this, 'translateIncludes'),
|
||||
'|=' => array($this, 'translateDashMatch'),
|
||||
'^=' => array($this, 'translatePrefixMatch'),
|
||||
'$=' => array($this, 'translateSuffixMatch'),
|
||||
'*=' => array($this, 'translateSubstringMatch'),
|
||||
'!=' => array($this, 'translateDifferent'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateExists(XPathExpr $xpath, $attribute, $value)
|
||||
{
|
||||
return $xpath->addCondition($attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateEquals(XPathExpr $xpath, $attribute, $value)
|
||||
{
|
||||
return $xpath->addCondition(sprintf('%s = %s', $attribute, Translator::getXpathLiteral($value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateIncludes(XPathExpr $xpath, $attribute, $value)
|
||||
{
|
||||
return $xpath->addCondition($value ? sprintf(
|
||||
'%1$s and contains(concat(\' \', normalize-space(%1$s), \' \'), %2$s)',
|
||||
$attribute,
|
||||
Translator::getXpathLiteral(' '.$value.' ')
|
||||
) : '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateDashMatch(XPathExpr $xpath, $attribute, $value)
|
||||
{
|
||||
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.'-')
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translatePrefixMatch(XPathExpr $xpath, $attribute, $value)
|
||||
{
|
||||
return $xpath->addCondition($value ? sprintf(
|
||||
'%1$s and starts-with(%1$s, %2$s)',
|
||||
$attribute,
|
||||
Translator::getXpathLiteral($value)
|
||||
) : '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateSuffixMatch(XPathExpr $xpath, $attribute, $value)
|
||||
{
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateSubstringMatch(XPathExpr $xpath, $attribute, $value)
|
||||
{
|
||||
return $xpath->addCondition($value ? sprintf(
|
||||
'%1$s and contains(%1$s, %2$s)',
|
||||
$attribute,
|
||||
Translator::getXpathLiteral($value)
|
||||
) : '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateDifferent(XPathExpr $xpath, $attribute, $value)
|
||||
{
|
||||
return $xpath->addCondition(sprintf(
|
||||
$value ? 'not(%1$s) or %1$s != %2$s' : '%s != %s',
|
||||
$attribute,
|
||||
Translator::getXpathLiteral($value)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'attribute-matching';
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class CombinationExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCombinationTranslators()
|
||||
{
|
||||
return array(
|
||||
' ' => array($this, 'translateDescendant'),
|
||||
'>' => array($this, 'translateChild'),
|
||||
'+' => array($this, 'translateDirectAdjacent'),
|
||||
'~' => array($this, 'translateIndirectAdjacent'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param XPathExpr $combinedXpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateDescendant(XPathExpr $xpath, XPathExpr $combinedXpath)
|
||||
{
|
||||
return $xpath->join('/descendant-or-self::*/', $combinedXpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param XPathExpr $combinedXpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateChild(XPathExpr $xpath, XPathExpr $combinedXpath)
|
||||
{
|
||||
return $xpath->join('/', $combinedXpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param XPathExpr $combinedXpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath)
|
||||
{
|
||||
return $xpath
|
||||
->join('/following-sibling::', $combinedXpath)
|
||||
->addNameTest()
|
||||
->addCondition('position() = 1');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param XPathExpr $combinedXpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath)
|
||||
{
|
||||
return $xpath->join('/following-sibling::', $combinedXpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'combination';
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
<?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 extension interface.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
interface ExtensionInterface
|
||||
{
|
||||
/**
|
||||
* Returns node translators.
|
||||
*
|
||||
* @return callable[]
|
||||
*/
|
||||
public function getNodeTranslators();
|
||||
|
||||
/**
|
||||
* Returns combination translators.
|
||||
*
|
||||
* @return callable[]
|
||||
*/
|
||||
public function getCombinationTranslators();
|
||||
|
||||
/**
|
||||
* Returns function translators.
|
||||
*
|
||||
* @return callable[]
|
||||
*/
|
||||
public function getFunctionTranslators();
|
||||
|
||||
/**
|
||||
* Returns pseudo-class translators.
|
||||
*
|
||||
* @return callable[]
|
||||
*/
|
||||
public function getPseudoClassTranslators();
|
||||
|
||||
/**
|
||||
* Returns attribute operation translators.
|
||||
*
|
||||
* @return callable[]
|
||||
*/
|
||||
public function getAttributeMatchingTranslators();
|
||||
|
||||
/**
|
||||
* Returns extension name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
}
|
@ -0,0 +1,198 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class FunctionExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctionTranslators()
|
||||
{
|
||||
return array(
|
||||
'nth-child' => array($this, 'translateNthChild'),
|
||||
'nth-last-child' => array($this, 'translateNthLastChild'),
|
||||
'nth-of-type' => array($this, 'translateNthOfType'),
|
||||
'nth-last-of-type' => array($this, 'translateNthLastOfType'),
|
||||
'contains' => array($this, 'translateContains'),
|
||||
'lang' => array($this, 'translateLang'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param FunctionNode $function
|
||||
* @param boolean $last
|
||||
* @param boolean $addNameTest
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function translateNthChild(XPathExpr $xpath, FunctionNode $function, $last = false, $addNameTest = true)
|
||||
{
|
||||
try {
|
||||
list($a, $b) = Parser::parseSeries($function->getArguments());
|
||||
} catch (SyntaxErrorException $e) {
|
||||
throw new ExpressionErrorException('Invalid series: '.implode(', ', $function->getArguments()), 0, $e);
|
||||
}
|
||||
|
||||
$xpath->addStarPrefix();
|
||||
if ($addNameTest) {
|
||||
$xpath->addNameTest();
|
||||
}
|
||||
|
||||
if (0 === $a) {
|
||||
return $xpath->addCondition('position() = '.($last ? 'last() - '.$b : $b));
|
||||
}
|
||||
|
||||
if ($last) {
|
||||
// todo: verify if this is right
|
||||
$a = - $a;
|
||||
$b = - $b;
|
||||
}
|
||||
|
||||
$conditions = 1 === $a
|
||||
? array()
|
||||
: array(sprintf('(position() %s) mod %s = 0', $b > 0 ? (string) (- $b) : '+'.(- $b), $a));
|
||||
|
||||
if ($b >= 0) {
|
||||
$conditions[] = 'position() >= '.$b;
|
||||
} elseif ($last) {
|
||||
$conditions[] = sprintf('position() < (last() %s)', $b);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
return empty($conditions) ? $xpath : $xpath->addCondition(implode(' and ', $conditions));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param FunctionNode $function
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function)
|
||||
{
|
||||
return $this->translateNthChild($xpath, $function, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param FunctionNode $function
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateNthOfType(XPathExpr $xpath, FunctionNode $function)
|
||||
{
|
||||
return $this->translateNthChild($xpath, $function, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param FunctionNode $function
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function)
|
||||
{
|
||||
if ('*' === $xpath->getElement()) {
|
||||
throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.');
|
||||
}
|
||||
|
||||
return $this->translateNthChild($xpath, $function, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param FunctionNode $function
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function translateContains(XPathExpr $xpath, FunctionNode $function)
|
||||
{
|
||||
$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())
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param FunctionNode $function
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function translateLang(XPathExpr $xpath, FunctionNode $function)
|
||||
{
|
||||
$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())
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'function';
|
||||
}
|
||||
}
|
@ -0,0 +1,238 @@
|
||||
<?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 cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class HtmlExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Translator $translator
|
||||
*/
|
||||
public function __construct(Translator $translator)
|
||||
{
|
||||
$translator
|
||||
->getExtension('node')
|
||||
->setFlag(NodeExtension::ELEMENT_NAME_IN_LOWER_CASE, true)
|
||||
->setFlag(NodeExtension::ATTRIBUTE_NAME_IN_LOWER_CASE, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPseudoClassTranslators()
|
||||
{
|
||||
return array(
|
||||
'checked' => array($this, 'translateChecked'),
|
||||
'link' => array($this, 'translateLink'),
|
||||
'disabled' => array($this, 'translateDisabled'),
|
||||
'enabled' => array($this, 'translateEnabled'),
|
||||
'selected' => array($this, 'translateSelected'),
|
||||
'invalid' => array($this, 'translateInvalid'),
|
||||
'hover' => array($this, 'translateHover'),
|
||||
'visited' => array($this, 'translateVisited'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctionTranslators()
|
||||
{
|
||||
return array(
|
||||
'lang' => array($this, 'translateLang'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateChecked(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath->addCondition(
|
||||
'(@checked '
|
||||
."and (name(.) = 'input' or name(.) = 'command')"
|
||||
."and (@type = 'checkbox' or @type = 'radio'))"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateLink(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath->addCondition("@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateDisabled(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath->addCondition(
|
||||
"("
|
||||
."@disabled and"
|
||||
."("
|
||||
."(name(.) = 'input' and @type != 'hidden')"
|
||||
." or name(.) = 'button'"
|
||||
." or name(.) = 'select'"
|
||||
." or name(.) = 'textarea'"
|
||||
." or name(.) = 'command'"
|
||||
." or name(.) = 'fieldset'"
|
||||
." or name(.) = 'optgroup'"
|
||||
." or name(.) = 'option'"
|
||||
.")"
|
||||
.") or ("
|
||||
."(name(.) = 'input' and @type != 'hidden')"
|
||||
." or name(.) = 'button'"
|
||||
." or name(.) = 'select'"
|
||||
." or name(.) = 'textarea'"
|
||||
.")"
|
||||
." and ancestor::fieldset[@disabled]"
|
||||
);
|
||||
// todo: in the second half, add "and is not a descendant of that fieldset element's first legend element child, if any."
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateEnabled(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath->addCondition(
|
||||
'('
|
||||
.'@href and ('
|
||||
."name(.) = 'a'"
|
||||
." or name(.) = 'link'"
|
||||
." or name(.) = 'area'"
|
||||
.')'
|
||||
.') or ('
|
||||
.'('
|
||||
."name(.) = 'command'"
|
||||
." or name(.) = 'fieldset'"
|
||||
." or name(.) = 'optgroup'"
|
||||
.')'
|
||||
.' and not(@disabled)'
|
||||
.') or ('
|
||||
.'('
|
||||
."(name(.) = 'input' and @type != 'hidden')"
|
||||
." or name(.) = 'button'"
|
||||
." or name(.) = 'select'"
|
||||
." or name(.) = 'textarea'"
|
||||
." or name(.) = 'keygen'"
|
||||
.')'
|
||||
." and not (@disabled or ancestor::fieldset[@disabled])"
|
||||
.') or ('
|
||||
."name(.) = 'option' and not("
|
||||
."@disabled or ancestor::optgroup[@disabled]"
|
||||
.')'
|
||||
.')'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param FunctionNode $function
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function translateLang(XPathExpr $xpath, FunctionNode $function)
|
||||
{
|
||||
$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(
|
||||
'ancestor-or-self::*[@lang][1][starts-with(concat('
|
||||
."translate(@%s, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '-')"
|
||||
.', %s)]',
|
||||
'lang',
|
||||
Translator::getXpathLiteral(strtolower($arguments[0]->getValue()).'-')
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateSelected(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath->addCondition("(@selected and name(.) = 'option')");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateInvalid(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath->addCondition('0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateHover(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath->addCondition('0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateVisited(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath->addCondition('0');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'html';
|
||||
}
|
||||
}
|
@ -0,0 +1,270 @@
|
||||
<?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\Node;
|
||||
use Symfony\Component\CssSelector\XPath\Translator;
|
||||
use Symfony\Component\CssSelector\XPath\XPathExpr;
|
||||
|
||||
/**
|
||||
* XPath expression translator node extension.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class NodeExtension extends AbstractExtension
|
||||
{
|
||||
const ELEMENT_NAME_IN_LOWER_CASE = 1;
|
||||
const ATTRIBUTE_NAME_IN_LOWER_CASE = 2;
|
||||
const ATTRIBUTE_VALUE_IN_LOWER_CASE = 4;
|
||||
|
||||
/**
|
||||
* @var Translator
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $flags;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Translator $translator
|
||||
* @param int $flags
|
||||
*/
|
||||
public function __construct(Translator $translator, $flags = 0)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->flags = $flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $flag
|
||||
* @param boolean $on
|
||||
*
|
||||
* @return NodeExtension
|
||||
*/
|
||||
public function setFlag($flag, $on)
|
||||
{
|
||||
if ($on && !$this->hasFlag($flag)) {
|
||||
$this->flags += $flag;
|
||||
}
|
||||
|
||||
if (!$on && $this->hasFlag($flag)) {
|
||||
$this->flags -= $flag;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $flag
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasFlag($flag)
|
||||
{
|
||||
return $this->flags & $flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNodeTranslators()
|
||||
{
|
||||
return array(
|
||||
'Selector' => array($this, 'translateSelector'),
|
||||
'CombinedSelector' => array($this, 'translateCombinedSelector'),
|
||||
'Negation' => array($this, 'translateNegation'),
|
||||
'Function' => array($this, 'translateFunction'),
|
||||
'Pseudo' => array($this, 'translatePseudo'),
|
||||
'Attribute' => array($this, 'translateAttribute'),
|
||||
'Class' => array($this, 'translateClass'),
|
||||
'Hash' => array($this, 'translateHash'),
|
||||
'Element' => array($this, 'translateElement'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node\SelectorNode $node
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateSelector(Node\SelectorNode $node)
|
||||
{
|
||||
return $this->translator->nodeToXPath($node->getTree());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node\CombinedSelectorNode $node
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateCombinedSelector(Node\CombinedSelectorNode $node)
|
||||
{
|
||||
return $this->translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node\NegationNode $node
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateNegation(Node\NegationNode $node)
|
||||
{
|
||||
$xpath = $this->translator->nodeToXPath($node->getSelector());
|
||||
$subXpath = $this->translator->nodeToXPath($node->getSubSelector());
|
||||
$subXpath->addNameTest();
|
||||
|
||||
if ($subXpath->getCondition()) {
|
||||
return $xpath->addCondition(sprintf('not(%s)', $subXpath->getCondition()));
|
||||
}
|
||||
|
||||
return $xpath->addCondition('0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node\FunctionNode $node
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateFunction(Node\FunctionNode $node)
|
||||
{
|
||||
$xpath = $this->translator->nodeToXPath($node->getSelector());
|
||||
|
||||
return $this->translator->addFunction($xpath, $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node\PseudoNode $node
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translatePseudo(Node\PseudoNode $node)
|
||||
{
|
||||
$xpath = $this->translator->nodeToXPath($node->getSelector());
|
||||
|
||||
return $this->translator->addPseudoClass($xpath, $node->getIdentifier());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node\AttributeNode $node
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateAttribute(Node\AttributeNode $node)
|
||||
{
|
||||
$name = $node->getAttribute();
|
||||
$safe = $this->isSafeName($name);
|
||||
|
||||
if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) {
|
||||
$name = strtolower($name);
|
||||
}
|
||||
|
||||
if ($node->getNamespace()) {
|
||||
$name = sprintf('%s:%s', $node->getNamespace(), $name);
|
||||
$safe = $safe && $this->isSafeName($node->getNamespace());
|
||||
}
|
||||
|
||||
$attribute = $safe ? '@'.$name : sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name));
|
||||
$value = $node->getValue();
|
||||
$xpath = $this->translator->nodeToXPath($node->getSelector());
|
||||
|
||||
if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) {
|
||||
$value = strtolower($value);
|
||||
}
|
||||
|
||||
return $this->translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node\ClassNode $node
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateClass(Node\ClassNode $node)
|
||||
{
|
||||
$xpath = $this->translator->nodeToXPath($node->getSelector());
|
||||
|
||||
return $this->translator->addAttributeMatching($xpath, '~=', '@class', $node->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node\HashNode $node
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateHash(Node\HashNode $node)
|
||||
{
|
||||
$xpath = $this->translator->nodeToXPath($node->getSelector());
|
||||
|
||||
return $this->translator->addAttributeMatching($xpath, '=', '@id', $node->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node\ElementNode $node
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateElement(Node\ElementNode $node)
|
||||
{
|
||||
$element = $node->getElement();
|
||||
|
||||
if ($this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) {
|
||||
$element = strtolower($element);
|
||||
}
|
||||
|
||||
if ($element) {
|
||||
$safe = $this->isSafeName($element);
|
||||
} else {
|
||||
$element = '*';
|
||||
$safe = true;
|
||||
}
|
||||
|
||||
if ($node->getNamespace()) {
|
||||
$element = sprintf('%s:%s', $node->getNamespace(), $element);
|
||||
$safe = $safe && $this->isSafeName($node->getNamespace());
|
||||
}
|
||||
|
||||
$xpath = new XPathExpr('', $element);
|
||||
|
||||
if (!$safe) {
|
||||
$xpath->addNameTest();
|
||||
}
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'node';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if given name is safe.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function isSafeName($name)
|
||||
{
|
||||
return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name);
|
||||
}
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
<?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\XPath\XPathExpr;
|
||||
|
||||
/**
|
||||
* XPath expression translator pseudo-class extension.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class PseudoClassExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPseudoClassTranslators()
|
||||
{
|
||||
return array(
|
||||
'root' => array($this, 'translateRoot'),
|
||||
'first-child' => array($this, 'translateFirstChild'),
|
||||
'last-child' => array($this, 'translateLastChild'),
|
||||
'first-of-type' => array($this, 'translateFirstOfType'),
|
||||
'last-of-type' => array($this, 'translateLastOfType'),
|
||||
'only-child' => array($this, 'translateOnlyChild'),
|
||||
'only-of-type' => array($this, 'translateOnlyOfType'),
|
||||
'empty' => array($this, 'translateEmpty'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateRoot(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath->addCondition('not(parent::*)');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateFirstChild(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath
|
||||
->addStarPrefix()
|
||||
->addNameTest()
|
||||
->addCondition('position() = 1');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateLastChild(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath
|
||||
->addStarPrefix()
|
||||
->addNameTest()
|
||||
->addCondition('position() = last()');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function translateFirstOfType(XPathExpr $xpath)
|
||||
{
|
||||
if ('*' === $xpath->getElement()) {
|
||||
throw new ExpressionErrorException('"*:first-of-type" is not implemented.');
|
||||
}
|
||||
|
||||
return $xpath
|
||||
->addStarPrefix()
|
||||
->addCondition('position() = 1');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function translateLastOfType(XPathExpr $xpath)
|
||||
{
|
||||
if ('*' === $xpath->getElement()) {
|
||||
throw new ExpressionErrorException('"*:last-of-type" is not implemented.');
|
||||
}
|
||||
|
||||
return $xpath
|
||||
->addStarPrefix()
|
||||
->addCondition('position() = last()');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateOnlyChild(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath
|
||||
->addStarPrefix()
|
||||
->addNameTest()
|
||||
->addCondition('last() = 1');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function translateOnlyOfType(XPathExpr $xpath)
|
||||
{
|
||||
if ('*' === $xpath->getElement()) {
|
||||
throw new ExpressionErrorException('"*:only-of-type" is not implemented.');
|
||||
}
|
||||
|
||||
return $xpath->addCondition('last() = 1');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function translateEmpty(XPathExpr $xpath)
|
||||
{
|
||||
return $xpath->addCondition('not(*) and not(string-length())');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'pseudo-class';
|
||||
}
|
||||
}
|
302
src/Symfony/Component/CssSelector/XPath/Translator.php
Normal file
302
src/Symfony/Component/CssSelector/XPath/Translator.php
Normal file
@ -0,0 +1,302 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
|
||||
use Symfony\Component\CssSelector\Node\FunctionNode;
|
||||
use Symfony\Component\CssSelector\Node\NodeInterface;
|
||||
use Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
use Symfony\Component\CssSelector\Parser\Parser;
|
||||
use Symfony\Component\CssSelector\Parser\ParserInterface;
|
||||
use Symfony\Component\CssSelector\XPath\Extension;
|
||||
|
||||
/**
|
||||
* XPath expression translator interface.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class Translator implements TranslatorInterface
|
||||
{
|
||||
/**
|
||||
* @var ParserInterface
|
||||
*/
|
||||
private $mainParser;
|
||||
|
||||
/**
|
||||
* @var ParserInterface[]
|
||||
*/
|
||||
private $shortcutParsers = array();
|
||||
|
||||
/**
|
||||
* @var Extension\ExtensionInterface
|
||||
*/
|
||||
private $extensions = array();
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $nodeTranslators = array();
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $combinationTranslators = array();
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $functionTranslators = array();
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $pseudoClassTranslators = array();
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $attributeMatchingTranslators = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(ParserInterface $parser = null)
|
||||
{
|
||||
$this->mainParser = $parser ?: new Parser();
|
||||
|
||||
$this
|
||||
->registerExtension(new Extension\NodeExtension($this))
|
||||
->registerExtension(new Extension\CombinationExtension())
|
||||
->registerExtension(new Extension\FunctionExtension())
|
||||
->registerExtension(new Extension\PseudoClassExtension())
|
||||
->registerExtension(new Extension\AttributeMatchingExtension())
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $element
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getXpathLiteral($element)
|
||||
{
|
||||
if (false === strpos($element, "'")) {
|
||||
return "'".$element."'";
|
||||
}
|
||||
|
||||
if (false === strpos($element, '"')) {
|
||||
return '"'.$element.'"';
|
||||
}
|
||||
|
||||
$string = $element;
|
||||
$parts = array();
|
||||
while (true) {
|
||||
if (false !== $pos = strpos($string, "'")) {
|
||||
$parts[] = sprintf("'%s'", substr($string, 0, $pos));
|
||||
$parts[] = "\"'\"";
|
||||
$string = substr($string, $pos + 1);
|
||||
} else {
|
||||
$parts[] = "'$string'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf('concat(%s)', implode($parts, ', '));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::')
|
||||
{
|
||||
$selectors = $this->parseSelectors($cssExpr);
|
||||
|
||||
/** @var SelectorNode $selector */
|
||||
foreach ($selectors as $selector) {
|
||||
if (null !== $selector->getPseudoElement()) {
|
||||
throw new ExpressionErrorException('Pseudo-elements are not supported.');
|
||||
}
|
||||
}
|
||||
|
||||
$translator = $this;
|
||||
|
||||
return implode(' | ', array_map(function (SelectorNode $selector) use ($translator, $prefix) {
|
||||
return $translator->selectorToXPath($selector, $prefix);
|
||||
}, $selectors));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::')
|
||||
{
|
||||
return ($prefix ?: '').$this->nodeToXPath($selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an extension.
|
||||
*
|
||||
* @param Extension\ExtensionInterface $extension
|
||||
*
|
||||
* @return Translator
|
||||
*/
|
||||
public function registerExtension(Extension\ExtensionInterface $extension)
|
||||
{
|
||||
$this->extensions[$extension->getName()] = $extension;
|
||||
|
||||
$this->nodeTranslators = array_merge($this->nodeTranslators, $extension->getNodeTranslators());
|
||||
$this->combinationTranslators = array_merge($this->combinationTranslators, $extension->getCombinationTranslators());
|
||||
$this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators());
|
||||
$this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators());
|
||||
$this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return Extension\ExtensionInterface
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function getExtension($name)
|
||||
{
|
||||
if (!isset($this->extensions[$name])) {
|
||||
throw new ExpressionErrorException('Extension "'.$name.'" not registered.');
|
||||
}
|
||||
|
||||
return $this->extensions[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a shortcut parser.
|
||||
*
|
||||
* @param ParserInterface $shortcut
|
||||
*
|
||||
* @return Translator
|
||||
*/
|
||||
public function registerParserShortcut(ParserInterface $shortcut)
|
||||
{
|
||||
$this->shortcutParsers[] = $shortcut;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NodeInterface $node
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function nodeToXPath(NodeInterface $node)
|
||||
{
|
||||
if (!isset($this->nodeTranslators[$node->getNodeName()])) {
|
||||
throw new ExpressionErrorException('Node "'.$node->getNodeName().'" not supported.');
|
||||
}
|
||||
|
||||
return call_user_func($this->nodeTranslators[$node->getNodeName()], $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $combiner
|
||||
* @param NodeInterface $xpath
|
||||
* @param NodeInterface $combinedXpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function addCombination($combiner, NodeInterface $xpath, NodeInterface $combinedXpath)
|
||||
{
|
||||
if (!isset($this->combinationTranslators[$combiner])) {
|
||||
throw new ExpressionErrorException('Combiner "'.$combiner.'" not supported.');
|
||||
}
|
||||
|
||||
return call_user_func($this->combinationTranslators[$combiner], $this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param FunctionNode $function
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function addFunction(XPathExpr $xpath, FunctionNode $function)
|
||||
{
|
||||
if (!isset($this->functionTranslators[$function->getName()])) {
|
||||
throw new ExpressionErrorException('Function "'.$function->getName().'" not supported.');
|
||||
}
|
||||
|
||||
return call_user_func($this->functionTranslators[$function->getName()], $xpath, $function);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $pseudoClass
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function addPseudoClass(XPathExpr $xpath, $pseudoClass)
|
||||
{
|
||||
if (!isset($this->pseudoClassTranslators[$pseudoClass])) {
|
||||
throw new ExpressionErrorException('Pseudo-class "'.$pseudoClass.'" not supported.');
|
||||
}
|
||||
|
||||
return call_user_func($this->pseudoClassTranslators[$pseudoClass], $xpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $operator
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function addAttributeMatching(XPathExpr $xpath, $operator, $attribute, $value)
|
||||
{
|
||||
if (!isset($this->attributeMatchingTranslators[$operator])) {
|
||||
throw new ExpressionErrorException('Attribute matcher operator "'.$operator.'" not supported.');
|
||||
}
|
||||
|
||||
return call_user_func($this->attributeMatchingTranslators[$operator], $xpath, $attribute, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $css
|
||||
*
|
||||
* @return SelectorNode[]
|
||||
*/
|
||||
private function parseSelectors($css)
|
||||
{
|
||||
foreach ($this->shortcutParsers as $shortcut) {
|
||||
$tokens = $shortcut->parse($css);
|
||||
|
||||
if (!empty($tokens)) {
|
||||
return $tokens;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->mainParser->parse($css);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
|
||||
/**
|
||||
* XPath expression translator interface.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
interface TranslatorInterface
|
||||
{
|
||||
/**
|
||||
* Translates a CSS selector to an XPath expression.
|
||||
*
|
||||
* @param string $cssExpr
|
||||
* @param string $prefix
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::');
|
||||
|
||||
/**
|
||||
* Translates a parsed selector node to an XPath expression
|
||||
*
|
||||
* @param SelectorNode $selector
|
||||
* @param string $prefix
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::');
|
||||
}
|
140
src/Symfony/Component/CssSelector/XPath/XPathExpr.php
Normal file
140
src/Symfony/Component/CssSelector/XPath/XPathExpr.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* XPath expression translator interface.
|
||||
*
|
||||
* This component is a port of the Python cssselector library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
class XPathExpr
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $element;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $condition;
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param string $element
|
||||
* @param string $condition
|
||||
* @param boolean $starPrefix
|
||||
*/
|
||||
public function __construct($path = '', $element = '*', $condition = '', $starPrefix = false)
|
||||
{
|
||||
$this->path = $path;
|
||||
$this->element = $element;
|
||||
$this->condition = $condition;
|
||||
|
||||
if ($starPrefix) {
|
||||
$this->addStarPrefix();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getElement()
|
||||
{
|
||||
return $this->element;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $condition
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function addCondition($condition)
|
||||
{
|
||||
$this->condition = $this->condition ? sprintf('%s and (%s)', $this->condition, $condition) : $condition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCondition()
|
||||
{
|
||||
return $this->condition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function addNameTest()
|
||||
{
|
||||
if ('*' !== $this->element) {
|
||||
$this->addCondition('name() = '.Translator::getXpathLiteral($this->element));
|
||||
$this->element = '*';
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function addStarPrefix()
|
||||
{
|
||||
$this->path .= '*/';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins another XPathExpr with a combiner.
|
||||
*
|
||||
* @param string $combiner
|
||||
* @param XPathExpr $expr
|
||||
*
|
||||
* @return XPathExpr
|
||||
*/
|
||||
public function join($combiner, XPathExpr $expr)
|
||||
{
|
||||
$path = $this->__toString().$combiner;
|
||||
|
||||
if ('*/' !== $expr->path) {
|
||||
$path .= $expr->path;
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->element = $expr->element;
|
||||
$this->condition = $expr->condition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$path = $this->path.$this->element;
|
||||
$condition = null === $this->condition || '' === $this->condition ? '' : '['.$this->condition.']';
|
||||
|
||||
return $path.$condition;
|
||||
}
|
||||
}
|
@ -1,254 +0,0 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* XPathExpr represents an XPath expression.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class XPathExpr
|
||||
{
|
||||
private $prefix;
|
||||
private $path;
|
||||
private $element;
|
||||
private $condition;
|
||||
private $starPrefix;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $prefix Prefix for the XPath expression.
|
||||
* @param string $path Actual path of the expression.
|
||||
* @param string $element The element in the expression.
|
||||
* @param string $condition A condition for the expression.
|
||||
* @param Boolean $starPrefix Indicates whether to use a star prefix.
|
||||
*/
|
||||
public function __construct($prefix = null, $path = null, $element = '*', $condition = null, $starPrefix = false)
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
$this->path = $path;
|
||||
$this->element = $element;
|
||||
$this->condition = $condition;
|
||||
$this->starPrefix = $starPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the prefix of this XPath expression.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrefix()
|
||||
{
|
||||
return $this->prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path of this XPath expression.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers whether this XPath expression has a star prefix.
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
public function hasStarPrefix()
|
||||
{
|
||||
return $this->starPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the element of this XPath expression.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getElement()
|
||||
{
|
||||
return $this->element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the condition of this XPath expression.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCondition()
|
||||
{
|
||||
return $this->condition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string representation for this XPath expression.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$path = '';
|
||||
if (null !== $this->prefix) {
|
||||
$path .= $this->prefix;
|
||||
}
|
||||
|
||||
if (null !== $this->path) {
|
||||
$path .= $this->path;
|
||||
}
|
||||
|
||||
$path .= $this->element;
|
||||
|
||||
if ($this->condition) {
|
||||
$path .= sprintf('[%s]', $this->condition);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a condition to this XPath expression.
|
||||
* Any pre-existent condition will be ANDed to it.
|
||||
*
|
||||
* @param string $condition The condition to add.
|
||||
*/
|
||||
public function addCondition($condition)
|
||||
{
|
||||
if ($this->condition) {
|
||||
$this->condition = sprintf('%s and (%s)', $this->condition, $condition);
|
||||
} else {
|
||||
$this->condition = $condition;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a prefix to this XPath expression.
|
||||
* It will be prepended to any pre-existent prefixes.
|
||||
*
|
||||
* @param string $prefix The prefix to add.
|
||||
*/
|
||||
public function addPrefix($prefix)
|
||||
{
|
||||
if ($this->prefix) {
|
||||
$this->prefix = $prefix.$this->prefix;
|
||||
} else {
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a condition to this XPath expression using the name of the element
|
||||
* as the desired value.
|
||||
* This method resets the element to '*'.
|
||||
*/
|
||||
public function addNameTest()
|
||||
{
|
||||
if ($this->element == '*') {
|
||||
// We weren't doing a test anyway
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addCondition(sprintf('name() = %s', XPathExpr::xpathLiteral($this->element)));
|
||||
$this->element = '*';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a star prefix to this XPath expression.
|
||||
* This method will prepend a '*' to the path and set the star prefix flag
|
||||
* to true.
|
||||
*/
|
||||
public function addStarPrefix()
|
||||
{
|
||||
/*
|
||||
Adds a /* prefix if there is no prefix. This is when you need
|
||||
to keep context's constrained to a single parent.
|
||||
*/
|
||||
if ($this->path) {
|
||||
$this->path .= '*/';
|
||||
} else {
|
||||
$this->path = '*/';
|
||||
}
|
||||
|
||||
$this->starPrefix = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins this XPath expression with $other (another XPath expression) using
|
||||
* $combiner to join them.
|
||||
*
|
||||
* @param string $combiner The combiner string.
|
||||
* @param XPathExpr $other The other XPath expression to combine with
|
||||
* this one.
|
||||
*/
|
||||
public function join($combiner, $other)
|
||||
{
|
||||
$prefix = (string) $this;
|
||||
|
||||
$prefix .= $combiner;
|
||||
$path = $other->getPrefix().$other->getPath();
|
||||
|
||||
/* We don't need a star prefix if we are joining to this other
|
||||
prefix; so we'll get rid of it */
|
||||
if ($other->hasStarPrefix() && '*/' == $path) {
|
||||
$path = '';
|
||||
}
|
||||
$this->prefix = $prefix;
|
||||
$this->path = $path;
|
||||
$this->element = $other->getElement();
|
||||
$this->condition = $other->GetCondition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an XPath literal for $s.
|
||||
*
|
||||
* @param mixed $s Can either be a Node\ElementNode or a string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function xpathLiteral($s)
|
||||
{
|
||||
if ($s instanceof Node\ElementNode) {
|
||||
// This is probably a symbol that looks like an expression...
|
||||
$s = $s->formatElement();
|
||||
} else {
|
||||
$s = (string) $s;
|
||||
}
|
||||
|
||||
if (false === strpos($s, "'")) {
|
||||
return sprintf("'%s'", $s);
|
||||
}
|
||||
|
||||
if (false === strpos($s, '"')) {
|
||||
return sprintf('"%s"', $s);
|
||||
}
|
||||
|
||||
$string = $s;
|
||||
$parts = array();
|
||||
while (true) {
|
||||
if (false !== $pos = strpos($string, "'")) {
|
||||
$parts[] = sprintf("'%s'", substr($string, 0, $pos));
|
||||
$parts[] = "\"'\"";
|
||||
$string = substr($string, $pos + 1);
|
||||
} else {
|
||||
$parts[] = "'$string'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf('concat(%s)', implode($parts, ', '));
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* XPathExprOr represents XPath |'d expressions.
|
||||
*
|
||||
* Note that unfortunately it isn't the union, it's the sum, so duplicate elements will appear.
|
||||
*
|
||||
* This component is a port of the Python lxml library,
|
||||
* which is copyright Infrae and distributed under the BSD license.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class XPathExprOr extends XPathExpr
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $items The items in the expression.
|
||||
* @param string $prefix Optional prefix for the expression.
|
||||
*/
|
||||
public function __construct($items, $prefix = null)
|
||||
{
|
||||
$this->items = $items;
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string representation of this |'d expression.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$prefix = $this->getPrefix();
|
||||
|
||||
$tmp = array();
|
||||
foreach ($this->items as $i) {
|
||||
$tmp[] = sprintf('%s%s', $prefix, $i);
|
||||
}
|
||||
|
||||
return implode($tmp, ' | ');
|
||||
}
|
||||
}
|
@ -10,6 +10,10 @@
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Jean-François Simon",
|
||||
"email": "jeanfrancois.simon@sensiolabs.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "http://symfony.com/contributors"
|
||||
|
Reference in New Issue
Block a user