Skip to content

Commit fac9687

Browse files
Test a later sibling itself against a sibling-relative :has()
find() now searches descendants only (#73), so the per-sibling search in :has(+ ...) and :has(~ ...) no longer saw the sibling it started from. Test the sibling itself first, and search below it only when the group goes on to a child or descendant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent be759b5 commit fac9687

1 file changed

Lines changed: 16 additions & 8 deletions

File tree

‎src/CSS/DOMTraverser/PseudoClass.php‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -409,9 +409,9 @@ protected function containsExactly($node, $value): bool
409409
*/
410410
protected function has($node, $selector): bool
411411
{
412-
foreach ($this->relativeSelector($selector) as [$group, $combinator, $nextOnly]) {
412+
foreach ($this->relativeSelector($selector) as [$group, $combinator, $nextOnly, $descends]) {
413413
if (! ($combinator & (SimpleSelector::ADJACENT | SimpleSelector::SIBLING))) {
414-
if ($this->findsAny($node, $node, $group)) {
414+
if ($this->findsAny($node, $node, $group, false)) {
415415
return true;
416416
}
417417
continue;
@@ -423,7 +423,10 @@ protected function has($node, $selector): bool
423423
continue;
424424
}
425425

426-
if ($this->findsAny($next, $node, $group)) {
426+
// The sibling can match itself, and its descendants when the group goes on to them (`+ ul li`).
427+
if ($this->findsAny($next, $node, $group, true)
428+
|| ($descends && $this->findsAny($next, $node, $group, false))
429+
) {
427430
return true;
428431
}
429432

@@ -443,7 +446,8 @@ protected function has($node, $selector): bool
443446
* which becomes :scope. Any other group (`li`) is a descendant of :scope.
444447
*
445448
* @return array
446-
* One [group, anchor combinator, whether only the next sibling can match] per group.
449+
* One [group, anchor combinator, whether only the next sibling can match, whether the group
450+
* reaches below the sibling it starts from] per group.
447451
* @throws ParseException
448452
*/
449453
protected function relativeSelector($selector): array
@@ -470,28 +474,32 @@ protected function relativeSelector($selector): array
470474

471475
// `+ li` can only be the next sibling, unless the group goes on to another one (`+ li ~ li`).
472476
$nextOnly = $anchor->combinator === SimpleSelector::ADJACENT;
477+
$descends = false;
473478
foreach (array_slice($group, 0, -1) as $simpleSelector) {
474479
if ($simpleSelector->combinator & $sibling) {
475480
$nextOnly = false;
481+
} elseif ($simpleSelector->combinator) {
482+
$descends = true;
476483
}
477484
}
478485

479-
$groups[] = [$group, $anchor->combinator, $nextOnly];
486+
$groups[] = [$group, $anchor->combinator, $nextOnly, $descends];
480487
}
481488

482489
return $this->relativeSelectors[$selector] = $groups;
483490
}
484491

485492
/**
486-
* Whether $group matches $root or any of its descendants.
493+
* Whether $group matches $root itself or, when $self is FALSE, any of its descendants.
487494
*
488495
* @param DOMElement $root
489496
* @param DOMElement $scope
490497
* @param SimpleSelector[] $group
498+
* @param bool $self
491499
*
492500
* @throws ParseException
493501
*/
494-
protected function findsAny(DOMElement $root, DOMElement $scope, array $group): bool
502+
protected function findsAny(DOMElement $root, DOMElement $scope, array $group, bool $self): bool
495503
{
496504
$splos = new SPLObjectStorage();
497505
$splos->offsetSet($root);
@@ -501,7 +509,7 @@ protected function findsAny(DOMElement $root, DOMElement $scope, array $group):
501509
foreach ($group as $simpleSelector) {
502510
$copy[] = clone $simpleSelector;
503511
}
504-
$traverser = new DOMTraverser($splos, false, $scope);
512+
$traverser = new DOMTraverser($splos, $self, $scope);
505513

506514
return count($traverser->findGroups([$copy])->matches()) > 0;
507515
}

0 commit comments

Comments
 (0)