Repository navigation
Count positional *Until() stops across the document, as jQuery does - #131
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #131 +/- ##
============================================
+ Coverage 90.69% 91.23% +0.54%
- Complexity 1424 1441 +17
============================================
Files 25 26 +1
Lines 3170 3183 +13
============================================
+ Hits 2875 2904 +29
+ Misses 295 279 -16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
gravitypdf-bot
force-pushed
the
fix/issue-118
branch
from
October 26, 2026 04:58
3c50203 to
b68476c
Compare
This was referenced Oct 26, 2026
The stop selector of nextUntil(), prevUntil() and parentsUntil() was tested one node at a time, so a positional pseudo-class saw a one-element set. jQuery tests it with .is(), which counts across the document, and closest() never matches a positional selector. Fixes #118. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gravitypdf-bot
force-pushed
the
fix/issue-118
branch
from
October 26, 2026 05:35
b68476c to
5d673e9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Behaviour change. The walk-and-stop methods tested each node against the selector on its own. A positional pseudo-class therefore only ever saw a one-element set:
What jQuery does, and which option this takes
The issue offered two options: batch each walk into one candidate set, or document the per-node behaviour. jQuery 3.7.1 does neither:
*Until()stop is tested withjQuery(elem).is(until). For a positional selector (rneedsContext),.is()evaluatesjQuery(selector)against the whole document. SonextUntil(':eq(3)')stops at the document's fourth element, wherever that is.closest()skips positional selectors outright. Its source says "Positional selectors never match, since there's no selection context".next(),prev()andparent()look at the adjacent node only and filter the combined results.Batching (option 1) would match none of these. It would make
nextUntil(':eq(1)')stop at the walk's second sibling, which jQuery never does. This PR follows jQuery instead:nextUntil(),prevUntil()andparentsUntil(),walk()tests the stop with the newNodeMatcher::matchesInDocument(). For a positional selector it runs the selector once from the document element throughDOMTraverser, the same pathreplaceAll()uses, on the first node a walk reaches, and answers from that set. Other selectors fall through to the memoized per-nodematches(), so the common path costs nothing extra. The positional check is the existing, memoizedUtil::selectorHasPositionalPseudoClass(). It ignores positions inside:has(), which count per element (Make :has() match in find() #125).closest()returns nothing for a positional selector.next(),prev()andparent()keep the per-node test (option 2: documented in their DocBlocks). QueryPath'snext($selector)walks to the nearest matching sibling, unlike jQuery's, so there is no jQuery result set to filter. For a single starting element, per-node gives the same answer as jQuery::firstmatches and:eq(1)doesn't.The "positional selectors need a set" rule lives in
NodeMatcher, next to the per-node test it corrects, sowalk()has one stop test. The one exception isclosest()'s early return. No change tosrc/CSS/.jQuery vs before vs after
Document:
<r><i#a/><i#b.k/><i#c/><i#d.k/><i#e/></r>, with document order r, a, b, c, d, e. The ancestor document isr > s#s1 > t#t1 > u#u1andr > s#s2 > t#t2 > u#u2.#a.nextUntil(':eq(1)')(issue example)#a.nextUntil(':first')#a.nextUntil(':eq(3)')[a, b].nextUntil(':last')#e.prevUntil(':eq(2)')#e.prevUntil('.k:first')#u2.parentsUntil(':first')#u2.parentsUntil(':eq(4)')#u1.closest(':first')#u1.closest('t:first')#a.next(':first')[c, a].next(':first')Interplay with #117
The set methods (
prevAll(),parents(),siblings(), …) count a positional selector within the set in document order (#66). That does not depend on the result order from #117.Issue118Test::testSetMethodsCountPositionsInDocumentOrderpins those results under the #117 ordering. jQuery counts in walk order instead:$('#e').prevAll(':first')is#d, while QueryPath returns#a. Making the two agree would mean changing howDOMTraverserorders positional filters, so it is left out of this PR (#134).Tests
tests/Issues/Issue118Test.phphas seven tests: the issue example, one per*Until()method,closest(), per-nodenext()/prev()/parent(), and set-method positions. Every expected value comes from jQuery 3.7.1 on jsdom, except the two cases marked as QueryPath-specific. Five of the seven fail onmain.vendor/bin/phpunit: 705 tests, 1976 assertions, 0 failures (1 skipped).composer run lintandcomposer run lint:min-phpare clean.Rebased onto #130
Rebased onto the updated #130. The document-wide stop moved from a
documentMatches()helper and a second stop set inwalk()intoNodeMatcher::matchesInDocument(), which also builds the set only once a walk reaches a candidate.Issue118Testuses the sharedselectIds()/idList()helpers fromTestCase.QueryPath's own
is()still counts a positional selector within the match set, where jQuery's.is()counts across the document, sonextUntil(':eq(3)')andis(':eq(3)')use different counts. That is outside this PR.Fixes #118.
🤖 Generated with Claude Code