diff --git a/src/Framework/DomQuery.php b/src/Framework/DomQuery.php
index bc00eba8..621baefa 100644
--- a/src/Framework/DomQuery.php
+++ b/src/Framework/DomQuery.php
@@ -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));
}
@@ -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)
diff --git a/tests/Framework/DomQuery.css2Xpath.phpt b/tests/Framework/DomQuery.css2Xpath.phpt
index ee4c80c1..cc1fee3c 100644
--- a/tests/Framework/DomQuery.css2Xpath.phpt
+++ b/tests/Framework/DomQuery.css2Xpath.phpt
@@ -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'),
);
diff --git a/tests/Framework/DomQuery.fromXml.phpt b/tests/Framework/DomQuery.fromXml.phpt
index 5033c16d..d598e4d6 100644
--- a/tests/Framework/DomQuery.fromXml.phpt
+++ b/tests/Framework/DomQuery.fromXml.phpt
@@ -7,6 +7,41 @@ use Tester\DomQuery;
require __DIR__ . '/../bootstrap.php';
-$q = DomQuery::fromXml('hello');
-Assert::true($q->has('body'));
-Assert::false($q->has('p'));
+
+$xml = <<<'XML'
+
+ - Item 1
+ - Item 2
+
+ - Item 3
+
+
+ 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'));