/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import sassScanner = require ('./sassScanner'); import sassErrors = require('./sassErrors'); import scanner = require('vs/languages/css/common/parser/cssScanner'); import cssParser = require('vs/languages/css/common/parser/cssParser'); import nodes = require('vs/languages/css/common/parser/cssNodes'); import errors = require('vs/languages/css/common/parser/cssErrors'); /// /// A parser for Sass /// http://sass-lang.com/documentation/file.SASS_REFERENCE.html /// export class SassParser extends cssParser.Parser { public constructor() { super(new sassScanner.SassScanner()); } public _parseStylesheetStatement():nodes.Node { return super._parseStylesheetStatement() || this._parseVariableDeclaration() || this._parseWarnAndDebug() || this._parseControlStatement() || this._parseMixinDeclaration() || this._parseMixinContent() || this._parseMixinReference() // @include || this._parseFunctionDeclaration(); } public _parseImport():nodes.Node { var node = this.create(nodes.Import); if(!this.accept(scanner.TokenType.AtKeyword, '@import')) { return null; } if (!this.accept(scanner.TokenType.URI) && !this.accept(scanner.TokenType.String)) { return this.finish(node, errors.ParseError.URIOrStringExpected); } while (this.accept(scanner.TokenType.Comma)) { if (!this.accept(scanner.TokenType.URI) && !this.accept(scanner.TokenType.String)) { return this.finish(node, errors.ParseError.URIOrStringExpected); } } node.setMedialist(this._parseMediaList()); return this.finish(node); } // Sass variables: $font-size: 12px; public _parseVariableDeclaration(panic:scanner.TokenType[]=[]): nodes.VariableDeclaration { var node = this.create(nodes.VariableDeclaration); if (!node.setVariable(this._parseVariable())) { return null; } if (!this.accept(scanner.TokenType.Colon, ':')) { return this.finish(node, errors.ParseError.ColonExpected); } node.colonPosition = this.prevToken.offset; if (!node.setValue(this._parseExpr())) { return this.finish(node, errors.ParseError.VariableValueExpected, [], panic); } if (this.accept(scanner.TokenType.Exclamation)) { if (!this.accept(scanner.TokenType.Ident, 'default', true)) { return this.finish(node, errors.ParseError.UnknownKeyword); } } if (this.peek(scanner.TokenType.SemiColon)) { node.semicolonPosition = this.token.offset; // not part of the declaration, but useful information for code assist } return this.finish(node); } public _parseMediaFeatureName() : nodes.Node { return this._parseFunction() || this._parseIdent() || this._parseVariable(); // first function, the indent } public _parseKeyframeSelector():nodes.Node { return super._parseKeyframeSelector() || this._parseMixinContent(); } public _parseVariable(): nodes.Variable { var node = this.create(nodes.Variable); if (!this.accept(sassScanner.VariableName)) { return null; } return node; } public _parseIdent(referenceTypes?: nodes.ReferenceType[]): nodes.Identifier { var node = this.create(nodes.Identifier); node.referenceTypes = referenceTypes; var hasContent = false; while (this.accept(scanner.TokenType.Ident) || node.addChild(this._parseInterpolation())) { hasContent = true; if (!this.hasWhitespace() && this.accept(scanner.TokenType.Delim, '-')) { // '-' is a valid char inside a ident (special treatment here to support #{foo}-#{bar}) } if (this.hasWhitespace()) { break; } } return hasContent ? this.finish(node) : null; } public _parseTerm(): nodes.Term { var term = super._parseTerm(); if (term) { return term; } term = this.create(nodes.Term); if (term.setExpression(this._parseVariable())) { return this.finish(term); } return null; } public _parseInterpolation():nodes.Node { var node = this.create(nodes.Interpolation); if (this.accept(sassScanner.InterpolationFunction)) { if (!node.addChild(this._parseBinaryExpr())) { return this.finish(node, errors.ParseError.ExpressionExpected); } if (!this.accept(scanner.TokenType.CurlyR)) { return this.finish(node, errors.ParseError.RightCurlyExpected); } return this.finish(node); } return null; } public _parseOperator(): nodes.Node { var node = this.createNode(nodes.NodeType.Operator); if (this.peek(sassScanner.EqualsOperator) || this.peek(sassScanner.NotEqualsOperator) || this.peek(sassScanner.GreaterEqualsOperator) || this.peek(sassScanner.SmallerEqualsOperator) || this.peek(scanner.TokenType.Delim, '>') || this.peek(scanner.TokenType.Delim, '<') || this.peek(scanner.TokenType.Ident, 'and') || this.peek(scanner.TokenType.Ident, 'or') || this.peek(scanner.TokenType.Delim, '%') ) { var node = this.createNode(nodes.NodeType.Operator); this.consumeToken(); return this.finish(node); } return super._parseOperator(); } public _parseUnaryOperator(): nodes.Node { if (this.peek(scanner.TokenType.Ident, 'not')) { var node = this.create(nodes.Node); this.consumeToken(); return this.finish(node); } return super._parseUnaryOperator(); } public _parseRuleSetDeclaration() : nodes.Node { if (this.peek(scanner.TokenType.AtKeyword)) { return this._parseKeyframe() // nested @keyframe || this._parseImport() // nested @import || this._parseMedia() // nested @media || this._parseFontFace() // nested @font-face || this._parseWarnAndDebug() // @warn and @debug statements || this._parseControlStatement() // @if, @while, @for, @each || this._parseFunctionDeclaration() // @function || this._parseExtends() // @extends || this._parseMixinReference() // @include || this._parseMixinContent() // @content || this._parseMixinDeclaration(); // nested @mixin } return this._parseVariableDeclaration() // variable declaration || this._tryParseRuleset(true) // nested ruleset || super._parseRuleSetDeclaration(); // try css ruleset declaration as last so in the error case, the ast will contain a declaration } public _parseDeclaration(resyncStopTokens?:scanner.TokenType[]): nodes.Declaration { var node = this.create(nodes.Declaration); if (!node.setProperty(this._parseProperty())) { return null; } if (!this.accept(scanner.TokenType.Colon, ':')) { return this.finish(node, errors.ParseError.ColonExpected, [ scanner.TokenType.Colon ], resyncStopTokens); } node.colonPosition = this.prevToken.offset; var hasContent = false; if (node.setValue(this._parseExpr())) { hasContent = true; node.addChild(this._parsePrio()); } if (this.peek(scanner.TokenType.CurlyL)) { node.setNestedProperties(this._parseNestedProperties()); } else { if (!hasContent) { return this.finish(node, errors.ParseError.PropertyValueExpected); } } if (this.peek(scanner.TokenType.SemiColon)) { node.semicolonPosition = this.token.offset; // not part of the declaration, but useful information for code assist } return this.finish(node); } public _parseNestedProperties(): nodes.NestedProperties { var node = this.create(nodes.NestedProperties); return this._parseBody(node, this._parseDeclaration.bind(this)); } public _parseExtends(): nodes.Node { var node = this.create(nodes.ExtendsReference); if (this.accept(scanner.TokenType.AtKeyword, '@extend')) { if (!node.setSelector(this._parseSimpleSelector())) { return this.finish(node, errors.ParseError.SelectorExpected); } if (this.accept(scanner.TokenType.Exclamation)) { if (!this.accept(scanner.TokenType.Ident, 'optional', true)) { return this.finish(node, errors.ParseError.UnknownKeyword); } } return this.finish(node); } return null; } public _parseSimpleSelectorBody(): nodes.Node { return this._parseSelectorCombinator() || this._parseSelectorPlaceholder() || super._parseSimpleSelectorBody(); } public _parseSelectorCombinator():nodes.Node { var node = this.createNode(nodes.NodeType.SelectorCombinator); if (this.accept(scanner.TokenType.Delim, '&')) { while (!this.hasWhitespace() && (this.accept(scanner.TokenType.Delim, '-') || node.addChild(this._parseIdent()) || this.accept(scanner.TokenType.Delim, '&'))) { // support &-foo } return this.finish(node); } return null; } public _parseSelectorPlaceholder():nodes.Node { var node = this.createNode(nodes.NodeType.SelectorPlaceholder); if (this.accept(scanner.TokenType.Delim, '%')) { this._parseIdent(); return this.finish(node); } return null; } public _parseWarnAndDebug(): nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword, '@debug') && !this.peek(scanner.TokenType.AtKeyword, '@warn')) { return null; } var node = this.createNode(nodes.NodeType.Debug); this.consumeToken(); // @debug or @warn node.addChild(this._parseExpr()); // optional return this.finish(node); } public _parseControlStatement(parseStatement: () => nodes.Node = this._parseRuleSetDeclaration.bind(this)): nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword)) { return null; } return this._parseIfStatement(parseStatement) || this._parseForStatement(parseStatement) || this._parseEachStatement(parseStatement) || this._parseWhileStatement(parseStatement); } public _parseIfStatement(parseStatement: () => nodes.Node): nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword, '@if')) { return null; } return this._internalParseIfStatement(parseStatement); } private _internalParseIfStatement(parseStatement: () => nodes.Node): nodes.IfStatement { var node = this.create(nodes.IfStatement); this.consumeToken(); // @if or if if (!node.setExpression(this._parseBinaryExpr())) { return this.finish(node, errors.ParseError.ExpressionExpected); } this._parseBody(node, parseStatement); if (this.accept(scanner.TokenType.AtKeyword, '@else')) { if (this.peek(scanner.TokenType.Ident, 'if')) { node.setElseClause(this._internalParseIfStatement(parseStatement)); } else if (this.peek(scanner.TokenType.CurlyL)) { var elseNode = this.create(nodes.ElseStatement); this._parseBody(elseNode, parseStatement); node.setElseClause(elseNode); } } return this.finish(node); } public _parseForStatement(parseStatement: () => nodes.Node): nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword, '@for')) { return null; } var node = this.create(nodes.ForStatement); this.consumeToken(); // @for if (!node.setVariable(this._parseVariable())) { return this.finish(node, errors.ParseError.VariableNameExpected, [ scanner.TokenType.CurlyR ]); } if (!this.accept(scanner.TokenType.Ident, 'from')) { return this.finish(node, sassErrors.ParseError.FromExpected, [ scanner.TokenType.CurlyR ]); } if (!node.addChild(this._parseBinaryExpr())) { return this.finish(node, errors.ParseError.ExpressionExpected, [ scanner.TokenType.CurlyR ]); } if (!this.accept(scanner.TokenType.Ident, 'to') && !this.accept(scanner.TokenType.Ident, 'through')) { return this.finish(node, sassErrors.ParseError.ThroughOrToExpected, [ scanner.TokenType.CurlyR ]); } if (!node.addChild(this._parseBinaryExpr())) { return this.finish(node, errors.ParseError.ExpressionExpected, [ scanner.TokenType.CurlyR ]); } return this._parseBody(node, parseStatement); } public _parseEachStatement(parseStatement: () => nodes.Node): nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword, '@each')) { return null; } var node = this.create(nodes.EachStatement); this.consumeToken(); // @each if (!node.setVariable(this._parseVariable())) { return this.finish(node, errors.ParseError.VariableNameExpected, [ scanner.TokenType.CurlyR ]); } if (!this.accept(scanner.TokenType.Ident, 'in')) { return this.finish(node, sassErrors.ParseError.InExpected, [ scanner.TokenType.CurlyR ]); } if (!node.addChild(this._parseExpr())) { return this.finish(node, errors.ParseError.ExpressionExpected, [ scanner.TokenType.CurlyR ]); } return this._parseBody(node, parseStatement); } public _parseWhileStatement(parseStatement: () => nodes.Node): nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword, '@while')) { return null; } var node = this.create(nodes.WhileStatement); this.consumeToken(); // @while if (!node.addChild(this._parseBinaryExpr())) { return this.finish(node, errors.ParseError.ExpressionExpected, [ scanner.TokenType.CurlyR ]); } return this._parseBody(node, parseStatement); } public _parseFunctionBodyDeclaration(): nodes.Node { return this._parseVariableDeclaration() || this._parseReturnStatement() || this._parseControlStatement(this._parseFunctionBodyDeclaration.bind(this)); } public _parseFunctionDeclaration(): nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword, '@function')) { return null; } var node = this.create(nodes.FunctionDeclaration); this.consumeToken(); // @function if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Function ]))) { return this.finish(node, errors.ParseError.IdentifierExpected, [ scanner.TokenType.CurlyR ]); } if (!this.accept(scanner.TokenType.ParenthesisL)) { return this.finish(node, errors.ParseError.LeftParenthesisExpected, [ scanner.TokenType.CurlyR ] ); } if (node.getParameters().addChild(this._parseParameterDeclaration())) { while (this.accept(scanner.TokenType.Comma)) { if (!node.getParameters().addChild(this._parseParameterDeclaration())) { return this.finish(node, errors.ParseError.VariableNameExpected); } } } if (!this.accept(scanner.TokenType.ParenthesisR)) { return this.finish(node, errors.ParseError.RightParenthesisExpected, [ scanner.TokenType.CurlyR ] ); } return this._parseBody(node, this._parseFunctionBodyDeclaration.bind(this)); } public _parseReturnStatement(): nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword, '@return')) { return null; } var node = this.createNode(nodes.NodeType.ReturnStatement); this.consumeToken(); // @function if (!node.addChild(this._parseExpr())) { return this.finish(node, errors.ParseError.ExpressionExpected); } return this.finish(node); } public _parseMixinDeclaration():nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword, '@mixin')) { return null; } var node = this.create(nodes.MixinDeclaration); this.consumeToken(); if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Mixin]))) { return this.finish(node, errors.ParseError.IdentifierExpected, [ scanner.TokenType.CurlyR ]); } if (this.accept(scanner.TokenType.ParenthesisL)) { if (node.getParameters().addChild(this._parseParameterDeclaration())) { while (this.accept(scanner.TokenType.Comma)) { if (!node.getParameters().addChild(this._parseParameterDeclaration())) { return this.finish(node, errors.ParseError.VariableNameExpected); } } } if (!this.accept(scanner.TokenType.ParenthesisR)) { return this.finish(node, errors.ParseError.RightParenthesisExpected, [ scanner.TokenType.CurlyR ] ); } } return this._parseBody(node, this._parseRuleSetDeclaration.bind(this)); } public _parseParameterDeclaration():nodes.Node { var node = this.create(nodes.FunctionParameter); if (!node.setIdentifier(this._parseVariable())) { return null; } if (this.accept(sassScanner.Ellipsis)) { // ok } if (this.accept(scanner.TokenType.Colon)) { if (!node.setDefaultValue(this._parseExpr(true))) { return this.finish(node, errors.ParseError.VariableValueExpected, [], [scanner.TokenType.Comma, scanner.TokenType.ParenthesisR]); } } return this.finish(node); } public _parseMixinContent():nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword, '@content')) { return null; } var node = this.createNode(nodes.NodeType.MixinContent); this.consumeToken(); return this.finish(node); } public _parseMixinReference():nodes.Node { if (!this.peek(scanner.TokenType.AtKeyword, '@include')) { return null; } var node = this.create(nodes.MixinReference); this.consumeToken(); if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Mixin]))) { return this.finish(node, errors.ParseError.IdentifierExpected, [ scanner.TokenType.CurlyR ]); } if (this.accept(scanner.TokenType.ParenthesisL)) { if (node.getArguments().addChild(this._parseFunctionArgument())) { while (this.accept(scanner.TokenType.Comma)) { if (!node.getArguments().addChild(this._parseFunctionArgument())) { return this.finish(node, errors.ParseError.ExpressionExpected); } } } if (!this.accept(scanner.TokenType.ParenthesisR)) { return this.finish(node, errors.ParseError.RightParenthesisExpected); } } if (this.peek(scanner.TokenType.CurlyL)) { var content = this.create(nodes.BodyDeclaration); this._parseBody(content, this._parseMixinReferenceBodyStatement.bind(this)); node.setContent(content); } return this.finish(node); } public _parseMixinReferenceBodyStatement(): nodes.Node { return this._parseRuleSetDeclaration() || this._parseKeyframeSelector(); } public _parseFunctionArgument():nodes.Node { // [variableName ':'] expression | variableName '...' var node = this.create(nodes.FunctionArgument); var pos = this.mark(); var argument = this._parseVariable(); if (argument) { if (!this.accept(scanner.TokenType.Colon)) { if (this.accept(sassScanner.Ellipsis)) { // optional node.setValue(argument); return this.finish(node); } else { this.restoreAtMark(pos); } } else { node.setIdentifier(argument); } } let functionArgumentNode= super._parseFunctionArgument(); if (functionArgumentNode && node.setValue(functionArgumentNode.getValue())) { return this.finish(node); } return null; } }