NodeExtension.php 7.0 KB

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