Transform 'css pseudo selector' expression to xpath expression

@param string $expression
@param string[] $new_path_tokens

@return string transformed expression (xpath)
@throws \Exception
489305
private static function transformCssPseudoSelector($expression, array &$new_path_tokens)
    {
        if (preg_match('|not\((.+)\)|i', $expression, $matches)) {
            $parts = explode(',', $matches[1]);
            foreach ($parts as &$part) {
                $part = trim($part);
                $part = 'self::'.ltrim(self::transform($part), '/');
            }
            $not_selector = implode(' or ', $parts);
            return '[not('.$not_selector.')]';
        } elseif (preg_match('|contains\((.+)\)|i', $expression, $matches)) {
            return '[text()[contains(.,\''.$matches[1].'\')]]'; // contain the specified text
        } elseif (preg_match('|has\((.+)\)|i', $expression, $matches)) {
            if (strpos($matches[1], '> ') === 0) {
                return '[child::' . ltrim(self::transform($matches[1]), '/') .']';
            } else {
                return '[descendant::' . ltrim(self::transform($matches[1]), '/') .']';
            }
        } elseif ($expression === 'first' || $expression === 'last') { // new path inside selection
            array_unshift($new_path_tokens, '(');
            $new_path_tokens[] = ')';
        }

        //  static replacement

        $pseudo_class_selectors = array(
            'disabled' => '[@disabled]',
            'first-child' => '[not(preceding-sibling::*)]',
            'last-child' => '[not(following-sibling::*)]',
            'only-child' => '[not(preceding-sibling::*) and not(following-sibling::*)]',
            'empty' => '[count(*) = 0 and string-length() = 0]',
            'not-empty' => '[count(*) > 0 or string-length() > 0]',
            'parent' => '[count(*) > 0]',
            'header' => '[self::h1 or self::h2 or self::h3 or self::h5 or self::h5 or self::h6]',
            'odd' => '[position() mod 2 = 0]',
            'even' => '[position() mod 2 = 1]',
            'first' => '[1]',
            'last' => '[last()]',
            'root' => '[not(parent::*)]'
        );

        if (!isset($pseudo_class_selectors[$expression])) {
            throw new \Exception('Pseudo class '.$expression.' unknown');
        }

        return $pseudo_class_selectors[$expression];
    }
660534
public function find($expression)
    {
        $xpath = new Dom\XPath($this);

        $xpath->registerNamespace("php", "http://php.net/xpath");
        $xpath->registerPhpFunctions();

        return $xpath->query($expression);
    }
386013
protected function _css_toxpath_selector($css_selector) {
		$xpath = $css_selector;

		// if there was passed a xpath expression
		if (strpos($xpath, 'xpath:') !== false) return substr($xpath, 6);

		// child elements
		$xpath = preg_replace('/([a-z0-9_-])\s+([a-z0-9_-])/', '$1//$2', $xpath);

		// direct child elements ( > )
		$xpath = preg_replace('/\s*>\s*/', '/', $xpath);

		// element with attribute
		$xpath = preg_replace('/\[([^\]]+)\]/', '[@$1]', $xpath);

		// element with starts-with selector
		$xpath = preg_replace('/\[@(.+)\^=(.+)\]/i', '[starts-with(@$1, $2)]', $xpath);

		// element with contains selector
		$xpath = preg_replace('/\[@(.+)\*=(.+)\]/i', '[contains(@$1, $2)]', $xpath);

		// element by id
		$xpath = preg_replace('/#([a-z0-9_-]+)/', '*[@id="$1"]', $xpath);

		// elements by class
		$xpath = preg_replace('/\.([a-z0-9_-]+)/', '*[contains(concat(" ", @class, " ")," $1 ")]', $xpath);

		// cleanup such elements div*
		$xpath = preg_replace('/([a-z0-9_-]+)\*/', '$1', $xpath);

		// first-child
		$xpath = preg_replace('/:first-child/', '[1]', $xpath);

		// nth-child
		$xpath = preg_replace('/:nth-child\((\d+)\)/', '[$1]', $xpath);

		// following elements ( + )
		$xpath = preg_replace('/\s*\+\s*/', '/following-sibling::*[1]/self::', $xpath);

		return '//' . $xpath;
	}
