FunctionExtension.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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. public function getFunctionTranslators(): array
  30. {
  31. return [
  32. 'nth-child' => $this->translateNthChild(...),
  33. 'nth-last-child' => $this->translateNthLastChild(...),
  34. 'nth-of-type' => $this->translateNthOfType(...),
  35. 'nth-last-of-type' => $this->translateNthLastOfType(...),
  36. 'contains' => $this->translateContains(...),
  37. 'lang' => $this->translateLang(...),
  38. ];
  39. }
  40. /**
  41. * @throws ExpressionErrorException
  42. */
  43. public function translateNthChild(XPathExpr $xpath, FunctionNode $function, bool $last = false, bool $addNameTest = true): XPathExpr
  44. {
  45. try {
  46. [$a, $b] = Parser::parseSeries($function->getArguments());
  47. } catch (SyntaxErrorException $e) {
  48. throw new ExpressionErrorException(sprintf('Invalid series: "%s".', implode('", "', $function->getArguments())), 0, $e);
  49. }
  50. $xpath->addStarPrefix();
  51. if ($addNameTest) {
  52. $xpath->addNameTest();
  53. }
  54. if (0 === $a) {
  55. return $xpath->addCondition('position() = '.($last ? 'last() - '.($b - 1) : $b));
  56. }
  57. if ($a < 0) {
  58. if ($b < 1) {
  59. return $xpath->addCondition('false()');
  60. }
  61. $sign = '<=';
  62. } else {
  63. $sign = '>=';
  64. }
  65. $expr = 'position()';
  66. if ($last) {
  67. $expr = 'last() - '.$expr;
  68. --$b;
  69. }
  70. if (0 !== $b) {
  71. $expr .= ' - '.$b;
  72. }
  73. $conditions = [sprintf('%s %s 0', $expr, $sign)];
  74. if (1 !== $a && -1 !== $a) {
  75. $conditions[] = sprintf('(%s) mod %d = 0', $expr, $a);
  76. }
  77. return $xpath->addCondition(implode(' and ', $conditions));
  78. // todo: handle an+b, odd, even
  79. // an+b means every-a, plus b, e.g., 2n+1 means odd
  80. // 0n+b means b
  81. // n+0 means a=1, i.e., all elements
  82. // an means every a elements, i.e., 2n means even
  83. // -n means -1n
  84. // -1n+6 means elements 6 and previous
  85. }
  86. public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function): XPathExpr
  87. {
  88. return $this->translateNthChild($xpath, $function, true);
  89. }
  90. public function translateNthOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
  91. {
  92. return $this->translateNthChild($xpath, $function, false, false);
  93. }
  94. /**
  95. * @throws ExpressionErrorException
  96. */
  97. public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
  98. {
  99. if ('*' === $xpath->getElement()) {
  100. throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.');
  101. }
  102. return $this->translateNthChild($xpath, $function, true, false);
  103. }
  104. /**
  105. * @throws ExpressionErrorException
  106. */
  107. public function translateContains(XPathExpr $xpath, FunctionNode $function): XPathExpr
  108. {
  109. $arguments = $function->getArguments();
  110. foreach ($arguments as $token) {
  111. if (!($token->isString() || $token->isIdentifier())) {
  112. throw new ExpressionErrorException('Expected a single string or identifier for :contains(), got '.implode(', ', $arguments));
  113. }
  114. }
  115. return $xpath->addCondition(sprintf(
  116. 'contains(string(.), %s)',
  117. Translator::getXpathLiteral($arguments[0]->getValue())
  118. ));
  119. }
  120. /**
  121. * @throws ExpressionErrorException
  122. */
  123. public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr
  124. {
  125. $arguments = $function->getArguments();
  126. foreach ($arguments as $token) {
  127. if (!($token->isString() || $token->isIdentifier())) {
  128. throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments));
  129. }
  130. }
  131. return $xpath->addCondition(sprintf(
  132. 'lang(%s)',
  133. Translator::getXpathLiteral($arguments[0]->getValue())
  134. ));
  135. }
  136. public function getName(): string
  137. {
  138. return 'function';
  139. }
  140. }