/*
-----------------------------------------------------------------------------
OMELET PARSER

Transforms a list of tokens (generated by lexer) into an AST.
-----------------------------------------------------------------------------
*/

var errors = require('./errors.js')
var __ = require('./util.js')

function Parser(tokens, options) {
    this.tokens = tokens
    this.index = 0
    this.line = 1
    this.indentLevel = 0

    this.t = this.tokens[this.index] ||
             {kind: 'eof', value: null, line: -1}
    this.tKind = this.t.kind

    // options
    options = options || {}
    this.originalCode = options.source || ''
    this.fileName = options.fileName

    this.topLevelDefinitions = {}

    this.inInterpolation = false
    this.inDotModifier = false
    this.seenNonExtend = false
    this.seenNonImport = false
    this.extendNode = null
    this.importNodes = []
    this.doctypeLine = -1
    this.disallowModifiers = false
    this.disallowTagInner = false
}

Parser.prototype = {

    constructor: Parser,

    error: function(message, line) {
        var err = errors.buildError(
            'Parser error',
            message,
            (line || this.line),
            this.originalCode)
        throw err
    },

    fail: function() {
        if (this.tKind === 'indent') {
            this.error('Unexpected indent after line ' +
                        this.t.line + '.', this.t.line)
        }
        this.error('Unexpected token \'' + this.tKind +
                     '\' on line ' + this.t.line + '.', this.t.line)
    },

    expect: function(kind) {
        if (this.tKind !== kind) {
            var vowel = /^[aeiou]/
            this.error('Expected a' + (vowel.test(kind) ? 'n' : '') +
                        ' \'' + kind + '\' token, but got a' +
                        (vowel.test(this.tKind) ? 'n' : '') +
                        ' \'' + this.tKind + '\' token.', this.t.line)
        }
    },

    expectOneOf: function(kinds) {
        if (kinds.indexOf(this.tKind) === -1) {
            var vowel = /^[aeiou]/
            this.error('Expected one of these tokens: ' + JSON.stringify(kinds) +
                        ', but got a' + (vowel.test(this.tKind) ? 'n' : '') +
                        ' \'' + this.tKind + '\' token.', this.t.line)
        }
    },

    consume: function(n) {
        this.index += n
        this.t = this.tokens[this.index] || {kind: 'eof', value: null, line: -1}
        this.tKind = this.t.kind
    },

    parseDefinition: function() {
        this.consume(1)
        this.disallowModifiers = true

        this.expect('identifier')
        var name = this.parseIdentifier(),
            params = [],
            line = name.line,
            lineEnd = name.lineEnd,
            param, body

        this.expect('begin-params')
        this.consume(1)

        param = this.parseIdentifier()
        while (param) {
            params.push(param)
            lineEnd = param.lineEnd
            param = this.parseIdentifier()
        }

        this.expect('end-params')
        this.consume(1)

        this.disallowModifiers = false

        if (this.tKind === 'indent') {
            // a block definition!

            body = this.parseList()

            if (!body) {
                body = this.parseBlock()
                lineEnd = body.lineEnd

                // if every node in the body is a definition, then
                // it's a Dictionary (otherwise, it's just some local
                // definitions mixed with other nodes)
                var isDictionary = true
                for (var i = 0; i < body.contents.length; i++) {
                    if (body.contents[i].kind !== 'Definition') {
                        isDictionary = false
                        break
                    }
                }
                if (isDictionary) {
                    body = {
                        kind: 'Dictionary',
                        entries: body.contents,
                        line: body.line,
                        lineEnd: lineEnd
                    }
                }
            }

        } else if (this.tKind === 'number') {
            body = this.parseNumber()
        } else if (this.tKind === 'string') {
            body = this.parseQuotedString()
        } else if (this.tKind === 'boolean') {
            body = this.parseBoolean()
        } else {
            body = []
            while (this.tKind !== 'eof' && this.tKind !== 'newline'
                && this.tKind !== 'indent' && this.tKind !== 'dedent') {
                var node = this.parseToken(this.t)
                body.push(node)
                lineEnd = node.lineEnd
            }
            if (body.length === 0) {
                this.error('Definition of \'' + name.value + '\' is empty. Found a \'' + this.tKind + '\' token but expected an indented block.')
            }
            body = {
                kind: 'Block',
                contents: body,
                line: body[0].line,
                lineEnd: lineEnd
            }
        }

        if (this.indentLevel === 0) {
            this.topLevelDefinitions[name.value] = true
        }

        return {
            kind: 'Definition',
            style: 'default',
            name: name,
            parameters: params,
            body: body,
            line: line,
            lineEnd: body.lineEnd
        }
    },

    parseTagDefinition: function() {
        this.consume(1)

        this.expect('tag-name')
        var name = this.t.value,
            line = this.t.line,
            body = []
        this.consume(1)

        this.disallowTagInner = true
        body = this.parseTag()
        this.disallowTagInner = false

        return {
            kind: 'Definition',
            style: 'tag',
            name: name,
            parameters: [],
            body: body,
            line: line,
            lineEnd: line
        }
    },

    parseAttribute: function() {
        if (this.tKind === 'attribute-name') {
            var name = this.t.value,
                line = this.t.line,
                isBoolean = this.t.isBooleanAttr,
                value
            this.consume(1)
            if (isBoolean) {
                value = {
                    kind: 'String',
                    value: name,
                    line: line,
                    lineEnd: line
                }
            } else {
                value = this.parseQuotedString() || this.parseInterpolation()
            }
            return {
                kind: 'Attribute',
                name: {
                    kind: 'String',
                    value: name,
                    line: line,
                    lineEnd: line
                },
                value: value,
                line: line,
                lineEnd: line
            }
        }
    },

    parseTag: function() {
        var tagName = this.t.value,
            attributes = [],
            line = this.t.line,
            inner = []
        this.consume(1)

        var attr, classAttr, node

        attr = this.parseAttribute()
        while (attr) {
            var name = attr.name.value
            if (name === 'class') {
                if (classAttr) {
                    classAttr.value.value += ' ' + attr.value.value
                } else {
                    classAttr = attr
                }
            } else {
                attributes.push(attr)
            }
            attr = this.parseAttribute()
        }
        if (classAttr) {
            attributes.unshift(classAttr)
        }

        // Tag inner elements are disallowed when defining new tags,
        // so we just grab all of the components of the tag header (which
        // could be multiple, if it's a multitag) and build a new header-only Tag.
        if (this.disallowTagInner) {
            while (this.tKind === 'tag') {
                node = this.parseTag()
                inner.push(node)
            }
            return {
                kind: 'Tag',
                name: tagName,
                attributes: __.mergeClassAttributes(attributes),
                tagStyle: 'header-only',
                inner: {
                    kind: 'Block',
                    contents: inner,
                    line: line,
                    lineEnd: line
                },
                line: line,
                lineEnd: line
            }
        }

        inner = this.parseBlock()
        if (inner) {
            return {
                kind: 'Tag',
                name: tagName,
                attributes: __.mergeClassAttributes(attributes),
                tagStyle: 'block',
                inner: inner,
                line: line,
                lineEnd: inner.contents.length ? inner.lineEnd : line
            }
        } else {
            inner = []
            var lineEnd
            while (this.tKind !== 'eof' && this.tKind !== 'newline'
                && this.tKind !== 'indent' && this.tKind !== 'dedent') {
                if (this.inInterpolation && this.tKind === 'end-interpolation') {
                    break
                }
                node = this.parseToken(this.t)
                inner.push(node)
                lineEnd = node.lineEnd
            }
            return {
                kind: 'Tag',
                name: tagName,
                attributes: __.mergeClassAttributes(attributes),
                tagStyle: 'line',
                inner: {
                    kind: 'Block',
                    contents: inner,
                    line: inner.length ? inner[0].line : line,
                    lineEnd: lineEnd || line
                },
                line: line,
                lineEnd: lineEnd || line
            }
        }
    },

    parseText: function() {
        var val = this.t.value,
            line = this.t.line
        this.consume(1)
        return {
            kind: 'String',
            value: val,
            line: line,
            lineEnd: line
        }
    },

    parseDoctype: function() {
        if (this.tKind === 'doctype') {
            if (this.doctypeLine > -1) {
                this.error('Only one doctype is allowed per document. Previous doctype found on line ' + this.doctypeLine + '.', this.t.line)
            } else {
                this.doctypeLine = this.t.line
            }
            var val = this.t.value,
                line = this.t.line
            this.consume(1)
            return {
                kind: 'Doctype',
                name: {
                    kind: 'String',
                    value: val,
                    line: line,
                    lineEnd: line
                },
                line: line,
                lineEnd: line
            }
        }
    },

    parseInterpolation: function() {
        if (this.tKind === 'begin-interpolation') {

            var leaveUnescaped = !!this.t.noEscape

            this.consume(1)

            if (this.tKind === 'identifier') {
                var name = this.parseIdentifier(),
                    args = [],
                    filters = null,
                    line = name.line,
                    lineEnd = name.line,
                    arg

                arg = this.parseIdentifier() || this.parseQuotedString() ||
                      this.parseNumber() || this.parseBoolean()
                while (arg) {
                    args.push(arg)
                    lineEnd = arg.line
                    arg = this.parseIdentifier() || this.parseQuotedString() ||
                          this.parseNumber() || this.parseBoolean()
                }

                filters = this.parseFilterSequence() || null

                this.expect('end-interpolation')
                this.consume(1)

                return {
                    kind: 'Interpolation',
                    name: name,
                    leaveUnescaped: leaveUnescaped,
                    positionalArgs: true,
                    context: null,
                    arguments: args,
                    filters: filters,
                    line: line,
                    lineEnd: filters ? filters.lineEnd : lineEnd
                }
            } else if (this.tKind === 'tag') {

                this.inInterpolation = true
                var tag = this.parseTag()
                tag.tagStyle = 'interpolated'
                this.inInterpolation = false

                this.expect('end-interpolation')
                this.consume(1)

                return tag
            }
        }
    },

    parseFilterSequence: function() {
        if (this.tKind === 'begin-filter-sequence') {
            this.consume(1)

            var filters = [],
                line = this.t.line,
                lineEnd = this.t.line,
                filter

            filter = this.parseFilter()
            while (filter) {
                filters.push(filter)
                lineEnd = filter.lineEnd
                filter = this.parseFilter()
            }

            this.expect('end-filter-sequence')
            this.consume(1)

            return {
                kind: 'FilterSequence',
                sequence: filters,
                line: line,
                lineEnd: lineEnd
            }
        }
    },

    parseFilter: function() {
        if (this.tKind === 'begin-filter') {
            this.consume(1)

            this.expect('identifier')
            var name = this.parseIdentifier(),
                line = name.line,
                lineEnd = name.lineEnd,
                args = [],
                arg

            arg = this.parseNumber() || this.parseBoolean() ||
                  this.parseQuotedString() || this.parseIdentifier() ||
                  this.parseFilterSequence()
            while (arg) {
                args.push(arg)
                lineEnd = arg.lineEnd
                arg = this.parseNumber() || this.parseBoolean() ||
                      this.parseQuotedString() || this.parseIdentifier() ||
                      this.parseFilterSequence()
            }

            this.expect('end-filter')
            this.consume(1)

            return {
                kind: 'Filter',
                name: name,
                arguments: args,
                line: line,
                lineEnd: lineEnd
            }
        }
    },

    parseModifier: function() {
        if (this.tKind === 'dot') {
            this.consume(1)
            this.expect('identifier')
            this.inDotModifier = true
            var ident = this.parseIdentifier()
            this.inDotModifier = false
            return {
                kind: 'Modifier',
                value: {
                    kind: 'String',
                    value: ident.value,
                    line: ident.line,
                    lineEnd: ident.lineEnd
                },
                line: ident.line,
                lineEnd: ident.lineEnd
            }
        } else if (this.tKind === 'lbracket') {
            this.consume(1)
            var mod = this.parseIdentifier() ||
                      this.parseNumber() ||
                      this.parseQuotedString()
            this.expect('rbracket')
            this.consume(1)
            return {
                kind: 'Modifier',
                value: mod,
                line: mod.line,
                lineEnd: mod.lineEnd
            }
        }
    },

    parseIdentifier: function() {
        if (this.tKind === 'identifier') {
            var name = this.t.value,
                line = this.t.line,
                lineEnd = this.t.line,
                modifiers = [],
                modifier

            this.consume(1)

            if (!this.inDotModifier) {
                modifier = this.parseModifier()
                while (modifier) {
                    if (this.disallowModifiers) {
                        this.error('Unexpected modifier \'' + modifier.value.value +
                                '\' found on left side of \'=\' in definition.', line)
                    }
                    modifiers.push(modifier)
                    modifier = this.parseModifier()
                }
            }

            return {
                kind: 'Identifier',
                value: name,
                modifiers: modifiers,
                line: line,
                lineEnd: lineEnd
            }
        }
    },

    parseBoolean: function() {
        if (this.tKind === 'boolean') {
            var value = this.t.value,
                line = this.t.line,
                lineEnd = this.t.line
            this.consume(1)
            return {
                kind: 'Boolean',
                value: value,
                line: line,
                lineEnd: lineEnd
            }
        }
    },

    parseNumber: function() {
        if (this.tKind === 'number') {
            var value = this.t.value,
                line = this.t.line,
                lineEnd = this.t.line
            this.consume(1)
            return {
                kind: 'Number',
                value: value,
                line: line,
                lineEnd: lineEnd
            }
        }
    },

    parseQuotedString: function() {
        if (this.tKind === 'string') {
            var value = this.t.value,
                line = this.t.line,
                lineEnd = this.t.line
            this.consume(1)
            return {
                kind: 'String',
                value: value,
                line: line,
                lineEnd: lineEnd
            }
        }
    },

    parseRange: function() {
        if (this.tKind === 'begin-range') {
            this.consume(1)
            var startValue = this.t.value,
                line = this.t.line,
                lineEnd = this.t.line
            this.consume(1)
            var endValue = this.t.value
            this.consume(2)
            return {
                kind: 'Range',
                startValue: startValue,
                endValue: endValue,
                line: line,
                lineEnd: lineEnd
            }
        }
    },

    /**
     * Parse a mode block.
     */
    parseMode: function() {
        var name = this.t.value,
            line = this.t.line,
            lineEnd = this.t.line,
            content = []
        this.consume(1)

        while (this.tKind === 'raw') {
            content.push(this.t.value)
            lineEnd = this.t.line
            this.consume(1)
        }

        return {
            kind: 'Mode',
            name: name,
            value: content.join('\n'),
            line: line,
            lineEnd: lineEnd
        }
    },

    /**
     * parseBlock: Parse a block, which starts with an
     * indent and ends with a dedent.
     */
    parseBlock: function() {
        if (this.tKind === 'indent') {
            this.consume(1)
            this.indentLevel++

            var block = [],
                line = this.t.line,
                lineEnd = this.t.line
            while (this.tKind !== 'eof' && this.tKind !== 'dedent') {
                var node = this.parseToken(this.t)
                if (node) {
                    block.push(node)
                    lineEnd = node.lineEnd
                }
            }
            this.expect('dedent')
            this.consume(1)
            this.indentLevel--

            return {
                kind: 'Block',
                contents: block,
                line: line,
                lineEnd: lineEnd
            }
        }
    },

    parseList: function() {
        if (this.tKind === 'indent' &&
            this.tokens[this.index + 1].kind === 'begin-list-item') {
            this.consume(1)
            this.indentLevel++

            var elements = [],
                line = this.t.line,
                lineEnd = this.t.line,
                element

            element = this.parseListElement()
            while (element) {
                elements.push(element)
                lineEnd = element.lineEnd
                if (this.tKind === 'newline') {
                    this.consume(1)
                    this.line++
                }
                element = this.parseListElement()
            }
            this.expect('dedent')
            this.consume(1)
            this.indentLevel--

            return {
                kind: 'List',
                elements: elements,
                line: line,
                lineEnd: lineEnd
            }
        }
    },

    parseListElement: function() {
        if (this.tKind === 'begin-list-item') {
            var body,
                line = this.t.line,
                lineEnd = line

            this.consume(1)

            if (this.tKind === 'indent') {
                // a block definition!

                body = this.parseList()

                if (!body) {
                    body = this.parseBlock()
                    lineEnd = body.lineEnd

                    // if every node in the body is a definition, then
                    // it's a Dictionary (otherwise, it's just some local
                    // definitions mixed with other nodes)
                    var isDictionary = true
                    for (var i = 0; i < body.contents.length; i++) {
                        if (body.contents[i].kind !== 'Definition') {
                            isDictionary = false
                            break
                        }
                    }
                    if (isDictionary) {
                        body = {
                            kind: 'Dictionary',
                            entries: body.contents,
                            line: body.line,
                            lineEnd: lineEnd
                        }
                    }
                }

            } else if (this.tKind === 'number') {
                body = this.parseNumber()
            } else if (this.tKind === 'string') {
                body = this.parseQuotedString()
            } else if (this.tKind === 'boolean') {
                body = this.parseBoolean()
            } else {
                body = []
                while (this.tKind !== 'eof' && this.tKind !== 'newline'
                    && this.tKind !== 'indent' && this.tKind !== 'dedent') {
                    var node = this.parseToken(this.t)
                    body.push(node)
                    lineEnd = node.lineEnd
                }
                if (body.length === 0) {
                    this.error('Empty list elements are not allowed. ' +
                        'For a list element that does not output anything, ' +
                        'use a quoted empty string ("").')
                }
                body = {
                    kind: 'Block',
                    contents: body,
                    line: body[0].line,
                    lineEnd: lineEnd
                }
            }

            return {
                kind: 'ListElement',
                body: body,
                line: lineEnd,
                lineEnd: lineEnd
            }

        }
    },

    parseExtend: function() {
        if (this.seenNonExtend) {
            this.error('An extend statement can only appear at the top of a file, ' +
                        'before any other statements or content.', this.t.line)
        }
        if (this.indentLevel > 0) {
            this.error('An extend statement can only appear at the top level of ' +
                        'a file, not in an indented block.', this.t.line)
        }
        if (this.extendNode) {
            this.error('Only one extend statement allowed per file. ' +
                        'This file already extends file \'' +
                        this.extendNode.file.value + '\' on line ' +
                        this.extendNode.line + '.', this.t.line)
        }
        this.consume(1)
        this.expect('path')

        var path = this.t.value,
            line = this.t.line,
            lineEnd = line

        this.consume(1)

        this.extendNode = {
            kind: 'Extend',
            file: {
                kind: 'Path',
                value: path,
                line: line,
                lineEnd: lineEnd
            },
            line: line,
            lineEnd: lineEnd
        }
        return
    },

    parseImport: function() {
        if (this.seenNonImport) {
            this.error('Import statements must appear at the top of a file, ' +
                        'after the extend statement (if you have one) but ' +
                        'before any other statements or content.', this.t.line)
        }
        if (this.indentLevel > 0) {
            this.error('An import statement can only appear at the top level of ' +
                        'a file, not in an indented block.', this.t.line)
        }

        var line = this.t.line,
            lineEnd = line,
            path,
            alias = null,
            expectAlias = true,
            style = 'file'

        if (this.tKind === 'import-dir') {
            style = 'directory'
        } else if (this.tKind === 'import-tags') {
            style = 'tags'
            expectAlias = false
        }

        this.consume(1)
        this.expect('path')
        path = {
            kind: 'Path',
            value: this.t.value,
            line: this.t.line,
            lineEnd: this.t.line
        }
        this.consume(1)

        if (expectAlias) {
            this.expect('identifier')
            this.disallowModifiers = true
            alias = this.parseIdentifier()
            this.disallowModifiers = false
        }

        this.importNodes.push({
            kind: 'Import',
            style: style,
            path: path,
            alias: alias,
            line: line,
            lineEnd: lineEnd
        })
        return
    },

    parseInclude: function() {
        this.consume(1)
        var line = this.t.line,
            lineEnd = line,
            context = null,
            included

        if (this.tKind === 'path') {
            included = {
                kind: 'Path',
                value: this.t.value,
                line: line,
                lineEnd: lineEnd
            }
            this.consume(1)
        } else {
            this.expect('identifier')
            included = this.parseIdentifier()
        }

        if (this.tKind === 'with') {
            this.consume(1)

            var block = this.parseBlock()

            if (!block) {
                this.error('Expected an indented set of definitions after \'with\' on line ' +
                    line + '. Found \'' + this.tKind + '\' token instead.')
            }

            lineEnd = block.lineEnd

            for (var i = 0; i < block.contents.length; i++) {
                var kind = block.contents[i].kind
                if (kind !== 'Definition' && kind !== 'Comment') {
                    this.error('Unexpected ' + block.contents[i].kind + ' node ' +
                        'found in \'with\' block. \'with\' blocks must contain ' +
                        'only definitions.', block.contents[i].line)
                }
            }
            context = {
                kind: 'Dictionary',
                entries: block.contents,
                line: block.line,
                lineEnd: lineEnd
            }
        }

        return {
            kind: 'Include',
            included: included,
            context: context,
            line: line,
            lineEnd: lineEnd
        }
    },

    parseFor: function() {
        var line = this.t.line,
            filters = null,
            iterator,
            data,
            body
        this.consume(1)

        this.expect('identifier')
        this.disallowModifiers = true
        iterator = this.parseIdentifier()
        this.disallowModifiers = false

        this.expectOneOf(['identifier', 'begin-range'])
        data = this.parseIdentifier() || this.parseRange()

        filters = this.parseFilterSequence() || null

        this.expect('indent')
        body = this.parseBlock()

        return {
            kind: 'For',
            iterator: iterator,
            data: data,
            filters: filters,
            body: body,
            line: line,
            lineEnd: body.lineEnd
        }
    },

    parseIf: function() {
        var line = this.t.line,
            lineEnd = line,
            predicate,
            negated = false,
            filters = null,
            thenCase = null,
            elifCases = [],
            elseCase = null

        this.consume(1)

        if (this.tKind === 'not') {
            negated = true
            this.consume(1)
        }

        this.expect('identifier')
        predicate = this.parseIdentifier()

        filters = this.parseFilterSequence() || null

        thenCase = this.parseBlock() || null

        while (this.tKind === 'begin-elif') {
            var elifLine = this.t.line,
                elifNegated = false,
                elifPredicate,
                elifFilters = null,
                elifThenCase = null
            this.consume(1)

            if (this.tKind === 'not') {
                elifNegated = true
                this.consume(1)
            }

            this.expect('identifier')
            elifPredicate = this.parseIdentifier()
            elifFilters = this.parseFilterSequence() || null
            elifThenCase = this.parseBlock() || null

            elifCases.push({
                kind: 'If',
                predicate: elifPredicate,
                negated: elifNegated,
                filters: elifFilters,
                thenCase: elifThenCase,
                elifCases: [],
                elseCase: null,
                line: elifLine,
                lineEnd: elifThenCase.lineEnd
            })
            lineEnd = elifThenCase.lineEnd
        }

        if (this.tKind === 'begin-else') {
            this.consume(1)
            elseCase = this.parseBlock() || null
            lineEnd = elseCase ? elseCase.lineEnd : lineEnd
        }

        return {
            kind: 'If',
            predicate: predicate,
            negated: negated,
            filters: filters,
            thenCase: thenCase,
            elifCases: elifCases,
            elseCase: elseCase,
            line: line,
            lineEnd: lineEnd
        }
    },

    parseComment: function() {
        var value = this.t.value,
            line = this.t.line,
            lineEnd = line

        this.consume(1)

        return {
            kind: 'Comment',
            value: value,
            line: line,
            lineEnd: lineEnd
        }
    },

    parseToken: function(token) {
        switch (token.kind) {
            case 'newline':
                this.consume(1)
                this.line++
                return
            case 'comment':
                return this.parseComment()
            case 'mode':
                return this.parseMode()
            case 'tag':
                return this.parseTag()
            case 'begin-definition':
                return this.parseDefinition()
            case 'begin-tag-definition':
                return this.parseTagDefinition()
            case 'begin-interpolation':
                return this.parseInterpolation()
            case 'text':
            case 'string':
                return this.parseText()
            case 'begin-for':
                return this.parseFor()
            case 'begin-if':
                return this.parseIf()
            case 'doctype':
                return this.parseDoctype()
            case 'include':
                return this.parseInclude()
            case 'import':
            case 'import-dir':
            case 'import-tags':
                return this.parseImport()
            case 'extend':
                return this.parseExtend()
            default:
                this.fail()
        }
    },

    parse: function() {
        var block = [],
            lineEnd = 1
        while (this.index < this.tokens.length) {
            var node = this.parseToken(this.t)
            if (node) {
                if ((!this.seenNonExtend || !this.seenNonImport) &&
                    node.kind !== 'Extend' && node.kind !== 'Comment') {
                    this.seenNonExtend = true
                    if (node.kind !== 'Import') {
                        this.seenNonImport = true
                    }
                }
                block.push(node)
                lineEnd = node.lineEnd
            }
        }
        return {
            kind: 'Document',
            topLevelDefinitions: this.topLevelDefinitions,
            extend: this.extendNode,
            imports: this.importNodes,
            contents: block,
            line: 1,
            lineEnd: lineEnd
        }
    }
}

module.exports.parse = function(tokens, options) {
    return (new Parser(tokens, options)).parse()
}
