lib/Tokenizer.php
public function tokenize($css) {
        $this->_css = $css;
        $this->_cssLC = strtolower($this->_css);
        $this->errors = [];

        $start = 0;
        $words = [];
        $items = [];
        $depth = 0;
        $functionDepth = 0;
        $expressionDepth = 0;
        $blockStart = 0;
        $selectorStart = 0;
        $error = false;
        $cssLength = strlen($this->_css);

        $validTokens = ['"', "'", '/*', '{', '}', ';', 'url(', '\\'];
        if ($this->lessSyntax) {
            $validTokens = array_merge($validTokens, ['(', ')', '//', '@{', '#{']);
        }

        $tokens = $this->_findTokens($validTokens);
        foreach($tokens as $token) {
            if ($token['index'] < $start) {
                continue;
            }

            switch ($token['token']) {
                case '//':
                    // Skip to end of line
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start),
                        'index' => $start,
                    ];
                    $start = $token['index'];

                    $end1 = strpos($this->_css, "\n", $start + 2);
                    $end2 = strpos($this->_css, "\r", $start + 2);
                    $end = $end1 === false ? $end2 : ($end2 === false ? $end1 : min($end1, $end2));
                    if ($end === false) {
                        // Last string
                        $start = $cssLength;
                        break;
                    }

                    $start = $end + 1;
                    break;

                case '/*':
                    // Skip to end of comment
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start),
                        'index' => $start,
                    ];
                    $start = $token['index'];

                    $end = strpos($this->_css, '*/', $start + 2);
                    if ($end === false) {
                        // Skip to end of file
                        if (!$this->ignoreErrors) {
                            $this->errors[] = new ParseError('Missing comment closing statement', $this->_css, $start);
                            $error = true;
                        }
                        $start = $cssLength;
                        break;
                    }

                    $start = $end + 2;
                    break;

                case '\\':
                    // Escaped character, skip next character
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start + 2),
                        'index' => $start,
                    ];
                    $start = $token['index'] + 2;
                    break;

                case 'url(':
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start),
                        'index' => $start,
                    ];
                    $start = $token['index'];

                    // Skip to end of URL
                    $end = $this->_findEndOfURL($start);
                    if (!is_numeric($end)) {
                        // Invalid URL - skip "url"
                        $words[] = [
                            'type'  => 'text',
                            'text'  => substr($this->_css, $start, 3),
                            'index' => $start,
                        ];
                        if (!$this->ignoreErrors) {
                            $this->errors[] = new ParseError('Incomplete URL', $this->_css, $start);
                            $error = true;
                        }
                        $start += 3;
                        break;
                    }
                    $words[] = [
                        'type'  => 'url',
                        'text'  => substr($this->_css, $start, $end - $start),
                        'index' => $start,
                    ];
                    $start = $end;
                    break;

                case '"':
                case '\'':
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start),
                        'index' => $start,
                    ];
                    $start = $token['index'];

                    // Skip to end of quoted string
                    $end = $this->_findEndOfQuotedString($token['token'], $start);
                    if ($end === false) {
                        // Missing closing quote
                        if ($this->ignoreErrors) {
                            $words[] = [
                                'type'  => 'text',
                                'text'  => substr($this->_css, $start, 1),
                                'index' => $start,
                            ];
                            $start ++;
                        }
                        else {
                            $this->errors[] = new ParseError('Missing closing ' . $token['token'], $this->_css, $start);
                            $error = true;
                            $words[] = [
                                'type'  => 'code',
                                'text'  => substr($this->_css, $start),
                                'index' => $start,
                            ];
                            $start = $cssLength;
                        }
                        break;
                    }
                    $words[] = [
                        'type'  => 'string',
                        'text'  => substr($this->_css, $start, $end - $start),
                        'index' => $start,
                    ];
                    $start = $end;
                    break;

                case ';':
                    if ($functionDepth > 0) {
                        break;
                    }
                    if ($this->splitRules) {
                        $words[] = [
                            'type'  => 'text',
                            'text'  => substr($this->_css, $start, $token['index'] - $start),
                            'index' => $start,
                        ];
                        $items[] = $this->_checkRule($words, $token['token'], $error);
                        if ($error) {
                            $items[count($items) - 1]['error'] = true;
                            $error = false;
                        }
                    }
                    $selectorStart = $start = $token['index'] + 1;
                    $words = [];
                    break;

                case '{':
                    // Get selector
                    if (!$this->splitRules) {
                        if ($selectorStart > $blockStart) {
                            $code = trim(substr($this->_css, $blockStart, $selectorStart - $blockStart));
                            if (strlen($code)) {
                                $items[] = [
                                    'token' => 'code',
                                    'code'  => $code,
                                    'index' => $blockStart,
                                ];
                                if ($error) {
                                    $items[count($items) - 1]['error'] = true;
                                    $error = false;
                                }
                            }
                        }
                    }
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start),
                        'index' => $start,
                    ];
                    $items[] = $this->_checkSelectors($words);

                    $blockStart = $selectorStart = $start = $token['index'] + 1;
                    $words = [];
                    $depth ++;
                    break;

                case '}':
                    if ($expressionDepth > 0) {
                        // LESS/SASS expression
                        if ($expressionDepth === 1 && $this->splitRules) {
                            // Find start of expression
                            $found = false;
                            $text = substr($this->_css, $start, $token['index'] - $start + 1);

                            for ($i = count($words) - 1; $i >= 0; $i --) {
                                if (!empty($words[$i]['beforeExpression'])) {
                                    $found = true;
                                    unset($words[$i]['beforeExpression']);
                                    if ($i === count($words) - 1) {
                                        // Previous token starts expression - do not change word tokens
                                        $words[] = [
                                            'type'  => 'expression',
                                            'text'  => $text,
                                            'index' => $start
                                        ];
                                    } else {
                                        // Merge with previous tokens
                                        $start = $words[$i + 1]['index'];
                                        $words = array_slice($words, 0, $i + 1);
                                        $words[] = [
                                            'type'  => 'expression',
                                            'text'  => $text,
                                            'index' => $start
                                        ];
                                    }
                                    break;
                                }
                                $text = $words[$i]['text'] . $text;
                            }
                            if (!$found) {
                                $words[] = [
                                    'type'  => 'expression',
                                    'text'  => $text,
                                    'index' => $start,
                                    'error' => true
                                ];
                            }
                            $start = $token['index'] + 1;
                        }
                        $expressionDepth --;
                        break;
                    }
                    // End of block
                    if ($this->splitRules) {
                        $words[] = [
                            'type'  => 'text',
                            'text'  => substr($this->_css, $start, $token['index'] - $start),
                            'index' => $start,
                        ];
                        $items[] = $this->_checkRule($words, '', $error);
                        if ($error) {
                            $items[count($items) - 1]['error'] = true;
                        }
                    } else {
                        $code = trim(substr($this->_css, $blockStart, $token['index'] - $blockStart));
                        if (strlen($code)) {
                            $items[] = [
                                'token' => 'code',
                                'code'  => $code,
                                'index' => $blockStart,
                            ];
                            if ($error) {
                                $items[count($items) - 1]['error'] = true;
                            }
                        }
                    }
                    $error = false;
                    $items[] = [
                        'token' => '}',
                        'index' => $token['index'],
                    ];

                    if (!$depth && !$this->ignoreErrors) {
                        $this->errors[] = new ParseError('Unexpected }', $this->_css, $token['index']);
                    }
                    $depth --;

                    $blockStart = $selectorStart = $start = $token['index'] + 1;
                    $words = [];
                    $functionDepth = 0;
                    break;

                case '(':
                    // Function with LESS syntax enabled
                    if ($this->splitRules) {
                        $row = [
                            'type'  => 'text',
                            'text'  => substr($this->_css, $start, $token['index'] - $start),
                            'index' => $start,
                        ];
                        if (!$functionDepth) {
                            $row['beforeFunction'] = true;
                        }
                        $words[] = $row;
                        $start = $token['index'];
                    }
                    $functionDepth ++;
                    break;

                case ')':
                    // End of function with LESS syntax enabled
                    if ($functionDepth === 1 && $this->splitRules) {
                        // Find start of function
                        $found = false;
                        $text = substr($this->_css, $start, $token['index'] - $start + 1);

                        for ($i = count($words) - 1; $i >= 0; $i --) {
                            if (!empty($words[$i]['beforeFunction'])) {
                                $found = true;
                                unset($words[$i]['beforeFunction']);
                                if ($i === count($words) - 1) {
                                    // Previous token starts function - do not change word tokens
                                    $words[] = [
                                        'type'  => 'function',
                                        'text'  => $text,
                                        'index' => $start
                                    ];
                                } else {
                                    // Merge with previous tokens
                                    $start = $words[$i + 1]['index'];
                                    $words = array_slice($words, 0, $i + 1);
                                    $words[] = [
                                        'type'  => 'function',
                                        'text'  => $text,
                                        'index' => $start
                                    ];
                                }
                                break;
                            }
                            $text = $words[$i]['text'] . $text;
                        }
                        if (!$found) {
                            $words[] = [
                                'type'  => 'function',
                                'text'  => $text,
                                'index' => $start,
                                'error' => true
                            ];
                        }
                        $start = $token['index'] + 1;
                    }
                    $functionDepth --;
                    if ($functionDepth < 0) {
                        $functionDepth = 0;
                    }
                    break;

                case '@{':
                case '#{':
                    // Expression with LESS/SASS syntax enabled
                    if ($this->splitRules) {
                        $row = [
                            'type'  => 'text',
                            'text'  => substr($this->_css, $start, $token['index'] - $start + 2),
                            'index' => $start,
                        ];
                        if (!$expressionDepth) {
                            $row['beforeExpression'] = true;
                        }
                        $words[] = $row;
                        $start = $token['index'] + 2;
                    }
                    $expressionDepth ++;
                    break;
            }
        }

        if ($depth > 0 && !$this->ignoreErrors) {
            $this->errors[] = new ParseError('Missing }', $this->_css, $cssLength);
        }

        // Add remaining code
        if ($this->splitRules) {
            $words[] = [
                'type'  => 'text',
                'text'  => substr($this->_css, $start),
                'index' => $start,
            ];
            $items[] = $this->_checkRule($words, '', $error);
            if ($error) {
                $items[count($items) - 1]['error'] = true;
            }
        } else {
            $code = trim(substr($this->_css, $blockStart));
            if (strlen($code)) {
                $items[] = [
                    'token' => 'code',
                    'code'  => $code,
                    'index' => $blockStart,
                ];
                if ($error) {
                    $items[count($items) - 1]['error'] = true;
                }
            }
        }

        return array_values(array_filter($items, function($item) {
            return $item !== false;
        }));
    }
