diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,6 +1,21 @@
 # ED-E
 
-[![Build Status](https://img.shields.io/travis/brendanhay/ede/develop.svg?maxAge=2592000)](https://travis-ci.org/brendanhay/ede)
+[![MPL2][license-badge]][license]
+[![Build][build-badge]][build]
+[![Hackage][hackage-badge]][hackage]
+[![Nix][nix-badge]][nix]
+[![Cachix][cachix-badge]][cachix]
+
+[license]: https://opensource.org/licenses/MPL-2.0
+[license-badge]: https://img.shields.io/badge/license-MPL%202.0-blue.svg
+[build]: https://github.com/brendanhay/ede/actions
+[build-badge]: https://github.com/brendanhay/ede/workflows/build/badge.svg
+[hackage]: http://hackage.haskell.org/package/ede
+[hackage-badge]: https://img.shields.io/hackage/v/ede.svg
+[nix]: https://nixos.org
+[nix-badge]: https://img.shields.io/badge/builtwith-nix-purple.svg
+[cachix]: https://amazonka.cachix.org
+[cachix-badge]: https://img.shields.io/badge/cachix-amazonka-purple.svg
 
 * [Introduction](#introduction)
 * [Syntax](#syntax)
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -15,6 +15,8 @@
 import qualified Options.Applicative.Help.Pretty as Pretty
 import qualified System.Exit as Exit
 import qualified Text.EDE as EDE
+import qualified Prettyprinter as PP
+import qualified Prettyprinter.Render.Terminal as PP
 
 data Options = Options
   { templateFile :: FilePath,
@@ -109,10 +111,10 @@
           >>= either fail pure . Parsec.parseOnly stdinParser
 
   EDE.parseFile (templateFile options) >>= \case
-    EDE.Failure err -> print err >> Exit.exitFailure
+    EDE.Failure err -> PP.putDoc (err <> PP.hardline) >> Exit.exitFailure
     EDE.Success tpl ->
       case EDE.render tpl ctx of
-        EDE.Failure err -> print err >> Exit.exitFailure
+        EDE.Failure err -> PP.putDoc (err <> PP.hardline) >> Exit.exitFailure
         EDE.Success output -> Text.Lazy.IO.putStr output
 
 stdinParser :: Parsec.Parser Aeson.Object
diff --git a/ede.cabal b/ede.cabal
--- a/ede.cabal
+++ b/ede.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               ede
-version:            0.3.1.0
+version:            0.3.2.0
 synopsis:
   Templating language with similar syntax and features to Liquid or Jinja2.
 
@@ -94,25 +94,28 @@
   main-is:        Main.hs
   ghc-options:    -threaded -rtsopts -with-rtsopts=-A128m
   build-depends:
-    , aeson                 >=0.8
+    , aeson                        >=0.8
     , attoparsec
-    , bytestring            >=0.10.4
+    , bytestring                   >=0.10.4
     , ede
-    , optparse-applicative  >=0.11
-    , text                  >=1.2
+    , optparse-applicative         >=0.11
+    , prettyprinter                >=1.6
+    , prettyprinter-ansi-terminal  >=1.1
+    , text                         >=1.2
 
-test-suite golden
+test-suite tests
   import:         base
   type:           exitcode-stdio-1.0
   hs-source-dirs: test
   main-is:        Main.hs
-  ghc-options:    -threaded -rtsopts -with-rtsopts=-A128m
+  ghc-options:    -threaded
   build-depends:
     , aeson
     , bifunctors
     , bytestring
     , directory
     , ede
+    , filepath
     , tasty
     , tasty-golden
     , text
diff --git a/lib/Text/EDE.hs b/lib/Text/EDE.hs
--- a/lib/Text/EDE.hs
+++ b/lib/Text/EDE.hs
@@ -141,7 +141,7 @@
 version :: Version
 version = Paths.version
 
--- | Parse Lazy 'Text.Lazy.Text' into a compiled 'Template'.
+-- | Parse a 'ByteString' into a compiled 'Template'.
 --
 -- Because this function is pure and does not resolve @include@s,
 -- encountering an @include@ expression during parsing will result in an 'Error'.
@@ -154,7 +154,7 @@
   Result Template
 parse = Monad.join . parseWith defaultSyntax (includeMap mempty) "Text.EDE.parse"
 
--- | Parse 'Text' into a compiled 'Template'.
+-- | Parse a 'ByteString' into a compiled 'Template'.
 --
 -- This function handles all @include@ expressions as 'FilePath's and performs
 -- recursive loading/parsing.
diff --git a/lib/Text/EDE/Internal/Parser.hs b/lib/Text/EDE/Internal/Parser.hs
--- a/lib/Text/EDE/Internal/Parser.hs
+++ b/lib/Text/EDE/Internal/Parser.hs
@@ -31,7 +31,7 @@
 import Control.Comonad.Cofree (Cofree ((:<)))
 import Control.Lens ((%=))
 import qualified Control.Lens as Lens
-import Control.Monad (MonadPlus, void)
+import Control.Monad (MonadPlus, void, unless)
 import Control.Monad.State.Strict (MonadState, StateT)
 import qualified Control.Monad.State.Strict as State
 import Control.Monad.Trans (lift)
@@ -137,7 +137,10 @@
 pragma :: Parser m => m ()
 pragma =
   void . Trifecta.many $ do
-    !xs <- pragmal *> Trifecta.symbol "EDE_SYNTAX" *> Trifecta.sepBy field spaces <* trimr pragmar
+    !xs <- pragmal
+      *> Trifecta.symbol "EDE_SYNTAX"
+      *> Trifecta.sepBy field spaces
+      <* trimr pragmar
 
     mapM_ (uncurry Lens.assign) xs
   where
@@ -161,7 +164,7 @@
 document =
   eapp
     <$> Trifecta.position
-    <*> Trifecta.many (statement <|> inline <|> fragment)
+    <*> Trifecta.many (blankLine <|> statement <|> inline <|> fragment)
 
 inline :: Parser m => m (Exp Delta)
 inline = Trifecta.between inlinel inliner term
@@ -191,7 +194,7 @@
 block :: Parser m => String -> m a -> m a
 block k p =
   Trifecta.try (multiLine (keyword k) p)
-    <|> singleLine (keyword k) p
+  <|> singleLine (keyword k) p
 
 multiLine :: Parser m => m b -> m a -> m a
 multiLine s =
@@ -201,6 +204,15 @@
 singleLine s =
   Trifecta.between (Trifecta.try (blockl *> s)) blockr
 
+blankLine :: Parser m => m (Exp Delta)
+blankLine = do
+  c <- Trifecta.Delta.column <$> Trifecta.position
+
+  unless (c == 0) $
+    fail "expected blank line"
+
+  ann (ELit . String . Text.singleton <$> Trifecta.newline)
+
 ifelif :: Parser m => m (Exp Delta)
 ifelif =
   eif
@@ -401,13 +413,13 @@
 
 triml :: Parser m => m a -> m a
 triml p = do
-  c <- Trifecta.Delta.column <$> Trifecta.position
-  if c == 0
-    then Trifecta.spaces *> p
-    else fail "left whitespace removal failed"
+    c <- Trifecta.Delta.column <$> Trifecta.position
+    if c == 0
+        then Trifecta.spaces *> p
+        else fail "left whitespace removal failed"
 
 trimr :: Parser m => m a -> m a
-trimr p = p <* Trifecta.spaces <* Trifecta.newline
+trimr p = p <* Trifecta.newline -- <* Trifecta.newline
 
 pragmak :: Parser m => String -> m ()
 pragmak = Trifecta.reserve pragmaStyle
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 
 -- Module      : Main
--- Copyright   : (c) 2013 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
@@ -16,57 +16,52 @@
 import qualified Data.Bifunctor as Bifunctor
 import qualified Data.ByteString as ByteString
 import qualified Data.ByteString.Lazy as ByteString.Lazy
+import Data.Functor ((<&>))
 import qualified Data.List as List
 import qualified Data.Maybe as Maybe
 import qualified Data.Text as Text
 import qualified Data.Text.Lazy.Encoding as Text.Lazy.Encoding
 import qualified Paths_ede as Paths
 import qualified System.Directory as Directory
-import qualified System.IO.Unsafe as IO.Unsafe
+import System.FilePath ((-<.>), (</>))
+import qualified System.FilePath as FilePath
 import qualified Test.Tasty as Tasty
 import qualified Test.Tasty.Golden as Tasty.Golden
 import qualified Text.EDE as EDE
 
--- FIXME: migrate to tasty's resource bracketing.
 main :: IO ()
-main =
+main = do
+  resources <-
+    Paths.getDataDir <&> (</> "test/resources")
+
+  templates <-
+    map (FilePath.combine resources) . filter (List.isSuffixOf ".ede")
+      <$> Directory.listDirectory resources
+
   Tasty.defaultMain $
     Tasty.testGroup "ED-E" $
-      IO.Unsafe.unsafePerformIO tests
+      map (test resources . FilePath.normalise) templates
 
-resources :: FilePath
-resources = IO.Unsafe.unsafePerformIO Paths.getDataDir
+test :: FilePath -> FilePath -> Tasty.TestTree
+test dir path =
+  Tasty.Golden.goldenVsStringDiff path diff (path -<.> ".golden") $ do
+    (context, template) <-
+      split <$> ByteString.readFile path
 
-include :: EDE.Resolver IO
-include = EDE.includeFile resources
+    result <-
+      EDE.parseWith
+        EDE.defaultSyntax
+        (EDE.includeFile dir)
+        (Text.pack path)
+        template
 
-tests :: IO [Tasty.TestTree]
-tests = files >>= mapM test
+    EDE.result
+      (error . show)
+      (pure . Text.Lazy.Encoding.encodeUtf8)
+      (result >>= flip EDE.render context)
   where
-    files :: IO [FilePath]
-    files =
-      map (resources ++) . filter (List.isSuffixOf ".ede")
-        <$> Directory.getDirectoryContents resources
-
-    test :: FilePath -> IO Tasty.TestTree
-    test f = do
-      (bs, n) <-
-        (,)
-          <$> ByteString.readFile f
-          <*> pure (List.takeWhile (/= '.') f)
-
-      let (js, src) = split bs
-          name = Text.pack (n ++ ".ede")
-
-      pure . Tasty.Golden.goldenVsStringDiff n diff (n ++ ".golden") $ do
-        r <- EDE.parseWith EDE.defaultSyntax include name src
-
-        EDE.result
-          (error . show)
-          (pure . Text.Lazy.Encoding.encodeUtf8)
-          (r >>= (`EDE.render` js))
-
-    diff r n = ["diff", "-u", r, n]
+    diff ref new =
+      ["diff", "-u", ref, new]
 
     split =
       Bifunctor.bimap input (ByteString.drop 4)
diff --git a/test/resources/case.ede b/test/resources/case.ede
--- a/test/resources/case.ede
+++ b/test/resources/case.ede
@@ -1,4 +1,4 @@
-{ "var": "BiNGo!" }
+{ "var": "BiNGo!", "list": ["foo", "bar", "buck"] }
 ---
     {% case var %}
     {% when "other" %}
@@ -15,3 +15,14 @@
 {% else %}
 El fino.
 {% endcase %}
+
+  {% for item in list %}
+    {% case item.value %}
+    {% when "foo" %}
+      FOO
+    {% when "bar" %}
+        BAR
+    {% else %}
+    BAZ
+    {% endcase %}
+  {% endfor %}
diff --git a/test/resources/case.golden b/test/resources/case.golden
--- a/test/resources/case.golden
+++ b/test/resources/case.golden
@@ -1,3 +1,7 @@
       "BiNGo!"
 
 El fino.
+
+      FOO
+        BAR
+    BAZ
