NodeExtension.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. $xpath = $translator->nodeToXPath($node->selector);
  87. foreach ($node->arguments as $argument) {
  88. $expr = $translator->nodeToXPath($argument);
  89. $expr->addNameTest();
  90. if ($condition = $expr->getCondition()) {
  91. $xpath->addCondition($condition, 'or');
  92. }
  93. }
  94. return $xpath;
  95. }
  96. public function translateSpecificityAdjustment(Node\SpecificityAdjustmentNode $node, Translator $translator): XPathExpr
  97. {
  98. $xpath = $translator->nodeToXPath($node->selector);
  99. foreach ($node->arguments as $argument) {
  100. $expr = $translator->nodeToXPath($argument);
  101. $expr->addNameTest();
  102. if ($condition = $expr->getCondition()) {
  103. $xpath->addCondition($condition, 'or');
  104. }
  105. }
  106. return $xpath;
  107. }
  108. public function translateFunction(Node\FunctionNode $node, Translator $translator): XPathExpr
  109. {
  110. $xpath = $translator->nodeToXPath($node->getSelector());
  111. return $translator->addFunction($xpath, $node);
  112. }
  113. public function translatePseudo(Node\PseudoNode $node, Translator $translator): XPathExpr
  114. {
  115. $xpath = $translator->nodeToXPath($node->getSelector());
  116. return $translator->addPseudoClass($xpath, $node->getIdentifier());
  117. }
  118. public function translateAttribute(Node\AttributeNode $node, Translator $translator): XPathExpr
  119. {
  120. $name = $node->getAttribute();
  121. $safe = $this->isSafeName($name);
  122. if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) {
  123. $name = strtolower($name);
  124. }
  125. if ($node->getNamespace()) {
  126. $name = \sprintf('%s:%s', $node->getNamespace(), $name);
  127. $safe = $safe && $this->isSafeName($node->getNamespace());
  128. }
  129. $attribute = $safe ? '@'.$name : \sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name));
  130. $value = $node->getValue();
  131. $xpath = $translator->nodeToXPath($node->getSelector());
  132. if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) {
  133. $value = strtolower($value);
  134. }
  135. return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value);
  136. }
  137. public function translateClass(Node\ClassNode $node, Translator $translator): XPathExpr
  138. {
  139. $xpath = $translator->nodeToXPath($node->getSelector());
  140. return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName());
  141. }
  142. public function translateHash(Node\HashNode $node, Translator $translator): XPathExpr
  143. {
  144. $xpath = $translator->nodeToXPath($node->getSelector());
  145. return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId());
  146. }
  147. public function translateElement(Node\ElementNode $node): XPathExpr
  148. {
  149. $element = $node->getElement();
  150. if ($element && $this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) {
  151. $element = strtolower($element);
  152. }
  153. if ($element) {
  154. $safe = $this->isSafeName($element);
  155. } else {
  156. $element = '*';
  157. $safe = true;
  158. }
  159. if ($node->getNamespace()) {
  160. $element = \sprintf('%s:%s', $node->getNamespace(), $element);
  161. $safe = $safe && $this->isSafeName($node->getNamespace());
  162. }
  163. $xpath = new XPathExpr('', $element);
  164. if (!$safe) {
  165. $xpath->addNameTest();
  166. }
  167. return $xpath;
  168. }
  169. public function getName(): string
  170. {
  171. return 'node';
  172. }
  173. private function isSafeName(string $name): bool
  174. {
  175. return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name);
  176. }
  177. }