You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

197 lines
5.7 KiB

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