1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 
<?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;
}