public function tokenize($css) {
        $this->_css = $css;
        $this->_cssLC = strtolower($this->_css);
        $this->errors = [];

        $start = 0;
        $words = [];
        $items = [];
        $depth = 0;
        $functionDepth = 0;
        $expressionDepth = 0;
        $blockStart = 0;
        $selectorStart = 0;
        $error = false;
        $cssLength = strlen($this->_css);

        $validTokens = ['"', "'", '/*', '{', '}', ';', 'url(', '\\'];
        if ($this->lessSyntax) {
            $validTokens = array_merge($validTokens, ['(', ')', '//', '@{', '#{']);
        }

        $tokens = $this->_findTokens($validTokens);
        foreach($tokens as $token) {
            if ($token['index'] < $start) {
                continue;
            }

            switch ($token['token']) {
                case '//':
                    // Skip to end of line
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start),
                        'index' => $start,
                    ];
                    $start = $token['index'];

                    $end1 = strpos($this->_css, "\n", $start + 2);
                    $end2 = strpos($this->_css, "\r", $start + 2);
                    $end = $end1 === false ? $end2 : ($end2 === false ? $end1 : min($end1, $end2));
                    if ($end === false) {
                        // Last string
                        $start = $cssLength;
                        break;
                    }

                    $start = $end + 1;
                    break;

                case '/*':
                    // Skip to end of comment
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start),
                        'index' => $start,
                    ];
                    $start = $token['index'];

                    $end = strpos($this->_css, '*/', $start + 2);
                    if ($end === false) {
                        // Skip to end of file
                        if (!$this->ignoreErrors) {
                            $this->errors[] = new ParseError('Missing comment closing statement', $this->_css, $start);
                            $error = true;
                        }
                        $start = $cssLength;
                        break;
                    }

                    $start = $end + 2;
                    break;

                case '\\':
                    // Escaped character, skip next character
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start + 2),
                        'index' => $start,
                    ];
                    $start = $token['index'] + 2;
                    break;

                case 'url(':
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start),
                        'index' => $start,
                    ];
                    $start = $token['index'];

                    // Skip to end of URL
                    $end = $this->_findEndOfURL($start);
                    if (!is_numeric($end)) {
                        // Invalid URL - skip "url"
                        $words[] = [
                            'type'  => 'text',
                            'text'  => substr($this->_css, $start, 3),
                            'index' => $start,
                        ];
                        if (!$this->ignoreErrors) {
                            $this->errors[] = new ParseError('Incomplete URL', $this->_css, $start);
                            $error = true;
                        }
                        $start += 3;
                        break;
                    }
                    $words[] = [
                        'type'  => 'url',
                        'text'  => substr($this->_css, $start, $end - $start),
                        'index' => $start,
                    ];
                    $start = $end;
                    break;

                case '"':
                case '\'':
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start),
                        'index' => $start,
                    ];
                    $start = $token['index'];

                    // Skip to end of quoted string
                    $end = $this->_findEndOfQuotedString($token['token'], $start);
                    if ($end === false) {
                        // Missing closing quote
                        if ($this->ignoreErrors) {
                            $words[] = [
                                'type'  => 'text',
                                'text'  => substr($this->_css, $start, 1),
                                'index' => $start,
                            ];
                            $start ++;
                        }
                        else {
                            $this->errors[] = new ParseError('Missing closing ' . $token['token'], $this->_css, $start);
                            $error = true;
                            $words[] = [
                                'type'  => 'code',
                                'text'  => substr($this->_css, $start),
                                'index' => $start,
                            ];
                            $start = $cssLength;
                        }
                        break;
                    }
                    $words[] = [
                        'type'  => 'string',
                        'text'  => substr($this->_css, $start, $end - $start),
                        'index' => $start,
                    ];
                    $start = $end;
                    break;

                case ';':
                    if ($functionDepth > 0) {
                        break;
                    }
                    if ($this->splitRules) {
                        $words[] = [
                            'type'  => 'text',
                            'text'  => substr($this->_css, $start, $token['index'] - $start),
                            'index' => $start,
                        ];
                        $items[] = $this->_checkRule($words, $token['token'], $error);
                        if ($error) {
                            $items[count($items) - 1]['error'] = true;
                            $error = false;
                        }
                    }
                    $selectorStart = $start = $token['index'] + 1;
                    $words = [];
                    break;

                case '{':
                    // Get selector
                    if (!$this->splitRules) {
                        if ($selectorStart > $blockStart) {
                            $code = trim(substr($this->_css, $blockStart, $selectorStart - $blockStart));
                            if (strlen($code)) {
                                $items[] = [
                                    'token' => 'code',
                                    'code'  => $code,
                                    'index' => $blockStart,
                                ];
                                if ($error) {
                                    $items[count($items) - 1]['error'] = true;
                                    $error = false;
                                }
                            }
                        }
                    }
                    $words[] = [
                        'type'  => 'text',
                        'text'  => substr($this->_css, $start, $token['index'] - $start),
                        'index' => $start,
                    ];
                    $items[] = $this->_checkSelectors($words);

                    $blockStart = $selectorStart = $start = $token['index'] + 1;
                    $words = [];
                    $depth ++;
                    break;

                case '}':
                    if ($expressionDepth > 0) {
                        // LESS/SASS expression
                        if ($expressionDepth === 1 && $this->splitRules) {
                            // Find start of expression
                            $found = false;
                            $text = substr($this->_css, $start, $token['index'] - $start + 1);

                            for ($i = count($words) - 1; $i >= 0; $i --) {
                                if (!empty($words[$i]['beforeExpression'])) {
                                    $found = true;
                                    unset($words[$i]['beforeExpression']);
                                    if ($i === count($words) - 1) {
                                        // Previous token starts expression - do not change word tokens
                                        $words[] = [
                                            'type'  => 'expression',
                                            'text'  => $text,
                                            'index' => $start
                                        ];
                                    } else {
                                        // Merge with previous tokens
                                        $start = $words[$i + 1]['index'];
                                        $words = array_slice($words, 0, $i + 1);
                                        $words[] = [
                                            'type'  => 'expression',
                                            'text'  => $text,
                                            'index' => $start
                                        ];
                                    }
                                    break;
                                }
                                $text = $words[$i]['text'] . $text;
                            }
                            if (!$found) {
                                $words[] = [
                                    'type'  => 'expression',
                                    'text'  => $text,
                                    'index' => $start,
                                    'error' => true
                                ];
                            }
                            $start = $token['index'] + 1;
                        }
                        $expressionDepth --;
                        break;
                    }
                    // End of block
                    if ($this->splitRules) {
                        $words[] = [
                            'type'  => 'text',
                            'text'  => substr($this->_css, $start, $token['index'] - $start),
                            'index' => $start,
                        ];
                        $items[] = $this->_checkRule($words, '', $error);
                        if ($error) {
                            $items[count($items) - 1]['error'] = true;
                        }
                    } else {
                        $code = trim(substr($this->_css, $blockStart, $token['index'] - $blockStart));
                        if (strlen($code)) {
                            $items[] = [
                                'token' => 'code',
                                'code'  => $code,
                                'index' => $blockStart,
                            ];
                            if ($error) {
                                $items[count($items) - 1]['error'] = true;
                            }
                        }
                    }
                    $error = false;
                    $items[] = [
                        'token' => '}',
                        'index' => $token['index'],
                    ];

                    if (!$depth && !$this->ignoreErrors) {
                        $this->errors[] = new ParseError('Unexpected }', $this->_css, $token['index']);
                    }
                    $depth --;

                    $blockStart = $selectorStart = $start = $token['index'] + 1;
                    $words = [];
                    $functionDepth = 0;
                    break;

                case '(':
                    // Function with LESS syntax enabled
                    if ($this->splitRules) {
                        $row = [
                            'type'  => 'text',
                            'text'  => substr($this->_css, $start, $token['index'] - $start),
                            'index' => $start,
                        ];
                        if (!$functionDepth) {
                            $row['beforeFunction'] = true;
                        }
                        $words[] = $row;
                        $start = $token['index'];
                    }
                    $functionDepth ++;
                    break;

                case ')':
                    // End of function with LESS syntax enabled
                    if ($functionDepth === 1 && $this->splitRules) {
                        // Find start of function
                        $found = false;
                        $text = substr($this->_css, $start, $token['index'] - $start + 1);

                        for ($i = count($words) - 1; $i >= 0; $i --) {
                            if (!empty($words[$i]['beforeFunction'])) {
                                $found = true;
                                unset($words[$i]['beforeFunction']);
                                if ($i === count($words) - 1) {
                                    // Previous token starts function - do not change word tokens
                                    $words[] = [
                                        'type'  => 'function',
                                        'text'  => $text,
                                        'index' => $start
                                    ];
                                } else {
                                    // Merge with previous tokens
                                    $start = $words[$i + 1]['index'];
                                    $words = array_slice($words, 0, $i + 1);
                                    $words[] = [
                                        'type'  => 'function',
                                        'text'  => $text,
                                        'index' => $start
                                    ];
                                }
                                break;
                            }
                            $text = $words[$i]['text'] . $text;
                        }
                        if (!$found) {
                            $words[] = [
                                'type'  => 'function',
                                'text'  => $text,
                                'index' => $start,
                                'error' => true
                            ];
                        }
                        $start = $token['index'] + 1;
                    }
                    $functionDepth --;
                    if ($functionDepth < 0) {
                        $functionDepth = 0;
                    }
                    break;

                case '@{':
                case '#{':
                    // Expression with LESS/SASS syntax enabled
                    if ($this->splitRules) {
                        $row = [
                            'type'  => 'text',
                            'text'  => substr($this->_css, $start, $token['index'] - $start + 2),
                            'index' => $start,
                        ];
                        if (!$expressionDepth) {
                            $row['beforeExpression'] = true;
                        }
                        $words[] = $row;
                        $start = $token['index'] + 2;
                    }
                    $expressionDepth ++;
                    break;
            }
        }

        if ($depth > 0 && !$this->ignoreErrors) {
            $this->errors[] = new ParseError('Missing }', $this->_css, $cssLength);
        }

        // Add remaining code
        if ($this->splitRules) {
            $words[] = [
                'type'  => 'text',
                'text'  => substr($this->_css, $start),
                'index' => $start,
            ];
            $items[] = $this->_checkRule($words, '', $error);
            if ($error) {
                $items[count($items) - 1]['error'] = true;
            }
        } else {
            $code = trim(substr($this->_css, $blockStart));
            if (strlen($code)) {
                $items[] = [
                    'token' => 'code',
                    'code'  => $code,
                    'index' => $blockStart,
                ];
                if ($error) {
                    $items[count($items) - 1]['error'] = true;
                }
            }
        }

        return array_values(array_filter($items, function($item) {
            return $item !== false;
        }));
    }
Convert css into list of tokens

@param string $css
@return array