fixed CssPrinter pretty print indent levels (#169)

* fixed CssPrinter pretty print indent levels

* review comments
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92dacab..75c5158 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 - Fixed a regression parsing declaration values containing spaces.
 - Add support for `lh` and `rlh` units.
 - Refactor the package example.
+- Addressed an issue with the indent level of the `CssPrinter` output.
 
 ## 0.17.2
 
diff --git a/lib/src/css_printer.dart b/lib/src/css_printer.dart
index 85e7978..e0507d1 100644
--- a/lib/src/css_printer.dart
+++ b/lib/src/css_printer.dart
@@ -7,35 +7,91 @@
 /// Visitor that produces a formatted string representation of the CSS tree.
 class CssPrinter extends Visitor {
   StringBuffer _buff = StringBuffer();
-  bool prettyPrint = true;
+  bool _prettyPrint = true;
   bool _isInKeyframes = false;
+  int _indent = 0;
+  bool _startOfLine = true;
 
   /// Walk the [tree] Stylesheet. [pretty] if true emits line breaks, extra
   /// spaces, friendly property values, etc., if false emits compacted output.
   @override
   void visitTree(StyleSheet tree, {bool pretty = false}) {
-    prettyPrint = pretty;
+    _prettyPrint = pretty;
     _buff = StringBuffer();
+    _indent = 0;
+    _startOfLine = true;
+
     visitStyleSheet(tree);
   }
 
   /// Appends [str] to the output buffer.
   void emit(String str) {
-    _buff.write(str);
+    if (_prettyPrint) {
+      if (_startOfLine) {
+        _startOfLine = false;
+        _buff.write(' ' * _indent);
+      }
+      _buff.write(str);
+    } else {
+      _buff.write(str);
+    }
+  }
+
+  void _emitLBrace() {
+    _indent += 2;
+    _buff.write('{');
+
+    if (_prettyPrint) {
+      _buff.writeln();
+      _startOfLine = true;
+    }
+  }
+
+  void _emitRBrace() {
+    _indent -= 2;
+
+    if (_prettyPrint) {
+      if (!_startOfLine) _buff.write('\n');
+      _buff.write('${' ' * _indent}}\n');
+      _startOfLine = true;
+    } else {
+      _buff.write('}');
+      if (_indent == 0) {
+        _buff.writeln();
+      }
+    }
+  }
+
+  void _emitSemicolon({bool forceLf = false}) {
+    if (_prettyPrint) {
+      _buff.write(';\n');
+      _startOfLine = true;
+    } else {
+      _buff.write(';');
+      if (forceLf) _buff.write('\n');
+    }
+  }
+
+  void _emitLf({bool force = false}) {
+    if (_prettyPrint) {
+      _buff.write('\n');
+      _startOfLine = true;
+    } else if (force) {
+      _buff.write('\n');
+    }
   }
 
   /// Returns the output buffer.
   @override
   String toString() => _buff.toString().trim();
 
-  String get _newLine => prettyPrint ? '\n' : '';
-  String get _sp => prettyPrint ? ' ' : '';
+  String get _sp => _prettyPrint ? ' ' : '';
 
   // TODO(terry): When adding obfuscation we'll need isOptimized (compact w/
   //              obfuscation) and have isTesting (compact no obfuscation) and
   //              isCompact would be !prettyPrint.  We'll need another boolean
   //              flag for obfuscation.
-  bool get _isTesting => !prettyPrint;
+  bool get _isTesting => !_prettyPrint;
 
   @override
   void visitCalcTerm(CalcTerm node) {
@@ -86,28 +142,30 @@
 
   @override
   void visitDocumentDirective(DocumentDirective node) {
-    emit('$_newLine@-moz-document ');
+    emit('@-moz-document ');
     node.functions.first.visit(this);
     for (var function in node.functions.skip(1)) {
       emit(',$_sp');
       function.visit(this);
     }
-    emit('$_sp{');
+    emit(_sp);
+    _emitLBrace();
     for (var ruleSet in node.groupRuleBody) {
       ruleSet.visit(this);
     }
-    emit('$_newLine}');
+    _emitRBrace();
   }
 
   @override
   void visitSupportsDirective(SupportsDirective node) {
-    emit('$_newLine@supports ');
+    emit('@supports ');
     node.condition!.visit(this);
-    emit('$_sp{');
+    emit(_sp);
+    _emitLBrace();
     for (var rule in node.groupRuleBody) {
       rule.visit(this);
     }
-    emit('$_newLine}');
+    _emitRBrace();
   }
 
   @override
@@ -143,29 +201,32 @@
 
   @override
   void visitViewportDirective(ViewportDirective node) {
-    emit('@${node.name}$_sp{$_newLine');
+    emit('@${node.name}$_sp');
+    _emitLBrace();
     node.declarations.visit(this);
-    emit('}');
+    _emitRBrace();
   }
 
   @override
   void visitMediaDirective(MediaDirective node) {
-    emit('$_newLine@media');
+    emit('@media');
     emitMediaQueries(node.mediaQueries.cast<MediaQuery>());
-    emit('$_sp{');
+    emit(_sp);
+    _emitLBrace();
     for (var ruleset in node.rules) {
       ruleset.visit(this);
     }
-    emit('$_newLine}');
+    _emitRBrace();
   }
 
   @override
   void visitHostDirective(HostDirective node) {
-    emit('$_newLine@host$_sp{');
+    emit('@host$_sp');
+    _emitLBrace();
     for (var ruleset in node.rules) {
       ruleset.visit(this);
     }
-    emit('$_newLine}');
+    _emitRBrace();
   }
 
   ///  @page : pseudoPage {
@@ -173,7 +234,7 @@
   ///  }
   @override
   void visitPageDirective(PageDirective node) {
-    emit('$_newLine@page');
+    emit('@page');
     if (node.hasIdent || node.hasPseudoPage) {
       if (node.hasIdent) emit(' ');
       emit(node._ident!);
@@ -182,17 +243,19 @@
 
     var declsMargin = node._declsMargin;
     var declsMarginLength = declsMargin.length;
-    emit('$_sp{$_newLine');
+    emit(_sp);
+    _emitLBrace();
     for (var i = 0; i < declsMarginLength; i++) {
       declsMargin[i].visit(this);
     }
-    emit('}');
+    _emitRBrace();
   }
 
   /// @charset "charset encoding"
   @override
   void visitCharsetDirective(CharsetDirective node) {
-    emit('$_newLine@charset "${node.charEncoding}";');
+    emit('@charset "${node.charEncoding}"');
+    _emitSemicolon(forceLf: true);
   }
 
   @override
@@ -201,51 +264,54 @@
 
     if (_isTesting) {
       // Emit assuming url() was parsed; most suite tests use url function.
-      emit(' @import url(${node.import})');
+      emit('@import url(${node.import})');
     } else if (isStartingQuote(node.import)) {
-      emit(' @import ${node.import}');
+      emit('@import ${node.import}');
     } else {
       // url(...) isn't needed only a URI can follow an @import directive; emit
       // url as a string.
-      emit(' @import "${node.import}"');
+      emit('@import "${node.import}"');
     }
     emitMediaQueries(node.mediaQueries);
-    emit(';');
+    _emitSemicolon(forceLf: true);
   }
 
   @override
   void visitKeyFrameDirective(KeyFrameDirective node) {
-    emit('$_newLine${node.keyFrameName} ');
+    emit('${node.keyFrameName} ');
     node.name!.visit(this);
-    emit('$_sp{$_newLine');
+    emit(_sp);
+    _emitLBrace();
     _isInKeyframes = true;
     for (final block in node._blocks) {
       block.visit(this);
     }
     _isInKeyframes = false;
-    emit('}');
+    _emitRBrace();
   }
 
   @override
   void visitFontFaceDirective(FontFaceDirective node) {
-    emit('$_newLine@font-face ');
-    emit('$_sp{$_newLine');
+    emit('@font-face');
+    emit(_sp);
+    _emitLBrace();
     node._declarations.visit(this);
-    emit('}');
+    _emitRBrace();
   }
 
   @override
   void visitKeyFrameBlock(KeyFrameBlock node) {
-    emit('$_sp$_sp');
     node._blockSelectors.visit(this);
-    emit('$_sp{$_newLine');
+    emit(_sp);
+    _emitLBrace();
     node._declarations.visit(this);
-    emit('$_sp$_sp}$_newLine');
+    _emitRBrace();
   }
 
   @override
   void visitStyletDirective(StyletDirective node) {
-    emit('/* @stylet export as ${node.dartClassName} */\n');
+    emit('/* @stylet export as ${node.dartClassName} */');
+    _emitLf(force: true);
   }
 
   @override
@@ -253,49 +319,51 @@
     bool isStartingQuote(String ch) => '\'"'.contains(ch);
 
     if (isStartingQuote(node._uri!)) {
-      emit(' @namespace ${node.prefix}"${node._uri}"');
+      emit('@namespace ${node.prefix}"${node._uri}"');
     } else {
       if (_isTesting) {
         // Emit exactly was we parsed.
-        emit(' @namespace ${node.prefix}url(${node._uri})');
+        emit('@namespace ${node.prefix}url(${node._uri})');
       } else {
         // url(...) isn't needed only a URI can follow a:
         //    @namespace prefix directive.
-        emit(' @namespace ${node.prefix}${node._uri}');
+        emit('@namespace ${node.prefix}${node._uri}');
       }
     }
-    emit(';');
+    _emitSemicolon(forceLf: true);
   }
 
   @override
   void visitVarDefinitionDirective(VarDefinitionDirective node) {
     visitVarDefinition(node.def);
-    emit(';$_newLine');
+    _emitSemicolon();
   }
 
   @override
   void visitMixinRulesetDirective(MixinRulesetDirective node) {
-    emit('@mixin ${node.name} {');
+    emit('@mixin ${node.name} ');
+    _emitLBrace();
     for (var ruleset in node.rulesets) {
       ruleset.visit(this);
     }
-    emit('}');
+    _emitRBrace();
   }
 
   @override
   void visitMixinDeclarationDirective(MixinDeclarationDirective node) {
-    emit('@mixin ${node.name} {\n');
+    emit('@mixin ${node.name}$_sp');
+    _emitLBrace();
     visitDeclarationGroup(node.declarations);
-    emit('}');
+    _emitRBrace();
   }
 
   /// Added optional newLine for handling @include at top-level vs/ inside of
   /// a declaration group.
   @override
   void visitIncludeDirective(IncludeDirective node, [bool topLevel = true]) {
-    if (topLevel) emit(_newLine);
+    if (topLevel) _emitLf();
     emit('@include ${node.name}');
-    emit(';');
+    _emitSemicolon(forceLf: true);
   }
 
   @override
@@ -305,11 +373,11 @@
 
   @override
   void visitRuleSet(RuleSet node) {
-    emit(_newLine);
     node.selectorGroup!.visit(this);
-    emit('$_sp{$_newLine');
+    emit(_sp);
+    _emitLBrace();
     node.declarationGroup.visit(this);
-    emit('}');
+    _emitRBrace();
   }
 
   @override
@@ -317,15 +385,12 @@
     var declarations = node.declarations;
     var declarationsLength = declarations.length;
     for (var i = 0; i < declarationsLength; i++) {
-      if (i > 0) emit(_newLine);
-      emit('$_sp$_sp');
       declarations[i].visit(this);
       // Don't emit the last semicolon in compact mode.
-      if (prettyPrint || i < declarationsLength - 1) {
-        emit(';');
+      if (_prettyPrint || i < declarationsLength - 1) {
+        _emitSemicolon();
       }
     }
-    if (declarationsLength > 0) emit(_newLine);
   }
 
   @override
@@ -333,11 +398,10 @@
     var marginSymName =
         TokenKind.idToValue(TokenKind.MARGIN_DIRECTIVES, node.margin_sym);
 
-    emit('@$marginSymName$_sp{$_newLine');
-
+    emit('@$marginSymName$_sp');
+    _emitLBrace();
     visitDeclarationGroup(node);
-
-    emit('}$_newLine');
+    _emitRBrace();
   }
 
   @override
@@ -619,6 +683,7 @@
   void visitExpressions(Expressions node) {
     var expressions = node.expressions;
     var expressionsLength = expressions.length;
+
     for (var i = 0; i < expressionsLength; i++) {
       // Add space seperator between terms without an operator.
       // TODO(terry): Should have a BinaryExpression to solve this problem.
diff --git a/lib/src/tree_printer.dart b/lib/src/tree_printer.dart
index 0398226..4b6fbf8 100644
--- a/lib/src/tree_printer.dart
+++ b/lib/src/tree_printer.dart
@@ -17,6 +17,7 @@
 class _TreePrinter extends Visitor {
   final TreeOutput output;
   final bool useSpan;
+
   _TreePrinter(this.output, this.useSpan) {
     output.printer = this;
   }
diff --git a/test/compiler_test.dart b/test/compiler_test.dart
index ad0bfd3..2879040 100644
--- a/test/compiler_test.dart
+++ b/test/compiler_test.dart
@@ -653,20 +653,20 @@
   expect(errors.isEmpty, true, reason: errors.toString());
   expect(prettyPrint(stylesheet), r'''
 @host {
-:scope {
-  white-space: nowrap;
-  overflow-style: marquee-line;
-  overflow-x: marquee;
-}
-* {
-  color: #f00;
-}
-*:hover {
-  font-weight: bold;
-}
-:nth-child(odd) {
-  color: #00f;
-}
+  :scope {
+    white-space: nowrap;
+    overflow-style: marquee-line;
+    overflow-x: marquee;
+  }
+  * {
+    color: #f00;
+  }
+  *:hover {
+    font-weight: bold;
+  }
+  :nth-child(odd) {
+    color: #00f;
+  }
 }''');
 }
 
diff --git a/test/declaration_test.dart b/test/declaration_test.dart
index ba94c73..0a1b91c 100644
--- a/test/declaration_test.dart
+++ b/test/declaration_test.dart
@@ -166,7 +166,7 @@
 }
 @-webkit-keyframes pulsate {
   0% {
-  -webkit-transform: translate3d(0, 0, 0) scale(1.0);
+    -webkit-transform: translate3d(0, 0, 0) scale(1.0);
   }
 }''';
 
@@ -311,9 +311,9 @@
 
   final generated = r'''
 @media screen, print {
-.foobar_screen {
-  width: 10px;
-}
+  .foobar_screen {
+    width: 10px;
+  }
 }
 @page {
   height: 22px;
@@ -323,14 +323,14 @@
   width: 10px;
 }
 @page bar:left {
-@top-left {
-  margin: 8px;
-}
+  @top-left {
+    margin: 8px;
+  }
 }
 @page {
-@top-left {
-  margin: 8px;
-}
+  @top-left {
+    margin: 8px;
+  }
   width: 10px;
 }
 @charset "ISO-8859-1";
@@ -355,12 +355,12 @@
 }''';
   var generated = '''
 @media screen AND (-webkit-min-device-pixel-ratio:0) {
-.todo-item .toggle {
-  background: none;
-}
-#todo-item .toggle {
-  height: 40px;
-}
+  .todo-item .toggle {
+    background: none;
+  }
+  #todo-item .toggle {
+    height: 40px;
+  }
 }''';
 
   var stylesheet = parseCss(input, errors: errors, opts: simpleOptions);
@@ -386,27 +386,27 @@
       border: 20px;
     }
   }''';
