feature #33144 [DomCrawler] Added Crawler::matches(), ::closest(), ::outerHtml() (lyrixx)

This PR was merged into the 4.4 branch.

Discussion
----------

[DomCrawler] Added Crawler::matches(), ::closest(), ::outerHtml()

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #18609
| License       | MIT
| Doc PR        | -

Commits
-------

9535f9e8d6 [DomCrawler] Added Crawler::matches(), ::closest(), ::outerHtml()
This commit is contained in:
Fabien Potencier 2019-08-22 11:04:18 +02:00
commit a77e326a9c
3 changed files with 157 additions and 0 deletions

View File

@ -5,6 +5,9 @@ CHANGELOG
-----
* Added `Form::getName()` method.
* Added `Crawler::matches()` method.
* Added `Crawler::closest()` method.
* Added `Crawler::outerHtml()` method.
4.3.0
-----

View File

@ -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 && '<!DOCTYPE html>' === $owner->saveXML($owner->childNodes[0])) {
$owner = $this->html5Parser;
}
return $owner->saveHTML($node);
}
/**
* Evaluates an XPath expression.
*

View File

@ -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'
<html lang="en">
<body>
<div id="foo" class="foo other">
<div>
<div id="bar" class="bar other"></div>
</div>
</div>
</body>
</html>
HTML;
$crawler = $this->createCrawler($this->getDoctype().$html);
$node = $crawler->filter($mainNodeSelector);
$this->assertSame($expected, $node->matches($selector));
}
public function testClosest()
{
$html = <<<'HTML'
<html lang="en">
<body>
<div class="lorem2 ok">
<div>
<div class="lorem3 ko"></div>
</div>
<div class="lorem1 ok">
<div id="foo" class="newFoo ok">
<div class="lorem1 ko"></div>
</div>
</div>
</div>
<div class="lorem2 ko">
</div>
</body>
</html>
HTML;
$crawler = $this->createCrawler($this->getDoctype().$html);
$foo = $crawler->filter('#foo');
$newFoo = $foo->closest('#foo');
$this->assertInstanceOf(Crawler::class, $newFoo);
$this->assertSame('newFoo ok', $newFoo->attr('class'));
$lorem1 = $foo->closest('.lorem1');
$this->assertInstanceOf(Crawler::class, $lorem1);
$this->assertSame('lorem1 ok', $lorem1->attr('class'));
$lorem2 = $foo->closest('.lorem2');
$this->assertInstanceOf(Crawler::class, $lorem2);
$this->assertSame('lorem2 ok', $lorem2->attr('class'));
$lorem3 = $foo->closest('.lorem3');
$this->assertNull($lorem3);
$notFound = $foo->closest('.not-found');
$this->assertNull($notFound);
}
public function testOuterHtml()
{
$html = <<<'HTML'
<html lang="en">
<body>
<div class="foo">
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</body>
</html>
HTML;
$crawler = $this->createCrawler($this->getDoctype().$html);
$bar = $crawler->filter('ul');
$output = $bar->outerHtml();
$output = str_replace([' ', "\n"], '', $output);
$expected = '<ul><li>1</li><li>2</li><li>3</li></ul>';
$this->assertSame($expected, $output);
}
public function testNextAll()
{
$crawler = $this->createTestCrawler()->filterXPath('//li')->eq(1);