1018149
public static function prepend($css, $path)
    {
        self::$_prependPath = $path;

        $css = self::_trimUrls($css);

        $css = self::_owlifySvgPaths($css);
        
        // append
        $pattern = '/@import\\s+([\'"])(.*?)[\'"]/';
        $css = preg_replace_callback($pattern, array(self::$className, '_processUriCB'), $css);

        $pattern = '/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/';
        $css = preg_replace_callback($pattern, array(self::$className, '_processUriCB'), $css);

        $css = self::_unOwlify($css);

        self::$_prependPath = null;

        return $css;
    }
57669
private function injectNsToXpathQuery($xpath, $ns)
    {
        $literals = array();
        $offset = 0;
        $keywords = array(
            '$$_AND_$$' => 'and',
            '$$_OR_$$' => 'or'
        );

        // identified escaped backslashes
        $extractor = function($m) use (&$literals, &$offset) {
            $placeholder = '$$_' . $offset . '_$$';
            $literals[$placeholder] = $m[0];
            $offset++;

            return $placeholder;
        };

        // Extract literals
        $modified = preg_replace_callback('~(\'|").*?\1~is', $extractor, $xpath);
        $modified = preg_replace_callback('~\b(and|or)\b~i', function($match) {
            return '$$_' . strtoupper(trim($match[1])) . '_$$';
        }, $modified);

        // Now find all unqualified names and prefix them with the namespace
        $modified = preg_replace('~(?<!@|:)\b[a-z][a-z0-9_-]*\b(?!:|\s*\()~i', $ns . ':$0', $modified);

        // re-insert keywords and literals
        $modified = strtr($modified, $keywords);
        $modified = strtr($modified, $literals);

        return $modified;
    }
1279015
private function buildExpressionForSelector(string $selector)
    {

        if ($expression = $this->resultCache->fetch($selector)) {
            return $expression;
        }

        try {
            $expression = $this->cssConverter->toXPath($selector);
        } catch (ExpressionErrorException $expressionErrorException) {

            // Allow for pseudo selectors
            // TODO: Find a way to validate this exception without checking strings
            if ($expressionErrorException->getMessage() !== 'Pseudo-elements are not supported.') {
                return false;
            }

            try {
                $tokens = explode(':', $selector);
                $expression = $this->cssConverter->toXPath((string)reset($tokens));
            } catch (Exception $e) {
                return false;
            }

        } catch (ExceptionInterface $e) {
            return false;
        }


        $this->resultCache->save($selector, $expression);

        return $expression;
    }
359233
function pathExpression (token, tokens) {
  const expression = []
  while (true) {
    if (!token) {
      break
    }
    if (token === ')') {
      tokens.unshift(token)
      break
    }
    if (includes('}]', token)) {
      throw new Error(`A fragment can't contain "${token}"`)
    }

    if (token === ',') {
      break
    }

    if (token === '[') {
      expression.push(escapedFragment(token, tokens))
    } else if (token === '(') {
      expression.push(pathExpressions(token, tokens))
    } else if (token === '{') {
      expression.push(customFragment(token, tokens))
    } else {
      expression.push(unescapedFragment(token, tokens))
    }
    token = tokens.shift()
  }
  return { _type: pathExpression.name, expression }
}
1331311
public function convertXPath($expr)
	{
		$expr = trim($expr);
		try
		{
			return $this->runner->convert($expr);
		}
		catch (RuntimeException $e)
		{
			// Do nothing
		}

		// Make sure the expression evaluates as a string
		if (!preg_match('(^[-\\w]*s(?:pace|tring)[-\\w]*\\()', $expr))
		{
			$expr = 'string(' . $expr . ')';
		}
		$expr = $this->exportXPath($expr);

		return '$this->xpath->evaluate(' . $expr . ',$node)';
	}
563578
public static function cssToXpath($selector, $prefix = '//')
    {
        $paths = [];

        while ($selector !== '') {
            list($xpath, $selector) = static::parseAndConvertSelector($selector, $prefix);

            if (substr($selector, 0, 1) === ',') {
                $selector = trim($selector, ', ');
            }

            $paths[] = $xpath;
        }

        return implode('|', $paths);
    }
475482
public static NodeList evaluateXpathExpression(Document dom, String xpathExpr)
			throws XPathExpressionException {
		XPathFactory factory = XPathFactory.newInstance();
		XPath xpath = factory.newXPath();
		XPathExpression expr = xpath.compile(xpathExpr);
		Object result = expr.evaluate(dom, XPathConstants.NODESET);
		return (NodeList) result;
	}
