FunctionExtension.php 5.1 KB

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