-  generated =
-      '''@media handheld AND (min-width:20em), screen AND (min-width:20em) {
-#id {
-  color: #f00;
-}
-.myclass {
-  height: 20px;
-}
+  generated = '''
+@media handheld AND (min-width:20em), screen AND (min-width:20em) {
+  #id {
+    color: #f00;
+  }
+  .myclass {
+    height: 20px;
+  }
 }
 @media print AND (min-resolution:300dpi) {
-#anotherId {
-  color: #fff;
-}
+  #anotherId {
+    color: #fff;
+  }
 }
 @media print AND (min-resolution:280dpcm) {
-#finalId {
-  color: #aaa;
-}
-.class2 {
-  border: 20px;
-}
+  #finalId {
+    color: #aaa;
+  }
+  .class2 {
+    border: 20px;
+  }
 }''';
 
   stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);
@@ -421,9 +421,12 @@
         font-size: 10em;
       }
     }''';
-  generated = '@media ONLY screen AND (min-device-width:4000px) '
-      'AND (min-device-height:2000px), screen AND (another:100px) {\n'
-      'html {\n  font-size: 10em;\n}\n}';
+  generated = '''
+@media ONLY screen AND (min-device-width:4000px) AND (min-device-height:2000px), screen AND (another:100px) {
+  html {
+    font-size: 10em;
+  }
+}''';
 
   stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);
 
