diff --git a/src/languageFacts/builtinData.ts b/src/languageFacts/builtinData.ts index 4750bc3d..dd8611a2 100644 --- a/src/languageFacts/builtinData.ts +++ b/src/languageFacts/builtinData.ts @@ -57,7 +57,8 @@ export const cssWideKeywords: { [name: string]: string } = { export const cssWideFunctions: { [name: string]: string } = { 'var()': 'Evaluates the value of a custom variable.', - 'calc()': 'Evaluates an mathematical expression. The following operators can be used: + - * /.' + 'calc()': 'Evaluates an mathematical expression. The following operators can be used: + - * /.', + 'if()': 'Evaluates a conditional expression.' }; export const imageFunctions: { [name: string]: string } = { diff --git a/src/parser/cssErrors.ts b/src/parser/cssErrors.ts index 07ae5ecb..690b2491 100644 --- a/src/parser/cssErrors.ts +++ b/src/parser/cssErrors.ts @@ -52,4 +52,5 @@ export const ParseError = { IdentifierOrWildcardExpected: new CSSIssueType('css-idorwildcardexpected', l10n.t("identifier or wildcard expected")), WildcardExpected: new CSSIssueType('css-wildcardexpected', l10n.t("wildcard expected")), IdentifierOrVariableExpected: new CSSIssueType('css-idorvarexpected', l10n.t("identifier or variable expected")), + IfConditionExpected: new CSSIssueType('css-ifconditionexpected', l10n.t("if condition expected")), }; diff --git a/src/parser/cssParser.ts b/src/parser/cssParser.ts index 588f24e4..43f17481 100644 --- a/src/parser/cssParser.ts +++ b/src/parser/cssParser.ts @@ -1154,6 +1154,61 @@ export class Parser { return this.finish(node); } + public _parseBooleanExpression(parseTest: () => nodes.Node | null): nodes.Node | null { + // ]> = not | + // [ [ and ]* + // | [ or ]* ] + + const node = this.create(nodes.Node); + + if (this.acceptIdent('not')) { + if (!node.addChild(this._parseBooleanExpressionGroup(parseTest))) { + return null; + } + } else { + if (!node.addChild(this._parseBooleanExpressionGroup(parseTest))) { + return null; + } + if (this.peekIdent('and')) { + while (this.acceptIdent('and')) { + if (!node.addChild(this._parseBooleanExpressionGroup(parseTest))) { + return null; + } + } + } else if (this.peekIdent('or')) { + while (this.acceptIdent('or')) { + if (!node.addChild(this._parseBooleanExpressionGroup(parseTest))) { + return null; + } + } + } + } + return this.finish(node); + } + + public _parseBooleanExpressionGroup(parseTest: () => nodes.Node | null) { + // = | ( ]> ) | + + const node = this.create(nodes.Node); + const pos = this.mark(); + + if (this.accept(TokenType.ParenthesisL)) { + if (node.addChild(this._parseBooleanExpression(parseTest))) { + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected, [], [TokenType.CurlyL]); + } + return this.finish(node); + } + this.restoreAtMark(pos); + } + + if (!node.addChild(parseTest())) { + return null; + }; + + return this.finish(node); + } + public _parseMediaCondition(): nodes.Node | null { // = | | | // = not @@ -2045,6 +2100,13 @@ export class Parser { const pos = this.mark(); const node = this.create(nodes.Function); + let parseArgument = this._parseFunctionArgument.bind(this); + let separator = TokenType.Comma; + if (this.peekIdent("if")) { + parseArgument = this._parseIfBranch.bind(this); + separator = TokenType.SemiColon; + } + if (!node.setIdentifier(this._parseFunctionIdentifier())) { return null; } @@ -2054,12 +2116,12 @@ export class Parser { return null; } - if (node.getArguments().addChild(this._parseFunctionArgument())) { - while (this.accept(TokenType.Comma)) { + if (node.getArguments().addChild(parseArgument())) { + while (this.accept(separator)) { if (this.peek(TokenType.ParenthesisR)) { break; } - if (!node.getArguments().addChild(this._parseFunctionArgument())) { + if (!node.getArguments().addChild(parseArgument())) { this.markError(node, ParseError.ExpressionExpected); } } @@ -2100,6 +2162,84 @@ export class Parser { return null; } + public _parseIfBranch() { + // = : ? + + const node = this.create(nodes.Node); + if (!node.addChild(this._parseIfCondition())) { + return this.finish(node, ParseError.IfConditionExpected, [], [TokenType.SemiColon]); + } + if (!this.accept(TokenType.Colon)) { + return this.finish(node, ParseError.ColonExpected, [], [TokenType.SemiColon]); + } + node.addChild(this._parseExpr()); + return this.finish(node); + } + + public _parseIfCondition(): nodes.Node | null { + // = ]> | else + + const node = this.create(nodes.Node); + + if (this.peekIdent("else")) { + node.addChild(this._parseIdent()); + return this.finish(node); + } + + return this._parseBooleanExpression(this._parseIfTest.bind(this)); + } + + public _parseIfTest(): nodes.Node | null { + // = + // supports( [ : ] | ) | + // media( | ) | + // style( ) + + const node = this.create(nodes.Node); + + if (this.acceptIdent('supports')) { + if (this.hasWhitespace() || !this.accept(TokenType.ParenthesisL)) { + return this.finish(node, ParseError.LeftParenthesisExpected, [], [TokenType.Colon]); + } + node.addChild(this._tryToParseDeclaration() || this._parseSupportsCondition()); + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected, [], [TokenType.Colon]); + } + return this.finish(node); + } + + if (this.acceptIdent('media')) { + if (this.hasWhitespace() || !this.accept(TokenType.ParenthesisL)) { + return this.finish(node, ParseError.LeftParenthesisExpected, [], [TokenType.Colon]); + } + const pos = this.mark(); + const condition = this._parseMediaCondition(); + if (condition && !condition.isErroneous()) { + node.addChild(condition); + } else { + this.restoreAtMark(pos); + node.addChild(this._parseMediaFeature()); + } + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected, [], [TokenType.Colon]); + } + return this.finish(node); + } + + if (this.acceptIdent('style')) { + if (this.hasWhitespace() || !this.accept(TokenType.ParenthesisL)) { + return this.finish(node, ParseError.LeftParenthesisExpected, [], [TokenType.Colon]); + } + node.addChild(this._parseStyleQuery()); + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected, [], [TokenType.Colon]); + } + return this.finish(node); + } + + return null; + } + public _parseHexColor(): nodes.Node | null { if (this.peekRegExp(TokenType.Hash, /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/g)) { const node = this.create(nodes.HexColorValue); diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index ea9d7f99..ad3eddfe 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -715,6 +715,74 @@ export class SCSSParser extends cssParser.Parser { return this._tryParseKeyframeSelector() || this._parseRuleSetDeclaration(); } + public _parseIfTest(): nodes.Node | null { + const node = this.create(nodes.Node); + + if (this.acceptIdent('sass')) { + if (this.hasWhitespace() || !this.accept(TokenType.ParenthesisL)) { + return this.finish(node, ParseError.LeftParenthesisExpected, [], [TokenType.CurlyL]); + } + node.addChild(this._parseExpr()); + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected, [], [TokenType.CurlyL]); + } + return this.finish(node); + } + + return super._parseIfTest(); + } + + public _parseFunction(): nodes.Function | null { + + const pos = this.mark(); + const node = this.create(nodes.Function); + + let isIf = this.peekIdent('if'); + + if (!node.setIdentifier(this._parseFunctionIdentifier())) { + return null; + } + + if (this.hasWhitespace() || !this.accept(TokenType.ParenthesisL)) { + this.restoreAtMark(pos); + return null; + } + + let firstArgument: nodes.Node | null; + let parseArgument = this._parseFunctionArgument.bind(this); + let separator = TokenType.Comma; + if (!isIf) { + firstArgument = this._parseFunctionArgument(); + } else { + const pos = this.mark(); + firstArgument = this._parseIfBranch(); + if (firstArgument && !firstArgument.isErroneous()) { + parseArgument = this._parseIfBranch.bind(this); + separator = TokenType.SemiColon; + } else { + this.restoreAtMark(pos); + firstArgument = this._parseFunctionArgument(); + } + } + + if (node.getArguments().addChild(firstArgument)) { + while (this.accept(separator)) { + if (this.peek(TokenType.ParenthesisR)) { + break; + } + if (!node.getArguments().addChild(parseArgument())) { + this.markError(node, ParseError.ExpressionExpected); + } + } + } + + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected); + } + return this.finish(node); + } + + public _parseFunctionArgument(): nodes.Node | null { // [variableName ':'] expression | variableName '...' const node = this.create(nodes.FunctionArgument); diff --git a/src/test/css/parser.test.ts b/src/test/css/parser.test.ts index 53a241a9..a08090de 100644 --- a/src/test/css/parser.test.ts +++ b/src/test/css/parser.test.ts @@ -611,6 +611,22 @@ suite('CSS - Parser', () => { assertFunction('let(--variable1, let(--variable2))', parser, parser._parseFunction.bind(parser)); assertFunction('fun(value1, value2)', parser, parser._parseFunction.bind(parser)); assertFunction('fun(value1,)', parser, parser._parseFunction.bind(parser)); + + // Builtin functions + // var + assertFunction('var(--some-variable)', parser, parser._parseFunction.bind(parser)); + // calc + assertFunction('calc(10px + 1rem)', parser, parser._parseFunction.bind(parser)); + // if + assertFunction('if(media(print): black; else: white;)', parser, parser._parseFunction.bind(parser)); + assertFunction('if(media(print): ; else: ;)', parser, parser._parseFunction.bind(parser)); + assertFunction('if(media(print): black; else: white)', parser, parser._parseFunction.bind(parser)); + assertFunction('if(style(--some-var: true): black)', parser, parser._parseFunction.bind(parser)); + // TODO: once https://github.com/microsoft/vscode-css-languageservice/pull/473 is merged, this should also work: + // assertFunction('if(style(--some-var): black)', parser, parser._parseFunction.bind(parser)); + assertFunction('if(else: white)', parser, parser._parseFunction.bind(parser)); + assertError('if()', parser, parser._parseFunction.bind(parser), ParseError.IfConditionExpected); + assertError('if(invalid: black;)', parser, parser._parseFunction.bind(parser), ParseError.IfConditionExpected); }); test('test token prio', function () { diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 5f0d11c4..c453e317 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -669,4 +669,11 @@ suite('SCSS - Parser', () => { assertNode('@font-face { unicode-range: U+0021-007F, u+1f49C, U+4??, U+??????; }', parser, parser._parseFontFace.bind(parser)); assertError('@font-face { font-style: normal font-stretch: normal; }', parser, parser._parseFontFace.bind(parser), ParseError.SemiColonExpected); }); + + test('if function', function() { + const parser = new SCSSParser(); + assertNode('if(true, black, white)', parser, parser._parseFunction.bind(parser)); + assertNode('if(sass(true): black; else: white;)', parser, parser._parseFunction.bind(parser)); + assertNode('if(sass($value == \'default\'): flex-gutter(); else: $value;)', parser, parser._parseFunction.bind(parser)); + }) });