Adds support for `@supports`. (#41)
Fixes dart-lang/angular2#330.
diff --git a/lib/parser.dart b/lib/parser.dart
index 2bdaad1..4c65a29 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -23,6 +23,12 @@
part 'src/tokenizer.dart';
part 'src/tokenkind.dart';
+enum ClauseType {
+ none,
+ conjunction,
+ disjunction,
+}
+
/** Used for parser lookup ahead (used for nested selectors Less support). */
class ParserState extends TokenizerState {
final Token peekToken;
@@ -490,6 +496,7 @@
* domain(<string>) | regexp(<string) ]# '{'
* declarations
* '}'
+ * supports: '@supports' supports_condition group_rule_body
*/
processDirective() {
var start = _peekToken.span;
@@ -771,7 +778,9 @@
_warning("@content not implemented.", _makeSpan(start));
return null;
case TokenKind.DIRECTIVE_MOZ_DOCUMENT:
- return processDocumentDirective(start);
+ return processDocumentDirective();
+ case TokenKind.DIRECTIVE_SUPPORTS:
+ return processSupportsDirective();
}
return null;
}
@@ -992,9 +1001,9 @@
return new IncludeDirective(name.name, params, span);
}
- /// Assumes '@' has already been consumed.
- DocumentDirective processDocumentDirective(SourceSpan start) {
- _next(); // '-moz-document'
+ DocumentDirective processDocumentDirective() {
+ var start = _peekToken.span;
+ _next(); // '@-moz-document'
var functions = <LiteralTerm>[];
do {
var function;
@@ -1033,8 +1042,84 @@
_eat(TokenKind.LBRACE);
var groupRuleBody = processGroupRuleBody();
_eat(TokenKind.RBRACE);
- return new DocumentDirective(
- functions, groupRuleBody, _makeSpan(start));
+ return new DocumentDirective(functions, groupRuleBody, _makeSpan(start));
+ }
+
+ SupportsDirective processSupportsDirective() {
+ var start = _peekToken.span;
+ _next(); // '@supports'
+ var condition = processSupportsCondition();
+ _eat(TokenKind.LBRACE);
+ var groupRuleBody = processGroupRuleBody();
+ _eat(TokenKind.RBRACE);
+ return new SupportsDirective(condition, groupRuleBody, _makeSpan(start));
+ }
+
+ SupportsCondition processSupportsCondition() {
+ if (_peekKind(TokenKind.IDENTIFIER)) {
+ return processSupportsNegation();
+ }
+
+ var start = _peekToken.span;
+ var conditions = <SupportsConditionInParens>[];
+ var clauseType = ClauseType.none;
+
+ while (true) {
+ conditions.add(processSupportsConditionInParens());
+
+ var type;
+ var text = _peekToken.text.toLowerCase();
+
+ if (text == 'and') {
+ type = ClauseType.conjunction;
+ } else if (text == 'or') {
+ type = ClauseType.disjunction;
+ } else {
+ break; // Done parsing clause.
+ }
+
+ if (clauseType == ClauseType.none) {
+ clauseType = type; // First operand and operator of clause.
+ } else if (clauseType != type) {
+ _error("Operators can't be mixed without a layer of parentheses",
+ _peekToken.span);
+ break;
+ }
+
+ _next(); // Consume operator.
+ }
+
+ if (clauseType == ClauseType.conjunction) {
+ return new SupportsConjunction(conditions, _makeSpan(start));
+ } else if (clauseType == ClauseType.disjunction) {
+ return new SupportsDisjunction(conditions, _makeSpan(start));
+ } else {
+ return conditions.first;
+ }
+ }
+
+ SupportsNegation processSupportsNegation() {
+ var start = _peekToken.span;
+ var text = _peekToken.text.toLowerCase();
+ if (text != 'not') return null;
+ _next(); // 'not'
+ var condition = processSupportsConditionInParens();
+ return new SupportsNegation(condition, _makeSpan(start));
+ }
+
+ SupportsConditionInParens processSupportsConditionInParens() {
+ var start = _peekToken.span;
+ _eat(TokenKind.LPAREN);
+ // Try to parse a condition.
+ var condition = processSupportsCondition();
+ if (condition != null) {
+ _eat(TokenKind.RPAREN);
+ return new SupportsConditionInParens.nested(condition, _makeSpan(start));
+ }
+ // Otherwise, parse a declaration.
+ var declaration = processDeclaration([]);
+ _eat(TokenKind.RPAREN);
+ return new SupportsConditionInParens(declaration, _makeSpan(start));
}
RuleSet processRuleSet([SelectorGroup selectorGroup]) {
diff --git a/lib/src/css_printer.dart b/lib/src/css_printer.dart
index 2ae6b66..0304fd7 100644
--- a/lib/src/css_printer.dart
+++ b/lib/src/css_printer.dart
@@ -91,6 +91,43 @@
emit('$_newLine}');
}
+ void visitSupportsDirective(SupportsDirective node) {
+ emit('$_newLine@supports ');
+ node.condition.visit(this);
+ emit('$_sp{');
+ for (var rule in node.groupRuleBody) {
+ rule.visit(this);
+ }
+ emit('$_newLine}');
+ }
+
+ void visitSupportsConditionInParens(SupportsConditionInParens node) {
+ emit('(');
+ node.condition.visit(this);
+ emit(')');
+ }
+
+ void visitSupportsNegation(SupportsNegation node) {
+ emit('not$_sp');
+ node.condition.visit(this);
+ }
+
+ void visitSupportsConjunction(SupportsConjunction node) {
+ node.conditions.first.visit(this);
+ for (var condition in node.conditions.skip(1)) {
+ emit('${_sp}and$_sp');
+ condition.visit(this);
+ }
+ }
+
+ void visitSupportsDisjunction(SupportsDisjunction node) {
+ node.conditions.first.visit(this);
+ for (var condition in node.conditions.skip(1)) {
+ emit('${_sp}or$_sp');
+ condition.visit(this);
+ }
+ }
+
void visitMediaDirective(MediaDirective node) {
emit('$_newLine@media');
emitMediaQueries(node.mediaQueries);
@@ -265,12 +302,11 @@
}
void visitDeclaration(Declaration node) {
- String importantAsString() => node.important ? '$_sp!important' : '';
-
- emit("${node.property}: ");
+ emit('${node.property}:$_sp');
node._expression.visit(this);
-
- emit("${importantAsString()}");
+ if (node.important) {
+ emit('$_sp!important');
+ }
}
void visitVarDefinition(VarDefinition node) {
diff --git a/lib/src/tokenkind.dart b/lib/src/tokenkind.dart
index f6a6c8c..48696ab 100644
--- a/lib/src/tokenkind.dart
+++ b/lib/src/tokenkind.dart
@@ -162,6 +162,7 @@
static const int DIRECTIVE_CONTENT = 656;
static const int DIRECTIVE_EXTEND = 657;
static const int DIRECTIVE_MOZ_DOCUMENT = 658;
+ static const int DIRECTIVE_SUPPORTS = 659;
// Media query operators
static const int MEDIA_OP_ONLY = 665; // Unary.
@@ -220,6 +221,7 @@
const {'type': TokenKind.DIRECTIVE_CONTENT, 'value': 'content'},
const {'type': TokenKind.DIRECTIVE_EXTEND, 'value': 'extend'},
const {'type': TokenKind.DIRECTIVE_MOZ_DOCUMENT, 'value': '-moz-document'},
+ const {'type': TokenKind.DIRECTIVE_SUPPORTS, 'value': 'supports'},
];
static const List<Map<String, dynamic>> MEDIA_OPERATORS = const [
diff --git a/lib/src/tree.dart b/lib/src/tree.dart
index 6321807..d6b9dbf 100644
--- a/lib/src/tree.dart
+++ b/lib/src/tree.dart
@@ -443,12 +443,103 @@
: super(span);
DocumentDirective clone() {
- return new DocumentDirective(this.functions, this.groupRuleBody, span);
+ var clonedFunctions = <LiteralTerm>[];
+ for (var function in functions) {
+ clonedFunctions.add(function.clone());
+ }
+ var clonedGroupRuleBody = <TreeNode>[];
+ for (var rule in groupRuleBody) {
+ clonedGroupRuleBody.add(rule.clone());
+ }
+ return new DocumentDirective(clonedFunctions, clonedGroupRuleBody, span);
}
visit(VisitorBase visitor) => visitor.visitDocumentDirective(this);
}
+class SupportsDirective extends Directive {
+ final SupportsCondition condition;
+ final List<TreeNode> groupRuleBody;
+
+ SupportsDirective(this.condition, this.groupRuleBody, SourceSpan span)
+ : super(span);
+
+ SupportsDirective clone() {
+ var clonedCondition = condition.clone();
+ var clonedGroupRuleBody = <TreeNode>[];
+ for (var rule in groupRuleBody) {
+ clonedGroupRuleBody.add(rule.clone());
+ }
+ return new SupportsDirective(clonedCondition, clonedGroupRuleBody, span);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitSupportsDirective(this);
+}
+
+abstract class SupportsCondition extends TreeNode {
+ SupportsCondition(SourceSpan span) : super(span);
+}
+
+class SupportsConditionInParens extends SupportsCondition {
+ /// A [Declaration] or nested [SupportsCondition].
+ final TreeNode condition;
+
+ SupportsConditionInParens(Declaration declaration, SourceSpan span)
+ : condition = declaration,
+ super(span);
+
+ SupportsConditionInParens.nested(SupportsCondition condition, SourceSpan span)
+ : condition = condition,
+ super(span);
+
+ SupportsConditionInParens clone() =>
+ new SupportsConditionInParens(condition.clone(), span);
+
+ visit(VisitorBase visitor) => visitor.visitSupportsConditionInParens(this);
+}
+
+class SupportsNegation extends SupportsCondition {
+ final SupportsConditionInParens condition;
+
+ SupportsNegation(this.condition, SourceSpan span) : super(span);
+
+ SupportsNegation clone() => new SupportsNegation(condition.clone(), span);
+
+ visit(VisitorBase visitor) => visitor.visitSupportsNegation(this);
+}
+
+class SupportsConjunction extends SupportsCondition {
+ final List<SupportsConditionInParens> conditions;
+
+ SupportsConjunction(this.conditions, SourceSpan span) : super(span);
+
+ SupportsConjunction clone() {
+ var clonedConditions = <SupportsCondition>[];
+ for (var condition in conditions) {
+ clonedConditions.add(condition.clone());
+ }
+ return new SupportsConjunction(clonedConditions, span);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitSupportsConjunction(this);
+}
+
+class SupportsDisjunction extends SupportsCondition {
+ final List<SupportsConditionInParens> conditions;
+
+ SupportsDisjunction(this.conditions, SourceSpan span) : super(span);
+
+ SupportsDisjunction clone() {
+ var clonedConditions = <SupportsCondition>[];
+ for (var condition in conditions) {
+ clonedConditions.add(condition.clone());
+ }
+ return new SupportsDisjunction(clonedConditions, span);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitSupportsDisjunction(this);
+}
+
class ImportDirective extends Directive {
/** import name specified. */
final String import;
diff --git a/lib/src/tree_printer.dart b/lib/src/tree_printer.dart
index 97403dc..7ae67e4 100644
--- a/lib/src/tree_printer.dart
+++ b/lib/src/tree_printer.dart
@@ -95,7 +95,42 @@
output.depth++;
output.writeNodeList('functions', node.functions);
output.writeNodeList('group rule body', node.groupRuleBody);
- super.visitDocumentDirective(node);
+ output.depth--;
+ }
+
+ void visitSupportsDirective(SupportsDirective node) {
+ heading('SupportsDirective', node);
+ output.depth++;
+ output.writeNode('condition', node.condition);
+ output.writeNodeList('group rule body', node.groupRuleBody);
+ output.depth--;
+ }
+
+ void visitSupportsConditionInParens(SupportsConditionInParens node) {
+ heading('SupportsConditionInParens', node);
+ output.depth++;
+ output.writeNode('condition', node.condition);
+ output.depth--;
+ }
+
+ void visitSupportsNegation(SupportsNegation node) {
+ heading('SupportsNegation', node);
+ output.depth++;
+ output.writeNode('condition', node.condition);
+ output.depth--;
+ }
+
+ void visitSupportsConjunction(SupportsConjunction node) {
+ heading('SupportsConjunction', node);
+ output.depth++;
+ output.writeNodeList('conditions', node.conditions);
+ output.depth--;
+ }
+
+ void visitSupportsDisjunction(SupportsDisjunction node) {
+ heading('SupportsDisjunction', node);
+ output.depth++;
+ output.writeNodeList('conditions', node.conditions);
output.depth--;
}
diff --git a/lib/visitor.dart b/lib/visitor.dart
index 016f20e..ad94033 100644
--- a/lib/visitor.dart
+++ b/lib/visitor.dart
@@ -21,6 +21,11 @@
visitTopLevelProduction(TopLevelProduction node);
visitDirective(Directive node);
visitDocumentDirective(DocumentDirective node);
+ visitSupportsDirective(SupportsDirective node);
+ visitSupportsConditionInParens(SupportsConditionInParens node);
+ visitSupportsNegation(SupportsNegation node);
+ visitSupportsConjunction(SupportsConjunction node);
+ visitSupportsDisjunction(SupportsDisjunction node);
visitMediaExpression(MediaExpression node);
visitMediaQuery(MediaQuery node);
visitMediaDirective(MediaDirective node);
@@ -158,6 +163,27 @@
_visitNodeList(node.groupRuleBody);
}
+ visitSupportsDirective(SupportsDirective node) {
+ node.condition.visit(this);
+ _visitNodeList(node.groupRuleBody);
+ }
+
+ visitSupportsConditionInParens(SupportsConditionInParens node) {
+ node.condition.visit(this);
+ }
+
+ visitSupportsNegation(SupportsNegation node) {
+ node.condition.visit(this);
+ }
+
+ visitSupportsConjunction(SupportsConjunction node) {
+ _visitNodeList(node.conditions);
+ }
+
+ visitSupportsDisjunction(SupportsDisjunction node) {
+ _visitNodeList(node.conditions);
+ }
+
visitMediaDirective(MediaDirective node) {
for (var mediaQuery in node.mediaQueries) {
visitMediaQuery(mediaQuery);
diff --git a/test/declaration_test.dart b/test/declaration_test.dart
index c301c63..a9a51cb 100644
--- a/test/declaration_test.dart
+++ b/test/declaration_test.dart
@@ -10,6 +10,14 @@
import 'testing.dart';
+void expectCss(String css, String expected) {
+ var errors = <Message>[];
+ var styleSheet = parseCss(css, errors: errors, opts: simpleOptions);
+ expect(styleSheet, isNotNull);
+ expect(errors, isEmpty);
+ expect(prettyPrint(styleSheet), expected);
+}
+
void testSimpleTerms() {
var errors = <Message>[];
final String input = r'''
@@ -517,6 +525,84 @@
expect(prettyPrint(styleSheet), expected);
}
+void testSupports() {
+ // Test single declaration condition.
+ var css = '''
+@supports (-webkit-appearance: none) {
+ div {
+ -webkit-appearance: none;
+ }
+}''';
+ var expected = '''@supports (-webkit-appearance: none) {
+div {
+ -webkit-appearance: none;
+}
+}''';
+ expectCss(css, expected);
+
+ // Test negation.
+ css = '''
+@supports not ( display: flex ) {
+ body { width: 100%; }
+}''';
+ expected = '''@supports not (display: flex) {
+body {
+ width: 100%;
+}
+}''';
+ expectCss(css, expected);
+
+ // Test clause with multiple conditions.
+ css = '''
+@supports (box-shadow: 0 0 2px black inset) or
+ (-moz-box-shadow: 0 0 2px black inset) or
+ (-webkit-box-shadow: 0 0 2px black inset) or
+ (-o-box-shadow: 0 0 2px black inset) {
+ .box {
+ box-shadow: 0 0 2px black inset;
+ }
+}''';
+ expected = '@supports (box-shadow: 0 0 2px #000 inset) or ' +
+ '(-moz-box-shadow: 0 0 2px #000 inset) or ' +
+ '(-webkit-box-shadow: 0 0 2px #000 inset) or ' +
+ '(-o-box-shadow: 0 0 2px #000 inset) {\n' +
+ '.box {\n' +
+ ' box-shadow: 0 0 2px #000 inset;\n' +
+ '}\n' +
+ '}';
+ expectCss(css, expected);
+
+ // Test conjunction and disjunction together.
+ css = '''
+@supports ((transition-property: color) or (animation-name: foo)) and
+ (transform: rotate(10deg)) {
+ div {
+ transition-property: color;
+ transform: rotate(10deg);
+ }
+}''';
+
+ expected = '@supports ' +
+ '((transition-property: color) or (animation-name: foo)) and ' +
+ '(transform: rotate(10deg)) {\n' +
+ 'div {\n' +
+ ' transition-property: color;\n' +
+ ' transform: rotate(10deg);\n' +
+ '}\n' +
+ '}';
+ expectCss(css, expected);
+
+ // Test that operators can't be mixed without parentheses.
+ css = '@supports (a: 1) and (b: 2) or (c: 3) {}';
+ var errors = <Message>[];
+ var styleSheet = parseCss(css, errors: errors, opts: simpleOptions);
+ expect(styleSheet, isNotNull);
+ expect(errors, isNotEmpty);
+ expect(errors.first.message,
+ "Operators can't be mixed without a layer of parentheses");
+ expect(errors.first.span.text, 'or');
+}
+
void testFontFace() {
var errors = <Message>[];
@@ -689,7 +775,7 @@
color: green !important;
}
''';
- final String generated = "div { color: green!important; }";
+ final String generated = "div { color:green!important; }";
var stylesheet = parseCss(input, errors: errors);
@@ -1184,6 +1270,7 @@
test('Newer CSS', testNewerCss);
test('Media Queries', testMediaQueries);
test('Document', testMozDocument);
+ test('Supports', testSupports);
test('Font-Face', testFontFace);
test('CSS file', testCssFile);
test('Compact Emitter', testCompactEmitter);