NodeExtension.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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. private int $flags;
  30. public function __construct(int $flags = 0)
  31. {
  32. $this->flags = $flags;
  33. }
  34. /**
  35. * @return $this
  36. */
  37. public function setFlag(int $flag, bool $on): static
  38. {
  39. if ($on && !$this->hasFlag($flag)) {
  40. $this->flags += $flag;
  41. }
  42. if (!$on && $this->hasFlag($flag)) {
  43. $this->flags -= $flag;
  44. }
  45. return $this;
  46. }
  47. public function hasFlag(int $flag): bool
  48. {
  49. return (bool) ($this->flags & $flag);
  50. }
  51. public function getNodeTranslators(): array
  52. {
  53. return [
  54. 'Selector' => $this->translateSelector(...),
  55. 'CombinedSelector' => $this->translateCombinedSelector(...),
  56. 'Negation' => $this->translateNegation(...),
  57. 'Function' => $this->translateFunction(...),
  58. 'Pseudo' => $this->translatePseudo(...),
  59. 'Attribute' => $this->translateAttribute(...),
  60. 'Class' => $this->translateClass(...),
  61. 'Hash' => $this->translateHash(...),
  62. 'Element' => $this->translateElement(...),
  63. ];
  64. }
  65. public function translateSelector(Node\SelectorNode $node, Translator $translator): XPathExpr
  66. {
  67. return $translator->nodeToXPath($node->getTree());
  68. }
  69. public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator): XPathExpr
  70. {
  71. return $translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector());
  72. }
  73. public function translateNegation(Node\NegationNode $node, Translator $translator): XPathExpr
  74. {
  75. $xpath = $translator->nodeToXPath($node->getSelector());
  76. $subXpath = $translator->nodeToXPath($node->getSubSelector());
  77. $subXpath->addNameTest();
  78. if ($subXpath->getCondition()) {
  79. return $xpath->addCondition(sprintf('not(%s)', $subXpath->getCondition()));
  80. }
  81. return $xpath->addCondition('0');
  82. }
  83. public function translateFunction(Node\FunctionNode $node, Translator $translator): XPathExpr
  84. {
  85. $xpath = $translator->nodeToXPath($node->getSelector());
  86. return $translator->addFunction($xpath, $node);
  87. }
  88. public function translatePseudo(Node\PseudoNode $node, Translator $translator): XPathExpr
  89. {
  90. $xpath = $translator->nodeToXPath($node->getSelector());
  91. return $translator->addPseudoClass($xpath, $node->getIdentifier());
  92. }
  93. public function translateAttribute(Node\AttributeNode $node, Translator $translator): XPathExpr
  94. {
  95. $name = $node->getAttribute();
  96. $safe = $this->isSafeName($name);
  97. if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) {
  98. $name = strtolower($name);
  99. }
  100. if ($node->getNamespace()) {
  101. $name = sprintf('%s:%s', $node->getNamespace(), $name);
  102. $safe = $safe && $this->isSafeName($node->getNamespace());
  103. }
  104. $attribute = $safe ? '@'.$name : sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name));
  105. $value = $node->getValue();
  106. $xpath = $translator->nodeToXPath($node->getSelector());
  107. if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) {
  108. $value = strtolower($value);
  109. }
  110. return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value);
  111. }
  112. public function translateClass(Node\ClassNode $node, Translator $translator): XPathExpr
  113. {
  114. $xpath = $translator->nodeToXPath($node->getSelector());
  115. return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName());
  116. }
  117. public function translateHash(Node\HashNode $node, Translator $translator): XPathExpr
  118. {
  119. $xpath = $translator->nodeToXPath($node->getSelector());
  120. return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId());
  121. }
  122. public function translateElement(Node\ElementNode $node): XPathExpr
  123. {
  124. $element = $node->getElement();
  125. if ($element && $this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) {
  126. $element = strtolower($element);
  127. }
  128. if ($element) {
  129. $safe = $this->isSafeName($element);
  130. } else {
  131. $element = '*';
  132. $safe = true;
  133. }
  134. if ($node->getNamespace()) {
  135. $element = sprintf('%s:%s', $node->getNamespace(), $element);
  136. $safe = $safe && $this->isSafeName($node->getNamespace());
  137. }
  138. $xpath = new XPathExpr('', $element);
  139. if (!$safe) {
  140. $xpath->addNameTest();
  141. }
  142. return $xpath;
  143. }
  144. public function getName(): string
  145. {
  146. return 'node';
  147. }
  148. private function isSafeName(string $name): bool
  149. {
  150. return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name);
  151. }
  152. }