friendship ended with social-app. php is my new best friend
1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\CssSelector\XPath\Extension; 13 14use Symfony\Component\CssSelector\Node; 15use Symfony\Component\CssSelector\XPath\Translator; 16use Symfony\Component\CssSelector\XPath\XPathExpr; 17 18/** 19 * XPath expression translator node extension. 20 * 21 * This component is a port of the Python cssselect library, 22 * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. 23 * 24 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> 25 * 26 * @internal 27 */ 28class NodeExtension extends AbstractExtension 29{ 30 public const ELEMENT_NAME_IN_LOWER_CASE = 1; 31 public const ATTRIBUTE_NAME_IN_LOWER_CASE = 2; 32 public const ATTRIBUTE_VALUE_IN_LOWER_CASE = 4; 33 34 public function __construct( 35 private int $flags = 0, 36 ) { 37 } 38 39 /** 40 * @return $this 41 */ 42 public function setFlag(int $flag, bool $on): static 43 { 44 if ($on && !$this->hasFlag($flag)) { 45 $this->flags += $flag; 46 } 47 48 if (!$on && $this->hasFlag($flag)) { 49 $this->flags -= $flag; 50 } 51 52 return $this; 53 } 54 55 public function hasFlag(int $flag): bool 56 { 57 return (bool) ($this->flags & $flag); 58 } 59 60 public function getNodeTranslators(): array 61 { 62 return [ 63 'Selector' => $this->translateSelector(...), 64 'CombinedSelector' => $this->translateCombinedSelector(...), 65 'Negation' => $this->translateNegation(...), 66 'Matching' => $this->translateMatching(...), 67 'SpecificityAdjustment' => $this->translateSpecificityAdjustment(...), 68 'Function' => $this->translateFunction(...), 69 'Pseudo' => $this->translatePseudo(...), 70 'Attribute' => $this->translateAttribute(...), 71 'Class' => $this->translateClass(...), 72 'Hash' => $this->translateHash(...), 73 'Element' => $this->translateElement(...), 74 ]; 75 } 76 77 public function translateSelector(Node\SelectorNode $node, Translator $translator): XPathExpr 78 { 79 return $translator->nodeToXPath($node->getTree()); 80 } 81 82 public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator): XPathExpr 83 { 84 return $translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector()); 85 } 86 87 public function translateNegation(Node\NegationNode $node, Translator $translator): XPathExpr 88 { 89 $xpath = $translator->nodeToXPath($node->getSelector()); 90 $subXpath = $translator->nodeToXPath($node->getSubSelector()); 91 $subXpath->addNameTest(); 92 93 if ($subXpath->getCondition()) { 94 return $xpath->addCondition(\sprintf('not(%s)', $subXpath->getCondition())); 95 } 96 97 return $xpath->addCondition('0'); 98 } 99 100 public function translateMatching(Node\MatchingNode $node, Translator $translator): XPathExpr 101 { 102 $xpath = $translator->nodeToXPath($node->selector); 103 104 foreach ($node->arguments as $argument) { 105 $expr = $translator->nodeToXPath($argument); 106 $expr->addNameTest(); 107 if ($condition = $expr->getCondition()) { 108 $xpath->addCondition($condition, 'or'); 109 } 110 } 111 112 return $xpath; 113 } 114 115 public function translateSpecificityAdjustment(Node\SpecificityAdjustmentNode $node, Translator $translator): XPathExpr 116 { 117 $xpath = $translator->nodeToXPath($node->selector); 118 119 foreach ($node->arguments as $argument) { 120 $expr = $translator->nodeToXPath($argument); 121 $expr->addNameTest(); 122 if ($condition = $expr->getCondition()) { 123 $xpath->addCondition($condition, 'or'); 124 } 125 } 126 127 return $xpath; 128 } 129 130 public function translateFunction(Node\FunctionNode $node, Translator $translator): XPathExpr 131 { 132 $xpath = $translator->nodeToXPath($node->getSelector()); 133 134 return $translator->addFunction($xpath, $node); 135 } 136 137 public function translatePseudo(Node\PseudoNode $node, Translator $translator): XPathExpr 138 { 139 $xpath = $translator->nodeToXPath($node->getSelector()); 140 141 return $translator->addPseudoClass($xpath, $node->getIdentifier()); 142 } 143 144 public function translateAttribute(Node\AttributeNode $node, Translator $translator): XPathExpr 145 { 146 $name = $node->getAttribute(); 147 $safe = $this->isSafeName($name); 148 149 if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) { 150 $name = strtolower($name); 151 } 152 153 if ($node->getNamespace()) { 154 $name = \sprintf('%s:%s', $node->getNamespace(), $name); 155 $safe = $safe && $this->isSafeName($node->getNamespace()); 156 } 157 158 $attribute = $safe ? '@'.$name : \sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name)); 159 $value = $node->getValue(); 160 $xpath = $translator->nodeToXPath($node->getSelector()); 161 162 if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) { 163 $value = strtolower($value); 164 } 165 166 return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value); 167 } 168 169 public function translateClass(Node\ClassNode $node, Translator $translator): XPathExpr 170 { 171 $xpath = $translator->nodeToXPath($node->getSelector()); 172 173 return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName()); 174 } 175 176 public function translateHash(Node\HashNode $node, Translator $translator): XPathExpr 177 { 178 $xpath = $translator->nodeToXPath($node->getSelector()); 179 180 return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId()); 181 } 182 183 public function translateElement(Node\ElementNode $node): XPathExpr 184 { 185 $element = $node->getElement(); 186 187 if ($element && $this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) { 188 $element = strtolower($element); 189 } 190 191 if ($element) { 192 $safe = $this->isSafeName($element); 193 } else { 194 $element = '*'; 195 $safe = true; 196 } 197 198 if ($node->getNamespace()) { 199 $element = \sprintf('%s:%s', $node->getNamespace(), $element); 200 $safe = $safe && $this->isSafeName($node->getNamespace()); 201 } 202 203 $xpath = new XPathExpr('', $element); 204 205 if (!$safe) { 206 $xpath->addNameTest(); 207 } 208 209 return $xpath; 210 } 211 212 public function getName(): string 213 { 214 return 'node'; 215 } 216 217 private function isSafeName(string $name): bool 218 { 219 return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name); 220 } 221}