@@ -439,7 +442,7 @@
     }''';
   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}';
+      '  html {\n    font-size: 10em;\n  }\n}';
 
   stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);
 
@@ -482,15 +485,16 @@
     }
   }
 }''';
-  generated = '''@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);
 }
@@ -504,10 +508,11 @@
     color: #000;
   }
 }''';
-  var expected = '''@-moz-document url-prefix() {
-div {
-  color: #000;
-}
+  var expected = '''
+@-moz-document url-prefix() {
+  div {
+    color: #000;
+  }
 }''';
   var styleSheet = parseCss(css, errors: errors);
   expect(styleSheet, isNotNull);
@@ -521,10 +526,11 @@
     color: #000;
   }
 }''';
-  expected = '''@-moz-document url-prefix("http://www.w3.org/Style/") {
-div {
-  color: #000;
-}
+  expected = '''
+@-moz-document url-prefix("http://www.w3.org/Style/") {
+  div {
+    color: #000;
+  }
 }''';
   styleSheet = parseCss(css, errors: errors);
   expect(styleSheet, isNotNull);
@@ -538,10 +544,11 @@
     color: #000;
   }
 }''';
-  expected = '''@-moz-document domain("google.com") {
-div {
-  color: #000;
-}
+  expected = '''
+@-moz-document domain("google.com") {
+  div {
+    color: #000;
+  }
 }''';
   styleSheet = parseCss(css, errors: errors);
   expect(styleSheet, isNotNull);
@@ -558,7 +565,7 @@
       'url("http://www.w3.org/"), '
       'url-prefix("http://www.w3.org/Style/"), '
       'domain("google.com"), '
-      'regexp("https:.*") {\ndiv {\n  color: #000;\n}\n}';
+      'regexp("https:.*") {\n  div {\n    color: #000;\n  }\n}';
   styleSheet = parseCss(css, errors: errors);
   expect(styleSheet, isNotNull);
   expect(errors, isEmpty);
@@ -573,10 +580,11 @@
     -webkit-appearance: none;
   }
 }''';
