<commit_before><?php

namespace DiDom;

use InvalidArgumentException;

class Query
{
    /**
     * Types of expressions.
     * 
     * @string
     */
    const TYPE_XPATH = 'TYPE_XPATH';
    const TYPE_CSS   = 'TYPE_CSS';

    /**
     * @var array
     */
    protected static $compiled = array();

    /**
     * Transform CSS expression to XPath.
     *
     * @param  string $expression XPath expression or CSS selector
     * @param  string $type the type of the expression
     * @return string
     */
    public static function compile($expression, $type = self::TYPE_CSS)
    {
        if ($type == Query::TYPE_XPATH) {
            return $expression;
        }

        if (array_key_exists($expression, static::$compiled)) {
            return static::$compiled[$expression];
        }

        $xpath = static::cssToXpath($expression);

        static::$compiled[$expression] = $xpath;

        return $xpath;
    }

    /**
     * Transform CSS expression to XPath.
     *
     * @param  string $selector
     * @return string
     */
    protected static function cssToXpath($selector)
    {
        $path = (string) $selector;

        if (strstr($path, ',')) {

            $paths       = explode(',', $path);
            $expressions = array();

            foreach ($paths as $path) {

                $xpath = static::cssToXpath(trim($path));

                if (is_string($xpath)) {
                    $expressions[] = $xpath;
                } elseif (is_array($xpath)) {
                    $expressions = array_merge($expressions, $xpath);
                }
            }

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

        $paths    = array('//');
        $path     = preg_replace('|\s+>\s+|', '>', $path);
        $segments = preg_split('/\s+/', $path);

        foreach ($segments as $key => $segment) {
            $pathSegment = static::tokenize($segment);

            if (0 == $key) {
                if (0 === strpos($pathSegment, '[contains(')) {
                    $paths[0] .= '*' . ltrim($pathSegment, '*');
                } else {
                    $paths[0] .= $pathSegment;
                }

                continue;
            }
            if (0 === strpos($pathSegment, '[contains(')) {
                foreach ($paths as $pathKey => $xpath) {

                    $paths[$pathKey] .= '//*' . ltrim($pathSegment, '*');
                    $paths[]          = $xpath . $pathSegment;
                }
            } else {
                foreach ($paths as $pathKey => $xpath) {
                    $paths[$pathKey] .= '//' . $pathSegment;
                }
            }
        }

        if (1 == count($paths)) {
            return $paths[0];
        }

        $xpath = implode('|', $paths);

        return $xpath;
    }

    /**
     * Tokenize CSS expressions to XPath.
     *
     * @param  string $expression
     * @return string
     */
    protected static function tokenize($expression)
    {
        // Child selectors
        $expression = str_replace('>', '/', $expression);

        // IDs
        $expression = preg_replace('|#([a-z][a-z0-9_-]*)|i', '[@id=\'$1\']', $expression);
        $expression = preg_replace('|(?<![a-z0-9_-])(\[@id=)|i', '*$1', $expression);

        // arbitrary attribute strict equality
        $expression = preg_replace_callback(
            '|\[@?([a-z0-9_-]+)=[\'"]([^\'"]+)[\'"]\]|i',
            function ($matches) {
                return '[@' . strtolower($matches[1]) . "='" . $matches[2] . "']";
            },
            $expression
        );

        // arbitrary attribute contains full word
        $expression = preg_replace_callback(
            '|\[([a-z0-9_-]+)~=[\'"]([^\'"]+)[\'"]\]|i',
            function ($matches) {
                return "[contains(concat(' ', normalize-space(@" . strtolower($matches[1]) . "), ' '), ' "
                     . $matches[2] . " ')]";
            },
            $expression
        );

        // arbitrary attribute contains specified content
        $expression = preg_replace_callback(
            '|\[([a-z0-9_-]+)\*=[\'"]([^\'"]+)[\'"]\]|i',
            function ($matches) {
                return "[contains(@" . strtolower($matches[1]) . ", '"
                     . $matches[2] . "')]";
            },
            $expression
        );

        // Classes
        if (false === strpos($expression, "[@")) {
            $expression = preg_replace(
                '|\.([a-z][a-z0-9_-]*)|i',
                "[contains(concat(' ', normalize-space(@class), ' '), ' \$1 ')]",
                $expression
            );
        }

        /** ZF-9764 -- remove double asterisk */
        $expression = str_replace('**', '*', $expression);

        return $expression;
    }<commit_msg>Converter from CSS to XPath replaced by faster<commit_after><?php

namespace DiDom;

use InvalidArgumentException;

class Query
{
    /**
     * Types of expressions.
     * 
     * @string
     */
    const TYPE_XPATH = 'TYPE_XPATH';
    const TYPE_CSS   = 'TYPE_CSS';

    /**
     * @var array
     */
    protected static $compiled = array();

    /**
     * Transform CSS expression to XPath.
     *
     * @param  string $expression XPath expression or CSS selector
     * @param  string $type the type of the expression
     * @return string
     */
    public static function compile($expression, $type = self::TYPE_CSS)
    {
        if ($type == Query::TYPE_XPATH) {
            return $expression;
        }

        if (array_key_exists($expression, static::$compiled)) {
            return static::$compiled[$expression];
        }

        static::$compiled[$expression] = static::cssToXpath($expression);

        return static::$compiled[$expression];
    }

    /**
     * @param  string $selector CSS selector
     * @param  bool   $rel specifies whether the element is a direct descendant
     * @return string
     */
    public static function cssToXpath($selector, $rel = false)
    {
        $regexp = static::getRegExp();
        $xpath  = '';

        if (preg_match($regexp, $selector, $tokens)) {
            $attributes = array();

            // if the id attribute specified
            if (isset($tokens['id']) and $tokens['id'] !== '') {
                $attributes[] = "@id='".$tokens['id']."'";
            }

            // if the attributes specified
            if (isset($tokens['attr']) and $tokens['attr'] !== '') {
                // if specified only the attribute name
                if (!(isset($tokens['value']))) {
                    $attributes[] = "@".$tokens['attr'];
                } else {
                    $attrValue = !empty($tokens['value']) ? $tokens['value'] : '';
                    $attributes[] = "@".$tokens['attr']."='".$attrValue."'";
                }
            }

            //if the class attribute specified
            if (isset($tokens['class']) and $tokens['class'] !== '') {
                $attributes[] = 'contains(concat(" ", normalize-space(@class), " "), " '.$tokens['class'].' ")';
            }

            // if the pseudo class specified
            if (isset($tokens['pseudo']) and $tokens['pseudo'] !== '') {
                if ('first-child' === $tokens['pseudo']) {
                    $attributes[] = '1';
                } elseif ('last-child' === $tokens['pseudo']) {
                    $attributes[] = 'last()';
                } elseif ('nth-child' === $tokens['pseudo']) {
                    if (isset($tokens['expr']) and '' !== $tokens['expr']) {
                        $expression = $tokens['expr'];

                        if ('odd' === $expression) {
                            $attributes[] = '(position() -1) mod 2 = 0 and position() >= 1';
                        } elseif ('even' === $expression) {
                            $attributes[] = 'position() mod 2 = 0 and position() >= 0';
                        } elseif (preg_match("/^[0-9]+$/", $expression)) {
                            $attributes[] = 'position() = '.$expression;
                        } elseif (preg_match("/^((?P<mul>[0-9]+)n\+)(?P<pos>[0-9]+)$/is", $expression, $position)) {
                            if (isset($position['mul'])) {
                                $attributes[] = '(position() -'.$position['pos'].') mod '.$position['mul'].' = 0 and position() >= '.$position['pos'].'';
                            } else {
                                $attributes[] = ''.$expression.'';
                            }
                        }
                    }
                }
            }

            $xpath  = $rel ? '/' : '//';
            $xpath .= ($tokens['tag'] !== '') ? $tokens['tag'] : '*';

            if ($count = count($attributes)) {
                $xpath .= ($count > 1) ? '[('.implode(') and (', $attributes).')]' : '['.implode(' and ', $attributes).']';
            }

            $subs = trim(substr($selector, strlen($tokens[0])));

            // if is the direct descendant
            $direct = isset($tokens['rel']) ? $tokens['rel'] === '>' : false;

            if ($subs !== '') {
                $xpath .= static::cssToXpath($subs, $direct);
            }
        }

        return $xpath;
    }

    /**
     * @return string
     */
    protected static function getRegExp()
    {
        $tag = "(?P<tag>[a-z0-9]+)?";
        $attr = "(\[(?P<attr>\S+)=(?P<value>[^\]]+)\])?";
        $id = "(#(?P<id>[^\s:>#\.]+))?";
        $class = "(\.(?P<class>[^\s:>#\.]+))?";
        $child = "(first|last|nth)-child";
        $expr = "(\((?P<expr>[^\)]+)\))";
        $pseudo = "(:(?P<pseudo>".$child.")".$expr."?)?";
        $rel = "\s*(?P<rel>>)?";

        return "/".$tag.$attr.$id.$class.$pseudo.$rel."/isS";
    }
{'fraction_non_alphanumeric': 0.19324268644416975, 'fraction_numerical': 0.005253399258343634, 'mean_word_length': 1.5583662714097497, 'pattern_counts': {'":': 0, '<': 16, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6231854', 'n_tokens_mistral': 2869, 'n_tokens_neox': 2725, 'n_words': 551}