From eda1234f16028bbd09f88a70b18c2ab9e87e9f0a Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 16:59:52 +1100 Subject: [PATCH 1/3] Support :is() and :where() as aliases of :matches() Co-Authored-By: Claude Opus 5.5 --- src/CSS/DOMTraverser/PseudoClass.php | 4 ++- src/CSS/DOMTraverser/Util.php | 4 ++- tests/Issues/Issue136Test.php | 48 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 tests/Issues/Issue136Test.php diff --git a/src/CSS/DOMTraverser/PseudoClass.php b/src/CSS/DOMTraverser/PseudoClass.php index 5815aed..adc096d 100644 --- a/src/CSS/DOMTraverser/PseudoClass.php +++ b/src/CSS/DOMTraverser/PseudoClass.php @@ -261,6 +261,8 @@ public function elementMatches($pseudoclass, $node, $scope, $value = null) case 'has': return $this->has($node, $value); case 'matches': + case 'is': + case 'where': return $this->is($node, $value); case 'not': if (empty($value)) { @@ -541,7 +543,7 @@ protected function findsAny(DOMElement $root, DOMElement $scope, array $group, b } /** - * Provides :matches pseudoclass, which tests the node itself. + * Provides :matches pseudoclass (and its :is and :where aliases), which tests the node itself. * * @throws ParseException */ diff --git a/src/CSS/DOMTraverser/Util.php b/src/CSS/DOMTraverser/Util.php index 033b14a..a1d7f3b 100644 --- a/src/CSS/DOMTraverser/Util.php +++ b/src/CSS/DOMTraverser/Util.php @@ -56,6 +56,8 @@ class Util private const SELECTOR_ARGUMENT_PSEUDO_CLASSES = [ 'not', 'matches', + 'is', + 'where', ]; /** @@ -86,7 +88,7 @@ public static function isPositionalPseudoClass($name): bool * Check whether a pseudo-class has to be applied to the whole result set. * * That is true of the positional pseudo-classes themselves, and of - * :not()/:matches() when their argument contains one. + * :not()/:matches()/:is()/:where() when their argument contains one. * * @param string $name * @param mixed $value diff --git a/tests/Issues/Issue136Test.php b/tests/Issues/Issue136Test.php new file mode 100644 index 0000000..d35b084 --- /dev/null +++ b/tests/Issues/Issue136Test.php @@ -0,0 +1,48 @@ +'; + + /** + * @dataProvider aliases + */ + public function testAliasesMatchAsMatchesDoes(string $name) + { + $this->assertSame('b', $this->idList(qp(self::XML)->find("li:$name(#b)"))); + $this->assertSame('b', $this->idList(qp(self::XML)->find(":$name(#b)"))); + $this->assertSame('a,b', $this->idList(qp(self::XML)->find("li:$name(#a, #b)"))); + $this->assertSame('a', $this->idList(qp(self::XML)->find("li:not(:$name(.x))"))); + $this->assertSame('b,c', $this->idList(qp(self::XML, 'li')->filter(":$name(.x)"))); + $this->assertTrue(qp(self::XML, '#c')->is(":$name(.x)")); + } + + /** + * @dataProvider aliases + */ + public function testPositionalArgumentIsAppliedToTheSet(string $name) + { + $this->assertSame('a', $this->idList(qp(self::XML)->find("li:$name(:first)"))); + $this->assertSame('b', $this->idList(qp(self::XML)->find("li:$name(.x:first)"))); + $this->assertSame('a,c', $this->idList(qp(self::XML)->find("li:not(:$name(:eq(1)))"))); + } + + public function aliases(): array + { + return [ + 'is' => ['is'], + 'where' => ['where'], + 'upper case' => ['IS'], + ]; + } +} From 12e9c6da73c75dbc78a7ac17b8540be8c2c214fd Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 17:18:22 +1100 Subject: [PATCH 2/3] Add a CHANGELOG entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 527a87d..e08c746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ QueryPath Changelog - Consolidate the sibling and ancestor walks behind `next()`, `prev()`, `nextAll()`, `prevAll()`, `nextUntil()`, `prevUntil()`, `parent()`, `parents()`, `parentsUntil()` and `closest()` into one helper, so a fix to one no longer has to be repeated in its mirror (issue #115). As a side effect, `parentsUntil()` no longer loops forever on an element that is not attached to the document, and `nextUntil('')` and `prevUntil('')` treat an empty selector as no selector, as the other methods already did, instead of throwing `QueryPath\CSS\ParseException`. `closest('')` returns an empty set, as in jQuery. `nextUntil()`, `prevUntil()` and `parentsUntil()` also stop at an element already collected from an earlier starting element, as `nextAll()` and `prevAll()` do, so `nextUntil()` on each of 2000 siblings falls from about 100 ms to 1 ms - **Behaviour change.** `prevAll()`, `prevUntil()`, `nextAll()`, `nextUntil()`, `siblings()`, `parent()` and `closest()` now return the results for a multi-element set in jQuery's order, instead of grouped by starting element (issue #117). `prevAll()` and `prevUntil()` return reverse document order, as `parents()` and `parentsUntil()` already did, and the others return document order. `qp($xml, 'c')->add('d')->prevAll()` now returns `c, b, a` rather than `b, a, c`. `next()` and `prev()` keep the order of the starting elements, as in jQuery, and the results for a single starting element are unchanged - **Behaviour change.** A positional pseudo-class (`:first`, `:eq()`, `:last`, ...) in the stop selector of `nextUntil()`, `prevUntil()` and `parentsUntil()` now counts across the whole document, as jQuery's does, instead of seeing each sibling or ancestor as a one-element set (issue #118). `nextUntil(':first')` used to stop at once and now walks to the end, and `nextUntil(':eq(3)')` stops at the fourth element in the document. `closest()` no longer matches a positional selector at all, as in jQuery, where `closest(':first')` used to return the element itself. `next()`, `prev()` and `parent()` still test each candidate on its own, which agrees with jQuery for a single starting element +- Add `:is()` and `:where()` as aliases of `:matches()` (issue #136). QueryPath doesn't use specificity, so `:where()` matches exactly as `:is()` does, and a positional argument (`:is(:first)`) is applied to the whole set as it is for `:matches()` # 4.2.0 From 30aacb1ae69ca61798f144c22b8cf477436b10a1 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 17:26:26 +1100 Subject: [PATCH 3/3] Tidy comments Co-Authored-By: Claude Opus 5.5 --- tests/Issues/Issue136Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Issues/Issue136Test.php b/tests/Issues/Issue136Test.php index d35b084..81cc279 100644 --- a/tests/Issues/Issue136Test.php +++ b/tests/Issues/Issue136Test.php @@ -5,7 +5,7 @@ * Regression tests for https://github.com/GravityPDF/querypath/issues/136 * * `:is()` and `:where()` are the CSS Selectors 4 names for `:matches()`. QueryPath does not use - * specificity, so `:where()` matches exactly as `:is()` does. + * specificity, so `:where()` matches as `:is()` does. */ namespace QueryPathTests;