FunctionExtension.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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\Exception\ExpressionErrorException;
  12. use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
  13. use Symfony\Component\CssSelector\Node\FunctionNode;
  14. use Symfony\Component\CssSelector\Parser\Parser;
  15. use Symfony\Component\CssSelector\XPath\Translator;
  16. use Symfony\Component\CssSelector\XPath\XPathExpr;
  17. /**
  18. * XPath expression translator function extension.
  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 FunctionExtension extends AbstractExtension
  28. {
  29. /**
  30. * {@inheritdoc}
  31. */
  32. public function getFunctionTranslators()
  33. {
  34. return array(
  35. 'nth-child' => array($this, 'translateNthChild'),
  36. 'nth-last-child' => array($this, 'translateNthLastChild'),
  37. 'nth-of-type' => array($this, 'translateNthOfType'),
  38. 'nth-last-of-type' => array($this, 'translateNthLastOfType'),
  39. 'contains' => array($this, 'translateContains'),
  40. 'lang' => array($this, 'translateLang'),
  41. );
  42. }
  43. /**
  44. * @param XPathExpr $xpath
  45. * @param FunctionNode $function
  46. * @param bool $last
  47. * @param bool $addNameTest
  48. *
  49. * @return XPathExpr
  50. *
  51. * @throws ExpressionErrorException
  52. */
  53. public function translateNthChild(XPathExpr $xpath, FunctionNode $function, $last = false, $addNameTest = true)
  54. {
  55. try {
  56. list($a, $b) = Parser::parseSeries($function->getArguments());
  57. } catch (SyntaxErrorException $e) {
  58. throw new ExpressionErrorException(sprintf('Invalid series: %s', implode(', ', $function->getArguments())), 0, $e);
  59. }
  60. $xpath->addStarPrefix();
  61. if ($addNameTest) {
  62. $xpath->addNameTest();
  63. }
  64. if (0 === $a) {
  65. return $xpath->addCondition('position() = '.($last ? 'last() - '.($b - 1) : $b));
  66. }
  67. if ($a < 0) {
  68. if ($b < 1) {
  69. return $xpath->addCondition('false()');
  70. }
  71. $sign = '<=';
  72. } else {
  73. $sign = '>=';
  74. }
  75. $expr = 'position()';
  76. if ($last) {
  77. $expr = 'last() - '.$expr;
  78. --$b;
  79. }
  80. if (0 !== $b) {
  81. $expr .= ' - '.$b;
  82. }
  83. $conditions = array(sprintf('%s %s 0', $expr, $sign));
  84. if (1 !== $a && -1 !== $a) {
  85. $conditions[] = sprintf('(%s) mod %d = 0', $expr, $a);
  86. }
  87. return $xpath->addCondition(implode(' and ', $conditions));
  88. // todo: handle an+b, odd, even
  89. // an+b means every-a, plus b, e.g., 2n+1 means odd
  90. // 0n+b means b
  91. // n+0 means a=1, i.e., all elements
  92. // an means every a elements, i.e., 2n means even
  93. // -n means -1n
  94. // -1n+6 means elements 6 and previous
  95. }
  96. /**
  97. * @param XPathExpr $xpath
  98. * @param FunctionNode $function
  99. *
  100. * @return XPathExpr
  101. */
  102. public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function)
  103. {
  104. return $this->translateNthChild($xpath, $function, true);
  105. }
  106. /**
  107. * @param XPathExpr $xpath
  108. * @param FunctionNode $function
  109. *
  110. * @return XPathExpr
  111. */
  112. public function translateNthOfType(XPathExpr $xpath, FunctionNode $function)
  113. {
  114. return $this->translateNthChild($xpath, $function, false, false);
  115. }
  116. /**
  117. * @param XPathExpr $xpath
  118. * @param FunctionNode $function
  119. *
  120. * @return XPathExpr
  121. *
  122. * @throws ExpressionErrorException
  123. */
  124. public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function)
  125. {
  126. if ('*' === $xpath->getElement()) {
  127. throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.');
  128. }
  129. return $this->translateNthChild($xpath, $function, true, false);
  130. }
  131. /**
  132. * @param XPathExpr $xpath
  133. * @param FunctionNode $function
  134. *
  135. * @return XPathExpr
  136. *
  137. * @throws ExpressionErrorException
  138. */
  139. public function translateContains(XPathExpr $xpath, FunctionNode $function)
  140. {
  141. $arguments = $function->getArguments();
  142. foreach ($arguments as $token) {
  143. if (!($token->isString() || $token->isIdentifier())) {
  144. throw new ExpressionErrorException(
  145. 'Expected a single string or identifier for :contains(), got '
  146. .implode(', ', $arguments)
  147. );
  148. }
  149. }
  150. return $xpath->addCondition(sprintf(
  151. 'contains(string(.), %s)',
  152. Translator::getXpathLiteral($arguments[0]->getValue())
  153. ));
  154. }
  155. /**
  156. * @param XPathExpr $xpath
  157. * @param FunctionNode $function
  158. *
  159. * @return XPathExpr
  160. *
  161. * @throws ExpressionErrorException
  162. */
  163. public function translateLang(XPathExpr $xpath, FunctionNode $function)
  164. {
  165. $arguments = $function->getArguments();
  166. foreach ($arguments as $token) {
  167. if (!($token->isString() || $token->isIdentifier())) {
  168. throw new ExpressionErrorException(
  169. 'Expected a single string or identifier for :lang(), got '
  170. .implode(', ', $arguments)
  171. );
  172. }
  173. }
  174. return $xpath->addCondition(sprintf(
  175. 'lang(%s)',
  176. Translator::getXpathLiteral($arguments[0]->getValue())
  177. ));
  178. }
  179. /**
  180. * {@inheritdoc}
  181. */
  182. public function getName()
  183. {
  184. return 'function';
  185. }
  186. }