Add partial support for calc

The expression is not fully parsed into the AST.

closes https://github.com/dart-lang/csslib/issues/17

R=kevmoo@google.com, yjbanov@google.com, kevinmoo@google.com

Review URL: https://codereview.chromium.org//1407333002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d14f0b..b4ee9e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.12.2
+
+ * Fix to handle calc functions however, the expressions are treated as a
+   LiteralTerm and not fully parsed into the AST.
+
 ## 0.12.1
 
  * Fix to handling of escapes in strings.
diff --git a/lib/parser.dart b/lib/parser.dart
index a715a9a..b3a22d6 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -2188,6 +2188,8 @@
         var nameValue = identifier(); // Snarf up the ident we'll remap, maybe.
 
         if (!ieFilter && _maybeEat(TokenKind.LPAREN)) {
+          var calc = processCalc(nameValue);
+          if (calc != null) return calc;
           // FUNCTION
           return processFunction(nameValue);
         }
@@ -2442,6 +2444,64 @@
     }
   }
 
+  //  TODO(terry): Hack to gobble up the calc expression as a string looking
+  //               for the matching RPAREN the expression is not parsed into the
+  //               AST.
+  //
+  //  grammar should be:
+  //
+  //    <calc()> = calc( <calc-sum> )
+  //    <calc-sum> = <calc-product> [ [ '+' | '-' ] <calc-product> ]*
+  //    <calc-product> = <calc-value> [ '*' <calc-value> | '/' <number> ]*
+  //    <calc-value> = <number> | <dimension> | <percentage> | ( <calc-sum> )
+  //
+  String processCalcExpression() {
+    var inString = tokenizer._inString;
+    tokenizer._inString = false;
+
+    // Gobble up everything until we hit our stop token.
+    var stringValue = new StringBuffer();
+    var left = 1;
+    var matchingParens = false;
+    while (_peek() != TokenKind.END_OF_FILE && !matchingParens) {
+      var token = _peek();
+      if (token == TokenKind.LPAREN)
+        left++;
+      else if (token == TokenKind.RPAREN)
+        left--;
+
+      matchingParens = left == 0;
+      if (!matchingParens) stringValue.write(_next().text);
+    }
+
+    if (!matchingParens) {
+      _error("problem parsing function expected ), ", _peekToken.span);
+    }
+
+    tokenizer._inString = inString;
+
+    return stringValue.toString();
+  }
+
+  CalcTerm processCalc(Identifier func) {
+    var start = _peekToken.span;
+
+    var name = func.name;
+    if (name == 'calc') {
+      // TODO(terry): Implement expression parsing properly.
+      String expression = processCalcExpression();
+      var calcExpr = new LiteralTerm(expression, expression, _makeSpan(start));
+
+      if (!_maybeEat(TokenKind.RPAREN)) {
+        _error("problem parsing function expected ), ", _peekToken.span);
+      }
+
+      return new CalcTerm(name, name, calcExpr, _makeSpan(start));
+    }
+
+    return null;
+  }
+
   //  Function grammar:
   //
   //  function:     IDENT '(' expr ')'
@@ -2466,9 +2526,6 @@
         }
 
         return new UriTerm(urlParam, _makeSpan(start));
-      case 'calc':
-        // TODO(terry): Implement expression handling...
-        break;
       case 'var':
         // TODO(terry): Consider handling var in IE specific filter/progid.  This
         //              will require parsing entire IE specific syntax e.g.,
diff --git a/lib/src/css_printer.dart b/lib/src/css_printer.dart
index 125b5ae..a62ca47 100644
--- a/lib/src/css_printer.dart
+++ b/lib/src/css_printer.dart
@@ -38,6 +38,12 @@
   //              flag for obfuscation.
   bool get _isTesting => !prettyPrint;
 
+  void visitCalcTerm(CalcTerm node) {
+    emit('${node.text}(');
+    node.expr.visit(this);
+    emit(')');
+  }
+
   void visitCssComment(CssComment node) {
     emit('/* ${node.comment} */');
   }
diff --git a/lib/src/tree.dart b/lib/src/tree.dart
index 75e9629..ba8370e 100644
--- a/lib/src/tree.dart
+++ b/lib/src/tree.dart
@@ -44,6 +44,21 @@
   String get name => 'not';
 }
 
+// calc(...)
+// TODO(terry): Hack to handle calc however the expressions should be fully
+//              parsed and in the AST.
+class CalcTerm extends LiteralTerm {
+  final LiteralTerm expr;
+
+  CalcTerm(var value, String t, this.expr, SourceSpan span)
+      : super(value, t, span);
+
+  CalcTerm clone() => new CalcTerm(value, text, expr.clone(), span);
+  visit(VisitorBase visitor) => visitor.visitCalcTerm(this);
+
+  String toString() => "$text($expr)";
+}
+
 // /*  ....   */
 class CssComment extends TreeNode {
   final String comment;
diff --git a/lib/src/tree_printer.dart b/lib/src/tree_printer.dart
index 030a868..9b0a6c2 100644
--- a/lib/src/tree_printer.dart
+++ b/lib/src/tree_printer.dart
@@ -46,6 +46,13 @@
     heading('Directive', node);
   }
 
