NodeExtension.php 7.0 KB

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