diff --git a/src/parser/cssErrors.ts b/src/parser/cssErrors.ts index d92259f1..f31f0d4c 100644 --- a/src/parser/cssErrors.ts +++ b/src/parser/cssErrors.ts @@ -48,4 +48,5 @@ export let ParseError = { UnknownKeyword: new CSSIssueType('css-unknownkeyword', localize('unknown.keyword', "unknown keyword")), SelectorExpected: new CSSIssueType('css-selectorexpected', localize('expected.selector', "selector expected")), StringLiteralExpected: new CSSIssueType('css-stringliteralexpected', localize('expected.stringliteral', "string literal expected")), + CustomPropertyNameExpected: new CSSIssueType('css-custompropertynameexpected', localize('expected.custompropertyname', "custom property name expected")), }; diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index 4cb0178f..250bc558 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -71,7 +71,8 @@ export enum NodeType { FunctionArgument, KeyframeSelector, ViewPort, - Document + Document, + AtApplyRule } export enum ReferenceType { @@ -496,6 +497,19 @@ export class SimpleSelector extends Node { } } +export class AtApplyRule extends Node { + public endOfAtApply: number; + public semicolonPosition: number; + + constructor(offset: number, length: number) { + super(offset, length); + } + + public get type(): NodeType { + return NodeType.AtApplyRule; + } +} + export abstract class AbstractDeclaration extends Node { // positions for code assist @@ -1245,7 +1259,7 @@ export class MixinReference extends Node { this.namespaces = new Nodelist(this); } return this.namespaces; - } + } public setIdentifier(node: Identifier): boolean { return this.setNode('identifier', node, 0); diff --git a/src/parser/cssParser.ts b/src/parser/cssParser.ts index e4187713..fe1992c0 100644 --- a/src/parser/cssParser.ts +++ b/src/parser/cssParser.ts @@ -260,7 +260,39 @@ export class Parser { } public _parseRuleSetDeclaration(): nodes.Node { - return this._parseDeclaration(); + return this._tryToParseAtApply() || this._parseDeclaration(); + } + + /** + * Parses declarations like: + * @apply --my-theme; + * + * Follows https://tabatkins.github.io/specs/css-apply-rule/#using + */ + public _tryToParseAtApply(): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@apply')) { + return null; + } + const node = this.create(nodes.AtApplyRule) as nodes.AtApplyRule; + node.endOfAtApply = this.token.offset + this.token.len; + this.consumeToken(); + + if (!this.peek(TokenType.Ident)) { + return this.finish(node, ParseError.IdentifierExpected); + } + /** + * A css custom property name is any identifier that starts with two + * dashes: + * https://www.w3.org/TR/css-variables/#typedef-custom-property-name + */ + if (!this.peekRegExp(TokenType.Ident, /^--/)) { + return this.finish(node, ParseError.CustomPropertyNameExpected); + } + this.accept(TokenType.Ident); + if (this.peek(TokenType.SemiColon)) { + node.semicolonPosition = this.token.offset; + } + return this.finish(node); } public _needsSemicolonAfter(node: nodes.Node): boolean { @@ -284,6 +316,7 @@ export class Parser { case nodes.NodeType.MediaQuery: case nodes.NodeType.Debug: case nodes.NodeType.Import: + case nodes.NodeType.AtApplyRule: return true; case nodes.NodeType.MixinReference: return !(node).getContent(); @@ -515,8 +548,8 @@ export class Parser { } public _parseMediaDeclaration(): nodes.Node { - return this._tryParseRuleset(false) - || this._tryToParseDeclaration() + return this._tryParseRuleset(false) + || this._tryToParseDeclaration() || this._parseStylesheetStatement(); } diff --git a/src/test/css/parser.test.ts b/src/test/css/parser.test.ts index 6de5fda4..4657d237 100644 --- a/src/test/css/parser.test.ts +++ b/src/test/css/parser.test.ts @@ -235,6 +235,9 @@ suite('CSS - Parser', () => { assertNode('boo { prop: value; }', parser, parser._parseRuleset.bind(parser)); assertNode('boo { prop: value; prop: value }', parser, parser._parseRuleset.bind(parser)); assertNode('boo { prop: value; prop: value; }', parser, parser._parseRuleset.bind(parser)); + assertNode('boo { @apply --custom-prop; }', parser, parser._parseRuleset.bind(parser)); + assertNode('boo { @apply --custom-prop }', parser, parser._parseRuleset.bind(parser)); + assertNode('boo { @apply --custom-prop; background-color: red }', parser, parser._parseRuleset.bind(parser)); }); test('Ruleset /Panic/', function () { @@ -244,6 +247,9 @@ suite('CSS - Parser', () => { assertError('boo { prop }', parser, parser._parseRuleset.bind(parser), ParseError.ColonExpected); assertError('boo { prop: ; far: 12em; }', parser, parser._parseRuleset.bind(parser), ParseError.PropertyValueExpected); // assertNode('boo { prop: ; 1ar: 12em; }', parser, parser._parseRuleset.bind(parser)); + assertError('boo { @apply }', parser, parser._parseRuleset.bind(parser), ParseError.IdentifierExpected); + assertError('boo { @apply not-custom-prop}', parser, parser._parseRuleset.bind(parser), ParseError.CustomPropertyNameExpected); + assertError('boo { @apply --custom-prop background: red}', parser, parser._parseRuleset.bind(parser), ParseError.SemiColonExpected); }); test('selector', function () { diff --git a/src/test/css/scanner.test.ts b/src/test/css/scanner.test.ts index bb93748c..88a4b66e 100644 --- a/src/test/css/scanner.test.ts +++ b/src/test/css/scanner.test.ts @@ -82,6 +82,7 @@ suite('CSS - Scanner', () => { assertSingleToken(scanner, '@charset', 8, 0, '@charset', TokenType.Charset); assertSingleToken(scanner, '@-mport', 7, 0, '@-mport', TokenType.AtKeyword); assertSingleToken(scanner, '@\u00f0mport', 7, 0, '@\u00f0mport', TokenType.AtKeyword); + assertSingleToken(scanner, '@apply', 6, 0, '@apply', TokenType.AtKeyword); assertSingleToken(scanner, '@', 1, 0, '@', TokenType.Delim); });