Supports at-rules nested within at-rules (#52)

* Supports at-rules nested within at-rules

Fixes #50.

* Rename `processRuleSet()` to `processRule()`
diff --git a/lib/parser.dart b/lib/parser.dart
index cd4f6c0..6688507 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -211,17 +211,11 @@
     var start = _peekToken.span;
     while (!_maybeEat(TokenKind.END_OF_FILE) && !_peekKind(TokenKind.RBRACE)) {
       // TODO(terry): Need to handle charset.
-      var directive = processDirective();
-      if (directive != null) {
-        productions.add(directive);
-        _maybeEat(TokenKind.SEMICOLON);
+      final rule = processRule();
+      if (rule != null) {
+        productions.add(rule);
       } else {
-        RuleSet ruleset = processRuleSet();
-        if (ruleset != null) {
-          productions.add(ruleset);
-        } else {
-          break;
-        }
+        break;
       }
     }
 
@@ -523,12 +517,12 @@
         // Any medias?
         var media = processMediaQueryList();
 
-        List<TreeNode> rulesets = [];
+        List<TreeNode> rules = [];
         if (_maybeEat(TokenKind.LBRACE)) {
           while (!_maybeEat(TokenKind.END_OF_FILE)) {
-            RuleSet ruleset = processRuleSet();
-            if (ruleset == null) break;
-            rulesets.add(ruleset);
+            final rule = processRule();
+            if (rule == null) break;
+            rules.add(rule);
           }
 
           if (!_maybeEat(TokenKind.RBRACE)) {
@@ -537,17 +531,17 @@
         } else {
           _error('expected { after media before ruleset', _peekToken.span);
         }
-        return new MediaDirective(media, rulesets, _makeSpan(start));
+        return new MediaDirective(media, rules, _makeSpan(start));
 
       case TokenKind.DIRECTIVE_HOST:
         _next();
 
-        List<TreeNode> rulesets = [];
+        List<TreeNode> rules = [];
         if (_maybeEat(TokenKind.LBRACE)) {
           while (!_maybeEat(TokenKind.END_OF_FILE)) {
-            RuleSet ruleset = processRuleSet();
-            if (ruleset == null) break;
-            rulesets.add(ruleset);
+            final rule = processRule();
+            if (rule == null) break;
+            rules.add(rule);
           }
 
           if (!_maybeEat(TokenKind.RBRACE)) {
@@ -556,7 +550,7 @@
         } else {
           _error('expected { after host before ruleset', _peekToken.span);
         }
-        return new HostDirective(rulesets, _makeSpan(start));
+        return new HostDirective(rules, _makeSpan(start));
 
       case TokenKind.DIRECTIVE_PAGE:
         /*
@@ -708,11 +702,11 @@
 
         start = _peekToken.span;
         while (!_maybeEat(TokenKind.END_OF_FILE)) {
-          RuleSet ruleset = processRuleSet();
-          if (ruleset == null) {
+          final rule = processRule();
+          if (rule == null) {
             break;
           }
-          productions.add(ruleset);
+          productions.add(rule);
         }
 
         _eat(TokenKind.RBRACE);
@@ -1121,8 +1115,13 @@
     return new ViewportDirective(name, declarations, _makeSpan(start));
   }
 
-  RuleSet processRuleSet([SelectorGroup selectorGroup]) {
+  TreeNode processRule([SelectorGroup selectorGroup]) {
     if (selectorGroup == null) {
+      final directive = processDirective();
+      if (directive != null) {
+        _maybeEat(TokenKind.SEMICOLON);
+        return directive;
+      }
       selectorGroup = processSelectorGroup();
     }
     if (selectorGroup != null) {
@@ -1135,14 +1134,9 @@
   List<TreeNode> processGroupRuleBody() {
     var nodes = <TreeNode>[];
     while (!(_peekKind(TokenKind.RBRACE) || _peekKind(TokenKind.END_OF_FILE))) {
-      var directive = processDirective();
-      if (directive != null) {
-        nodes.add(directive);
-        continue;
-      }
-      var ruleSet = processRuleSet();
-      if (ruleSet != null) {
-        nodes.add(ruleSet);
+      var rule = processRule();
+      if (rule != null) {
+        nodes.add(rule);
         continue;
       }
       break;
@@ -1211,7 +1205,7 @@
       var selectorGroup = _nestedSelector();
       while (selectorGroup != null) {
         // Nested selector so process as a ruleset.
-        var ruleset = processRuleSet(selectorGroup);
+        var ruleset = processRule(selectorGroup);
         decls.add(ruleset);
         selectorGroup = _nestedSelector();
       }
diff --git a/lib/src/analyzer.dart b/lib/src/analyzer.dart
index 7c7372c..6c6ba6d 100644
--- a/lib/src/analyzer.dart
+++ b/lib/src/analyzer.dart
@@ -415,9 +415,9 @@
   _MediaRulesReplacer(this._ruleSet, this._newRules);
 
   visitMediaDirective(MediaDirective node) {
-    var index = node.rulesets.indexOf(_ruleSet);
+    var index = node.rules.indexOf(_ruleSet);
     if (index != -1) {
-      node.rulesets.insertAll(index + 1, _newRules);
+      node.rules.insertAll(index + 1, _newRules);
       _foundAndReplaced = true;
     }
   }
diff --git a/lib/src/css_printer.dart b/lib/src/css_printer.dart
index ac26acb..91c2861 100644
--- a/lib/src/css_printer.dart
+++ b/lib/src/css_printer.dart
@@ -141,7 +141,7 @@
     emit('$_newLine@media');
     emitMediaQueries(node.mediaQueries);
     emit('$_sp{');
-    for (var ruleset in node.rulesets) {
+    for (var ruleset in node.rules) {
       ruleset.visit(this);
     }
     emit('$_newLine}');
@@ -149,7 +149,7 @@
 
   void visitHostDirective(HostDirective node) {
     emit('$_newLine@host$_sp{');
-    for (var ruleset in node.rulesets) {
+    for (var ruleset in node.rules) {
       ruleset.visit(this);
     }
     emit('$_newLine}');
diff --git a/lib/src/tree.dart b/lib/src/tree.dart
index 509e708..7e68473 100644
--- a/lib/src/tree.dart
+++ b/lib/src/tree.dart
@@ -638,9 +638,9 @@
 
 class MediaDirective extends Directive {
   final List<MediaQuery> mediaQueries;
-  final List<RuleSet> rulesets;
+  final List<TreeNode> rules;
 
-  MediaDirective(this.mediaQueries, this.rulesets, SourceSpan span)
+  MediaDirective(this.mediaQueries, this.rules, SourceSpan span)
       : super(span);
 
   MediaDirective clone() {
@@ -648,27 +648,27 @@
     for (var mediaQuery in mediaQueries) {
       cloneQueries.add(mediaQuery.clone());
     }
-    var cloneRulesets = <RuleSet>[];
-    for (var ruleset in rulesets) {
-      cloneRulesets.add(ruleset.clone());
+    var cloneRules = <TreeNode>[];
+    for (var rule in rules) {
+      cloneRules.add(rule.clone());
     }
-    return new MediaDirective(cloneQueries, cloneRulesets, span);
+    return new MediaDirective(cloneQueries, cloneRules, span);
   }
 
   visit(VisitorBase visitor) => visitor.visitMediaDirective(this);
 }
 
 class HostDirective extends Directive {
-  final List<RuleSet> rulesets;
+  final List<TreeNode> rules;
 
-  HostDirective(this.rulesets, SourceSpan span) : super(span);
+  HostDirective(this.rules, SourceSpan span) : super(span);
 
   HostDirective clone() {
-    var cloneRulesets = <RuleSet>[];
-    for (var ruleset in rulesets) {
-      cloneRulesets.add(ruleset.clone());
+    var cloneRules = <TreeNode>[];
+    for (var rule in rules) {
+      cloneRules.add(rule.clone());
     }
-    return new HostDirective(cloneRulesets, span);
+    return new HostDirective(cloneRules, span);
   }
 
   visit(VisitorBase visitor) => visitor.visitHostDirective(this);
@@ -771,20 +771,20 @@
 
 class StyletDirective extends Directive {
   final String dartClassName;
-  final List<RuleSet> rulesets;
+  final List<TreeNode> rules;
 
-  StyletDirective(this.dartClassName, this.rulesets, SourceSpan span)
+  StyletDirective(this.dartClassName, this.rules, SourceSpan span)
       : super(span);
 
   bool get isBuiltIn => false;
   bool get isExtension => true;
 
   StyletDirective clone() {
-    var cloneRulesets = <RuleSet>[];
-    for (var ruleset in rulesets) {
-      cloneRulesets.add(ruleset.clone());
+    var cloneRules = <TreeNode>[];
+    for (var rule in rules) {
+      cloneRules.add(rule.clone());
     }
-    return new StyletDirective(dartClassName, cloneRulesets, span);
+    return new StyletDirective(dartClassName, cloneRules, span);
   }
 
   visit(VisitorBase visitor) => visitor.visitStyletDirective(this);
diff --git a/lib/src/tree_printer.dart b/lib/src/tree_printer.dart
index ba35dec..4f196c0 100644
--- a/lib/src/tree_printer.dart
+++ b/lib/src/tree_printer.dart
@@ -85,7 +85,7 @@
     heading('MediaDirective', node);
     output.depth++;
     output.writeNodeList('media queries', node.mediaQueries);
-    output.writeNodeList('rule sets', node.rulesets);
+    output.writeNodeList('rule sets', node.rules);
     super.visitMediaDirective(node);
     output.depth--;
   }
@@ -191,7 +191,7 @@
     heading('StyletDirective', node);
     output.writeValue('dartClassName', node.dartClassName);
     output.depth++;
-    output.writeNodeList('rulesets', node.rulesets);
+    output.writeNodeList('rulesets', node.rules);
     output.depth--;
   }
 
diff --git a/lib/visitor.dart b/lib/visitor.dart
index eff9e7b..6e3af18 100644
--- a/lib/visitor.dart
+++ b/lib/visitor.dart
@@ -190,18 +190,12 @@
   }
 
   visitMediaDirective(MediaDirective node) {
-    for (var mediaQuery in node.mediaQueries) {
-      visitMediaQuery(mediaQuery);
-    }
-    for (var ruleset in node.rulesets) {
-      visitRuleSet(ruleset);
-    }
+    _visitNodeList(node.mediaQueries);
+    _visitNodeList(node.rules);
   }
 
   visitHostDirective(HostDirective node) {
-    for (var ruleset in node.rulesets) {
-      visitRuleSet(ruleset);
-    }
+    _visitNodeList(node.rules);
   }
 
   visitPageDirective(PageDirective node) {
@@ -237,7 +231,7 @@
   }
 
   visitStyletDirective(StyletDirective node) {
-    _visitNodeList(node.rulesets);
+    _visitNodeList(node.rules);
   }
 
   visitNamespaceDirective(NamespaceDirective node) {}
diff --git a/test/declaration_test.dart b/test/declaration_test.dart
index 782c33c..ab1f70d 100644
--- a/test/declaration_test.dart
+++ b/test/declaration_test.dart
@@ -478,6 +478,30 @@
   expect(
       errors.first.message, contains('expected { after media before ruleset'));
   expect(errors.first.span.text, '(');
+
+  // Test nested at-rules.
+  input = '''
+@media (min-width: 840px) {
+  .cell {
+    width: calc(33% - 16px);
+  }
+  @supports (display: grid) {
+    .cell {
+      grid-column-end: span 4;
+    }
+  }
+}''';
+  generated = '''@media (min-width:840px) {
+.cell {
+  width: calc(33% - 16px);
+}
+@supports (display: grid) {
+.cell {
+  grid-column-end: span 4;
+}
+}
+}''';
+  expectCss(input, generated);
 }
 
 void testMozDocument() {