+  void visitCalcTerm(CalcTerm node) {
+    heading('CalcTerm', node);
+    output.depth++;
+    super.visitCalcTerm(node);
+    output.depth--;
+  }
+
   void visitCssComment(CssComment node) {
     heading('Comment', node);
     output.depth++;
diff --git a/lib/visitor.dart b/lib/visitor.dart
index 593e43a..b6babbd 100644
--- a/lib/visitor.dart
+++ b/lib/visitor.dart
@@ -13,6 +13,7 @@
 part 'src/tree_printer.dart';
 
 abstract class VisitorBase {
+  visitCalcTerm(CalcTerm node);
   visitCssComment(CssComment node);
   visitCommentDefinition(CommentDefinition node);
   visitStyleSheet(StyleSheet node);
@@ -132,6 +133,11 @@
 
   visitDirective(Directive node) {}
 
+  visitCalcTerm(CalcTerm node) {
+    visitLiteralTerm(node);
+    visitLiteralTerm(node.expr);
+  }
+
   visitCssComment(CssComment node) {}
 
   visitCommentDefinition(CommentDefinition node) {}
diff --git a/pubspec.yaml b/pubspec.yaml
index ab709a9..96f403e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: csslib
-version: 0.12.2-dev
+version: 0.12.2
 author: Polymer.dart Team <web-ui-dev@dartlang.org>
 description: A library for parsing CSS.
 homepage: https://github.com/dart-lang/csslib
diff --git a/test/declaration_test.dart b/test/declaration_test.dart
index 702921d..de589d7 100644
--- a/test/declaration_test.dart
+++ b/test/declaration_test.dart
@@ -1015,12 +1015,50 @@
   expect(decl.expression.span.text, '50px');
 }
 
-void testDeclarationSpanWithCalc() {
+void simpleCalc() {
   final input = r'''.foo { height: calc(100% - 55px); }''';
   var stylesheet = parseCss(input);
   var decl = stylesheet.topLevels.single.declarationGroup.declarations.single;
-  // This fails, with span being "height: calc("
-  expect(decl.span.text, 'height: calc(100% - 55px);');
+  expect(decl.span.text, 'height: calc(100% - 55px)');
+}
+
+void complexCalc() {
+  final input = r'''.foo { left: calc((100%/3 - 2) * 1em - 2 * 1px); }''';
+  var stylesheet = parseCss(input);
+  var decl = stylesheet.topLevels.single.declarationGroup.declarations.single;
+  expect(decl.span.text, 'left: calc((100%/3 - 2) * 1em - 2 * 1px)');
+}
+
+void twoCalcs() {
+  final input = r'''.foo { margin: calc(1rem - 2px) calc(1rem - 1px); }''';
+  var stylesheet = parseCss(input);
+  var decl = stylesheet.topLevels.single.declarationGroup.declarations.single;
+  expect(decl.span.text, 'margin: calc(1rem - 2px) calc(1rem - 1px)');
+}
+
+void selectorWithCalcs() {
+  var errors = [];
+  final String input = r'''
+.foo {
+  width: calc(1em + 5 * 2em);
+  height: calc(1px + 2%) !important;
+  border: 5px calc(1pt + 2cm) 6px calc(1em + 1in + 2px) red;
+  border: calc(5px + 1em) 0px 1px calc(10 + 20 + 1px);
+  margin: 25px calc(50px + (100% / (3 - 1em) - 20%)) calc(10px + 10 * 20) calc(100% - 10px);
+}''';
+  final String generated = r'''
+.foo {
+  width: calc(1em + 5 * 2em);
+  height: calc(1px + 2%) !important;
+  border: 5px calc(1pt + 2cm) 6px calc(1em + 1in + 2px) #f00;
+  border: calc(5px + 1em) 0px 1px calc(10 + 20 + 1px);
+  margin: 25px calc(50px + (100% / (3 - 1em) - 20%)) calc(10px + 10 * 20) calc(100% - 10px);
+}''';
+
+  var stylesheet = parseCss(input, errors: errors);
+  expect(stylesheet != null, true);
+  expect(errors.isEmpty, true, reason: errors.toString());
+  expect(prettyPrint(stylesheet), generated);
 }
 
 main() {
@@ -1042,7 +1080,11 @@
   test('Expression spans', testExpressionSpans,
       skip: 'expression spans are broken'
             ' (https://github.com/dart-lang/csslib/issues/15)');
-  test('Declaration span containing calc()', testDeclarationSpanWithCalc,
-      skip: 'calc() declarations are broken'
-            ' (https://github.com/dart-lang/csslib/issues/17)');
+  group('calc function', () {
+    test('simple calc', simpleCalc);
+    test('single complex', complexCalc);
+    test('two calc terms for same declaration', twoCalcs);
+    test('selector with many calc declarations', selectorWithCalcs);
+  });
 }
+