Skip to content

Commit 73f8707

Browse files
committed
Parse @supports rule. Fixes #39
1 parent 26af8d5 commit 73f8707

File tree

4 files changed

+146
-20
lines changed

4 files changed

+146
-20
lines changed

src/parser/cssErrors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,6 @@ export let ParseError = {
4747
UnknownAtRule: new CSSIssueType('css-unknownatrule', localize('unknown.atrule', "at-rule unknown")),
4848
UnknownKeyword: new CSSIssueType('css-unknownkeyword', localize('unknown.keyword', "unknown keyword")),
4949
SelectorExpected: new CSSIssueType('css-selectorexpected', localize('expected.selector', "selector expected")),
50-
StringLiteralExpected: new CSSIssueType('css-stringliteralexpected', localize('expected.stringliteral', "string literal expected"))
50+
StringLiteralExpected: new CSSIssueType('css-stringliteralexpected', localize('expected.stringliteral', "string literal expected")),
51+
WhitespaceExpected: new CSSIssueType('css-whitespaceexpected', localize('expected.whitespace', "whitespace expected"))
5152
};

src/parser/cssNodes.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ export enum NodeType {
7676
AtApplyRule,
7777
CustomPropertyDeclaration,
7878
CustomPropertySet,
79-
ListEntry
79+
ListEntry,
80+
Supports,
81+
SupportsCondition
8082
}
8183

8284
export enum ReferenceType {
@@ -576,7 +578,7 @@ export class CustomPropertyDeclaration extends AbstractDeclaration {
576578

577579
public getPropertySet(): CustomPropertySet {
578580
return this.propertySet;
579-
}
581+
}
580582
}
581583

582584
export class CustomPropertySet extends BodyDeclaration {
@@ -1021,6 +1023,18 @@ export class Media extends BodyDeclaration {
10211023
}
10221024
}
10231025

