Merge pull request #49 from leonsenft/add-vendor-prefixed-calc

Adds support for vendor-prefixed calc()
diff --git a/lib/parser.dart b/lib/parser.dart
index 54f3f4d..3dff61e 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -375,27 +375,19 @@
   List<MediaQuery> processMediaQueryList() {
     var mediaQueries = <MediaQuery>[];
 
-    bool firstTime = true;
-    var mediaQuery;
     do {
-      mediaQuery = processMediaQuery(firstTime == true);
+      var mediaQuery = processMediaQuery();
       if (mediaQuery != null) {
         mediaQueries.add(mediaQuery);
-        firstTime = false;
-        continue;
+      } else {
+        break;
       }
-
-      // Any more more media types separated by comma.
-      if (!_maybeEat(TokenKind.COMMA)) break;
-
-      // Yep more media types start again.
-      firstTime = true;
-    } while ((!firstTime && mediaQuery != null) || firstTime);
+    } while (_maybeEat(TokenKind.COMMA));
 
     return mediaQueries;
   }
 
-  MediaQuery processMediaQuery([bool startQuery = true]) {
+  MediaQuery processMediaQuery() {
     // Grammar: [ONLY | NOT]? S* media_type S*
     //          [ AND S* MediaExpr ]* | MediaExpr [ AND S* MediaExpr ]*
 
@@ -407,41 +399,39 @@
     var unaryOp = TokenKind.matchMediaOperator(op, 0, opLen);
     if (unaryOp != -1) {
       if (isChecked) {
-        if (startQuery && unaryOp != TokenKind.MEDIA_OP_NOT ||
+        if (unaryOp != TokenKind.MEDIA_OP_NOT ||
             unaryOp != TokenKind.MEDIA_OP_ONLY) {
           _warning("Only the unary operators NOT and ONLY allowed",
               _makeSpan(start));
         }
-        if (!startQuery && unaryOp != TokenKind.MEDIA_OP_AND) {
-          _warning("Only the binary AND operator allowed", _makeSpan(start));
-        }
       }
       _next();
       start = _peekToken.span;
     }
 
     var type;
-    if (startQuery && unaryOp != TokenKind.MEDIA_OP_AND) {
-      // Get the media type.
-      if (_peekIdentifier()) type = identifier();
-    }
+    // Get the media type.
+    if (_peekIdentifier()) type = identifier();
 
     var exprs = <MediaExpression>[];
 
-    if (unaryOp == -1 || unaryOp == TokenKind.MEDIA_OP_AND) {
-      var andOp = false;
-      while (true) {
-        var expr = processMediaExpression(andOp);
-        if (expr == null) break;
-
-        exprs.add(expr);
+    while (true) {
+      // Parse AND if query has a media_type or previous expression.
+      var andOp = exprs.isNotEmpty || type != null;
+      if (andOp) {
         op = _peekToken.text;
         opLen = op.length;
-        andOp = TokenKind.matchMediaOperator(op, 0, opLen) ==
-            TokenKind.MEDIA_OP_AND;
-        if (!andOp) break;
+        if (TokenKind.matchMediaOperator(op, 0, opLen) !=
+            TokenKind.MEDIA_OP_AND) {
+          break;
+        }
         _next();
       }
+
+      var expr = processMediaExpression(andOp);
+      if (expr == null) break;
+
+      exprs.add(expr);
     }
 
     if (unaryOp != -1 || type != null || exprs.length > 0) {
@@ -457,17 +447,16 @@
     if (_maybeEat(TokenKind.LPAREN)) {
       if (_peekIdentifier()) {
         var feature = identifier(); // Media feature.
-        while (_maybeEat(TokenKind.COLON)) {
-          var startExpr = _peekToken.span;
-          var exprs = processExpr();
-          if (_maybeEat(TokenKind.RPAREN)) {
-            return new MediaExpression(
-                andOperator, feature, exprs, _makeSpan(startExpr));
-          } else if (isChecked) {
-            _warning("Missing parenthesis around media expression",
-                _makeSpan(start));
-            return null;
-          }
+        var exprs = _maybeEat(TokenKind.COLON)
+            ? processExpr()
+            : new Expressions(_makeSpan(_peekToken.span));
+        if (_maybeEat(TokenKind.RPAREN)) {
+          return new MediaExpression(
+              andOperator, feature, exprs, _makeSpan(start));
+        } else if (isChecked) {
+          _warning(
+              "Missing parenthesis around media expression", _makeSpan(start));
+          return null;
         }
       } else if (isChecked) {
         _warning("Missing media feature in media expression", _makeSpan(start));
@@ -781,6 +770,9 @@
         return processDocumentDirective();
       case TokenKind.DIRECTIVE_SUPPORTS:
         return processSupportsDirective();
+      case TokenKind.DIRECTIVE_VIEWPORT:
+      case TokenKind.DIRECTIVE_MS_VIEWPORT:
+        return processViewportDirective();
     }
     return null;
   }
@@ -1122,6 +1114,13 @@
     return new SupportsConditionInParens(declaration, _makeSpan(start));
   }
 
+  ViewportDirective processViewportDirective() {
+    var start = _peekToken.span;
+    var name = _next().text;
+    var declarations = processDeclarations();
+    return new ViewportDirective(name, declarations, _makeSpan(start));
+  }
+
   RuleSet processRuleSet([SelectorGroup selectorGroup]) {
     if (selectorGroup == null) {
       selectorGroup = processSelectorGroup();
diff --git a/lib/src/css_printer.dart b/lib/src/css_printer.dart
index 0304fd7..ac26acb 100644
--- a/lib/src/css_printer.dart
+++ b/lib/src/css_printer.dart
@@ -54,8 +54,11 @@
 
   void visitMediaExpression(MediaExpression node) {
     emit(node.andOperator ? ' AND ' : ' ');
-    emit('(${node.mediaFeature}:');
-    visitExpressions(node.exprs);
+    emit('(${node.mediaFeature}');
+    if (node.exprs.expressions.isNotEmpty) {
+      emit(':');
+      visitExpressions(node.exprs);
+    }
     emit(')');
   }
 
@@ -68,11 +71,11 @@
     }
   }
 
-  void emitMediaQueries(queries) {
+  void emitMediaQueries(List<MediaQuery> queries) {
     var queriesLen = queries.length;
     for (var i = 0; i < queriesLen; i++) {
       var query = queries[i];
-      if (query.hasMediaType && i > 0) emit(',');
+      if (i > 0) emit(',');
       visitMediaQuery(query);
     }
   }
@@ -128,6 +131,12 @@
     }
   }
 
+  void visitViewportDirective(ViewportDirective node) {
+    emit('@${node.name}$_sp{$_newLine');
+    node.declarations.visit(this);
+    emit('}');
+  }
+
   void visitMediaDirective(MediaDirective node) {
     emit('$_newLine@media');
     emitMediaQueries(node.mediaQueries);
diff --git a/lib/src/tokenkind.dart b/lib/src/tokenkind.dart
index 48696ab..14cf3c9 100644
--- a/lib/src/tokenkind.dart
+++ b/lib/src/tokenkind.dart
@@ -163,6 +163,8 @@
   static const int DIRECTIVE_EXTEND = 657;
   static const int DIRECTIVE_MOZ_DOCUMENT = 658;
   static const int DIRECTIVE_SUPPORTS = 659;
+  static const int DIRECTIVE_VIEWPORT = 660;
+  static const int DIRECTIVE_MS_VIEWPORT = 661;
 
   // Media query operators
   static const int MEDIA_OP_ONLY = 665; // Unary.
@@ -222,6 +224,8 @@
     const {'type': TokenKind.DIRECTIVE_EXTEND, 'value': 'extend'},
     const {'type': TokenKind.DIRECTIVE_MOZ_DOCUMENT, 'value': '-moz-document'},
     const {'type': TokenKind.DIRECTIVE_SUPPORTS, 'value': 'supports'},
+    const {'type': TokenKind.DIRECTIVE_VIEWPORT, 'value': 'viewport'},
+    const {'type': TokenKind.DIRECTIVE_MS_VIEWPORT, 'value': '-ms-viewport'},
   ];
 
   static const List<Map<String, dynamic>> MEDIA_OPERATORS = const [
diff --git a/lib/src/tree.dart b/lib/src/tree.dart
index d6b9dbf..509e708 100644
--- a/lib/src/tree.dart
+++ b/lib/src/tree.dart
@@ -540,6 +540,19 @@
   visit(VisitorBase visitor) => visitor.visitSupportsDisjunction(this);
 }
 
+class ViewportDirective extends Directive {
+  final String name;
+  final DeclarationGroup declarations;
+
+  ViewportDirective(this.name, this.declarations, SourceSpan span)
+      : super(span);
+
+  ViewportDirective clone() =>
+      new ViewportDirective(name, declarations.clone(), span);
+
+  visit(VisitorBase visitor) => visitor.visitViewportDirective(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 7ae67e4..ba35dec 100644
--- a/lib/src/tree_printer.dart
+++ b/lib/src/tree_printer.dart
@@ -134,6 +134,13 @@
     output.depth--;
   }
 
+  void visitViewportDirective(ViewportDirective node) {
+    heading('ViewportDirective', node);
+    output.depth++;
+    super.visitViewportDirective(node);
+    output.depth--;
+  }
+
   void visitPageDirective(PageDirective node) {
     heading('PageDirective', node);
     output.depth++;
diff --git a/lib/visitor.dart b/lib/visitor.dart
index ad94033..eff9e7b 100644
--- a/lib/visitor.dart
+++ b/lib/visitor.dart
@@ -26,6 +26,7 @@
   visitSupportsNegation(SupportsNegation node);
   visitSupportsConjunction(SupportsConjunction node);
   visitSupportsDisjunction(SupportsDisjunction node);
+  visitViewportDirective(ViewportDirective node);
   visitMediaExpression(MediaExpression node);
   visitMediaQuery(MediaQuery node);
   visitMediaDirective(MediaDirective node);
@@ -184,6 +185,10 @@
     _visitNodeList(node.conditions);
   }
 
+  visitViewportDirective(ViewportDirective node) {
+    node.declarations.visit(this);
+  }
+
   visitMediaDirective(MediaDirective node) {
     for (var mediaQuery in node.mediaQueries) {
       visitMediaQuery(mediaQuery);
diff --git a/test/declaration_test.dart b/test/declaration_test.dart
index 199322b..733a198 100644
--- a/test/declaration_test.dart
+++ b/test/declaration_test.dart
@@ -411,13 +411,13 @@
 
   input = '''
 @media only screen and (min-device-width: 4000px) and
-    (min-device-height: 2000px), screen (another: 100px) {
+    (min-device-height: 2000px), screen AND (another: 100px) {
       html {
         font-size: 10em;
       }
     }''';
   generated = '@media ONLY screen AND (min-device-width:4000px) '
-      'AND (min-device-height:2000px), screen (another:100px) {\n'
+      'AND (min-device-height:2000px), screen AND (another:100px) {\n'
       'html {\n  font-size: 10em;\n}\n}';
 
   stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);
@@ -427,14 +427,14 @@
   expect(prettyPrint(stylesheet), generated);
 
   input = '''
-@media screen,print (min-device-width: 4000px) and
-    (min-device-height: 2000px), screen (another: 100px) {
+@media screen,print AND (min-device-width: 4000px) and
+    (min-device-height: 2000px), screen AND (another: 100px) {
       html {
         font-size: 10em;
       }
     }''';
-  generated = '@media screen, print (min-device-width:4000px) AND '
-      '(min-device-height:2000px), screen (another:100px) {\n'
+  generated = '@media screen, print AND (min-device-width:4000px) AND '
+      '(min-device-height:2000px), screen AND (another:100px) {\n'
       'html {\n  font-size: 10em;\n}\n}';
 
   stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);
@@ -444,15 +444,29 @@
   expect(prettyPrint(stylesheet), generated);
 
   input = '''
-@import "test.css" ONLY screen, NOT print (min-device-width: 4000px);''';
-  generated =
-      '@import "test.css" ONLY screen, NOT print (min-device-width:4000px);';
+@import "test.css" ONLY screen, NOT print AND (min-device-width: 4000px);''';
+  generated = '@import "test.css" ONLY screen, '
+      'NOT print AND (min-device-width:4000px);';
 
   stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);
 
   expect(stylesheet != null, true);
   expect(errors.isEmpty, true, reason: errors.toString());
   expect(prettyPrint(stylesheet), generated);
+
+  var css = '@media (min-device-width:400px) {\n}';
+  expectCss(css, css);
+
+  css = '@media all AND (tranform-3d), (-webkit-transform-3d) {\n}';
+  expectCss(css, css);
+
+  // Test that AND operator is required between media type and expressions.
+  css = '@media screen (min-device-width:400px';
+  stylesheet = parseCss(css, errors: errors..clear(), opts: simpleOptions);
+  expect(errors, isNotEmpty);
+  expect(
+      errors.first.message, contains('expected { after media before ruleset'));
+  expect(errors.first.span.text, '(');
 }
 
 void testMozDocument() {
@@ -603,6 +617,36 @@
   expect(errors.first.span.text, 'or');
 }
 
+void testViewport() {
+  // No declarations.
+  var css = '@viewport {\n}';
+  expectCss(css, css);
+
+  // All declarations.
+  css = '''
+@viewport {
+  min-width: auto;
+  max-width: 800px;
+  width: 400px;
+  min-height: 50%;
+  max-height: 200px;
+  height: 100px 200px;
+  zoom: auto;
+  min-zoom: 0.75;
+  max-zoom: 40%;
+  user-zoom: fixed;
+  orientation: landscape;
+}''';
+  expectCss(css, css);
+
+  // Vendor specific.
+  css = '''
+@-ms-viewport {
+  width: device-width;
+}''';
+  expectCss(css, css);
+}
+
 void testFontFace() {
   var errors = <Message>[];
 
@@ -1285,6 +1329,7 @@
   test('Media Queries', testMediaQueries);
   test('Document', testMozDocument);
   test('Supports', testSupports);
+  test('Viewport', testViewport);
   test('Font-Face', testFontFace);
   test('CSS file', testCssFile);
   test('Compact Emitter', testCompactEmitter);