From bd1df221dc894b7fbbebf6813a1202ecd6ab461d Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 16:58:43 +1100 Subject: [PATCH 01/16] Return no siblings for an element with no parent, without warnings siblings() read the parent's childNodes without checking for a parent, so an element outside any tree raised two warnings. Fixes #139. Co-Authored-By: Claude Opus 5.5 --- src/Helpers/QueryFilters.php | 6 ++++-- tests/Issues/Issue139Test.php | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/Issues/Issue139Test.php diff --git a/src/Helpers/QueryFilters.php b/src/Helpers/QueryFilters.php index 8b73c7b..f3114e4 100644 --- a/src/Helpers/QueryFilters.php +++ b/src/Helpers/QueryFilters.php @@ -940,8 +940,10 @@ public function siblings($selector = null): Query { $found = new SplObjectStorage(); foreach ($this->matches as $m) { - $parent = $m->parentNode; - foreach ($parent->childNodes as $n) { + if ($m->parentNode === null) { + continue; + } + foreach ($m->parentNode->childNodes as $n) { if ($n->nodeType === XML_ELEMENT_NODE && $n !== $m) { $found->offsetSet($n); } diff --git a/tests/Issues/Issue139Test.php b/tests/Issues/Issue139Test.php new file mode 100644 index 0000000..2c64b3d --- /dev/null +++ b/tests/Issues/Issue139Test.php @@ -0,0 +1,40 @@ +')->get(0)->ownerDocument; + $lone = $doc->createElement('lone'); + + $this->assertSame(0, qp($lone)->siblings()->count()); + $this->assertSame(0, qp($lone)->siblings('*')->count()); + } + + public function testSiblingsSkipsAParentlessElementInASet() + { + $qp = qp('', '#b'); + $lone = $qp->get(0)->ownerDocument->createElement('lone'); + + $this->assertSame('a,c', $this->idList($qp->add($lone)->siblings())); + } + + public function testSiblingsInADetachedSubtree() + { + $doc = qp('')->get(0)->ownerDocument; + $div = $doc->createElement('div'); + $a = $div->appendChild($doc->createElement('a')); + $b = $div->appendChild($doc->createElement('b')); + + $this->assertSame([$b], qp($a)->siblings()->get()); + } +} From e3820347dee952ecd8ca961447b7082222cb5d4c Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 16:59:06 +1100 Subject: [PATCH 02/16] Copy replaceAll() replacements with their children replaceAll() made a shallow clone of the replacement and a shallow import of that, so each replaced node arrived with its attributes but no children. Import the replacement deeply instead. Fixes #135 Co-Authored-By: Claude Opus 5.5 --- src/Helpers/QueryMutators.php | 3 +-- tests/Issues/Issue135Test.php | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/Issues/Issue135Test.php diff --git a/src/Helpers/QueryMutators.php b/src/Helpers/QueryMutators.php index 93ab6b6..ee388f5 100644 --- a/src/Helpers/QueryMutators.php +++ b/src/Helpers/QueryMutators.php @@ -847,8 +847,7 @@ public function replaceAll($selector, DOMDocument $document): Query } foreach ((new DOMTraverser($scope))->find($selector)->matches() as $item) { - $node = $replacement->cloneNode(); - $node = $document->importNode($node); + $node = $document->importNode($replacement, true); $item->parentNode->replaceChild($node, $item); } diff --git a/tests/Issues/Issue135Test.php b/tests/Issues/Issue135Test.php new file mode 100644 index 0000000..87f051c --- /dev/null +++ b/tests/Issues/Issue135Test.php @@ -0,0 +1,47 @@ +

')->get(0)->ownerDocument; + + qp('hi', 'x')->replaceAll('p', $document); + + $copies = qp($document)->children(); + $this->assertSame(['x', 'x'], array_map(function ($node) { + return $node->tagName; + }, $copies->get())); + foreach ($copies->get() as $copy) { + $this->assertSame('1', $copy->getAttribute('a')); + $this->assertSame('hi', $copy->textContent); + $this->assertSame(1, qp($copy)->children('y')->count()); + } + } + + public function testReplaceAllInsertsSeparateCopies() + { + $document = qp('