-  var expected = '''@supports (-webkit-appearance: none) {
-div {
-  -webkit-appearance: none;
-}
+  var expected = '''
+@supports (-webkit-appearance: none) {
+  div {
+    -webkit-appearance: none;
+  }
 }''';
   expectCss(css, expected);
 
@@ -585,10 +593,11 @@
 @supports not ( display: flex ) {
   body { width: 100%; }
 }''';
-  expected = '''@supports not (display: flex) {
-body {
-  width: 100%;
-}
+  expected = '''
+@supports not (display: flex) {
+  body {
+    width: 100%;
+  }
 }''';
   expectCss(css, expected);
 
@@ -606,9 +615,9 @@
       '(-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'
+      '  .box {\n'
+      '    box-shadow: 0 0 2px #000 inset;\n'
+      '  }\n'
       '}';
   expectCss(css, expected);
 
@@ -625,10 +634,10 @@
   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'
+      '  div {\n'
+      '    transition-property: color;\n'
+      '    transform: rotate(10deg);\n'
+      '  }\n'
       '}';
   expectCss(css, expected);
 
@@ -682,7 +691,7 @@
   src: url(fonts/BBCBengali.ttf) format("opentype");
   unicode-range: U+0A-FF, U+980-9FF, U+????, U+3???;
 }''';
