Skip to content

Commit

Permalink
added DomQuery::matches()
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed May 16, 2024
1 parent 8c33846 commit b8accfe
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 33 deletions.
14 changes: 12 additions & 2 deletions src/Framework/DomQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public static function fromXml(string $xml): self
*/
public function find(string $selector): array
{
return $this->xpath(self::css2xpath($selector));
$base = str_starts_with($selector, '>') ? 'self' : 'descendant';
return $this->xpath($base . '::' . self::css2xpath($selector));
}


Expand All @@ -79,12 +80,21 @@ public function has(string $selector): bool
}


/**
* Determines if the current element matches the specified CSS selector.
*/
public function matches(string $selector): bool
{
return (bool) $this->xpath('self::' . self::css2xpath($selector));
}


/**
* Converts a CSS selector into an XPath expression.
*/
public static function css2xpath(string $css): string
{
$xpath = './/*';
$xpath = '*';
preg_match_all(<<<'XX'
/
([#.:]?)([a-z][a-z0-9_-]*)| # id, class, pseudoclass (1,2)
Expand Down
56 changes: 28 additions & 28 deletions tests/Framework/DomQuery.css2Xpath.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -8,73 +8,73 @@ use Tester\DomQuery;
require __DIR__ . '/../bootstrap.php';

test('type selectors', function () {
Assert::same('.//*', DomQuery::css2xpath('*'));
Assert::same('.//foo', DomQuery::css2xpath('foo'));
Assert::same('*', DomQuery::css2xpath('*'));
Assert::same('foo', DomQuery::css2xpath('foo'));
});


test('#ID', function () {
Assert::same(".//*[@id='foo']", DomQuery::css2xpath('#foo'));
Assert::same(".//*[@id='id']", DomQuery::css2xpath('*#id'));
Assert::same("*[@id='foo']", DomQuery::css2xpath('#foo'));
Assert::same("*[@id='id']", DomQuery::css2xpath('*#id'));
});


test('class', function () {
Assert::same(".//*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", DomQuery::css2xpath('.foo'));
Assert::same(".//*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", DomQuery::css2xpath('*.foo'));
Assert::same(".//*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')][contains(concat(' ', normalize-space(@class), ' '), ' bar ')]", DomQuery::css2xpath('.foo.bar'));
Assert::same("*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", DomQuery::css2xpath('.foo'));
Assert::same("*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", DomQuery::css2xpath('*.foo'));
Assert::same("*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')][contains(concat(' ', normalize-space(@class), ' '), ' bar ')]", DomQuery::css2xpath('.foo.bar'));
});


test('attribute selectors', function () {
Assert::same('.//div[@foo]', DomQuery::css2xpath('div[foo]'));
Assert::same(".//div[@foo='bar']", DomQuery::css2xpath('div[foo=bar]'));
Assert::same(".//*[@foo='bar']", DomQuery::css2xpath('[foo="bar"]'));
Assert::same(".//div[@foo='bar']", DomQuery::css2xpath('div[foo="bar"]'));
Assert::same(".//div[@foo='bar']", DomQuery::css2xpath("div[foo='bar']"));
Assert::same(".//div[@foo='bar']", DomQuery::css2xpath('div[Foo="bar"]'));
Assert::same(".//div[contains(concat(' ', normalize-space(@foo), ' '), ' bar ')]", DomQuery::css2xpath('div[foo~="bar"]'));
Assert::same(".//div[contains(@foo, 'bar')]", DomQuery::css2xpath('div[foo*="bar"]'));
Assert::same(".//div[starts-with(@foo, 'bar')]", DomQuery::css2xpath('div[foo^="bar"]'));
Assert::same(".//div[substring(@foo, string-length(@foo)-0)='bar']", DomQuery::css2xpath('div[foo$="bar"]'));
Assert::same(".//div[@foo='bar[]']", DomQuery::css2xpath("div[foo='bar[]']"));
Assert::same(".//div[@foo='bar[]']", DomQuery::css2xpath('div[foo="bar[]"]'));
Assert::same('div[@foo]', DomQuery::css2xpath('div[foo]'));
Assert::same("div[@foo='bar']", DomQuery::css2xpath('div[foo=bar]'));
Assert::same("*[@foo='bar']", DomQuery::css2xpath('[foo="bar"]'));
Assert::same("div[@foo='bar']", DomQuery::css2xpath('div[foo="bar"]'));
Assert::same("div[@foo='bar']", DomQuery::css2xpath("div[foo='bar']"));
Assert::same("div[@foo='bar']", DomQuery::css2xpath('div[Foo="bar"]'));
Assert::same("div[contains(concat(' ', normalize-space(@foo), ' '), ' bar ')]", DomQuery::css2xpath('div[foo~="bar"]'));
Assert::same("div[contains(@foo, 'bar')]", DomQuery::css2xpath('div[foo*="bar"]'));
Assert::same("div[starts-with(@foo, 'bar')]", DomQuery::css2xpath('div[foo^="bar"]'));
Assert::same("div[substring(@foo, string-length(@foo)-0)='bar']", DomQuery::css2xpath('div[foo$="bar"]'));
Assert::same("div[@foo='bar[]']", DomQuery::css2xpath("div[foo='bar[]']"));
Assert::same("div[@foo='bar[]']", DomQuery::css2xpath('div[foo="bar[]"]'));
});


test('variants', function () {
Assert::same(".//*[@id='foo']|//*[@id='bar']", DomQuery::css2xpath('#foo, #bar'));
Assert::same(".//*[@id='foo']|//*[@id='bar']", DomQuery::css2xpath('#foo,#bar'));
Assert::same(".//*[@id='foo']|//*[@id='bar']", DomQuery::css2xpath('#foo ,#bar'));
Assert::same("*[@id='foo']|//*[@id='bar']", DomQuery::css2xpath('#foo, #bar'));
Assert::same("*[@id='foo']|//*[@id='bar']", DomQuery::css2xpath('#foo,#bar'));
Assert::same("*[@id='foo']|//*[@id='bar']", DomQuery::css2xpath('#foo ,#bar'));
});


test('descendant combinator', function () {
Assert::same(
".//div[@id='foo']//*[contains(concat(' ', normalize-space(@class), ' '), ' bar ')]",
"div[@id='foo']//*[contains(concat(' ', normalize-space(@class), ' '), ' bar ')]",
DomQuery::css2xpath('div#foo .bar'),
);
Assert::same(
'.//div//*//p',
'div//*//p',
DomQuery::css2xpath('div * p'),
);
});


test('child combinator', function () {
Assert::same(".//div[@id='foo']/span", DomQuery::css2xpath('div#foo>span'));
Assert::same(".//div[@id='foo']/span", DomQuery::css2xpath('div#foo > span'));
Assert::same("div[@id='foo']/span", DomQuery::css2xpath('div#foo>span'));
Assert::same("div[@id='foo']/span", DomQuery::css2xpath('div#foo > span'));
});


test('general sibling combinator', function () {
Assert::same('.//div/following-sibling::span', DomQuery::css2xpath('div ~ span'));
Assert::same('div/following-sibling::span', DomQuery::css2xpath('div ~ span'));
});


test('complex', function () {
Assert::same(
".//div[@id='foo']//span[contains(concat(' ', normalize-space(@class), ' '), ' bar ')]"
"div[@id='foo']//span[contains(concat(' ', normalize-space(@class), ' '), ' bar ')]"
. "|//*[@id='bar']//li[contains(concat(' ', normalize-space(@class), ' '), ' baz ')]//a",
DomQuery::css2xpath('div#foo span.bar, #bar li.baz a'),
);
Expand Down
41 changes: 38 additions & 3 deletions tests/Framework/DomQuery.fromXml.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,41 @@ use Tester\DomQuery;

require __DIR__ . '/../bootstrap.php';

$q = DomQuery::fromXml('<xml><body>hello</body></xml>');
Assert::true($q->has('body'));
Assert::false($q->has('p'));

$xml = <<<'XML'
<root>
<item id="test1" class="foo">Item 1</item>
<item id="test2" class="bar">Item 2</item>
<container>
<item id="test3" class="foo">Item 3</item>
</container>
</root>
XML;

$dom = DomQuery::fromXml($xml);
Assert::type(DomQuery::class, $dom);

// root
Assert::true($dom->matches('root'));
Assert::false($dom->has('root'));

// find
$results = $dom->find('.foo');
Assert::count(2, $results);
Assert::type(DomQuery::class, $results[0]);
Assert::type(DomQuery::class, $results[1]);

// children
$results = $dom->find('> item');
Assert::count(2, $results);

// has
Assert::true($dom->has('#test1'));
Assert::false($dom->has('#nonexistent'));
Assert::false($dom->find('container')[0]->has('#test1'));
Assert::true($dom->find('container')[0]->has('#test3'));

// matches
$subItem = $dom->find('#test1')[0];
Assert::true($subItem->matches('.foo'));
Assert::false($subItem->matches('.bar'));

0 comments on commit b8accfe

Please sign in to comment.