')->get(0)->ownerDocument; + $source = qp('', 'x'); + + $source->replaceAll('p', $document); + + $copies = qp($document)->find('x')->get(); + $this->assertCount(2, $copies); + $this->assertNotSame($copies[0], $copies[1]); + $this->assertNotSame($copies[0]->firstChild, $copies[1]->firstChild); + $this->assertSame(1, qp($source->get(0)->ownerDocument)->find('x > y')->count()); + } +} From 46dd8125554542779138b7d6aede9bdcdc2174f6 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 16:59:36 +1100 Subject: [PATCH 03/16] Count negative indexes from the end in eq(), get() and slice() getNthMatch() returned nothing for a negative index, so eq(-1) was empty and get(-1) NULL, and slice() kept every match from a negative start. As in jQuery, a negative index now counts back from the end of the set. Fixes #142. Co-Authored-By: Claude Opus 5.5 --- src/DOM.php | 14 +++++++--- src/DOMQuery.php | 5 ++-- src/Helpers/QueryFilters.php | 11 +++++--- tests/Issues/Issue142Test.php | 48 +++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 tests/Issues/Issue142Test.php diff --git a/src/DOM.php b/src/DOM.php index dbd889b..6af548b 100644 --- a/src/DOM.php +++ b/src/DOM.php @@ -621,14 +621,20 @@ protected function isXMLish($string) * The internal data structure used in DOMQuery does not have * strong random access support, so we suppliment it with this method. * - * @param $index + * @param int $index + * Zero-based. A negative index counts back from the end of the matches. * - * @return object|void + * @return object|null + * NULL if the index is out of range. */ protected function getNthMatch(int $index) { - if ($index < 0 || $index > $this->matches->count()) { - return; + $count = $this->matches->count(); + if ($index < 0) { + $index += $count; + } + if ($index < 0 || $index >= $count) { + return null; } $i = 0; diff --git a/src/DOMQuery.php b/src/DOMQuery.php index 4c9028b..b4cddd2 100644 --- a/src/DOMQuery.php +++ b/src/DOMQuery.php @@ -266,7 +266,8 @@ public function count(): int * in QueryPath, see {@link examples/techniques.php}. * * @param int|null $index - * If specified, then only this index value will be returned. If this + * If specified, then only this index value will be returned. A negative + * index counts back from the end, so -1 is the last match. If this * index is out of bounds, a NULL will be returned. * @param boolean $asObject * If this is TRUE, an SplObjectStorage object will be returned @@ -281,7 +282,7 @@ public function count(): int public function get($index = null, $asObject = false) { if ($index !== null) { - return ($this->count() > $index) ? $this->getNthMatch($index) : null; + return $this->getNthMatch($index); } // Retain support for legacy. if (! $asObject) { diff --git a/src/Helpers/QueryFilters.php b/src/Helpers/QueryFilters.php index f3114e4..a77d5e7 100644 --- a/src/Helpers/QueryFilters.php +++ b/src/Helpers/QueryFilters.php @@ -263,7 +263,8 @@ public function map($callback): Query * Narrow the items in this object down to only a slice of the starting items. * * @param integer $start - * Where in the list of matches to begin the slice. + * Where in the list of matches to begin the slice. A negative index counts + * back from the end. * @param integer $length * The number of items to include in the slice. If nothing is specified, the * all remaining matches (from $start onward) will be included in the sliced @@ -280,6 +281,9 @@ public function slice($start, $length = 0): Query if ($start >= $this->count()) { return $this->inst($found, null); } + if ($start < 0) { + $start += $this->count(); + } $i = $j = 0; foreach ($this->matches as $m) { @@ -620,8 +624,9 @@ public function parentsUntil($selector = null): Query * with {@link end()}. * * @param $index - * The index of the element to keep. The rest will be - * discarded. + * The zero-based index of the item to keep. The rest will be + * discarded. A negative index counts back from the end, so -1 keeps + * the last item. An index out of range gives an empty set. * * @return DOMQuery * @throws ParseException diff --git a/tests/Issues/Issue142Test.php b/tests/Issues/Issue142Test.php new file mode 100644 index 0000000..89f07bd --- /dev/null +++ b/tests/Issues/Issue142Test.php @@ -0,0 +1,48 @@ +'; + + public function testEqCountsANegativeIndexFromTheEnd() + { + $qp = qp(self::XML, 'i'); + + $this->assertSame('e', $this->idList($qp->eq(-1))); + $this->assertSame('d', $this->idList($qp->eq(-2))); + $this->assertSame('a', $this->idList($qp->eq(-5))); + $this->assertSame('', $this->idList($qp->eq(-6))); + $this->assertSame('', $this->idList($qp->eq(5))); + $this->assertSame('', $this->idList(qp(self::XML, 'nothing')->eq(-1))); + } + + public function testGetCountsANegativeIndexFromTheEnd() + { + $qp = qp(self::XML, 'i'); + + $this->assertSame('e', $qp->get(-1)->getAttribute('id')); + $this->assertSame('a', $qp->get(-5)->getAttribute('id')); + $this->assertNull($qp->get(-6)); + $this->assertNull($qp->get(5)); + $this->assertNull(qp(self::XML, 'nothing')->get(-1)); + } + + public function testSliceCountsANegativeStartFromTheEnd() + { + $qp = qp(self::XML, 'i'); + + $this->assertSame('d,e', $this->idList($qp->slice(-2))); + $this->assertSame('e', $this->idList($qp->slice(-1))); + $this->assertSame('a,b,c,d,e', $this->idList($qp->slice(-5))); + $this->assertSame('a,b,c,d,e', $this->idList($qp->slice(-9))); + } +} From 762e097ef8a72dbd8ebc83ba10dbf5ffcea4ef4d Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 16:59:52 +1100 Subject: [PATCH 04/16] 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 36a96c4e05110e90657c01f33eefb14030a2b593 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 17:00:15 +1100 Subject: [PATCH 05/16] Take an exclusive end index as slice()'s second argument, as jQuery does slice($start, $length) followed array_slice(), with a length of 0 meaning "to the end". It is now slice($start, $end), as in jQuery: the slice stops before $end, a negative $end counts back from the end, and an omitted $end runs to the end of the set. Fixes #144. Co-Authored-By: Claude Opus 5.5 --- src/Helpers/QueryFilters.php | 42 +++++++++++++++---------------- tests/Issues/Issue144Test.php | 43 ++++++++++++++++++++++++++++++++ tests/QueryPath/DOMQueryTest.php | 2 +- 3 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 tests/Issues/Issue144Test.php diff --git a/src/Helpers/QueryFilters.php b/src/Helpers/QueryFilters.php index a77d5e7..0447933 100644 --- a/src/Helpers/QueryFilters.php +++ b/src/Helpers/QueryFilters.php @@ -262,37 +262,37 @@ public function map($callback): Query /** * Narrow the items in this object down to only a slice of the starting items. * - * @param integer $start - * Where in the list of matches to begin the slice. A negative index counts - * back from the end. - * @param integer $length - * The number of items to include in the slice. If nothing is specified, the - * all remaining matches (from $start onward) will be included in the sliced - * list. + * As in jQuery's .slice(), the slice runs from $start up to, but not + * including, $end. Both are zero-based indexes, and a negative index counts + * back from the end, so slice(1, -1) drops the first and last items. + * + * @param integer $start + * The index of the first item to include. + * @param integer|null $end + * The index to stop before. If omitted, the slice runs to the end of the + * matches. An $end at or before $start gives an empty set. * * @return DOMQuery * @throws ParseException - * @see array_slice() */ - public function slice($start, $length = 0): Query + public function slice($start, $end = null): Query { - $end = $length; - $found = new SplObjectStorage(); - if ($start >= $this->count()) { - return $this->inst($found, null); - } - if ($start < 0) { - $start += $this->count(); + $count = $this->count(); + $start = $start < 0 ? $start + $count : $start; + if ($end === null) { + $end = $count; + } elseif ($end < 0) { + $end += $count; } - $i = $j = 0; + $found = new SplObjectStorage(); + $i = 0; foreach ($this->matches as $m) { + if ($i >= $end) { + break; + } if ($i >= $start) { - if ($end > 0 && $j >= $end) { - break; - } $found->offsetSet($m); - ++$j; } ++$i; } diff --git a/tests/Issues/Issue144Test.php b/tests/Issues/Issue144Test.php new file mode 100644 index 0000000..33ecd9b --- /dev/null +++ b/tests/Issues/Issue144Test.php @@ -0,0 +1,43 @@ +'; + + /** + * @dataProvider sliceProvider + */ + public function testSliceTakesAnEndIndex(array $args, string $expected) + { + $this->assertSame($expected, $this->idList(qp(self::XML, 'i')->slice(...$args))); + } + + public function sliceProvider(): array + { + return [ + 'slice(1, 3)' => [[1, 3], 'b,c'], + 'slice(1, -1)' => [[1, -1], 'b,c,d'], + 'slice(2, 0)' => [[2, 0], ''], + 'slice(0)' => [[0], 'a,b,c,d,e'], + 'slice(2)' => [[2], 'c,d,e'], + 'slice(1, 9)' => [[1, 9], 'b,c,d,e'], + 'slice(3, 2)' => [[3, 2], ''], + 'slice(2, 2)' => [[2, 2], ''], + 'slice(-3, -1)' => [[-3, -1], 'c,d'], + 'slice(-2, 5)' => [[-2, 5], 'd,e'], + 'slice(0, -5)' => [[0, -5], ''], + 'slice(0, -9)' => [[0, -9], ''], + 'slice(-9, 2)' => [[-9, 2], 'a,b'], + 'slice(5)' => [[5], ''], + ]; + } +} diff --git a/tests/QueryPath/DOMQueryTest.php b/tests/QueryPath/DOMQueryTest.php index 674f925..90e10b6 100644 --- a/tests/QueryPath/DOMQueryTest.php +++ b/tests/QueryPath/DOMQueryTest.php @@ -1011,7 +1011,7 @@ public function testSlice() $this->assertEquals(0, qp($file, 'li')->slice(9)->count()); // The first item should be #two, the last #three - $qp = qp($file, 'li')->slice(1, 2); + $qp = qp($file, 'li')->slice(1, 3); $this->assertEquals(2, $qp->count()); $this->assertEquals('two', $qp->attr('id')); $this->assertEquals('three', $qp->eq(1)->attr('id')); From a70ffc47f075bbd32b5b7ce3dfc65b3ba7fedc09 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 17:00:37 +1100 Subject: [PATCH 06/16] Deprecate SimpleSelector::notEmpty() and make it the inverse of isBlank() Co-Authored-By: Claude Opus 5.5 --- src/CSS/SimpleSelector.php | 14 ++++++------- tests/QueryPath/CSS/SelectorTest.php | 31 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/CSS/SimpleSelector.php b/src/CSS/SimpleSelector.php index 0bae098..55f8cdf 100644 --- a/src/CSS/SimpleSelector.php +++ b/src/CSS/SimpleSelector.php @@ -196,17 +196,17 @@ public function isBlank(): bool } /** + * Whether this restricts the elements it matches. The inverse of isBlank(). + * + * As with isBlank(), the combinator is not counted: it relates this simple selector to the next, + * and does not narrow what this one matches. + * * @return bool + * @deprecated Use ! isBlank(). This will be removed in 5.0. */ public function notEmpty(): bool { - return ! empty($this->element) - && ! empty($this->id) - && ! empty($this->classes) - && ! empty($this->combinator) - && ! empty($this->attributes) - && ! empty($this->pseudoClasses) - && ! empty($this->pseudoElements); + return ! $this->isBlank(); } public function __toString() diff --git a/tests/QueryPath/CSS/SelectorTest.php b/tests/QueryPath/CSS/SelectorTest.php index f34c54c..0a580dd 100644 --- a/tests/QueryPath/CSS/SelectorTest.php +++ b/tests/QueryPath/CSS/SelectorTest.php @@ -131,4 +131,35 @@ public function testIterator() $iterator = $selector->getIterator(); $this->assertInstanceOf('\Iterator', $iterator); } + + /** + * @dataProvider notEmptySelectors + */ + public function testNotEmptyIsTheInverseOfIsBlank($selector, $index, $expected) + { + $simpleSelector = $this->parse($selector)->toArray()[0][$index]; + + $this->assertSame($expected, $simpleSelector->notEmpty()); + $this->assertSame(! $expected, $simpleSelector->isBlank()); + } + + public function notEmptySelectors(): array + { + return [ + 'element' => ['li', 0, true], + 'ID' => ['#a', 0, true], + 'class' => ['.a', 0, true], + 'attribute' => ['[a]', 0, true], + 'pseudo-class' => [':first-child', 0, true], + 'pseudo-element' => ['::before', 0, true], + 'every part' => ['ns|li#a.b[c]:first-child::before', 0, true], + 'element with a combinator' => ['ul > li', 1, true], + 'blank with a combinator' => ['> li', 1, false], + ]; + } + + public function testNotEmptyOnANewSelector() + { + $this->assertFalse((new SimpleSelector())->notEmpty()); + } } From 068339d3bad136d8f1f4753791ee458d7bdb2686 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 17:00:57 +1100 Subject: [PATCH 07/16] Count a positional compound left of a combinator across the document in filter() In filterNodes(), a non-subject compound carrying a positional pseudo-class was resolved by searching the candidates' descendants, so filter('ul:first > li') and is('ul:first > li') matched nothing. It is now resolved from the document element, as jQuery's .filter() does. Fixes #132. Co-Authored-By: Claude Opus 5.5 --- src/CSS/DOMTraverser.php | 27 ++++++++++++++-- tests/Issues/Issue132Test.php | 61 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 tests/Issues/Issue132Test.php diff --git a/src/CSS/DOMTraverser.php b/src/CSS/DOMTraverser.php index 7f443c9..bed6495 100644 --- a/src/CSS/DOMTraverser.php +++ b/src/CSS/DOMTraverser.php @@ -173,8 +173,9 @@ public function find($selector): DOMTraverser * * Unlike find(), which searches the descendants of the nodes it starts from, only the given * nodes can match: for ``, `b` matches the `b` element and not `a`. Combinators are - * still resolved against the document, so `div > p` works, and positional pseudo-classes - * (`:first`, `:eq(n)`, ...) count within the given set. Nodes that are not elements never match. + * still resolved against the document, so `div > p` works. Positional pseudo-classes (`:first`, + * `:eq(n)`, ...) on the subject count within the given set, and on a compound left of a + * combinator across the document, as in jQuery's .filter(). Nodes that are not elements never match. * * @param SplObjectStorage $nodes * The candidates, all from one document. @@ -336,8 +337,13 @@ protected function resolveSubject(array $selectorGroup, int $index): SplObjectSt } else { // initialMatch() consumes the parts of the simple selector it matched on. Give it a copy, // so the caller's parsed selector can be reused. + // When the given nodes are tested as themselves, a positional compound left of a combinator + // counts across the document, as jQuery's .filter() does. $selectorGroup[$index] = clone $selectorGroup[$index]; - $candidates = $this->initialMatch($selectorGroup[$index], $this->matches); + $candidates = $this->initialMatch( + $selectorGroup[$index], + $this->initialized ? $this->documentScope() : $this->matches + ); } $setFilters = $selectorGroup[$index]->setFilterPseudoClasses(); @@ -377,6 +383,21 @@ protected function resolveSubject(array $selectorGroup, int $index): SplObjectSt return $this->toMatches($matched); } + /** + * The document element, standing in for the document as it does in qp($document). + * + * @return SplObjectStorage + */ + protected function documentScope(): SplObjectStorage + { + $scope = $this->newMatches(); + if ($this->dom instanceof DOMDocument && $this->dom->documentElement) { + $scope->offsetSet($this->dom->documentElement); + } + + return $scope; + } + /** * Apply a single set-level pseudo-class to an ordered list of nodes. * diff --git a/tests/Issues/Issue132Test.php b/tests/Issues/Issue132Test.php new file mode 100644 index 0000000..c915774 --- /dev/null +++ b/tests/Issues/Issue132Test.php @@ -0,0 +1,61 @@ +
'; + + /** + * @testWith ["ul:first > li", "a,b"] + * ["ul:eq(1) > li", "c"] + * ["ul:last li", "c"] + * ["ul:eq(0) + ul > li", "c"] + * ["ul:gt(0) > li:first", "c"] + * ["ul:first > li:last", "b"] + * ["ul:first > li, ul:last > li", "a,b,c"] + */ + public function testFilterCountsALeftHandPositionalCompoundAcrossTheDocument($selector, $expected) + { + $this->assertSame($expected, $this->idList(qp(self::XML, 'li')->filter($selector))); + } + + public function testTheIssueExamples() + { + $this->assertSame('a,b', $this->idList(qp(self::XML)->find('ul:first > li'))); + $this->assertSame('a,b', $this->idList(qp(self::XML, 'li')->filter('ul:first > li'))); + $this->assertTrue(qp(self::XML, 'li')->is('ul:first > li')); + $this->assertSame('c', $this->idList(qp(self::XML, 'li')->filter('ul:eq(1) > li'))); + } + + public function testTheCompoundIsNotLimitedToTheNodesInHand() + { + // u1 is not in the set, but is still the document's first