-  final generated = '''@font-face  {
+  final generated = '''@font-face {
   font-family: BBCBengali;
   src: url("fonts/BBCBengali.ttf") format("opentype");
   unicode-range: U+0A-FF, U+980-9FF, U+????, U+3???;
@@ -698,7 +707,7 @@
   src: url(http://example.com/fonts/Gentium.ttf);
   src: url(http://example.com/fonts/Gentium.ttf);
 }''';
-  final generated1 = '''@font-face  {
+  final generated1 = '''@font-face {
   font-family: Gentium;
   src: url("http://example.com/fonts/Gentium.ttf");
   src: url("http://example.com/fonts/Gentium.ttf");
@@ -715,7 +724,7 @@
      url(basic-sans-serif.ttf) format("opentype"),
      local(Gentium Bold);
 }''';
-  final generated2 = '@font-face  {\n'
+  final generated2 = '@font-face {\n'
       '  src: url("ideal-sans-serif.woff") '
       'format("woff"), url("basic-sans-serif.ttf") '
       'format("opentype"), local(Gentium Bold);\n}';
@@ -734,7 +743,7 @@
   font-weight: bold;
 }''';
   final generated3 = '''
-@font-face  {
+@font-face {
   font-family: MyGentium Text Ornaments;
   src: local(Gentium Bold), local(Gentium-Bold), url("GentiumBold.ttf");
   font-weight: bold;
@@ -751,7 +760,7 @@
   src: local(STIXGeneral), url(/stixfonts/STIXGeneral.otf);
   unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
 }''';
-  final generated4 = '''@font-face  {
+  final generated4 = '''@font-face {
   font-family: STIXGeneral;
   src: local(STIXGeneral), url("/stixfonts/STIXGeneral.otf");
   unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
@@ -799,33 +808,34 @@
 }
 ''';
 
-  final generated = '@import "simple.css"; '
-      '@import "test.css" print; '
-      '@import "test.css" screen, print; '
-      '@import "http://google.com/maps/maps.css";\n'
-      'div[href^="test"] {\n'
-      '  height: 10px;\n'
-      '}\n'
-      '@-webkit-keyframes pulsate {\n'
-      '  from {\n'
-      '  -webkit-transform: translate3d(0, 0, 0) scale(1.0);\n'
-      '  }\n'
-      '  10% {\n'
-      '  -webkit-transform: translate3d(0, 0, 0) scale(1.0);\n'
-      '  }\n'
-      '  30% {\n'
-      '  -webkit-transform: translate3d(0, 2, 0) scale(1.0);\n'
-      '  }\n'
-      '}\n'
-      '.foobar {\n'
-      '  grid-columns: 10px ("content" 1fr 10px) [4];\n'
-      '}\n'
-      '.test-background {\n'
-      '  background: url("http://www.foo.com/bar.png");\n'
-      '}\n'
-      '.test-background-with-multiple-properties {\n'
-      '  background: #000 url("http://www.foo.com/bar.png");\n'
-      '}';
+  final generated = '''
+@import "simple.css";
+@import "test.css" print;
+@import "test.css" screen, print;
+@import "http://google.com/maps/maps.css";
+div[href^="test"] {
+  height: 10px;
+}
+@-webkit-keyframes pulsate {
+  from {
+    -webkit-transform: translate3d(0, 0, 0) scale(1.0);
+  }
+  10% {
+    -webkit-transform: translate3d(0, 0, 0) scale(1.0);
+  }
+  30% {
+    -webkit-transform: translate3d(0, 2, 0) scale(1.0);
+  }
+}
+.foobar {
+  grid-columns: 10px ("content" 1fr 10px) [4];
+}
+.test-background {
+  background: url("http://www.foo.com/bar.png");
+}
+.test-background-with-multiple-properties {
+  background: #000 url("http://www.foo.com/bar.png");
+}''';
   var stylesheet = parseCss(input, errors: errors);
 
   expect(errors.isEmpty, true, reason: errors.toString());
@@ -865,14 +875,15 @@
   color: rgba(0, 0, 0, 0.2);
 }
 ''';
-  final generated = 'div{color:green!important;background:red blue green}'
-      '.foo p[bar]{color:blue}'
-      '@page{@top-left{color:red}}'
-      '@page:first{}'
-      '@page foo:first{}'
-      '@media screen AND (max-width:800px){div{font-size:24px}}'
-      '@keyframes foo{0%{transform:scaleX(0)}}'
-      'div{color:rgba(0,0,0,0.2)}';
+  final generated = '''
+div{color:green!important;background:red blue green}
+.foo p[bar]{color:blue}
+@page{@top-left{color:red}}
+@page:first{}
+@page foo:first{}
+@media screen AND (max-width:800px){div{font-size:24px}}
+@keyframes foo{0%{transform:scaleX(0)}}
+div{color:rgba(0,0,0,0.2)}''';
 
   var stylesheet = parseCss(input, errors: errors);
 
@@ -1145,7 +1156,8 @@
   }
 }''';
 
-  final generated = '''.testIE-6 {
+  final generated = '''
+.testIE-6 {
   _zoom: 5;
 }
 .clearfix {
@@ -1180,42 +1192,42 @@
 }
 @-webkit-keyframes progress-bar-stripes {
   from {
-  background-position: 40px 0;
+    background-position: 40px 0;
   }
   to {
-  background-position: 0 0;
+    background-position: 0 0;
   }
 }
 @-moz-keyframes progress-bar-stripes {
   from {
-  background-position: 40px 0;
+    background-position: 40px 0;
   }
   to {
-  background-position: 0 0;
+    background-position: 0 0;
   }
 }
 @keyframes progress-bar-stripes {
   from {
-  background-position: 40px 0;
+    background-position: 40px 0;
   }
   to {
-  background-position: 0 0;
+    background-position: 0 0;
   }
 }
 @-o-keyframes progress-bar-stripes {
   from {
-  background-position: 40px 0;
+    background-position: 40px 0;
   }
   to {
-  background-position: 0 0;
+    background-position: 0 0;
   }
 }
 @keyframes progress-bar-stripes {
   from {
-  background-position: 40px 0;
+    background-position: 40px 0;
   }
   to {
-  background-position: 0 0;
+    background-position: 0 0;
   }
 }''';
 
diff --git a/test/keyframes_test.dart b/test/keyframes_test.dart
index 0c9456f..3560c0d 100644
--- a/test/keyframes_test.dart
+++ b/test/keyframes_test.dart
@@ -17,8 +17,8 @@
     final expected = r'''
 @keyframes ping {
   75%, 100% {
-  transform: scale(2);
-  opacity: 0;
+    transform: scale(2);
+    opacity: 0;
   }
 }''';
     expect(prettyPrint(stylesheet), expected);
diff --git a/test/mixin_test.dart b/test/mixin_test.dart
index 0432b1c..a60674a 100644
--- a/test/mixin_test.dart
+++ b/test/mixin_test.dart
@@ -397,7 +397,6 @@
 }
 ''', r'''
 var-values: #f00, #0f0, #00f;
-
 .primary {
   color: #f00;
   background-color: #0f0;
diff --git a/test/nested_test.dart b/test/nested_test.dart
index a594969..a579868 100644
--- a/test/nested_test.dart
+++ b/test/nested_test.dart
@@ -238,7 +238,7 @@
 
 void complexNest() {
   final input = '''
-@font-face  { font-family: arial; }
+@font-face { font-family: arial; }
 div { color: #f0f0f0; }
 #header + div {
   color: url(abc.png);
@@ -276,7 +276,7 @@
 span { color: #1f1f2f; }
 ''';
 
-  final generated = r'''@font-face  {
+  final generated = r'''@font-face {
   font-family: arial;
 }
 div {
@@ -353,17 +353,18 @@
   }
 }
 ''';
-  final generated = r'''@media screen AND (-webkit-min-device-pixel-ratio:0) {
-#toggle-all {
-  image: url("test.jpb");
-  color: #f00;
-}
-#toggle-all div, #toggle-all table {
-  background: none;
-}
-#toggle-all div a, #toggle-all table a {
-  width: 100px;
-}
+  final generated = r'''
+@media screen AND (-webkit-min-device-pixel-ratio:0) {
+  #toggle-all {
+    image: url("test.jpb");
+    color: #f00;
+  }
+  #toggle-all div, #toggle-all table {
+    background: none;
+  }
+  #toggle-all div a, #toggle-all table a {
+    width: 100px;
+  }
 }''';
 
   compileAndValidate(input, generated);
diff --git a/test/testing.dart b/test/testing.dart
index d93c4a3..00e6ce7 100644
--- a/test/testing.dart
+++ b/test/testing.dart
@@ -10,16 +10,18 @@
 
 export 'package:csslib/src/preprocessor_options.dart';
 
-const simpleOptionsWithCheckedAndWarningsAsErrors = PreprocessorOptions(
-    useColors: false,
-    checked: true,
-    warningsAsErrors: true,
-    inputFile: 'memory');
+const PreprocessorOptions simpleOptionsWithCheckedAndWarningsAsErrors =
+    PreprocessorOptions(
+  useColors: false,
+  checked: true,
+  warningsAsErrors: true,
+  inputFile: 'memory',
+);
 
-const simpleOptions =
+const PreprocessorOptions simpleOptions =
     PreprocessorOptions(useColors: false, inputFile: 'memory');
 
-const options = PreprocessorOptions(
+const PreprocessorOptions options = PreprocessorOptions(
     useColors: false, warningsAsErrors: true, inputFile: 'memory');
 
 /// Spin-up CSS parser in checked mode to detect any problematic CSS.  Normally,
@@ -49,17 +51,11 @@
         {List<Message>? errors, PreprocessorOptions? opts}) =>
     compileCss(input, errors: errors, polyfill: true, opts: opts);
 
-/// CSS emitter walks the style sheet tree and emits readable CSS.
-final _emitCss = CssPrinter();
-
-/// Simple Visitor does nothing but walk tree.
-final _cssVisitor = Visitor();
-
 /// Pretty printer for CSS.
 String prettyPrint(StyleSheet ss) {
   // Walk the tree testing basic Visitor class.
   walkTree(ss);
-  return (_emitCss..visitTree(ss, pretty: true)).toString();
+  return (CssPrinter()..visitTree(ss, pretty: true)).toString();
 }
 
 /// Helper function to emit compact (non-pretty printed) CSS for suite test
@@ -67,12 +63,12 @@
 /// expected suite test results.
 String compactOutput(StyleSheet ss) {
   walkTree(ss);
-  return (_emitCss..visitTree(ss, pretty: false)).toString();
+  return (CssPrinter()..visitTree(ss, pretty: false)).toString();
 }
 
 /// Walks the style sheet tree does nothing; insures the basic walker works.
 void walkTree(StyleSheet ss) {
-  _cssVisitor.visitTree(ss);
+  Visitor().visitTree(ss);
 }
 
 String dumpTree(StyleSheet ss) => treeToDebugString(ss);
diff --git a/test/var_test.dart b/test/var_test.dart
index 079955f..482fda9 100644
--- a/test/var_test.dart
+++ b/test/var_test.dart
@@ -179,12 +179,12 @@
   content: var(content);
   text-shadow: var(text-shadow);
 }
-@font-face  {
+@font-face {
   font-family: var(font-family);
   src: var(src);
   unicode-range: var(unicode-range);
 }
-@font-face  {
+@font-face {
   font-family: var(font-family);
   src: var(src-1);
   unicode-range: var(unicode-range-1);
@@ -214,12 +214,12 @@
   content: "✔";
   text-shadow: 0 -1px 0 #bfbfbf;
 }
-@font-face  {
+@font-face {
   font-family: Gentium;
   src: url("http://example.com/fonts/Gentium.ttf");
   unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
 }
-@font-face  {
+@font-face {
   font-family: Gentium;
   src: local(Gentium Bold), local(Gentium-Bold), url("GentiumBold.ttf");
   unicode-range: U+0A-FF, U+980-9FF, U+????, U+3???;
@@ -602,12 +602,12 @@
   content: var(content);
   text-shadow: var(text-shadow);
 }
-@font-face  {
+@font-face {
   font-family: var(font-family);
   src: var(src);
   unicode-range: var(unicode-range);
 }
-@font-face  {
+@font-face {
   font-family: var(font-family);
   src: var(src-1);
   unicode-range: var(unicode-range-1);
@@ -637,12 +637,12 @@
   content: "✔";
   text-shadow: 0 -1px 0 #bfbfbf;
 }
-@font-face  {
+@font-face {
   font-family: Gentium;
   src: url("http://example.com/fonts/Gentium.ttf");
   unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
 }
-@font-face  {
+@font-face {
   font-family: Gentium;
   src: local(Gentium Bold), local(Gentium-Bold), url("GentiumBold.ttf");
   unicode-range: U+0A-FF, U+980-9FF, U+????, U+3???;
@@ -667,7 +667,6 @@
   final generated = '''
 var-color-background: #f00;
 var-color-foreground: #00f;
-
 .test {
   background-color: var(color-background);
   color: var(color-foreground);
@@ -692,7 +691,6 @@
   final generated2 = '''
 var-color-background: #f00;
 var-color-foreground: #00f;
-
 .test {
   background-color: var(color-background);
   color: var(color-foreground);
@@ -720,7 +718,6 @@
   final generated = '''
 var-color-background: #f00;
 var-color-foreground: #00f;
-
 .test {
   background-color: var(color-background);
   color: var(color-foreground);
@@ -745,7 +742,6 @@
   final generated2 = '''
 var-color-background: #f00;
 var-color-foreground: #00f;
-
 .test {
   background-color: var(color-background);
   color: var(color-foreground);
diff --git a/test/visitor_test.dart b/test/visitor_test.dart
index f845993..a70f91f 100644
--- a/test/visitor_test.dart
+++ b/test/visitor_test.dart
@@ -109,4 +109,34 @@
 void main() {
   test('Class Visitors', testClassVisitors);
   test('Polyfill', testPolyFill);
+  test('pretty-print', testPrettyPrint);
+}
+
+void testPrettyPrint() {
+  final input = '''
+.good { color: red; }
+@media screen { .better { color: blue; } }
+.best { color: green }''';
+
+  var styleSheet = parseCss(input);
+
+  // pretty print
+  expect(prettyPrint(styleSheet), '''
+.good {
+  color: #f00;
+}
+@media screen {
+  .better {
+    color: #00f;
+  }
+}
+.best {
+  color: #008000;
+}''');
+
+  // compact output
+  expect(compactOutput(styleSheet), '''
+.good{color:red}
+@media screen{.better{color:blue}}
+.best{color:green}''');
 }