Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\CssSelector\XPath\Extension; Chris@0: Chris@0: use Symfony\Component\CssSelector\Exception\ExpressionErrorException; Chris@0: use Symfony\Component\CssSelector\Exception\SyntaxErrorException; Chris@0: use Symfony\Component\CssSelector\Node\FunctionNode; Chris@0: use Symfony\Component\CssSelector\Parser\Parser; Chris@0: use Symfony\Component\CssSelector\XPath\Translator; Chris@0: use Symfony\Component\CssSelector\XPath\XPathExpr; Chris@0: Chris@0: /** Chris@0: * XPath expression translator function extension. Chris@0: * Chris@0: * This component is a port of the Python cssselect library, Chris@0: * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. Chris@0: * Chris@0: * @author Jean-François Simon Chris@0: * Chris@0: * @internal Chris@0: */ Chris@0: class FunctionExtension extends AbstractExtension Chris@0: { Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getFunctionTranslators() Chris@0: { Chris@17: return [ Chris@17: 'nth-child' => [$this, 'translateNthChild'], Chris@17: 'nth-last-child' => [$this, 'translateNthLastChild'], Chris@17: 'nth-of-type' => [$this, 'translateNthOfType'], Chris@17: 'nth-last-of-type' => [$this, 'translateNthLastOfType'], Chris@17: 'contains' => [$this, 'translateContains'], Chris@17: 'lang' => [$this, 'translateLang'], Chris@17: ]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param XPathExpr $xpath Chris@0: * @param FunctionNode $function Chris@0: * @param bool $last Chris@0: * @param bool $addNameTest Chris@0: * Chris@0: * @return XPathExpr Chris@0: * Chris@0: * @throws ExpressionErrorException Chris@0: */ Chris@0: public function translateNthChild(XPathExpr $xpath, FunctionNode $function, $last = false, $addNameTest = true) Chris@0: { Chris@0: try { Chris@0: list($a, $b) = Parser::parseSeries($function->getArguments()); Chris@0: } catch (SyntaxErrorException $e) { Chris@0: throw new ExpressionErrorException(sprintf('Invalid series: %s', implode(', ', $function->getArguments())), 0, $e); Chris@0: } Chris@0: Chris@0: $xpath->addStarPrefix(); Chris@0: if ($addNameTest) { Chris@0: $xpath->addNameTest(); Chris@0: } Chris@0: Chris@0: if (0 === $a) { Chris@0: return $xpath->addCondition('position() = '.($last ? 'last() - '.($b - 1) : $b)); Chris@0: } Chris@0: Chris@0: if ($a < 0) { Chris@0: if ($b < 1) { Chris@0: return $xpath->addCondition('false()'); Chris@0: } Chris@0: Chris@0: $sign = '<='; Chris@0: } else { Chris@0: $sign = '>='; Chris@0: } Chris@0: Chris@0: $expr = 'position()'; Chris@0: Chris@0: if ($last) { Chris@0: $expr = 'last() - '.$expr; Chris@0: --$b; Chris@0: } Chris@0: Chris@0: if (0 !== $b) { Chris@0: $expr .= ' - '.$b; Chris@0: } Chris@0: Chris@17: $conditions = [sprintf('%s %s 0', $expr, $sign)]; Chris@0: Chris@0: if (1 !== $a && -1 !== $a) { Chris@0: $conditions[] = sprintf('(%s) mod %d = 0', $expr, $a); Chris@0: } Chris@0: Chris@0: return $xpath->addCondition(implode(' and ', $conditions)); Chris@0: Chris@0: // todo: handle an+b, odd, even Chris@0: // an+b means every-a, plus b, e.g., 2n+1 means odd Chris@0: // 0n+b means b Chris@0: // n+0 means a=1, i.e., all elements Chris@0: // an means every a elements, i.e., 2n means even Chris@0: // -n means -1n Chris@0: // -1n+6 means elements 6 and previous Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return XPathExpr Chris@0: */ Chris@0: public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function) Chris@0: { Chris@0: return $this->translateNthChild($xpath, $function, true); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return XPathExpr Chris@0: */ Chris@0: public function translateNthOfType(XPathExpr $xpath, FunctionNode $function) Chris@0: { Chris@0: return $this->translateNthChild($xpath, $function, false, false); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return XPathExpr Chris@0: * Chris@0: * @throws ExpressionErrorException Chris@0: */ Chris@0: public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function) Chris@0: { Chris@0: if ('*' === $xpath->getElement()) { Chris@0: throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.'); Chris@0: } Chris@0: Chris@0: return $this->translateNthChild($xpath, $function, true, false); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return XPathExpr Chris@0: * Chris@0: * @throws ExpressionErrorException Chris@0: */ Chris@0: public function translateContains(XPathExpr $xpath, FunctionNode $function) Chris@0: { Chris@0: $arguments = $function->getArguments(); Chris@0: foreach ($arguments as $token) { Chris@0: if (!($token->isString() || $token->isIdentifier())) { Chris@17: throw new ExpressionErrorException('Expected a single string or identifier for :contains(), got '.implode(', ', $arguments)); Chris@0: } Chris@0: } Chris@0: Chris@0: return $xpath->addCondition(sprintf( Chris@0: 'contains(string(.), %s)', Chris@0: Translator::getXpathLiteral($arguments[0]->getValue()) Chris@0: )); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return XPathExpr Chris@0: * Chris@0: * @throws ExpressionErrorException Chris@0: */ Chris@0: public function translateLang(XPathExpr $xpath, FunctionNode $function) Chris@0: { Chris@0: $arguments = $function->getArguments(); Chris@0: foreach ($arguments as $token) { Chris@0: if (!($token->isString() || $token->isIdentifier())) { Chris@17: throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments)); Chris@0: } Chris@0: } Chris@0: Chris@0: return $xpath->addCondition(sprintf( Chris@0: 'lang(%s)', Chris@0: Translator::getXpathLiteral($arguments[0]->getValue()) Chris@0: )); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getName() Chris@0: { Chris@0: return 'function'; Chris@0: } Chris@0: }