diff --git a/src/Symfony/Component/CssSelector/CHANGELOG.md b/src/Symfony/Component/CssSelector/CHANGELOG.md index be10abee92..f40b8d6f20 100644 --- a/src/Symfony/Component/CssSelector/CHANGELOG.md +++ b/src/Symfony/Component/CssSelector/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +2.8.0 +----- + + * Added the ConverterInterface and the Converter implementation as a non-static API for the component. + * Deprecated the `CssSelector` static API of the component. + 2.1.0 ----- diff --git a/src/Symfony/Component/CssSelector/Converter.php b/src/Symfony/Component/CssSelector/Converter.php new file mode 100644 index 0000000000..3723c33d91 --- /dev/null +++ b/src/Symfony/Component/CssSelector/Converter.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector; + +use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser; +use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser; +use Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser; +use Symfony\Component\CssSelector\Parser\Shortcut\HashParser; +use Symfony\Component\CssSelector\XPath\Extension\HtmlExtension; +use Symfony\Component\CssSelector\XPath\Translator; + +/** + * @author Christophe Coevoet + * + * @api + */ +class Converter implements ConverterInterface +{ + private $translator; + + /** + * @param bool $html Whether HTML support should be enabled. Disable it for XML documents. + */ + public function __construct($html = true) + { + $this->translator = new Translator(); + + if ($html) { + $this->translator->registerExtension(new HtmlExtension($this->translator)); + } + + $this->translator + ->registerParserShortcut(new EmptyStringParser()) + ->registerParserShortcut(new ElementParser()) + ->registerParserShortcut(new ClassParser()) + ->registerParserShortcut(new HashParser()) + ; + } + + /** + * {@inheritdoc} + */ + public function toXPath($cssExpr, $prefix = 'descendant-or-self::') + { + return $this->translator->cssToXPath($cssExpr, $prefix); + } +} diff --git a/src/Symfony/Component/CssSelector/ConverterInterface.php b/src/Symfony/Component/CssSelector/ConverterInterface.php new file mode 100644 index 0000000000..3321f10d1d --- /dev/null +++ b/src/Symfony/Component/CssSelector/ConverterInterface.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector; + +/** + * ConverterInterface is the main entry point of the component and can convert CSS + * selectors to XPath expressions. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * Copyright (c) 2007-2012 Ian Bicking and contributors. See AUTHORS + * for more details. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. Neither the name of Ian Bicking nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL IAN BICKING OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @author Christophe Coevoet + * + * @api + */ +interface ConverterInterface +{ + /** + * Translates a CSS expression to its XPath equivalent. + * + * Optionally, a prefix can be added to the resulting XPath + * expression with the $prefix parameter. + * + * @param string $cssExpr The CSS expression. + * @param string $prefix An optional prefix for the XPath expression. + * + * @return string + * + * @api + */ + public function toXPath($cssExpr, $prefix = 'descendant-or-self::'); + +} diff --git a/src/Symfony/Component/CssSelector/CssSelector.php b/src/Symfony/Component/CssSelector/CssSelector.php index 82c9283ea2..eb61c93afe 100644 --- a/src/Symfony/Component/CssSelector/CssSelector.php +++ b/src/Symfony/Component/CssSelector/CssSelector.php @@ -11,12 +11,7 @@ namespace Symfony\Component\CssSelector; -use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser; -use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser; -use Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser; -use Symfony\Component\CssSelector\Parser\Shortcut\HashParser; -use Symfony\Component\CssSelector\XPath\Extension\HtmlExtension; -use Symfony\Component\CssSelector\XPath\Translator; +@trigger_error('The '.__NAMESPACE__.'\CssSelector class is deprecated since version 2.8 and will be removed in 3.0. Use directly the \Symfony\Component\CssSelector\Converter class instead.', E_USER_DEPRECATED); /** * CssSelector is the main entry point of the component and can convert CSS @@ -62,6 +57,8 @@ use Symfony\Component\CssSelector\XPath\Translator; * * @author Fabien Potencier * + * @deprecated as of 2.8, will be removed in 3.0. Use the \Symfony\Component\CssSelector\Converter class instead. + * * @api */ class CssSelector @@ -82,20 +79,9 @@ class CssSelector */ public static function toXPath($cssExpr, $prefix = 'descendant-or-self::') { - $translator = new Translator(); + $converter = new Converter(self::$html); - if (self::$html) { - $translator->registerExtension(new HtmlExtension($translator)); - } - - $translator - ->registerParserShortcut(new EmptyStringParser()) - ->registerParserShortcut(new ElementParser()) - ->registerParserShortcut(new ClassParser()) - ->registerParserShortcut(new HashParser()) - ; - - return $translator->cssToXPath($cssExpr, $prefix); + return $converter->toXPath($cssExpr, $prefix); } /** diff --git a/src/Symfony/Component/CssSelector/Tests/ConverterTest.php b/src/Symfony/Component/CssSelector/Tests/ConverterTest.php new file mode 100644 index 0000000000..239c92fff1 --- /dev/null +++ b/src/Symfony/Component/CssSelector/Tests/ConverterTest.php @@ -0,0 +1,36 @@ + + * + * 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\Converter; + +class ConverterTest extends \PHPUnit_Framework_TestCase +{ + public function testCssToXPath() + { + $converter = new Converter(); + + $this->assertEquals('descendant-or-self::*', $converter->toXPath('')); + $this->assertEquals('descendant-or-self::h1', $converter->toXPath('h1')); + $this->assertEquals("descendant-or-self::h1[@id = 'foo']", $converter->toXPath('h1#foo')); + $this->assertEquals("descendant-or-self::h1[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", $converter->toXPath('h1.foo')); + $this->assertEquals('descendant-or-self::foo:h1', $converter->toXPath('foo|h1')); + $this->assertEquals('descendant-or-self::h1', $converter->toXPath('H1')); + } + + public function testCssToXPathXml() + { + $converter = new Converter(false); + + $this->assertEquals('descendant-or-self::H1', $converter->toXPath('H1')); + } +} diff --git a/src/Symfony/Component/CssSelector/Tests/CssSelectorTest.php b/src/Symfony/Component/CssSelector/Tests/CssSelectorTest.php index 61ab80eec8..06eb0d2306 100644 --- a/src/Symfony/Component/CssSelector/Tests/CssSelectorTest.php +++ b/src/Symfony/Component/CssSelector/Tests/CssSelectorTest.php @@ -13,6 +13,9 @@ namespace Symfony\Component\CssSelector\Tests; use Symfony\Component\CssSelector\CssSelector; +/** + * @group legacy + */ class CssSelectorTest extends \PHPUnit_Framework_TestCase { public function testCssToXPath() diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index 38881d2f97..043fa46a49 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -11,7 +11,7 @@ namespace Symfony\Component\DomCrawler; -use Symfony\Component\CssSelector\CssSelector; +use Symfony\Component\CssSelector\Converter; /** * Crawler eases navigation of a list of \DOMElement objects. @@ -42,6 +42,13 @@ class Crawler extends \SplObjectStorage */ private $baseHref; + /** + * Whether the Crawler contains HTML or XML content (used when converting CSS to XPath) + * + * @var bool + */ + private $isHtml = true; + /** * Constructor. * @@ -263,6 +270,8 @@ class Crawler extends \SplObjectStorage libxml_disable_entity_loader($disableEntities); $this->addDocument($dom); + + $this->isHtml = false; } /** @@ -349,11 +358,11 @@ class Crawler extends \SplObjectStorage { foreach ($this as $i => $node) { if ($i == $position) { - return new static($node, $this->uri, $this->baseHref); + return $this->createSubCrawler($node); } } - return new static(null, $this->uri, $this->baseHref); + return $this->createSubCrawler(null); } /** @@ -378,7 +387,7 @@ class Crawler extends \SplObjectStorage { $data = array(); foreach ($this as $i => $node) { - $data[] = $closure(new static($node, $this->uri, $this->baseHref), $i); + $data[] = $closure($this->createSubCrawler($node), $i); } return $data; @@ -394,7 +403,7 @@ class Crawler extends \SplObjectStorage */ public function slice($offset = 0, $length = -1) { - return new static(iterator_to_array(new \LimitIterator($this, $offset, $length)), $this->uri); + return $this->createSubCrawler(iterator_to_array(new \LimitIterator($this, $offset, $length))); } /** @@ -412,12 +421,12 @@ class Crawler extends \SplObjectStorage { $nodes = array(); foreach ($this as $i => $node) { - if (false !== $closure(new static($node, $this->uri, $this->baseHref), $i)) { + if (false !== $closure($this->createSubCrawler($node), $i)) { $nodes[] = $node; } } - return new static($nodes, $this->uri, $this->baseHref); + return $this->createSubCrawler($nodes); } /** @@ -459,7 +468,7 @@ class Crawler extends \SplObjectStorage throw new \InvalidArgumentException('The current node list is empty.'); } - return new static($this->sibling($this->getNode(0)->parentNode->firstChild), $this->uri, $this->baseHref); + return $this->createSubCrawler($this->sibling($this->getNode(0)->parentNode->firstChild)); } /** @@ -477,7 +486,7 @@ class Crawler extends \SplObjectStorage throw new \InvalidArgumentException('The current node list is empty.'); } - return new static($this->sibling($this->getNode(0)), $this->uri, $this->baseHref); + return $this->createSubCrawler($this->sibling($this->getNode(0))); } /** @@ -495,7 +504,7 @@ class Crawler extends \SplObjectStorage throw new \InvalidArgumentException('The current node list is empty.'); } - return new static($this->sibling($this->getNode(0), 'previousSibling'), $this->uri, $this->baseHref); + return $this->createSubCrawler($this->sibling($this->getNode(0), 'previousSibling')); } /** @@ -522,7 +531,7 @@ class Crawler extends \SplObjectStorage } } - return new static($nodes, $this->uri, $this->baseHref); + return $this->createSubCrawler($nodes); } /** @@ -542,7 +551,7 @@ class Crawler extends \SplObjectStorage $node = $this->getNode(0)->firstChild; - return new static($node ? $this->sibling($node) : array(), $this->uri, $this->baseHref); + return $this->createSubCrawler($node ? $this->sibling($node) : array()); } /** @@ -679,7 +688,7 @@ class Crawler extends \SplObjectStorage // If we dropped all expressions in the XPath while preparing it, there would be no match if ('' === $xpath) { - return new static(null, $this->uri, $this->baseHref); + return $this->createSubCrawler(null); } return $this->filterRelativeXPath($xpath); @@ -700,12 +709,14 @@ class Crawler extends \SplObjectStorage */ public function filter($selector) { - if (!class_exists('Symfony\\Component\\CssSelector\\CssSelector')) { - throw new \RuntimeException('Unable to filter with a CSS selector as the Symfony CssSelector is not installed (you can use filterXPath instead).'); + if (!class_exists('Symfony\\Component\\CssSelector\\Converter')) { + throw new \RuntimeException('Unable to filter with a CSS selector as the Symfony CssSelector 2.8+ is not installed (you can use filterXPath instead).'); } + $converter = new Converter($this->isHtml); + // The CssSelector already prefixes the selector with descendant-or-self:: - return $this->filterRelativeXPath(CssSelector::toXPath($selector)); + return $this->filterRelativeXPath($converter->toXPath($selector)); } /** @@ -1019,7 +1030,7 @@ class Crawler extends \SplObjectStorage { $prefixes = $this->findNamespacePrefixes($xpath); - $crawler = new static(null, $this->uri, $this->baseHref); + $crawler = $this->createSubCrawler(null); foreach ($this as $node) { $domxpath = $this->createDOMXPath($node->ownerDocument, $prefixes); @@ -1189,4 +1200,19 @@ class Crawler extends \SplObjectStorage return array(); } + + /** + * Creates a crawler for some subnodes + * + * @param \DOMElement|\DOMElement[]|\DOMNodeList|null $nodes + * + * @return static + */ + private function createSubCrawler($nodes) + { + $crawler = new static($nodes, $this->uri, $this->baseHref); + $crawler->isHtml = $this->isHtml; + + return $crawler; + } } diff --git a/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php index 4cfd06f5c5..cdd87dd7a8 100755 --- a/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\DomCrawler\Tests; -use Symfony\Component\CssSelector\CssSelector; use Symfony\Component\DomCrawler\Crawler; class CrawlerTest extends \PHPUnit_Framework_TestCase @@ -618,16 +617,12 @@ EOF public function testFilterWithNamespace() { - CssSelector::disableHtmlExtension(); - $crawler = $this->createTestXmlCrawler()->filter('yt|accessControl'); $this->assertCount(2, $crawler, '->filter() automatically registers namespaces'); } public function testFilterWithMultipleNamespaces() { - CssSelector::disableHtmlExtension(); - $crawler = $this->createTestXmlCrawler()->filter('media|group yt|aspectRatio'); $this->assertCount(1, $crawler, '->filter() automatically registers namespaces'); $this->assertSame('widescreen', $crawler->text()); diff --git a/src/Symfony/Component/DomCrawler/composer.json b/src/Symfony/Component/DomCrawler/composer.json index 4b3b5f2df3..79b79d82a4 100644 --- a/src/Symfony/Component/DomCrawler/composer.json +++ b/src/Symfony/Component/DomCrawler/composer.json @@ -20,7 +20,7 @@ }, "require-dev": { "symfony/phpunit-bridge": "~2.7|~3.0.0", - "symfony/css-selector": "~2.3|~3.0.0" + "symfony/css-selector": "~2.8|~3.0.0" }, "suggest": { "symfony/css-selector": ""