FunctionNode.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien.potencier@symfony-project.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\Node;
  11. use Symfony\Component\CssSelector\SyntaxError;
  12. use Symfony\Component\CssSelector\XPathExpr;
  13. /**
  14. * FunctionNode represents a "selector:name(expr)" node.
  15. *
  16. * This component is a port of the Python lxml library,
  17. * which is copyright Infrae and distributed under the BSD license.
  18. *
  19. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  20. */
  21. class FunctionNode implements NodeInterface
  22. {
  23. static protected $unsupported = array('target', 'lang', 'enabled', 'disabled');
  24. protected $selector;
  25. protected $type;
  26. protected $name;
  27. protected $expr;
  28. /**
  29. * Constructor.
  30. *
  31. * @param NodeInterface $selector The XPath expression
  32. * @param string $type
  33. * @param string $name
  34. * @param XPathExpr $expr
  35. */
  36. public function __construct($selector, $type, $name, $expr)
  37. {
  38. $this->selector = $selector;
  39. $this->type = $type;
  40. $this->name = $name;
  41. $this->expr = $expr;
  42. }
  43. /**
  44. * {@inheritDoc}
  45. */
  46. public function __toString()
  47. {
  48. return sprintf('%s[%s%s%s(%s)]', __CLASS__, $this->selector, $this->type, $this->name, $this->expr);
  49. }
  50. /**
  51. * {@inheritDoc}
  52. * @throws SyntaxError When unsupported or unknown pseudo-class is found
  53. */
  54. public function toXpath()
  55. {
  56. $sel_path = $this->selector->toXpath();
  57. if (in_array($this->name, self::$unsupported)) {
  58. throw new SyntaxError(sprintf('The pseudo-class %s is not supported', $this->name));
  59. }
  60. $method = '_xpath_'.str_replace('-', '_', $this->name);
  61. if (!method_exists($this, $method)) {
  62. throw new SyntaxError(sprintf('The pseudo-class %s is unknown', $this->name));
  63. }
  64. return $this->$method($sel_path, $this->expr);
  65. }
  66. /**
  67. * undocumented function
  68. *
  69. * @param XPathExpr $xpath
  70. * @param mixed $expr
  71. * @param string $last
  72. * @param string $addNameTest
  73. * @return XPathExpr
  74. */
  75. protected function _xpath_nth_child($xpath, $expr, $last = false, $addNameTest = true)
  76. {
  77. list($a, $b) = $this->parseSeries($expr);
  78. if (!$a && !$b && !$last) {
  79. // a=0 means nothing is returned...
  80. $xpath->addCondition('false() and position() = 0');
  81. return $xpath;
  82. }
  83. if ($addNameTest) {
  84. $xpath->addNameTest();
  85. }
  86. $xpath->addStarPrefix();
  87. if ($a == 0) {
  88. if ($last) {
  89. $b = sprintf('last() - %s', $b);
  90. }
  91. $xpath->addCondition(sprintf('position() = %s', $b));
  92. return $xpath;
  93. }
  94. if ($last) {
  95. // FIXME: I'm not sure if this is right
  96. $a = -$a;
  97. $b = -$b;
  98. }
  99. if ($b > 0) {
  100. $b_neg = -$b;
  101. } else {
  102. $b_neg = sprintf('+%s', -$b);
  103. }
  104. if ($a != 1) {
  105. $expr = array(sprintf('(position() %s) mod %s = 0', $b_neg, $a));
  106. } else {
  107. $expr = array();
  108. }
  109. if ($b >= 0) {
  110. $expr[] = sprintf('position() >= %s', $b);
  111. } elseif ($b < 0 && $last) {
  112. $expr[] = sprintf('position() < (last() %s)', $b);
  113. }
  114. $expr = implode($expr, ' and ');
  115. if ($expr) {
  116. $xpath->addCondition($expr);
  117. }
  118. return $xpath;
  119. /* FIXME: handle an+b, odd, even
  120. an+b means every-a, plus b, e.g., 2n+1 means odd
  121. 0n+b means b
  122. n+0 means a=1, i.e., all elements
  123. an means every a elements, i.e., 2n means even
  124. -n means -1n
  125. -1n+6 means elements 6 and previous */
  126. }
  127. /**
  128. * undocumented function
  129. *
  130. * @param XPathExpr $xpath
  131. * @param XPathExpr $expr
  132. * @return XPathExpr
  133. */
  134. protected function _xpath_nth_last_child($xpath, $expr)
  135. {
  136. return $this->_xpath_nth_child($xpath, $expr, true);
  137. }
  138. /**
  139. * undocumented function
  140. *
  141. * @param XPathExpr $xpath
  142. * @param XPathExpr $expr
  143. * @return XPathExpr
  144. */
  145. protected function _xpath_nth_of_type($xpath, $expr)
  146. {
  147. if ($xpath->getElement() == '*') {
  148. throw new SyntaxError('*:nth-of-type() is not implemented');
  149. }
  150. return $this->_xpath_nth_child($xpath, $expr, false, false);
  151. }
  152. /**
  153. * undocumented function
  154. *
  155. * @param XPathExpr $xpath
  156. * @param XPathExpr $expr
  157. * @return XPathExpr
  158. */
  159. protected function _xpath_nth_last_of_type($xpath, $expr)
  160. {
  161. return $this->_xpath_nth_child($xpath, $expr, true, false);
  162. }
  163. /**
  164. * undocumented function
  165. *
  166. * @param XPathExpr $xpath
  167. * @param XPathExpr $expr
  168. * @return XPathExpr
  169. */
  170. protected function _xpath_contains($xpath, $expr)
  171. {
  172. // text content, minus tags, must contain expr
  173. if ($expr instanceof ElementNode) {
  174. $expr = $expr->formatElement();
  175. }
  176. // FIXME: lower-case is only available with XPath 2
  177. //$xpath->addCondition(sprintf('contains(lower-case(string(.)), %s)', XPathExpr::xpathLiteral(strtolower($expr))));
  178. $xpath->addCondition(sprintf('contains(string(.), %s)', XPathExpr::xpathLiteral($expr)));
  179. // FIXME: Currently case insensitive matching doesn't seem to be happening
  180. return $xpath;
  181. }
  182. /**
  183. * undocumented function
  184. *
  185. * @param XPathExpr $xpath
  186. * @param XPathExpr $expr
  187. * @return XPathExpr
  188. */
  189. protected function _xpath_not($xpath, $expr)
  190. {
  191. // everything for which not expr applies
  192. $expr = $expr->toXpath();
  193. $cond = $expr->getCondition();
  194. // FIXME: should I do something about element_path?
  195. $xpath->addCondition(sprintf('not(%s)', $cond));
  196. return $xpath;
  197. }
  198. /**
  199. * Parses things like '1n+2', or 'an+b' generally, returning (a, b)
  200. *
  201. * @param mixed $s
  202. * @return array
  203. */
  204. protected function parseSeries($s)
  205. {
  206. if ($s instanceof ElementNode) {
  207. $s = $s->formatElement();
  208. }
  209. if (!$s || '*' == $s) {
  210. // Happens when there's nothing, which the CSS parser thinks of as *
  211. return array(0, 0);
  212. }
  213. if (is_string($s)) {
  214. // Happens when you just get a number
  215. return array(0, $s);
  216. }
  217. if ('odd' == $s) {
  218. return array(2, 1);
  219. }
  220. if ('even' == $s) {
  221. return array(2, 0);
  222. }
  223. if ('n' == $s) {
  224. return array(1, 0);
  225. }
  226. if (false === strpos($s, 'n')) {
  227. // Just a b
  228. return array(0, intval((string) $s));
  229. }
  230. list($a, $b) = explode('n', $s);
  231. if (!$a) {
  232. $a = 1;
  233. } elseif ('-' == $a || '+' == $a) {
  234. $a = intval($a.'1');
  235. } else {
  236. $a = intval($a);
  237. }
  238. if (!$b) {
  239. $b = 0;
  240. } elseif ('-' == $b || '+' == $b) {
  241. $b = intval($b.'1');
  242. } else {
  243. $b = intval($b);
  244. }
  245. return array($a, $b);
  246. }
  247. }