Repository navigation
Walk sibling and ancestor axes through one helper - #129
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #129 +/- ##
============================================
+ Coverage 90.69% 91.12% +0.43%
- Complexity 1424 1431 +7
============================================
Files 25 26 +1
Lines 3170 3167 -3
============================================
+ Hits 2875 2886 +11
+ Misses 295 281 -14 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was referenced Oct 26, 2026
next(), prev(), nextAll(), prevAll(), nextUntil(), prevUntil(), parent(), parents(), parentsUntil() and closest() were each a copy of the same loop. They now share QueryFilters::walk(). Fixes #115. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'all' was 'until' with no stop selector, so walk() now takes a bool and the *Until() methods share nextAll()'s stop at an already-collected element. Ordering and filtering move back to the callers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gravitypdf-bot
force-pushed
the
fix/issue-115
branch
from
October 26, 2026 04:51
c8aa1a4 to
bff4d32
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.
The sibling and ancestor traversals in
QueryFilterswere each a hand-written copy of one loop. They differed only in the DOM property they followed and in when they stopped, so a fix to one had to be repeated in its mirror (#72 had to touch every copy).What changed
One private helper now walks an axis from each matched element and collects the elements it passes:
next()/prev()walk('nextSibling' / 'previousSibling', $selector, true)nextAll()/prevAll()filterCandidates(walk(…Sibling, null, false), $selector)nextUntil()/prevUntil()walk(…Sibling, $selector, false)parent()walk('parentNode', $selector, true)parents()filterCandidates(sortReverseDocumentOrder(walk('parentNode', null, false)), $selector)parentsUntil()sortReverseDocumentOrder(walk('parentNode', $selector, false))closest()walk('parentNode', $selector, true, true)*All()is*Until()with nothing to stop at, filtered afterwards. Every walk that is not first-match stops at an element an earlier starting element already collected, because everything beyond it was collected with it, up to the same stop.getParentElements()is gone. Ordering and selector filtering stay in the public methods;walk()only walks.siblings()keeps its own loop, because it iterates the parent's children instead of walking an axis. Its results still go through the samefilterCandidates().Results are unchanged. I checked this two ways: the existing suite (including
Issue62Test) passes untouched, and the output of 45 traversal calls on multi-element sets, with and without selectors, is byte-identical before and after.Side effects (edge cases, now consistent)
parentsUntil()on an element that is not attached to the document used to loop forever, readingnodeTypeofnull. It now stops at the top of the detached subtree, asparents()already did.nextUntil('')andprevUntil('')threwParseException. They now treat an empty selector as no selector, asparentsUntil(''),next('')andparent('')already did, and as jQuery does.closest('')threwParseException, andclosest(null)threw aTypeError. Both now return an empty set, as jQuery'sclosest('')does.parents(),nextUntil(),prevUntil()andparentsUntil()now stop walking at the first element already collected from an earlier starting element, asnextAll()andprevAll()already did.nextUntil()on each of 2000 siblings falls from about 97 ms to 0.7 ms (nextUntil('#none'): 212 ms to 6 ms).Tests
The existing tests are unchanged. Three tests are added to
DOMQueryTest:testParentsUntilOutsideTheDocument(fails onmain)testTraversalTreatsAnEmptySelectorAsNone(fails onmain)testUntilWalksFromElementsOnEachOthersPath, which pins the early stop for*Until()walks that start on each other's pathsvendor/bin/phpunit: 693 tests, 1935 assertions, 0 failures (1 skipped).composer run lintandcomposer run lint:min-phpare clean.Rebased onto #128
Rebased onto
mainwith #127 and #128. The only conflict was the CHANGELOG; both entries are kept. #128 madeNodeMatcherreturn nodes in input order, which makes filtering before or after the reverse-document sort equivalent;parents()sorts first, as before.Fixes #115.
🤖 Generated with Claude Code