public function tokenize($callback = null)
    {
        $this->reset();
        if (!is_callable($callback)) {
            $callback = function() {};
        }

        for ($line = 1, $i = 0; $i < $this->strlen; $i++) {
            if (mb_substr($this->source, $i, 1) === "\n") {
                $line++;
            }

            switch (true) {
                //section
                case mb_substr($this->source, $i, 3) == '{{{#':
                    $i = $this->addNode($i, self::TYPE_SECTION_OPEN, $line, 4, 6, $callback);
                    break;
                case mb_substr($this->source, $i, 3) == '{{#':
                    $i = $this->addNode($i, self::TYPE_SECTION_OPEN, $line, 3, 5, $callback);
                    break;
                case mb_substr($this->source, $i, 3) == '{{{/':
                    $i = $this->addNode($i, self::TYPE_SECTION_CLOSE, $line, 4, 6, $callback);
                    break;
                case mb_substr($this->source, $i, 3) == '{{/':
                    $i = $this->addNode($i, self::TYPE_SECTION_CLOSE, $line, 3, 5, $callback);
                    break;

                //variable
                case mb_substr($this->source, $i, 3) == '{{{':
                    $i = $this->addNode($i, self::TYPE_VARIABLE_ESCAPE, $line, 3, 6, $callback);
                    break;
                case mb_substr($this->source, $i, 2) == '{{':
                    $i = $this->addNode($i, self::TYPE_VARIABLE_UNESCAPE, $line, 2, 4, $callback);
                    break;

                //text
                default:
                    $this->buffer .= mb_substr($this->source, $i, 1);
                    break;
            }
        }

        $this->flushText($i, $callback);
        return $this;
    }