PageRenderTime 25ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/symfony/symfony/src/Symfony/Component/CssSelector/CssSelector.php

https://github.com/nattaphat/hgis
PHP | 322 lines | 187 code | 40 blank | 95 comment | 67 complexity | 7ad4d336654bb03a8bd5e6aad85984ce MD5 | raw file
  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;
  11. use Symfony\Component\CssSelector\Exception\ParseException;
  12. /**
  13. * CssSelector is the main entry point of the component and can convert CSS
  14. * selectors to XPath expressions.
  15. *
  16. * $xpath = CssSelector::toXpath('h1.foo');
  17. *
  18. * This component is a port of the Python lxml library,
  19. * which is copyright Infrae and distributed under the BSD license.
  20. *
  21. * @author Fabien Potencier <fabien@symfony.com>
  22. *
  23. * @api
  24. */
  25. class CssSelector
  26. {
  27. /**
  28. * Translates a CSS expression to its XPath equivalent.
  29. * Optionally, a prefix can be added to the resulting XPath
  30. * expression with the $prefix parameter.
  31. *
  32. * @param mixed $cssExpr The CSS expression.
  33. * @param string $prefix An optional prefix for the XPath expression.
  34. *
  35. * @return string
  36. *
  37. * @throws ParseException When got None for xpath expression
  38. *
  39. * @api
  40. */
  41. public static function toXPath($cssExpr, $prefix = 'descendant-or-self::')
  42. {
  43. if (is_string($cssExpr)) {
  44. if (!$cssExpr) {
  45. return $prefix.'*';
  46. }
  47. if (preg_match('#^\w+\s*$#u', $cssExpr, $match)) {
  48. return $prefix.trim($match[0]);
  49. }
  50. if (preg_match('~^(\w*)#(\w+)\s*$~u', $cssExpr, $match)) {
  51. return sprintf("%s%s[@id = '%s']", $prefix, $match[1] ? $match[1] : '*', $match[2]);
  52. }
  53. if (preg_match('#^(\w*)\.(\w+)\s*$#u', $cssExpr, $match)) {
  54. return sprintf("%s%s[contains(concat(' ', normalize-space(@class), ' '), ' %s ')]", $prefix, $match[1] ? $match[1] : '*', $match[2]);
  55. }
  56. $parser = new self();
  57. $cssExpr = $parser->parse($cssExpr);
  58. }
  59. $expr = $cssExpr->toXpath();
  60. // @codeCoverageIgnoreStart
  61. if (!$expr) {
  62. throw new ParseException(sprintf('Got None for xpath expression from %s.', $cssExpr));
  63. }
  64. // @codeCoverageIgnoreEnd
  65. if ($prefix) {
  66. $expr->addPrefix($prefix);
  67. }
  68. return (string) $expr;
  69. }
  70. /**
  71. * Parses an expression and returns the Node object that represents
  72. * the parsed expression.
  73. *
  74. * @param string $string The expression to parse
  75. *
  76. * @return Node\NodeInterface
  77. *
  78. * @throws \Exception When tokenizer throws it while parsing
  79. */
  80. public function parse($string)
  81. {
  82. $tokenizer = new Tokenizer();
  83. $stream = new TokenStream($tokenizer->tokenize($string), $string);
  84. try {
  85. return $this->parseSelectorGroup($stream);
  86. } catch (\Exception $e) {
  87. $class = get_class($e);
  88. throw new $class(sprintf('%s at %s -> %s', $e->getMessage(), implode($stream->getUsed(), ''), $stream->peek()), 0, $e);
  89. }
  90. }
  91. /**
  92. * Parses a selector group contained in $stream and returns
  93. * the Node object that represents the expression.
  94. *
  95. * @param TokenStream $stream The stream to parse.
  96. *
  97. * @return Node\NodeInterface
  98. */
  99. private function parseSelectorGroup($stream)
  100. {
  101. $result = array();
  102. while (true) {
  103. $result[] = $this->parseSelector($stream);
  104. if ($stream->peek() == ',') {
  105. $stream->next();
  106. } else {
  107. break;
  108. }
  109. }
  110. if (count($result) == 1) {
  111. return $result[0];
  112. }
  113. return new Node\OrNode($result);
  114. }
  115. /**
  116. * Parses a selector contained in $stream and returns the Node
  117. * object that represents it.
  118. *
  119. * @param TokenStream $stream The stream containing the selector.
  120. *
  121. * @return Node\NodeInterface
  122. *
  123. * @throws ParseException When expected selector but got something else
  124. */
  125. private function parseSelector($stream)
  126. {
  127. $result = $this->parseSimpleSelector($stream);
  128. while (true) {
  129. $peek = $stream->peek();
  130. if (',' == $peek || null === $peek) {
  131. return $result;
  132. } elseif (in_array($peek, array('+', '>', '~'))) {
  133. // A combinator
  134. $combinator = (string) $stream->next();
  135. // Ignore optional whitespace after a combinator
  136. while (' ' == $stream->peek()) {
  137. $stream->next();
  138. }
  139. } else {
  140. $combinator = ' ';
  141. }
  142. $consumed = count($stream->getUsed());
  143. $nextSelector = $this->parseSimpleSelector($stream);
  144. if ($consumed == count($stream->getUsed())) {
  145. throw new ParseException(sprintf("Expected selector, got '%s'", $stream->peek()));
  146. }
  147. $result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
  148. }
  149. return $result;
  150. }
  151. /**
  152. * Parses a simple selector (the current token) from $stream and returns
  153. * the resulting Node object.
  154. *
  155. * @param TokenStream $stream The stream containing the selector.
  156. *
  157. * @return Node\NodeInterface
  158. *
  159. * @throws ParseException When expected symbol but got something else
  160. */
  161. private function parseSimpleSelector($stream)
  162. {
  163. $peek = $stream->peek();
  164. if ('*' != $peek && !$peek->isType('Symbol')) {
  165. $element = $namespace = '*';
  166. } else {
  167. $next = $stream->next();
  168. if ('*' != $next && !$next->isType('Symbol')) {
  169. throw new ParseException(sprintf("Expected symbol, got '%s'", $next));
  170. }
  171. if ($stream->peek() == '|') {
  172. $namespace = $next;
  173. $stream->next();
  174. $element = $stream->next();
  175. if ('*' != $element && !$next->isType('Symbol')) {
  176. throw new ParseException(sprintf("Expected symbol, got '%s'", $next));
  177. }
  178. } else {
  179. $namespace = '*';
  180. $element = $next;
  181. }
  182. }
  183. $result = new Node\ElementNode($namespace, $element);
  184. $hasHash = false;
  185. while (true) {
  186. $peek = $stream->peek();
  187. if ('#' == $peek) {
  188. if ($hasHash) {
  189. /* You can't have two hashes
  190. (FIXME: is there some more general rule I'm missing?) */
  191. // @codeCoverageIgnoreStart
  192. break;
  193. // @codeCoverageIgnoreEnd
  194. }
  195. $stream->next();
  196. $result = new Node\HashNode($result, $stream->next());
  197. $hasHash = true;
  198. continue;
  199. } elseif ('.' == $peek) {
  200. $stream->next();
  201. $result = new Node\ClassNode($result, $stream->next());
  202. continue;
  203. } elseif ('[' == $peek) {
  204. $stream->next();
  205. $result = $this->parseAttrib($result, $stream);
  206. $next = $stream->next();
  207. if (']' != $next) {
  208. throw new ParseException(sprintf("] expected, got '%s'", $next));
  209. }
  210. continue;
  211. } elseif (':' == $peek || '::' == $peek) {
  212. $type = $stream->next();
  213. $ident = $stream->next();
  214. if (!$ident || !$ident->isType('Symbol')) {
  215. throw new ParseException(sprintf("Expected symbol, got '%s'", $ident));
  216. }
  217. if ($stream->peek() == '(') {
  218. $stream->next();
  219. $peek = $stream->peek();
  220. if ($peek->isType('String')) {
  221. $selector = $stream->next();
  222. } elseif ($peek->isType('Symbol') && is_int($peek)) {
  223. $selector = intval($stream->next());
  224. } else {
  225. // FIXME: parseSimpleSelector, or selector, or...?
  226. $selector = $this->parseSimpleSelector($stream);
  227. }
  228. $next = $stream->next();
  229. if (')' != $next) {
  230. throw new ParseException(sprintf("Expected ')', got '%s' and '%s'", $next, $selector));
  231. }
  232. $result = new Node\FunctionNode($result, $type, $ident, $selector);
  233. } else {
  234. $result = new Node\PseudoNode($result, $type, $ident);
  235. }
  236. continue;
  237. } else {
  238. if (' ' == $peek) {
  239. $stream->next();
  240. }
  241. break;
  242. }
  243. // FIXME: not sure what "negation" is
  244. }
  245. return $result;
  246. }
  247. /**
  248. * Parses an attribute from a selector contained in $stream and returns
  249. * the resulting AttribNode object.
  250. *
  251. * @param Node\NodeInterface $selector The selector object whose attribute
  252. * is to be parsed.
  253. * @param TokenStream $stream The container token stream.
  254. *
  255. * @return Node\AttribNode
  256. *
  257. * @throws ParseException When encountered unexpected selector
  258. */
  259. private function parseAttrib($selector, $stream)
  260. {
  261. $attrib = $stream->next();
  262. if ($stream->peek() == '|') {
  263. $namespace = $attrib;
  264. $stream->next();
  265. $attrib = $stream->next();
  266. } else {
  267. $namespace = '*';
  268. }
  269. if ($stream->peek() == ']') {
  270. return new Node\AttribNode($selector, $namespace, $attrib, 'exists', null);
  271. }
  272. $op = $stream->next();
  273. if (!in_array($op, array('^=', '$=', '*=', '=', '~=', '|=', '!='))) {
  274. throw new ParseException(sprintf("Operator expected, got '%s'", $op));
  275. }
  276. $value = $stream->next();
  277. if (!$value->isType('Symbol') && !$value->isType('String')) {
  278. throw new ParseException(sprintf("Expected string or symbol, got '%s'", $value));
  279. }
  280. return new Node\AttribNode($selector, $namespace, $attrib, $op, $value);
  281. }
  282. }