diff --git a/src/Symfony/Component/DomCrawler/CHANGELOG.md b/src/Symfony/Component/DomCrawler/CHANGELOG.md index 8e3b5728c3..ad8c47982e 100644 --- a/src/Symfony/Component/DomCrawler/CHANGELOG.md +++ b/src/Symfony/Component/DomCrawler/CHANGELOG.md @@ -5,6 +5,9 @@ CHANGELOG ----- * Added `Form::getName()` method. +* Added `Crawler::matches()` method. +* Added `Crawler::closest()` method. +* Added `Crawler::outerHtml()` method. 4.3.0 ----- diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index 017a48751b..6454669352 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -427,6 +427,45 @@ class Crawler implements \Countable, \IteratorAggregate return $this->createSubCrawler($this->sibling($this->getNode(0)->parentNode->firstChild)); } + public function matches(string $selector): bool + { + if (!$this->nodes) { + return false; + } + + $converter = $this->createCssSelectorConverter(); + $xpath = $converter->toXPath($selector, 'self::'); + + return 0 !== $this->filterRelativeXPath($xpath)->count(); + } + + /** + * Return first parents (heading toward the document root) of the Element that matches the provided selector. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill + * + * @throws \InvalidArgumentException When current node is empty + */ + public function closest(string $selector): ?self + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $domNode = $this->getNode(0); + + while (XML_ELEMENT_NODE === $domNode->nodeType) { + $node = $this->createSubCrawler($domNode); + if ($node->matches($selector)) { + return $node; + } + + $domNode = $node->getNode(0)->parentNode; + } + + return null; + } + /** * Returns the next siblings nodes of the current selection. * @@ -609,6 +648,22 @@ class Crawler implements \Countable, \IteratorAggregate return $html; } + public function outerHtml(): string + { + if (!\count($this)) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + $owner = $node->ownerDocument; + + if (null !== $this->html5Parser && '' === $owner->saveXML($owner->childNodes[0])) { + $owner = $this->html5Parser; + } + + return $owner->saveHTML($node); + } + /** * Evaluates an XPath expression. * diff --git a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php index c17ac333f3..5703982ac0 100644 --- a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php @@ -880,6 +880,105 @@ HTML; } } + public function provideMatchTests() + { + yield ['#foo', true, '#foo']; + yield ['#foo', true, '.foo']; + yield ['#foo', true, '.other']; + yield ['#foo', false, '.bar']; + + yield ['#bar', true, '#bar']; + yield ['#bar', true, '.bar']; + yield ['#bar', true, '.other']; + yield ['#bar', false, '.foo']; + } + + /** @dataProvider provideMatchTests */ + public function testMatch(string $mainNodeSelector, bool $expected, string $selector) + { + $html = <<<'HTML' + +
+