Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
  • Loading branch information...
commit d8556505778f93823c09925007e9eb9c73d8db52 2 parents bd53382 + c6f87d0
@fabpot fabpot authored
Showing with 6,298 additions and 2,161 deletions.
  1. +23 −278 src/Symfony/Component/CssSelector/CssSelector.php
  2. +62 −0 src/Symfony/Component/CssSelector/CssSelectorTest.php
  3. +24 −0 src/Symfony/Component/CssSelector/Exception/ExceptionInterface.php
  4. +24 −0 src/Symfony/Component/CssSelector/Exception/ExpressionErrorException.php
  5. +24 −0 src/Symfony/Component/CssSelector/Exception/InternalErrorException.php
  6. +3 −3 src/Symfony/Component/CssSelector/Exception/ParseException.php
  7. +73 −0 src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php
  8. +40 −0 src/Symfony/Component/CssSelector/Node/AbstractNode.php
  9. +0 −131 src/Symfony/Component/CssSelector/Node/AttribNode.php
  10. +124 −0 src/Symfony/Component/CssSelector/Node/AttributeNode.php
  11. +39 −23 src/Symfony/Component/CssSelector/Node/ClassNode.php
  12. +38 −88 src/Symfony/Component/CssSelector/Node/CombinedSelectorNode.php
  13. +34 −34 src/Symfony/Component/CssSelector/Node/ElementNode.php
  14. +37 −231 src/Symfony/Component/CssSelector/Node/FunctionNode.php
  15. +38 −22 src/Symfony/Component/CssSelector/Node/HashNode.php
  16. +75 −0 src/Symfony/Component/CssSelector/Node/NegationNode.php
  17. +17 −10 src/Symfony/Component/CssSelector/Node/NodeInterface.php
  18. +0 −61 src/Symfony/Component/CssSelector/Node/OrNode.php
  19. +26 −182 src/Symfony/Component/CssSelector/Node/PseudoNode.php
  20. +75 −0 src/Symfony/Component/CssSelector/Node/SelectorNode.php
  21. +78 −0 src/Symfony/Component/CssSelector/Node/Specificity.php
  22. +47 −0 src/Symfony/Component/CssSelector/Parser/Handler/CommentHandler.php
  23. +35 −0 src/Symfony/Component/CssSelector/Parser/Handler/HandlerInterface.php
  24. +67 −0 src/Symfony/Component/CssSelector/Parser/Handler/HashHandler.php
  25. +67 −0 src/Symfony/Component/CssSelector/Parser/Handler/IdentifierHandler.php
  26. +58 −0 src/Symfony/Component/CssSelector/Parser/Handler/NumberHandler.php
  27. +86 −0 src/Symfony/Component/CssSelector/Parser/Handler/StringHandler.php
  28. +44 −0 src/Symfony/Component/CssSelector/Parser/Handler/WhitespaceHandler.php
  29. +395 −0 src/Symfony/Component/CssSelector/Parser/Parser.php
  30. +34 −0 src/Symfony/Component/CssSelector/Parser/ParserInterface.php
  31. +126 −0 src/Symfony/Component/CssSelector/Parser/Reader.php
  32. +42 −0 src/Symfony/Component/CssSelector/Parser/Shortcut/ClassParser.php
  33. +41 −0 src/Symfony/Component/CssSelector/Parser/Shortcut/ElementParser.php
  34. +45 −0 src/Symfony/Component/CssSelector/Parser/Shortcut/EmptyStringParser.php
  35. +42 −0 src/Symfony/Component/CssSelector/Parser/Shortcut/HashParser.php
  36. +160 −0 src/Symfony/Component/CssSelector/Parser/Token.php
  37. +182 −0 src/Symfony/Component/CssSelector/Parser/TokenStream.php
  38. +78 −0 src/Symfony/Component/CssSelector/Parser/Tokenizer/Tokenizer.php
  39. +78 −0 src/Symfony/Component/CssSelector/Parser/Tokenizer/TokenizerEscaping.php
  40. +160 −0 src/Symfony/Component/CssSelector/Parser/Tokenizer/TokenizerPatterns.php
  41. +0 −71 src/Symfony/Component/CssSelector/Tests/CssSelectorTest.php
  42. +32 −0 src/Symfony/Component/CssSelector/Tests/Node/AbstractNodeTest.php
  43. +0 −43 src/Symfony/Component/CssSelector/Tests/Node/AttribNodeTest.php
  44. +37 −0 src/Symfony/Component/CssSelector/Tests/Node/AttributeNodeTest.php
  45. +12 −6 src/Symfony/Component/CssSelector/Tests/Node/ClassNodeTest.php
  46. +13 −14 src/Symfony/Component/CssSelector/Tests/Node/CombinedSelectorNodeTest.php
  47. +15 −10 src/Symfony/Component/CssSelector/Tests/Node/ElementNodeTest.php
  48. +27 −76 src/Symfony/Component/CssSelector/Tests/Node/FunctionNodeTest.php
  49. +12 −6 src/Symfony/Component/CssSelector/Tests/Node/HashNodeTest.php
  50. +33 −0 src/Symfony/Component/CssSelector/Tests/Node/NegationNodeTest.php
  51. +0 −43 src/Symfony/Component/CssSelector/Tests/Node/OrNodeTest.php
  52. +12 −35 src/Symfony/Component/CssSelector/Tests/Node/PseudoNodeTest.php
  53. +34 −0 src/Symfony/Component/CssSelector/Tests/Node/SelectorNodeTest.php
  54. +40 −0 src/Symfony/Component/CssSelector/Tests/Node/SpecificityTest.php
  55. +67 −0 src/Symfony/Component/CssSelector/Tests/Parser/Handler/AbstractHandlerTest.php
  56. +55 −0 src/Symfony/Component/CssSelector/Tests/Parser/Handler/CommentHandlerTest.php
  57. +49 −0 src/Symfony/Component/CssSelector/Tests/Parser/Handler/HashHandlerTest.php
  58. +49 −0 src/Symfony/Component/CssSelector/Tests/Parser/Handler/IdentifierHandlerTest.php
  59. +51 −0 src/Symfony/Component/CssSelector/Tests/Parser/Handler/NumberHandlerTest.php
  60. +50 −0 src/Symfony/Component/CssSelector/Tests/Parser/Handler/StringHandlerTest.php
  61. +44 −0 src/Symfony/Component/CssSelector/Tests/Parser/Handler/WhitespaceHandlerTest.php
  62. +247 −0 src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php
  63. +101 −0 src/Symfony/Component/CssSelector/Tests/Parser/ReaderTest.php
  64. +40 −0 src/Symfony/Component/CssSelector/Tests/Parser/Shortcut/ClassParserTest.php
  65. +42 −0 src/Symfony/Component/CssSelector/Tests/Parser/Shortcut/ElementParserTest.php
  66. +41 −0 src/Symfony/Component/CssSelector/Tests/Parser/Shortcut/HashParserTest.php
  67. +95 −0 src/Symfony/Component/CssSelector/Tests/Parser/TokenStreamTest.php
  68. +0 −72 src/Symfony/Component/CssSelector/Tests/TokenizerTest.php
  69. +48 −0 src/Symfony/Component/CssSelector/Tests/XPath/Fixtures/ids.html
  70. +11 −0 src/Symfony/Component/CssSelector/Tests/XPath/Fixtures/lang.xml
  71. +308 −0 src/Symfony/Component/CssSelector/Tests/XPath/Fixtures/shakespear.html
  72. +308 −0 src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php
  73. +0 −35 src/Symfony/Component/CssSelector/Tests/XPathExprTest.php
  74. +31 −0 src/Symfony/Component/CssSelector/Tests/bootstrap.php
  75. +0 −73 src/Symfony/Component/CssSelector/Token.php
  76. +0 −105 src/Symfony/Component/CssSelector/TokenStream.php
  77. +0 −201 src/Symfony/Component/CssSelector/Tokenizer.php
  78. +63 −0 src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php
  79. +173 −0 src/Symfony/Component/CssSelector/XPath/Extension/AttributeMatchingExtension.php
  80. +93 −0 src/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php
  81. +65 −0 src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php
  82. +198 −0 src/Symfony/Component/CssSelector/XPath/Extension/FunctionExtension.php
  83. +238 −0 src/Symfony/Component/CssSelector/XPath/Extension/HtmlExtension.php
  84. +270 −0 src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php
  85. +162 −0 src/Symfony/Component/CssSelector/XPath/Extension/PseudoClassExtension.php
  86. +302 −0 src/Symfony/Component/CssSelector/XPath/Translator.php
  87. +45 −0 src/Symfony/Component/CssSelector/XPath/TranslatorInterface.php
  88. +140 −0 src/Symfony/Component/CssSelector/XPath/XPathExpr.php
  89. +0 −254 src/Symfony/Component/CssSelector/XPathExpr.php
  90. +0 −54 src/Symfony/Component/CssSelector/XPathExprOr.php
  91. +4 −0 src/Symfony/Component/CssSelector/composer.json
