Translator.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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;
  11. use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
  12. use Symfony\Component\CssSelector\Node\FunctionNode;
  13. use Symfony\Component\CssSelector\Node\NodeInterface;
  14. use Symfony\Component\CssSelector\Node\SelectorNode;
  15. use Symfony\Component\CssSelector\Parser\Parser;
  16. use Symfony\Component\CssSelector\Parser\ParserInterface;
  17. /**
  18. * XPath expression translator interface.
  19. *
  20. * This component is a port of the Python cssselect library,
  21. * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
  22. *
  23. * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
  24. *
  25. * @internal
  26. */
  27. class Translator implements TranslatorInterface
  28. {
  29. /**
  30. * @var ParserInterface
  31. */
  32. private $mainParser;
  33. /**
  34. * @var ParserInterface[]
  35. */
  36. private $shortcutParsers = array();
  37. /**
  38. * @var Extension\ExtensionInterface
  39. */
  40. private $extensions = array();
  41. /**
  42. * @var array
  43. */
  44. private $nodeTranslators = array();
  45. /**
  46. * @var array
  47. */
  48. private $combinationTranslators = array();
  49. /**
  50. * @var array
  51. */
  52. private $functionTranslators = array();
  53. /**
  54. * @var array
  55. */
  56. private $pseudoClassTranslators = array();
  57. /**
  58. * @var array
  59. */
  60. private $attributeMatchingTranslators = array();
  61. /**
  62. * Constructor.
  63. */
  64. public function __construct(ParserInterface $parser = null)
  65. {
  66. $this->mainParser = $parser ?: new Parser();
  67. $this
  68. ->registerExtension(new Extension\NodeExtension())
  69. ->registerExtension(new Extension\CombinationExtension())
  70. ->registerExtension(new Extension\FunctionExtension())
  71. ->registerExtension(new Extension\PseudoClassExtension())
  72. ->registerExtension(new Extension\AttributeMatchingExtension())
  73. ;
  74. }
  75. /**
  76. * @param string $element
  77. *
  78. * @return string
  79. */
  80. public static function getXpathLiteral($element)
  81. {
  82. if (false === strpos($element, "'")) {
  83. return "'".$element."'";
  84. }
  85. if (false === strpos($element, '"')) {
  86. return '"'.$element.'"';
  87. }
  88. $string = $element;
  89. $parts = array();
  90. while (true) {
  91. if (false !== $pos = strpos($string, "'")) {
  92. $parts[] = sprintf("'%s'", substr($string, 0, $pos));
  93. $parts[] = "\"'\"";
  94. $string = substr($string, $pos + 1);
  95. } else {
  96. $parts[] = "'$string'";
  97. break;
  98. }
  99. }
  100. return sprintf('concat(%s)', implode($parts, ', '));
  101. }
  102. /**
  103. * {@inheritdoc}
  104. */
  105. public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::')
  106. {
  107. $selectors = $this->parseSelectors($cssExpr);
  108. /** @var SelectorNode $selector */
  109. foreach ($selectors as $index => $selector) {
  110. if (null !== $selector->getPseudoElement()) {
  111. throw new ExpressionErrorException('Pseudo-elements are not supported.');
  112. }
  113. $selectors[$index] = $this->selectorToXPath($selector, $prefix);
  114. }
  115. return implode(' | ', $selectors);
  116. }
  117. /**
  118. * {@inheritdoc}
  119. */
  120. public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::')
  121. {
  122. return ($prefix ?: '').$this->nodeToXPath($selector);
  123. }
  124. /**
  125. * Registers an extension.
  126. *
  127. * @param Extension\ExtensionInterface $extension
  128. *
  129. * @return Translator
  130. */
  131. public function registerExtension(Extension\ExtensionInterface $extension)
  132. {
  133. $this->extensions[$extension->getName()] = $extension;
  134. $this->nodeTranslators = array_merge($this->nodeTranslators, $extension->getNodeTranslators());
  135. $this->combinationTranslators = array_merge($this->combinationTranslators, $extension->getCombinationTranslators());
  136. $this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators());
  137. $this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators());
  138. $this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators());
  139. return $this;
  140. }
  141. /**
  142. * @param string $name
  143. *
  144. * @return Extension\ExtensionInterface
  145. *
  146. * @throws ExpressionErrorException
  147. */
  148. public function getExtension($name)
  149. {
  150. if (!isset($this->extensions[$name])) {
  151. throw new ExpressionErrorException(sprintf('Extension "%s" not registered.', $name));
  152. }
  153. return $this->extensions[$name];
  154. }
  155. /**
  156. * Registers a shortcut parser.
  157. *
  158. * @param ParserInterface $shortcut
  159. *
  160. * @return Translator
  161. */
  162. public function registerParserShortcut(ParserInterface $shortcut)
  163. {
  164. $this->shortcutParsers[] = $shortcut;
  165. return $this;
  166. }
  167. /**
  168. * @param NodeInterface $node
  169. *
  170. * @return XPathExpr
  171. *
  172. * @throws ExpressionErrorException
  173. */
  174. public function nodeToXPath(NodeInterface $node)
  175. {
  176. if (!isset($this->nodeTranslators[$node->getNodeName()])) {
  177. throw new ExpressionErrorException(sprintf('Node "%s" not supported.', $node->getNodeName()));
  178. }
  179. return call_user_func($this->nodeTranslators[$node->getNodeName()], $node, $this);
  180. }
  181. /**
  182. * @param string $combiner
  183. * @param NodeInterface $xpath
  184. * @param NodeInterface $combinedXpath
  185. *
  186. * @return XPathExpr
  187. *
  188. * @throws ExpressionErrorException
  189. */
  190. public function addCombination($combiner, NodeInterface $xpath, NodeInterface $combinedXpath)
  191. {
  192. if (!isset($this->combinationTranslators[$combiner])) {
  193. throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner));
  194. }
  195. return call_user_func($this->combinationTranslators[$combiner], $this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
  196. }
  197. /**
  198. * @param XPathExpr $xpath
  199. * @param FunctionNode $function
  200. *
  201. * @return XPathExpr
  202. *
  203. * @throws ExpressionErrorException
  204. */
  205. public function addFunction(XPathExpr $xpath, FunctionNode $function)
  206. {
  207. if (!isset($this->functionTranslators[$function->getName()])) {
  208. throw new ExpressionErrorException(sprintf('Function "%s" not supported.', $function->getName()));
  209. }
  210. return call_user_func($this->functionTranslators[$function->getName()], $xpath, $function);
  211. }
  212. /**
  213. * @param XPathExpr $xpath
  214. * @param string $pseudoClass
  215. *
  216. * @return XPathExpr
  217. *
  218. * @throws ExpressionErrorException
  219. */
  220. public function addPseudoClass(XPathExpr $xpath, $pseudoClass)
  221. {
  222. if (!isset($this->pseudoClassTranslators[$pseudoClass])) {
  223. throw new ExpressionErrorException(sprintf('Pseudo-class "%s" not supported.', $pseudoClass));
  224. }
  225. return call_user_func($this->pseudoClassTranslators[$pseudoClass], $xpath);
  226. }
  227. /**
  228. * @param XPathExpr $xpath
  229. * @param string $operator
  230. * @param string $attribute
  231. * @param string $value
  232. *
  233. * @throws ExpressionErrorException
  234. *
  235. * @return XPathExpr
  236. */
  237. public function addAttributeMatching(XPathExpr $xpath, $operator, $attribute, $value)
  238. {
  239. if (!isset($this->attributeMatchingTranslators[$operator])) {
  240. throw new ExpressionErrorException(sprintf('Attribute matcher operator "%s" not supported.', $operator));
  241. }
  242. return call_user_func($this->attributeMatchingTranslators[$operator], $xpath, $attribute, $value);
  243. }
  244. /**
  245. * @param string $css
  246. *
  247. * @return SelectorNode[]
  248. */
  249. private function parseSelectors($css)
  250. {
  251. foreach ($this->shortcutParsers as $shortcut) {
  252. $tokens = $shortcut->parse($css);
  253. if (!empty($tokens)) {
  254. return $tokens;
  255. }
  256. }
  257. return $this->mainParser->parse($css);
  258. }
  259. }