NodeExtension.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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. const ELEMENT_NAME_IN_LOWER_CASE = 1;
  27. const ATTRIBUTE_NAME_IN_LOWER_CASE = 2;
  28. const ATTRIBUTE_VALUE_IN_LOWER_CASE = 4;
  29. /**
  30. * @var int
  31. */
  32. private $flags;
  33. /**
  34. * @param int $flags
  35. */
  36. public function __construct($flags = 0)
  37. {
  38. $this->flags = $flags;
  39. }
  40. /**
  41. * @param int $flag
  42. * @param bool $on
  43. *
  44. * @return $this
  45. */
  46. public function setFlag($flag, $on)
  47. {
  48. if ($on && !$this->hasFlag($flag)) {
  49. $this->flags += $flag;
  50. }
  51. if (!$on && $this->hasFlag($flag)) {
  52. $this->flags -= $flag;
  53. }
  54. return $this;
  55. }
  56. /**
  57. * @param int $flag
  58. *
  59. * @return bool
  60. */
  61. public function hasFlag($flag)
  62. {
  63. return (bool) ($this->flags & $flag);
  64. }
  65. /**
  66. * {@inheritdoc}
  67. */
  68. public function getNodeTranslators()
  69. {
  70. return array(
  71. 'Selector' => array($this, 'translateSelector'),
  72. 'CombinedSelector' => array($this, 'translateCombinedSelector'),
  73. 'Negation' => array($this, 'translateNegation'),
  74. 'Function' => array($this, 'translateFunction'),
  75. 'Pseudo' => array($this, 'translatePseudo'),
  76. 'Attribute' => array($this, 'translateAttribute'),
  77. 'Class' => array($this, 'translateClass'),
  78. 'Hash' => array($this, 'translateHash'),
  79. 'Element' => array($this, 'translateElement'),
  80. );
  81. }
  82. /**
  83. * @param Node\SelectorNode $node
  84. * @param Translator $translator
  85. *
  86. * @return XPathExpr
  87. */
  88. public function translateSelector(Node\SelectorNode $node, Translator $translator)
  89. {
  90. return $translator->nodeToXPath($node->getTree());
  91. }
  92. /**
  93. * @param Node\CombinedSelectorNode $node
  94. * @param Translator $translator
  95. *
  96. * @return XPathExpr
  97. */
  98. public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator)
  99. {
  100. return $translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector());
  101. }
  102. /**
  103. * @param Node\NegationNode $node
  104. * @param Translator $translator
  105. *
  106. * @return XPathExpr
  107. */
  108. public function translateNegation(Node\NegationNode $node, Translator $translator)
  109. {
  110. $xpath = $translator->nodeToXPath($node->getSelector());
  111. $subXpath = $translator->nodeToXPath($node->getSubSelector());
  112. $subXpath->addNameTest();
  113. if ($subXpath->getCondition()) {
  114. return $xpath->addCondition(sprintf('not(%s)', $subXpath->getCondition()));
  115. }
  116. return $xpath->addCondition('0');
  117. }
  118. /**
  119. * @param Node\FunctionNode $node
  120. * @param Translator $translator
  121. *
  122. * @return XPathExpr
  123. */
  124. public function translateFunction(Node\FunctionNode $node, Translator $translator)
  125. {
  126. $xpath = $translator->nodeToXPath($node->getSelector());
  127. return $translator->addFunction($xpath, $node);
  128. }
  129. /**
  130. * @param Node\PseudoNode $node
  131. * @param Translator $translator
  132. *
  133. * @return XPathExpr
  134. */
  135. public function translatePseudo(Node\PseudoNode $node, Translator $translator)
  136. {
  137. $xpath = $translator->nodeToXPath($node->getSelector());
  138. return $translator->addPseudoClass($xpath, $node->getIdentifier());
  139. }
  140. /**
  141. * @param Node\AttributeNode $node
  142. * @param Translator $translator
  143. *
  144. * @return XPathExpr
  145. */
  146. public function translateAttribute(Node\AttributeNode $node, Translator $translator)
  147. {
  148. $name = $node->getAttribute();
  149. $safe = $this->isSafeName($name);
  150. if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) {
  151. $name = strtolower($name);
  152. }
  153. if ($node->getNamespace()) {
  154. $name = sprintf('%s:%s', $node->getNamespace(), $name);
  155. $safe = $safe && $this->isSafeName($node->getNamespace());
  156. }
  157. $attribute = $safe ? '@'.$name : sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name));
  158. $value = $node->getValue();
  159. $xpath = $translator->nodeToXPath($node->getSelector());
  160. if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) {
  161. $value = strtolower($value);
  162. }
  163. return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value);
  164. }
  165. /**
  166. * @param Node\ClassNode $node
  167. * @param Translator $translator
  168. *
  169. * @return XPathExpr
  170. */
  171. public function translateClass(Node\ClassNode $node, Translator $translator)
  172. {
  173. $xpath = $translator->nodeToXPath($node->getSelector());
  174. return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName());
  175. }
  176. /**
  177. * @param Node\HashNode $node
  178. * @param Translator $translator
  179. *
  180. * @return XPathExpr
  181. */
  182. public function translateHash(Node\HashNode $node, Translator $translator)
  183. {
  184. $xpath = $translator->nodeToXPath($node->getSelector());
  185. return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId());
  186. }
  187. /**
  188. * @param Node\ElementNode $node
  189. *
  190. * @return XPathExpr
  191. */
  192. public function translateElement(Node\ElementNode $node)
  193. {
  194. $element = $node->getElement();
  195. if ($this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) {
  196. $element = strtolower($element);
  197. }
  198. if ($element) {
  199. $safe = $this->isSafeName($element);
  200. } else {
  201. $element = '*';
  202. $safe = true;
  203. }
  204. if ($node->getNamespace()) {
  205. $element = sprintf('%s:%s', $node->getNamespace(), $element);
  206. $safe = $safe && $this->isSafeName($node->getNamespace());
  207. }
  208. $xpath = new XPathExpr('', $element);
  209. if (!$safe) {
  210. $xpath->addNameTest();
  211. }
  212. return $xpath;
  213. }
  214. /**
  215. * {@inheritdoc}
  216. */
  217. public function getName()
  218. {
  219. return 'node';
  220. }
  221. /**
  222. * Tests if given name is safe.
  223. *
  224. * @param string $name
  225. *
  226. * @return bool
  227. */
  228. private function isSafeName($name)
  229. {
  230. return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name);
  231. }
  232. }