View
301 src/Symfony/Component/CssSelector/CssSelector.php
@@ -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 @@
*
* $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::')
- {
- if (is_string($cssExpr)) {
- if (!$cssExpr) {
- return $prefix.'*';
- }
-
- 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);
- }
-
- $expr = $cssExpr->toXpath();
-
- // @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)
+ public static function toXPath($cssExpr, $prefix = 'descendant-or-self::', $html = true)
{
- $attrib = $stream->next();
- if ($stream->peek() == '|') {
- $namespace = $attrib;
- $stream->next();
- $attrib = $stream->next();
- } else {
- $namespace = '*';
- }
+ $translator = new Translator();
- if ($stream->peek() == ']') {
- return new Node\AttribNode($selector, $namespace, $attrib, 'exists', null);
+ if ($html) {
+ $translator->registerExtension(new HtmlExtension($translator));
}
- $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));
- }
+ $translator
+ ->registerParserShortcut(new EmptyStringParser())
+ ->registerParserShortcut(new ElementParser())
+ ->registerParserShortcut(new ClassParser())
+ ->registerParserShortcut(new HashParser())
+ ;
- return new Node\AttribNode($selector, $namespace, $attrib, $op, $value);
+ return $translator->cssToXPath($cssExpr, $prefix);
}
}
View
62 src/Symfony/Component/CssSelector/CssSelectorTest.php
@@ -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 ')]"),
+ );
+ }
+}
View
24 src/Symfony/Component/CssSelector/Exception/ExceptionInterface.php
@@ -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
+{
+}
View
24 src/Symfony/Component/CssSelector/Exception/ExpressionErrorException.php
@@ -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
+{
+}
View
24 src/Symfony/Component/CssSelector/Exception/InternalErrorException.php
@@ -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
+{
+}
View
6 src/Symfony/Component/CssSelector/Exception/ParseException.php
@@ -14,11 +14,11 @@
/**
* 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
{
}
View
73 src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php
@@ -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.');
+ }
+}
View
40 src/Symfony/Component/CssSelector/Node/AbstractNode.php
@@ -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;
+ }
+}
View
131 src/Symfony/Component/CssSelector/Node/AttribNode.php
@@ -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);
- }
-}
View
124 src/Symfony/Component/CssSelector/Node/AttributeNode.php
@@ -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);
+ }
+}
View
62 src/Symfony/Component/CssSelector/Node/ClassNode.php
@@ -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 __toString()
+ public function getSelector()
{
- return sprintf('%s[%s.%s]', __CLASS__, $this->selector, $this->className);
+ return $this->selector;
}
/**
- * {@inheritDoc}
+ * @return string
*/
- public function toXpath()
+ public function getName()
{
- $selXpath = $this->selector->toXpath();
- $selXpath->addCondition(sprintf("contains(concat(' ', normalize-space(@class), ' '), %s)", XPathExpr::xpathLiteral(' '.$this->className.' ')));
+ return $this->name;
+ }
- return $selXpath;
+ /**
+ * {@inheritdoc}
+ */
+ public function getSpecificity()
+ {
+ return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return sprintf('%s[%s.%s]', $this->getNodeName(), $this->selector, $this->name);
}
}
View
126 src/Symfony/Component/CssSelector/Node/CombinedSelectorNode.php
@@ -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',
- );
+ /**
+ * @var NodeInterface
+ */
+ private $selector;
- protected $selector;
- protected $combinator;
- protected $subselector;
+ /**
+ * @var string
+ */
+ private $combinator;
/**
- * The constructor.
- *
- * @param NodeInterface $selector The XPath selector
- * @param string $combinator The combinator
- * @param NodeInterface $subselector The sub XPath selector
+ * @var NodeInterface
*/
- public function __construct($selector, $combinator, $subselector)
- {
- $this->selector = $selector;
- $this->combinator = $combinator;
- $this->subselector = $subselector;
- }
+ private $subSelector;
/**
- * {@inheritDoc}
+ * @param NodeInterface $selector
+ * @param string $combinator
+ * @param NodeInterface $subSelector
*/
- public function __toString()
+ public function __construct(NodeInterface $selector, $combinator, NodeInterface $subSelector)
{
- $comb = $this->combinator == ' ' ? '<followed>' : $this->combinator;
-
- return sprintf('%s[%s %s %s]', __CLASS__, $this->selector, $comb, $this->subselector);
+ $this->selector = $selector;
+ $this->combinator = $combinator;
+ $this->subSelector = $subSelector;
}
/**
- * {@inheritDoc}
- * @throws ParseException When unknown combinator is found
+ * @return NodeInterface
*/
- public function toXpath()
+ public function getSelector()
{
- 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);
+ return $this->selector;
}
/**
- * 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
+ * @return string
*/
- protected function _xpath_descendant($xpath, $sub)
+ public function getCombinator()
{
- // when sub is a descendant in any way of xpath
- $xpath->join('/descendant::', $sub->toXpath());
-
- return $xpath;
+ return $this->combinator;
}
/**
- * 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
+ * @return NodeInterface
*/
- protected function _xpath_child($xpath, $sub)
+ public function getSubSelector()
{
- // when sub is an immediate child of xpath
- $xpath->join('/', $sub->toXpath());
-
- return $xpath;
+ return $this->subSelector;
}
/**
- * 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
+ * {@inheritdoc}
*/
- protected function _xpath_direct_adjacent($xpath, $sub)
+ public function getSpecificity()
{
- // when sub immediately follows xpath
- $xpath->join('/following-sibling::', $sub->toXpath());
- $xpath->addNameTest();
- $xpath->addCondition('position() = 1');
-
- return $xpath;
+ return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
}
/**
- * 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
+ * {@inheritdoc}
*/
- protected function _xpath_indirect_adjacent($xpath, $sub)
+ public function __toString()
{
- // when sub comes somewhere after xpath as a sibling
- $xpath->join('/following-sibling::', $sub->toXpath());
+ $combinator = ' ' === $this->combinator ? '<followed>' : $this->combinator;
- return $xpath;
+ return sprintf('%s[%s %s %s]', $this->getNodeName(), $this->selector, $combinator, $this->subSelector);
}
}
View
68 src/Symfony/Component/CssSelector/Node/ElementNode.php
@@ -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;
+
+ /**
+ * @var string|null
+ */
+ private $element;
/**
- * Constructor.
- *
- * @param string $namespace Namespace
- * @param string $element Element
+ * @param string|null $namespace
+ * @param string|null $element
*/
- public function __construct($namespace, $element)
+ public function __construct($namespace = null, $element = null)
{
$this->namespace = $namespace;
$this->element = $element;
}
/**
- * {@inheritDoc}
+ * @return null|string
*/
- public function __toString()
+ public function getNamespace()
{
- return sprintf('%s[%s]', __CLASS__, $this->formatElement());
+ return $this->namespace;
}
/**
- * Formats the element into a string.
- *
- * @return string Element as an XPath string
+ * @return null|string
*/
- public function formatElement()
+ public function getElement()
{
- if ($this->namespace == '*') {
- return $this->element;
- }
+ return $this->element;
+ }
- return sprintf('%s|%s', $this->namespace, $this->element);
+ /**
+ * {@inheritdoc}
+ */
+ public function getSpecificity()
+ {
+ return new Specificity(0, 0, $this->element ? 1 : 0);
}
/**
- * {@inheritDoc}
+ * {@inheritdoc}
*/
- public function toXpath()
+ public function __toString()
{
- if ($this->namespace == '*') {
- $el = strtolower($this->element);
- } else {
- // FIXME: Should we lowercase here?
- $el = sprintf('%s:%s', $this->namespace, $this->element);
- }
+ $element = $this->element ?: '*';
- return new XPathExpr(null, null, $el);
+ return sprintf('%s[%s]', $this->getNodeName(), $this->namespace ? $this->namespace.'|'.$element : $element);
}
}
View
268 src/Symfony/Component/CssSelector/Node/FunctionNode.php
@@ -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;
-
/**
- * Constructor.
- *
- * @param NodeInterface $selector The XPath expression
- * @param string $type
- * @param string $name
- * @param XPathExpr $expr
+ * @var NodeInterface
*/
- public function __construct($selector, $type, $name, $expr)
- {
- $this->selector = $selector;
- $this->type = $type;
- $this->name = $name;
- $this->expr = $expr;
- }
+ private $selector;
/**
- * {@inheritDoc}
+ * @var string
*/
- public function __toString()
- {
- return sprintf('%s[%s%s%s(%s)]', __CLASS__, $this->selector, $this->type, $this->name, $this->expr);
- }
+ private $name;
/**
- * {@inheritDoc}
- * @throws ParseException When unsupported or unknown pseudo-class is found
+ * @var Token[]
*/
- 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);
- }
+ private $arguments;
/**
- * 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
+ * @param NodeInterface $selector
+ * @param string $name
+ * @param Token[] $arguments
*/
- protected function _xpath_nth_last_child($xpath, $expr)
+ public function __construct(NodeInterface $selector, $name, array $arguments = array())
{
- return $this->_xpath_nth_child($xpath, $expr, true);
+ $this->selector = $selector;
+ $this->name = strtolower($name);
+ $this->arguments = $arguments;
}
/**
- * undocumented function
- *
- * @param XPathExpr $xpath
- * @param XPathExpr $expr
- *
- * @return XPathExpr
- *
- * @throws ParseException
+ * @return NodeInterface
*/
- protected function _xpath_nth_of_type($xpath, $expr)
+ public function getSelector()
{
- if ($xpath->getElement() == '*') {
- throw new ParseException('*:nth-of-type() is not implemented');
- }
-
- return $this->_xpath_nth_child($xpath, $expr, false, false);
+ return $this->selector;
}
/**
- * undocumented function
- *
- * @param XPathExpr $xpath
- * @param XPathExpr $expr
- *
- * @return XPathExpr
+ * @return string
*/
- protected function _xpath_nth_last_of_type($xpath, $expr)
+ public function getName()
{
- return $this->_xpath_nth_child($xpath, $expr, true, false);
+ return $this->name;
}
/**
- * undocumented function
- *
- * @param XPathExpr $xpath
- * @param XPathExpr $expr
- *
- * @return XPathExpr
+ * @return Token[]
*/
- protected function _xpath_contains($xpath, $expr)
+ public function getArguments()
{
- // 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;
+ return $this->arguments;
}
/**
- * undocumented function
- *
- * @param XPathExpr $xpath
- * @param XPathExpr $expr
- *
- * @return XPathExpr
+ * {@inheritdoc}
*/
- protected function _xpath_not($xpath, $expr)
+ public function getSpecificity()
{
- // 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;
+ return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
/**
- * Parses things like '1n+2', or 'an+b' generally, returning (a, b)
- *
- * @param mixed $s
- *
- * @return array
+ * {@inheritdoc}
*/
- protected function parseSeries($s)
+ public function __toString()
{
- 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);
- }
+ $arguments = implode(', ', array_map(function (Token $token) {
+ return "'".$token->getValue()."'";
+ }, $this->arguments));
- return array($a, $b);
+ return sprintf('%s[%s:%s(%s)]', $this->getNodeName(), $this->selector, $this->name, $arguments ? '['.$arguments.']' : '');
}
}
View
60 src/Symfony/Component/CssSelector/Node/HashNode.php
@@ -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);
}
}
View
75 src/Symfony/Component/CssSelector/Node/NegationNode.php
@@ -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);
+ }
+}
View
27 src/Symfony/Component/CssSelector/Node/NodeInterface.php
@@ -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();
+
+ /**
+ * Returns node's specificity.
+ *
+ * @return Specificity
+ */
+ public function getSpecificity();
/**
- * @return XPathExpr The XPath expression
+ * Returns node's string representation.
*
- * @throws ParseException When unknown operator is found
+ * @return string
*/
- public function toXpath();
+ public function __toString();
}
View
61 src/Symfony/Component/CssSelector/Node/OrNode.php
@@ -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);
- }
-}
View
208 src/Symfony/Component/CssSelector/Node/PseudoNode.php
@@ -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;
-
- /**
- * 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
- */
- public function __construct($element, $type, $ident)
- {
- $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;
- }
-
- /**
- * {@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
+ * @var NodeInterface
*/
- 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;
- }
+ private $selector;
/**
- * @param XPathExpr $xpath The XPath expression
- *
- * @return XPathExpr The modified XPath expression
- *
- * @throws ParseException If this element is the root element
+ * @var string
*/
- protected function xpath_root($xpath)
- {
- // if this element is the root element
- throw new ParseException();
- }
+ private $identifier;
/**
- * Marks this XPath expression as the first child.
- *
- * @param XPathExpr $xpath The XPath expression
- *
- * @return XPathExpr The modified expression
+ * @param NodeInterface $selector
+ * @param string $identifier
*/
- protected function xpath_first_child($xpath)
+ public function __construct(NodeInterface $selector, $identifier)
{
- $xpath->addStarPrefix();
- $xpath->addNameTest();
- $xpath->addCondition('position() = 1');
-
- return $xpath;
+ $this->selector = $selector;
+ $this->identifier = strtolower($identifier);
}
/**
- * Sets the XPath to be the last child.
- *
- * @param XPathExpr $xpath The XPath expression
- *
- * @return XPathExpr The modified expression
+ * @return NodeInterface
*/
- protected function xpath_last_child($xpath)
+ public function getSelector()
{
- $xpath->addStarPrefix();
- $xpath->addNameTest();
- $xpath->addCondition('position() = last()');
-
- return $xpath;
+ return $this->selector;
}
/**
- * Sets the XPath expression to be the first of type.
- *
- * @param XPathExpr $xpath The XPath expression
- *
- * @return XPathExpr The modified expression
- *
- * @throws ParseException
+ * @return string
*/
- protected function xpath_first_of_type($xpath)
+ public func