protected static function _tokenize($expression_src)
    {
        // Child selectors
        $expression = str_replace('>', '/', $expression_src);

        // 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',
            static function ($matches) {
                $attr = strtolower($matches[1]);
                $type = $matches[2];
                $val = trim($matches[3], "'\" \t");

                $field = ($attr === '' ? 'text()' : '@' . $attr);

                $items = [];
                foreach (explode(strpos($val, '|') !== false ? '|' : '&', $val) as $word) {
                    if ($type === '') {
                        $items[] = "$field='$word'";
                    } elseif ($type === '~') {
                        $items[] = "contains(concat(' ', normalize-space($field), ' '), ' $word ')";
                    } elseif ($type === '|') {
                        $item = "$field='$word' or starts-with($field,'$word-')";
                        $items[] = $word === $val ? $item : "($item)";
                    } elseif ($type === '!') {
                        $item = "not($field) or $field!='$word'";
                        $items[] = $word === $val ? $item : "($item)";
                    } else {
                        $items[] = ['*' => 'contains', '^' => 'starts-with', '$' => 'ends-with'][$type] . "($field, '$word')";
                    }
                }
                return '[' . implode(strpos($val, '|') !== false ? ' or ' : ' and ', $items) . ']';
            },
            $expression
        );

        //attribute contains specified content
        $expression = preg_replace_callback(
            '|\[(!?)([a-z][a-z0-9_-\|&]*)\]|i',
            static function ($matches) {
                $val = $matches[2];
                $op = strpos($val, '|') !== false ? '|' : '&';

                $items = [];
                foreach (explode($op, $val) as $word) {
                    $items[] = '@' . $word;
                }

                $r = '[' . implode($op === '|' ? ' or ' : ' and ', $items) . ']';
                return $matches[1] === '!' ? "not($r)" : $r;
            },
            $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;
    }