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;
}