Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
53 / 53
CombinedSelectorNodeTrait
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
5 / 5
28
100.00% covered (success)
100.00%
53 / 53
 matchDescendantCombinedSelectorNode
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
10 / 10
 matchChildCombinedSelectorNode
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
6 / 6
 matchAdjacentCombinedSelectorNode
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 matchGeneralSiblingCombinedSelectorNode
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
13 / 13
 matchCombinedSelectorNode
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
14 / 14
 match
n/a
0 / 0
1
n/a
0 / 0
<?php
namespace SDom\SelectorMatcher;
use SDom\Node\Element;
use Symfony\Component\CssSelector\Node\CombinedSelectorNode;
use Symfony\Component\CssSelector\Node\NodeInterface;
/**
 * @pattern E F
 * @meaning an F element descendant of an E element
 * @link https://www.w3.org/TR/css3-selectors/#descendant-combinators
 *
 * @pattern E > F
 * @meaning an F element child of an E element
 * @link https://www.w3.org/TR/css3-selectors/#child-combinators
 *
 * @pattern E + F
 * @meaning an F element immediately preceded by an E element
 * @link https://www.w3.org/TR/css3-selectors/#adjacent-sibling-combinators
 *
 * @pattern E ~ F
 * @meaning an F element preceded by an E element
 * @link https://www.w3.org/TR/css3-selectors/#general-sibling-combinators
 *
 * Trait ClassNodeTrait
 * @package SDom\SelectorMatcher
 */
trait CombinedSelectorNodeTrait
{
    /**
     * @param CombinedSelectorNode $token
     * @param Element $node
     * @param null|Element $effectiveRoot
     * @return bool
     */
    protected function matchDescendantCombinedSelectorNode(
        CombinedSelectorNode $token,
        Element $node,
        Element $effectiveRoot = null
    ): bool {
        // node must have a non-root parent
        if (null === $node->parent() || $effectiveRoot === $node->parent()) {
            return false;
        }
        // node must match the sub-selector
        if (!$this->match($token->getSubSelector(), $node, $effectiveRoot)) {
            return false;
        }
        $parent = $node;
        // node must have a parent that matches the selector, anywhere up the chain
        do {
            /** @var Element $parent */
            $parent = $parent->parent();
            if ($this->match($token->getSelector(), $parent, $effectiveRoot)) {
                return true;
            }
        } while (null !== $parent->parent() && $effectiveRoot !== $parent->parent());
        return false;
    }
    /**
     * @param CombinedSelectorNode $token
     * @param Element $node
     * @param null|Element $effectiveRoot
     * @return bool
     */
    protected function matchChildCombinedSelectorNode(
        CombinedSelectorNode $token,
        Element $node,
        Element $effectiveRoot = null
    ): bool {
        // node must have a non-root parent
        if (null === $node->parent() || $effectiveRoot === $node->parent()) {
            return false;
        }
        /** @var Element $parent */
        $parent = $node->parent();
        // node must match the sub-selector
        if (!$this->match($token->getSubSelector(), $node, $effectiveRoot)) {
            return false;
        }
        // node's parent must match the selector
        return $this->match($token->getSelector(), $parent, $effectiveRoot);
    }
    /**
     * @param CombinedSelectorNode $token
     * @param Element $node
     * @param null|Element $effectiveRoot
     * @return bool
     */
    protected function matchAdjacentCombinedSelectorNode(
        CombinedSelectorNode $token,
        Element $node,
        Element $effectiveRoot = null
    ): bool {
        // node must have a parent in order to determine position (index), parent CAN be the effective root
        if (null === $node->parent()) {
            return false;
        }
        /** @var Element $parent */
        $parent = $node->parent();
        // node must have an immediately preceding sibling that matches the selector
        // don't bother if the node is the first child (no siblings on the left)
        // ignored \InvalidArgumentException as $node is always a child of $parent
        $index = $parent->index($node);
        if (0 === $index) {
            return false;
        }
        // don't bother if the sibling is not an Element node
        // ignored \OutOfBoundsException as $index will always be within the list of children
        $sibling = $parent->get($index - 1);
        if (!$sibling instanceof Element) {
            return false;
        }
        // match the selector
        return $this->match($token->getSelector(), $sibling, $effectiveRoot);
    }
    /**
     * @param CombinedSelectorNode $token
     * @param Element $node
     * @param null|Element $effectiveRoot
     * @return bool
     */
    protected function matchGeneralSiblingCombinedSelectorNode(
        CombinedSelectorNode $token,
        Element $node,
        Element $effectiveRoot = null
    ): bool {
        // node must have a parent in order to determine position (index), parent CAN be the effective root
        if (null === $node->parent()) {
            return false;
        }
        /** @var Element $parent */
        $parent = $node->parent();
        // node must have a preceding sibling (may not be immediate) that matches the selector
        // don't bother if the node is the first child (no siblings on the left)
        // ignored \InvalidArgumentException as $node is always a child of $parent
        $index = $parent->index($node);
        if (0 === $index) {
            return false;
        }
        // test all preceding siblings & bail after the first successful match
        for ($i = $index - 1; $i >= 0; $i--) {
            // skip the sibling if it's not an Element node
            // ignored \OutOfBoundsException as $index will always be within the list of children
            $sibling = $parent->get($i);
            if (!$sibling instanceof Element) {
                continue;
            }
            // match the selector
            if ($this->match($token->getSelector(), $sibling, $effectiveRoot)) {
                return true;
            }
        }
        // no sibling matches the selector
        return false;
    }
    /**
     * @param CombinedSelectorNode $token
     * @param Element $node
     * @param null|Element $effectiveRoot
     * @return bool
     */
    protected function matchCombinedSelectorNode(
        CombinedSelectorNode $token,
        Element $node,
        Element $effectiveRoot = null
    ): bool {
        // node must match the sub selector
        if (!$this->match($token->getSubSelector(), $node, $effectiveRoot)) {
            return false;
        }
        switch ($token->getCombinator()) {
            case ' ':
                return $this->matchDescendantCombinedSelectorNode($token, $node, $effectiveRoot);
            case '>':
                return $this->matchChildCombinedSelectorNode($token, $node, $effectiveRoot);
            case '+':
                return $this->matchAdjacentCombinedSelectorNode($token, $node, $effectiveRoot);
            case '~':
                return $this->matchGeneralSiblingCombinedSelectorNode($token, $node, $effectiveRoot);
            default:
                throw new \RuntimeException(sprintf(
                    'Invalid combined selector combinator "%s".',
                    $token->getCombinator()
                ));
        }
    }
    /**
     * @param NodeInterface $token
     * @param Element $node
     * @param Element|null $effectiveRoot
     * @return bool
     */
    abstract public function match(NodeInterface $token, Element $node, Element $effectiveRoot = null): bool;
}