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);