795479
public function addQueryByCss($expression)
    {
        $pieces = explode(' ', $expression);
        foreach ($pieces as $piece) {
            $isTag = true;
            if (preg_match('/\./is', $piece)) {
                $pieceSplit = explode(".", $piece);
                $selector = empty($pieceSplit[0]) ? "*" : $pieceSplit[0];
                $class = $pieceSplit[1];
                $this->addQueryByClass($class, $selector);
                $isTag = false;
            }
            if (preg_match('/\#/is', $piece)) {
                $pieceSplit = explode("#", $piece);
                $selector = empty($pieceSplit[0]) ? "*" : $pieceSplit[0];
                $id = $pieceSplit[1];
                $this->addQueryById($id, $selector);
                $isTag = false;
            }
            if ($isTag) {
                $this->addQueryBySelector($piece);
            }
        }

        return $this;
    }
105656
public static function transform(string $path)
    {
        if (isset(self::$xpath_cache[$path])) {
            return self::$xpath_cache[$path];
        }

        $tmp_path = self::replaceCharInsideEnclosure($path, ',');
        if (strpos($tmp_path, ',') !== false) {
            $paths = explode(',', $tmp_path);
            $expressions = array();

            foreach ($paths as $single_path) {
                $single_path = str_replace("\0", ',', $single_path); // restore commas
                $xpath = self::transform(trim($single_path));
                $expressions[] = $xpath;
            }

            return implode('|', $expressions);
        }

        // replace spaces inside (), to correctly create tokens (restore later)

        $path_escaped = self::replaceCharInsideEnclosure($path, ' ');

        // create tokens and analyze to create segments

        $tokens = preg_split('/\s+/', $path_escaped);

        $segments = array();

        foreach ($tokens as $key => $token) {
            $token = str_replace("\0", ' ', $token); // restore spaces

            if ($segment = self::getSegmentFromToken($token, $key, $tokens)) {
                $segments[] = $segment;
            }
        }

        // use segments to create array with transformed tokens

        $new_path_tokens = self::transformCssSegments($segments);

        // result tokens to xpath

        $xpath_result = implode('', $new_path_tokens);

        self::$xpath_cache[$path] = $xpath_result;

        return $xpath_result;
    }
281576
public function buildExpression(Expression $expr = null)
    {
        if (!$this->defaultExpression) {
            $this->defaultExpression = Expr::method('isEnabled', Expr::same(true))
                ->andMethod('getTypeName', Expr::same(DiscoveryUrlGenerator::BINDING_TYPE))
                ->andMethod('getBinding', Expr::method('getQuery', Expr::endsWith('{,/**/*}')));
        }

        if (!$expr) {
            return $this->defaultExpression;
        }

        $traverser = new ExpressionTraverser();
        $traverser->addVisitor($this);

        return $this->defaultExpression->andX($traverser->traverse($expr));
    }
1265672
private function getCssFromAllStyleNodes(\DOMXPath $xPath)
    {
        $styleNodes = $xPath->query('//style');

        if ($styleNodes === false) {
            return '';
        }

        $css = '';
        /** @var \DOMNode $styleNode */
        foreach ($styleNodes as $styleNode) {
            $css .= "\n\n" . $styleNode->nodeValue;
            $styleNode->parentNode->removeChild($styleNode);
        }

        return $css;
    }
1148764
public function findXPath(string $xpath): NodeList {
        $results = $this->newNodeList();

        if ($this->isRemoved()) {
            return $results;
        }

        $domxpath = new \DOMXPath($this->document());

        foreach ($this->collection() as $node) {
            $results = $results->merge(
                $node->newNodeList($domxpath->query($xpath, $node))
            );
        }

        return $results;
    }
776359
public Node getNode(String expression) {
		return (Node) evalXPath(expression, null, XPathConstants.NODE);
	}
1167920
protected function compileExtends($expression)
	{
		if (starts_with($expression, '('))
		{
			$expression = substr($expression, 1, -1);
		}

		$data = "<?php echo \$__env->make($expression, array_except(get_defined_vars(), array('__data', '__path')))->render(); ?>";

		$this->footer[] = $data;

		return '';
	}