1026+
export class Supports extends BodyDeclaration {
1027+
1028+
constructor(offset: number, length: number) {
1029+
super(offset, length);
1030+
}
1031+
1032+
public get type(): NodeType {
1033+
return NodeType.Supports;
1034+
}
1035+
}
1036+
1037+
10241038
export class Document extends BodyDeclaration {
10251039

10261040
constructor(offset: number, length: number) {
@@ -1058,6 +1072,18 @@ export class MediaQuery extends Node {
10581072
}
10591073
}
10601074

1075+
export class SupportsCondition extends Node {
1076+
1077+
constructor(offset: number, length: number) {
1078+
super(offset, length);
1079+
}
1080+
1081+
public get type(): NodeType {
1082+
return NodeType.SupportsCondition;
1083+
}
1084+
}
1085+
1086+
10611087
export class Page extends BodyDeclaration {
10621088

10631089
constructor(offset: number, length: number) {

src/parser/cssParser.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export class Parser {
238238
|| this._parsePage()
239239
|| this._parseFontFace()
240240
|| this._parseKeyframe()
241+
|| this._parseSupports()
241242
|| this._parseViewPort()
242243
|| this._parseNamespace()
243244
|| this._parseDocument();
@@ -679,6 +680,89 @@ export class Parser {
679680
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
680681
}
681682

683+
public _parseSupports(isNested = false): nodes.Node {
684+
// SUPPORTS_SYM S* supports_condition '{' S* ruleset* '}' S*
685+
if (!this.peek(TokenType.AtKeyword, '@supports')) {
686+
return null;
687+
}
688+
689+
let node = <nodes.Supports>this.create(nodes.Supports);
690+
this.consumeToken(); // @supports
691+
node.addChild(this._parseSupportsCondition());
692+
693+
return this._parseBody(node, this._parseSupportsDeclaration.bind(this, isNested));
694+
}
695+
696+
public _parseSupportsDeclaration(isNested = false): nodes.Node {
697+
return this._parseStylesheetStatement();
698+
}
699+
700+
private _parseSupportsCondition(): nodes.Node {
701+
// supports_condition : supports_negation | supports_conjunction | supports_disjunction | supports_condition_in_parens ;
702+
// supports_condition_in_parens: ( '(' S* supports_condition S* ')' ) | supports_declaration_condition | general_enclosed ;
703+
// supports_negation: NOT S+ supports_condition_in_parens ;
704+
// supports_conjunction: supports_condition_in_parens ( S+ AND S+ supports_condition_in_parens )+;
705+
// supports_disjunction: supports_condition_in_parens ( S+ OR S+ supports_condition_in_parens )+;
706+
// supports_declaration_condition: '(' S* declaration ')';
707+
// general_enclosed: ( FUNCTION | '(' ) ( any | unused )* ')' ;
708+
let node = <nodes.SupportsCondition>this.create(nodes.SupportsCondition);
709+
710+
if (this.accept(TokenType.Ident, 'not', true)) {
711+
if (!this.hasWhitespace()) {
712+
return this.finish(node, ParseError.WhitespaceExpected);
713+
}
714+
node.addChild(this._parseSupportsConditionInParens());
715+
} else {
716+
node.addChild(this._parseSupportsConditionInParens());
717+
if (this.peekRegExp(TokenType.Ident, /^(and|or)$/i)) {
718+
let text = this.token.text;
719+
if (!this.hasWhitespace()) {
720+
return this.finish(node, ParseError.WhitespaceExpected);
721+
}
722+
while (this.accept(TokenType.Ident, text, true)) {
723+
if (!this.hasWhitespace()) {
724+
return this.finish(node, ParseError.WhitespaceExpected);
725+
}
726+
node.addChild(this._parseSupportsConditionInParens());
727+
}
728+
}
729+
}
730+
return this.finish(node);
731+
}
732+
733+
private _parseSupportsConditionInParens(): nodes.Node {
734+
let node = <nodes.SupportsCondition>this.create(nodes.SupportsCondition);
735+
if (this.accept(TokenType.ParenthesisL)) {
736+
if (!node.addChild(this._tryToParseDeclaration())) {
737+
if (!this._parseSupportsCondition()) {
738+
return this.finish(node, ParseError.ConditionExpected);
739+
}
740+
}
741+
if (!this.accept(TokenType.ParenthesisR)) {
742+
return this.finish(node, ParseError.RightParenthesisExpected, [TokenType.ParenthesisR], []);
743+
}
744+
return this.finish(node);
745+
} else if (this.peek(TokenType.Ident)) {
746+
let pos = this.mark();
747+
this.consumeToken();
748+
if (!this.hasWhitespace() && this.accept(TokenType.ParenthesisL)) {
749+
let openParentCount = 1;
750+
while (this.token.type !== TokenType.EOF && openParentCount !== 0) {
751+
if (this.token.type === TokenType.ParenthesisL) {
752+
openParentCount++;
753+
} else if (this.token.type === TokenType.ParenthesisR) {
754+
openParentCount--;
755+
}
756+
this.consumeToken();
757+
}
758+
return this.finish(node);
759+
} else {
760+
this.restoreAtMark(pos);
761+
}
762+
}
763+
return this.finish(node, ParseError.LeftParenthesisExpected, [], [TokenType.ParenthesisL]);
764+
}
765+
682766
public _parseMediaDeclaration(isNested = false): nodes.Node {
683767
return this._tryParseRuleset(isNested)
684768
|| this._tryToParseDeclaration()

src/test/css/parser.test.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
'use strict';
66

77
import * as assert from 'assert';
8-
import {Parser} from '../../parser/cssParser';
9-
import {TokenType} from '../../parser/cssScanner';
8+
import { Parser } from '../../parser/cssParser';
9+
import { TokenType } from '../../parser/cssScanner';
1010
import * as nodes from '../../parser/cssNodes';
11-
import {ParseError} from '../../parser/cssErrors';
11+
import { ParseError } from '../../parser/cssErrors';
1212

1313
export function assertNode(text: string, parser: Parser, f: () => nodes.Node): nodes.Node {
1414
let node = parser.internalParse(text, f);
@@ -45,7 +45,7 @@ export function assertError(text: string, parser: Parser, f: () => nodes.Node, e
4545

4646
suite('CSS - Parser', () => {
4747

48-
test('Test stylesheet', function () {
48+
test('stylesheet', function () {
4949
let parser = new Parser();
5050
assertNode('@charset "demo" ;', parser, parser._parseStylesheet.bind(parser));
5151
assertNode('body { margin: 0px; padding: 3em, 6em; }', parser, parser._parseStylesheet.bind(parser));
@@ -78,7 +78,7 @@ suite('CSS - Parser', () => {
7878
assertError('@charset \'utf8\'', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected);
7979
});
8080

81-
test('Stylesheet /Panic/', function () {
81+
test('stylesheet /panic/', function () {
8282
let parser = new Parser();
8383
assertError('#boo, far } \n.far boo {}', parser, parser._parseStylesheet.bind(parser), ParseError.LeftCurlyExpected);
8484
assertError('#boo, far { far: 43px; \n.far boo {}', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected);
@@ -121,15 +121,30 @@ suite('CSS - Parser', () => {
121121
assertError('@keyframes name { from, #123', parser, parser._parseKeyframe.bind(parser), ParseError.PercentageExpected);
122122
});
123123

124-
test('Test import', function () {
124+
test('@import', function () {
125125
let parser = new Parser();
126126
assertNode('@import "asdasdsa"', parser, parser._parseImport.bind(parser));
127127
assertNode('@ImPort "asdsadsa"', parser, parser._parseImport.bind(parser));
128128
assertNode('@import "asdasd" dsfsdf', parser, parser._parseImport.bind(parser));
129129
assertError('@import', parser, parser._parseImport.bind(parser), ParseError.URIOrStringExpected);
130130
});
131131

132-
test('Test media', function () {
132+
test('@supports', function () {
133+
let parser = new Parser();
134+
assertNode('@supports ( display: flexbox ) { body { display: flexbox } }', parser, parser._parseSupports.bind(parser));
135+
assertNode('@supports not (display: flexbox) { .outline { box-shadow: 2px 2px 2px black; /* unprefixed last */ } }', parser, parser._parseSupports.bind(parser));
136+
assertNode('@supports ( box-shadow: 2px 2px 2px black ) or ( -moz-box-shadow: 2px 2px 2px black ) or ( -webkit-box-shadow: 2px 2px 2px black ) { }', parser, parser._parseSupports.bind(parser));
137+
assertNode('@supports ((transition-property: color) or (animation-name: foo)) and (transform: rotate(10deg)) { }', parser, parser._parseSupports.bind(parser));
138+
assertNode('@supports ((display: flexbox)) { }', parser, parser._parseSupports.bind(parser));
139+
assertNode('@supports (display: flexbox !important) { }', parser, parser._parseSupports.bind(parser));
140+
assertNode('@supports (grid-area: auto) { @media screen and (min-width: 768px) { .me { } } }', parser, parser._parseSupports.bind(parser));
141+
assertError('@supports (transition-property: color) or (animation-name: foo) and (transform: rotate(10deg)) { }', parser, parser._parseSupports.bind(parser), ParseError.LeftCurlyExpected);
142+
assertError('@supports display: flexbox { }', parser, parser._parseSupports.bind(parser), ParseError.LeftParenthesisExpected);
143+
assertError('@supports (transition-property: color)or (animation-name: foo) { }', parser, parser._parseSupports.bind(parser), ParseError.WhitespaceExpected);
144+
assertError('@supports (transition-property: color) or(animation-name: foo) { }', parser, parser._parseSupports.bind(parser), ParseError.WhitespaceExpected);
145+
});
146+
147+
test('@media', function () {
133148
let parser = new Parser();
134149
assertNode('@media asdsa { }', parser, parser._parseMedia.bind(parser));
135150
assertNode('@meDia sadd{} ', parser, parser._parseMedia.bind(parser));
@@ -151,7 +166,7 @@ suite('CSS - Parser', () => {
151166
assertError('@media not screen and (color:#234567 { }', parser, parser._parseMedia.bind(parser), ParseError.RightParenthesisExpected);
152167
});
153168

154-
test('Test media_list', function () {
169+
test('media_list', function () {
155170
let parser = new Parser();
156171
assertNode('somename', parser, parser._parseMediaList.bind(parser));
157172
assertNode('somename, othername', parser, parser._parseMediaList.bind(parser));
@@ -164,7 +179,7 @@ suite('CSS - Parser', () => {
164179
assertNode('-asda34s', parser, parser._parseMedium.bind(parser));
165180
});
166181

167-
test('page', function () {
182+
test('@page', function () {
168183
let parser = new Parser();
169184
assertNode('@page : name{ }', parser, parser._parsePage.bind(parser));
170185
assertNode('@page :left, :right { }', parser, parser._parsePage.bind(parser));
@@ -213,7 +228,7 @@ suite('CSS - Parser', () => {
213228
assertNode('+', parser, parser._parseUnaryOperator.bind(parser));
214229
});
215230

216-
test('Property', function () {
231+
test('property', function () {
217232
let parser = new Parser();
218233
assertNode('asdsa', parser, parser._parseProperty.bind(parser));
219234
assertNode('asdsa334', parser, parser._parseProperty.bind(parser));
@@ -226,7 +241,7 @@ suite('CSS - Parser', () => {
226241
assertNode('somevar--', parser, parser._parseProperty.bind(parser));
227242
});
228243

229-
test('Ruleset', function () {
244+
test('ruleset', function () {
230245
let parser = new Parser();
231246
assertNode('name{ }', parser, parser._parseRuleset.bind(parser));
232247
assertNode(' name\n{ some : "asdas" }', parser, parser._parseRuleset.bind(parser));
@@ -255,7 +270,7 @@ suite('CSS - Parser', () => {
255270
assertNode('boo { @apply --custom-prop; background-color: red }', parser, parser._parseRuleset.bind(parser));
256271
});
257272

258-
test('Ruleset /Panic/', function () {
273+
test('ruleset /Panic/', function () {
259274
let parser = new Parser();
260275
// assertNode('boo { : value }', parser, parser._parseRuleset.bind(parser));
261276
assertError('boo { prop: ; }', parser, parser._parseRuleset.bind(parser), ParseError.PropertyValueExpected);
@@ -382,7 +397,7 @@ suite('CSS - Parser', () => {
382397
assertFunction('fun(value1, value2)', parser, parser._parseFunction.bind(parser));
383398
});
384399

385-
test('Test Token prio', function () {
400+
test('test token prio', function () {
386401
let parser = new Parser();
387402
assertNode('!important', parser, parser._parsePrio.bind(parser));
388403
assertNode('!/*demo*/important', parser, parser._parsePrio.bind(parser));
@@ -398,7 +413,7 @@ suite('CSS - Parser', () => {
398413
assertNode('#FFFFFFFF', parser, parser._parseHexColor.bind(parser));
399414
});
400415

401-
test('Test class', function () {
416+
test('test class', function () {
402417
let parser = new Parser();
403418
assertNode('.faa', parser, parser._parseClass.bind(parser));
404419
assertNode('faa', parser, parser._parseElementName.bind(parser));
@@ -407,20 +422,20 @@ suite('CSS - Parser', () => {
407422
});
408423

409424

410-
test('Prio', function () {
425+
test('prio', function () {
411426
let parser = new Parser();
412427
assertNode('!important', parser, parser._parsePrio.bind(parser));
413428
});
414429

415-
test('Expr', function () {
430+
test('expr', function () {
416431
let parser = new Parser();
417432
assertNode('45,5px', parser, parser._parseExpr.bind(parser));
418433
assertNode(' 45 , 5px ', parser, parser._parseExpr.bind(parser));
419434
assertNode('5/6', parser, parser._parseExpr.bind(parser));
420435
assertNode('36mm, -webkit-calc(100%-10px)', parser, parser._parseExpr.bind(parser));
421436
});
422437

423-
test('Url', function () {
438+
test('url', function () {
424439
let parser = new Parser();
425440
assertNode('url(\'http://msft.com\')', parser, parser._parseURILiteral.bind(parser));
426441
assertNode('url("http://msft.com")', parser, parser._parseURILiteral.bind(parser));

0 commit comments

Comments
 (0)