diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## Stache 2.2.0
+
+* Added the executable program.
+
 ## Stache 2.1.1
 
 * Fixed the bug related to incorrect indentation with nested partials.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 [![Hackage](https://img.shields.io/hackage/v/stache.svg?style=flat)](https://hackage.haskell.org/package/stache)
 [![Stackage Nightly](http://stackage.org/package/stache/badge/nightly)](http://stackage.org/nightly/package/stache)
 [![Stackage LTS](http://stackage.org/package/stache/badge/lts)](http://stackage.org/lts/package/stache)
-[![Build Status](https://travis-ci.org/stackbuilders/stache.svg?branch=master)](https://travis-ci.org/stackbuilders/stache)
+![CI](https://github.com/stackbuilders/stache/workflows/CI/badge.svg?branch=master)
 
 This is a Haskell implementation of Mustache templates. The implementation
 conforms to the version 1.1.3 of the official [Mustache
diff --git a/Text/Mustache.hs b/Text/Mustache.hs
--- a/Text/Mustache.hs
+++ b/Text/Mustache.hs
@@ -70,24 +70,26 @@
 --     * The manual: <https://mustache.github.io/mustache.5.html>
 --     * The specification: <https://github.com/mustache/spec>
 --     * Stack Builders Stache tutorial: <https://www.stackbuilders.com/tutorials/haskell/mustache-templates/>
-
 module Text.Mustache
   ( -- * Types
-    Template (..)
-  , Node (..)
-  , Key (..)
-  , PName (..)
-  , MustacheException (..)
-  , MustacheWarning (..)
-  , displayMustacheWarning
+    Template (..),
+    Node (..),
+    Key (..),
+    PName (..),
+    MustacheException (..),
+    MustacheWarning (..),
+    displayMustacheWarning,
+
     -- * Compiling
-  , compileMustacheDir
-  , compileMustacheDir'
-  , compileMustacheFile
-  , compileMustacheText
+    compileMustacheDir,
+    compileMustacheDir',
+    compileMustacheFile,
+    compileMustacheText,
+
     -- * Rendering
-  , renderMustache
-  , renderMustacheW )
+    renderMustache,
+    renderMustacheW,
+  )
 where
 
 import Text.Mustache.Compile
diff --git a/Text/Mustache/Compile.hs b/Text/Mustache/Compile.hs
--- a/Text/Mustache/Compile.hs
+++ b/Text/Mustache/Compile.hs
@@ -10,29 +10,29 @@
 -- Mustache 'Template' creation from file or a 'Text' value. You don't
 -- usually need to import the module, because "Text.Mustache" re-exports
 -- everything you may need, import that module instead.
-
 module Text.Mustache.Compile
-  ( compileMustacheDir
-  , compileMustacheDir'
-  , getMustacheFilesInDir
-  , getMustacheFilesInDir'
-  , isMustacheFile
-  , compileMustacheFile
-  , compileMustacheText )
+  ( compileMustacheDir,
+    compileMustacheDir',
+    getMustacheFilesInDir,
+    getMustacheFilesInDir',
+    isMustacheFile,
+    compileMustacheFile,
+    compileMustacheText,
+  )
 where
 
 import Control.Exception
 import Control.Monad.Except
+import qualified Data.Map as M
 import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
 import Data.Void
 import System.Directory
+import qualified System.FilePath as F
 import Text.Megaparsec
 import Text.Mustache.Parser
 import Text.Mustache.Type
-import qualified Data.Map        as M
-import qualified Data.Text       as T
-import qualified Data.Text.IO    as T
-import qualified System.FilePath as F
 
 -- | Compile all templates in specified directory and select one. Template
 -- files should have the extension @mustache@, (e.g. @foo.mustache@) to be
@@ -47,28 +47,35 @@
 -- 'getDirectoryContents', and 'T.readFile'.
 --
 -- > compileMustacheDir = complieMustacheDir' isMustacheFile
-
-compileMustacheDir :: MonadIO m
-  => PName             -- ^ Which template to select after compiling
-  -> FilePath          -- ^ Directory with templates
-  -> m Template        -- ^ The resulting template
+compileMustacheDir ::
+  MonadIO m =>
+  -- | Which template to select after compiling
+  PName ->
+  -- | Directory with templates
+  FilePath ->
+  -- | The resulting template
+  m Template
 compileMustacheDir = compileMustacheDir' isMustacheFile
 
 -- | The same as 'compileMustacheDir', but allows using a custom predicate
 -- for template selection.
 --
 -- @since 1.2.0
-
-compileMustacheDir' :: MonadIO m
-  => (FilePath -> Bool) -- ^ Template selection predicate
-  -> PName             -- ^ Which template to select after compiling
-  -> FilePath          -- ^ Directory with templates
-  -> m Template        -- ^ The resulting template
+compileMustacheDir' ::
+  MonadIO m =>
+  -- | Template selection predicate
+  (FilePath -> Bool) ->
+  -- | Which template to select after compiling
+  PName ->
+  -- | Directory with templates
+  FilePath ->
+  -- | The resulting template
+  m Template
 compileMustacheDir' predicate pname path =
-  getMustacheFilesInDir' predicate path >>=
-  fmap selectKey . foldM f (Template undefined M.empty)
+  getMustacheFilesInDir' predicate path
+    >>= fmap selectKey . foldM f (Template undefined M.empty)
   where
-    selectKey t = t { templateActual = pname }
+    selectKey t = t {templateActual = pname}
     f (Template _ old) fp = do
       Template _ new <- compileMustacheFile fp
       return (Template undefined (M.union new old))
@@ -77,32 +84,35 @@
 -- are absolute.
 --
 -- @since 0.2.2
-
-getMustacheFilesInDir :: MonadIO m
-  => FilePath          -- ^ Directory with templates
-  -> m [FilePath]
+getMustacheFilesInDir ::
+  MonadIO m =>
+  -- | Directory with templates
+  FilePath ->
+  m [FilePath]
 getMustacheFilesInDir = getMustacheFilesInDir' isMustacheFile
 
 -- | Return a list of templates found via a predicate in given directory.
 -- The returned paths are absolute.
 --
 -- @since 1.2.0
-
-getMustacheFilesInDir' :: MonadIO m
-  => (FilePath -> Bool) -- ^ Mustache file selection predicate
-  -> FilePath          -- ^ Directory with templates
-  -> m [FilePath]
-getMustacheFilesInDir' predicate path = liftIO $
-  getDirectoryContents path >>=
-  filterM f . fmap (F.combine path) >>=
-  mapM makeAbsolute
+getMustacheFilesInDir' ::
+  MonadIO m =>
+  -- | Mustache file selection predicate
+  (FilePath -> Bool) ->
+  -- | Directory with templates
+  FilePath ->
+  m [FilePath]
+getMustacheFilesInDir' predicate path =
+  liftIO $
+    getDirectoryContents path
+      >>= filterM f . fmap (F.combine path)
+      >>= mapM makeAbsolute
   where
     f p = (&& predicate p) <$> doesFileExist p
 
 -- | The default Mustache file predicate.
 --
 -- @since 1.2.0
-
 isMustacheFile :: FilePath -> Bool
 isMustacheFile path = F.takeExtension path == ".mustache"
 
@@ -110,10 +120,11 @@
 --
 -- The action can throw 'MustacheParserException' and the same exceptions as
 -- 'T.readFile'.
-
-compileMustacheFile :: MonadIO m
-  => FilePath          -- ^ Location of the file
-  -> m Template
+compileMustacheFile ::
+  MonadIO m =>
+  -- | Location of the file
+  FilePath ->
+  m Template
 compileMustacheFile path = liftIO $ do
   input <- T.readFile path
   withException (compile input)
@@ -123,11 +134,13 @@
 
 -- | Compile Mustache template from a lazy 'Text' value. The cache will
 -- contain only this template named according to given 'PName'.
-
-compileMustacheText
-  :: PName             -- ^ How to name the template?
-  -> Text              -- ^ The template to compile
-  -> Either (ParseErrorBundle Text Void) Template -- ^ The result
+compileMustacheText ::
+  -- | How to name the template?
+  PName ->
+  -- | The template to compile
+  Text ->
+  -- | The result
+  Either (ParseErrorBundle Text Void) Template
 compileMustacheText pname txt =
   Template pname . M.singleton pname <$> parseMustache "" txt
 
@@ -135,14 +148,14 @@
 -- Helpers
 
 -- | Build a 'PName' from given 'FilePath'.
-
 pathToPName :: FilePath -> PName
 pathToPName = PName . T.pack . F.takeBaseName
 
 -- | Throw 'MustacheException' if argument is 'Left' or return the result
 -- inside 'Right'.
-
-withException
-  :: Either (ParseErrorBundle Text Void) Template -- ^ Value to process
-  -> IO Template       -- ^ The result
+withException ::
+  -- | Value to process
+  Either (ParseErrorBundle Text Void) Template ->
+  -- | The result
+  IO Template
 withException = either (throwIO . MustacheParserException) return
diff --git a/Text/Mustache/Compile/TH.hs b/Text/Mustache/Compile/TH.hs
--- a/Text/Mustache/Compile/TH.hs
+++ b/Text/Mustache/Compile/TH.hs
@@ -1,3 +1,7 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
 -- |
 -- Module      :  Text.Mustache.Compile.TH
 -- Copyright   :  © 2016–present Stack Builders
@@ -13,28 +17,24 @@
 --
 -- At the moment, functions in this module only work with GHC 8 (they
 -- require at least @template-haskell-2.11@).
-
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes        #-}
-{-# LANGUAGE TemplateHaskell   #-}
-
 module Text.Mustache.Compile.TH
-  ( compileMustacheDir
-  , compileMustacheDir'
-  , compileMustacheFile
-  , compileMustacheText
-  , mustache )
+  ( compileMustacheDir,
+    compileMustacheDir',
+    compileMustacheFile,
+    compileMustacheText,
+    mustache,
+  )
 where
 
 import Control.Exception
 import Data.Text (Text)
+import qualified Data.Text as T
 import Language.Haskell.TH hiding (Dec)
 import Language.Haskell.TH.Quote (QuasiQuoter (..))
-import Language.Haskell.TH.Syntax (lift, addDependentFile)
+import Language.Haskell.TH.Syntax (addDependentFile, lift)
 import System.Directory
-import Text.Mustache.Type
-import qualified Data.Text             as T
 import qualified Text.Mustache.Compile as C
+import Text.Mustache.Type
 
 -- | Compile all templates in specified directory and select one. Template
 -- files should have the extension @mustache@, (e.g. @foo.mustache@) to be
@@ -43,11 +43,13 @@
 -- This version compiles the templates at compile time.
 --
 -- > compileMustacheDir = compileMustacheDir' isMustacheFile
-
-compileMustacheDir
-  :: PName             -- ^ Which template to select after compiling
-  -> FilePath          -- ^ Directory with templates
-  -> Q Exp             -- ^ The resulting template
+compileMustacheDir ::
+  -- | Which template to select after compiling
+  PName ->
+  -- | Directory with templates
+  FilePath ->
+  -- | The resulting template
+  Q Exp
 compileMustacheDir = compileMustacheDir' C.isMustacheFile
 
 -- | The same as 'compileMustacheDir', but allows using a custom predicate
@@ -56,12 +58,15 @@
 -- This version compiles the templates at compile time.
 --
 -- @since 1.2.0
-
-compileMustacheDir'
-  :: (FilePath -> Bool) -- ^ Template selection predicate
-  -> PName             -- ^ Which template to select after compiling
-  -> FilePath          -- ^ Directory with templates
-  -> Q Exp             -- ^ The resulting template
+compileMustacheDir' ::
+  -- | Template selection predicate
+  (FilePath -> Bool) ->
+  -- | Which template to select after compiling
+  PName ->
+  -- | Directory with templates
+  FilePath ->
+  -- | The resulting template
+  Q Exp
 compileMustacheDir' predicate pname path = do
   runIO (C.getMustacheFilesInDir' predicate path) >>= mapM_ addDependentFile
   (runIO . try) (C.compileMustacheDir' predicate pname path) >>= handleEither
@@ -69,10 +74,10 @@
 -- | Compile single Mustache template and select it.
 --
 -- This version compiles the template at compile time.
-
-compileMustacheFile
-  :: FilePath          -- ^ Location of the file
-  -> Q Exp
+compileMustacheFile ::
+  -- | Location of the file
+  FilePath ->
+  Q Exp
 compileMustacheFile path = do
   runIO (makeAbsolute path) >>= addDependentFile
   (runIO . try) (C.compileMustacheFile path) >>= handleEither
@@ -81,14 +86,15 @@
 -- only this template named according to given 'Key'.
 --
 -- This version compiles the template at compile time.
-
-compileMustacheText
-  :: PName             -- ^ How to name the template?
-  -> Text              -- ^ The template to compile
-  -> Q Exp
+compileMustacheText ::
+  -- | How to name the template?
+  PName ->
+  -- | The template to compile
+  Text ->
+  Q Exp
 compileMustacheText pname text =
   (handleEither . either (Left . MustacheParserException) Right)
-  (C.compileMustacheText pname text)
+    (C.compileMustacheText pname text)
 
 -- | Compile Mustache using QuasiQuoter. Usage:
 --
@@ -103,17 +109,17 @@
 -- with partials as usual.
 --
 -- @since 0.1.7
-
 mustache :: QuasiQuoter
-mustache = QuasiQuoter
-  { quoteExp  = compileMustacheText "quasi-quoted" . T.pack
-  , quotePat  = error "This usage is not supported."
-  , quoteType = error "This usage is not supported."
-  , quoteDec  = error "This usage is not supported." }
+mustache =
+  QuasiQuoter
+    { quoteExp = compileMustacheText "quasi-quoted" . T.pack,
+      quotePat = error "This usage is not supported.",
+      quoteType = error "This usage is not supported.",
+      quoteDec = error "This usage is not supported."
+    }
 
 -- | Given an 'Either' result return 'Right' and signal pretty-printed error
 -- if we have a 'Left'.
-
 handleEither :: Either MustacheException Template -> Q Exp
 handleEither val =
   case val of
@@ -126,5 +132,5 @@
     -- tools to parse the errors correctly.
     indentNicely x' =
       case lines x' of
-        []     -> ""
-        (x:xs) -> unlines (x : fmap (replicate 8 ' ' ++) xs)
+        [] -> ""
+        (x : xs) -> unlines (x : fmap (replicate 8 ' ' ++) xs)
diff --git a/Text/Mustache/Parser.hs b/Text/Mustache/Parser.hs
--- a/Text/Mustache/Parser.hs
+++ b/Text/Mustache/Parser.hs
@@ -1,3 +1,6 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
+
 -- |
 -- Module      :  Text.Mustache.Parser
 -- Copyright   :  © 2016–present Stack Builders
@@ -10,25 +13,22 @@
 -- Megaparsec parser for Mustache templates. You don't usually need to
 -- import the module, because "Text.Mustache" re-exports everything you may
 -- need, import that module instead.
-
-{-# LANGUAGE CPP               #-}
-{-# LANGUAGE OverloadedStrings #-}
-
 module Text.Mustache.Parser
-  ( parseMustache )
+  ( parseMustache,
+  )
 where
 
 import Control.Monad
 import Control.Monad.State.Strict
-import Data.Char (isSpace, isAlphaNum)
+import Data.Char (isAlphaNum, isSpace)
 import Data.Maybe (catMaybes)
 import Data.Text (Text)
+import qualified Data.Text as T
 import Data.Void
 import Text.Megaparsec
 import Text.Megaparsec.Char
-import Text.Mustache.Type
-import qualified Data.Text                  as T
 import qualified Text.Megaparsec.Char.Lexer as L
+import Text.Mustache.Type
 
 #if !MIN_VERSION_base(4,13,0)
 import Data.Semigroup ((<>))
@@ -38,31 +38,32 @@
 -- Parser
 
 -- | Parse a given Mustache template.
-
-parseMustache
-  :: FilePath
-     -- ^ Location of the file to parse
-  -> Text
-     -- ^ File contents (Mustache template)
-  -> Either (ParseErrorBundle Text Void) [Node]
-     -- ^ Parsed nodes or parse error
-parseMustache = parse $
-  evalStateT (pMustache eof) (St "{{" "}}" 0)
+parseMustache ::
+  -- | Location of the file to parse
+  FilePath ->
+  -- | File contents (Mustache template)
+  Text ->
+  -- | Parsed nodes or parse error
+  Either (ParseErrorBundle Text Void) [Node]
+parseMustache =
+  parse $
+    evalStateT (pMustache eof) (St "{{" "}}" 0)
 
 pMustache :: Parser () -> Parser [Node]
 pMustache = fmap catMaybes . manyTill (choice alts)
   where
     alts =
-      [ Nothing <$  withStandalone pComment
-      , Just    <$> pSection "#" Section
-      , Just    <$> pSection "^" InvertedSection
-      , Just    <$> pStandalone (pPartial Just)
-      , Just    <$> pPartial (const Nothing)
-      , Nothing <$  withStandalone pSetDelimiters
-      , Just    <$> pUnescapedVariable
-      , Just    <$> pUnescapedSpecial
-      , Just    <$> pEscapedVariable
-      , Just    <$> pTextBlock ]
+      [ Nothing <$ withStandalone pComment,
+        Just <$> pSection "#" Section,
+        Just <$> pSection "^" InvertedSection,
+        Just <$> pStandalone (pPartial Just),
+        Just <$> pPartial (const Nothing),
+        Nothing <$ withStandalone pSetDelimiters,
+        Just <$> pUnescapedVariable,
+        Just <$> pUnescapedSpecial,
+        Just <$> pEscapedVariable,
+        Just <$> pTextBlock
+      ]
 {-# INLINE pMustache #-}
 
 pTextBlock :: Parser Node
@@ -85,14 +86,14 @@
 pUnescapedSpecial :: Parser Node
 pUnescapedSpecial = do
   start <- gets openingDel
-  end   <- gets closingDel
+  end <- gets closingDel
   between (symbol $ start <> "{") (string $ "}" <> end) $
     UnescapedVar <$> pKey
 {-# INLINE pUnescapedSpecial #-}
 
 pSection :: Text -> (Key -> [Node] -> Node) -> Parser Node
 pSection suffix f = do
-  key   <- withStandalone (pTag suffix)
+  key <- withStandalone (pTag suffix)
   nodes <- (pMustache . withStandalone . pClosingTag) key
   return (f key nodes)
 {-# INLINE pSection #-}
@@ -108,7 +109,7 @@
 pComment :: Parser ()
 pComment = void $ do
   start <- gets openingDel
-  end   <- gets closingDel
+  end <- gets closingDel
   (void . symbol) (start <> "!")
   manyTill (anySingle <?> "character") (string end)
 {-# INLINE pComment #-}
@@ -116,14 +117,16 @@
 pSetDelimiters :: Parser ()
 pSetDelimiters = void $ do
   start <- gets openingDel
-  end   <- gets closingDel
+  end <- gets closingDel
   (void . symbol) (start <> "=")
   start' <- pDelimiter <* scn
-  end'   <- pDelimiter <* scn
+  end' <- pDelimiter <* scn
   (void . string) ("=" <> end)
-  modify' $ \st -> st { openingDel = start'
-                      , closingDel = end'
-                      }
+  modify' $ \st ->
+    st
+      { openingDel = start',
+        closingDel = end'
+      }
 {-# INLINE pSetDelimiters #-}
 
 pEscapedVariable :: Parser Node
@@ -141,14 +144,14 @@
 pTag :: Text -> Parser Key
 pTag suffix = do
   start <- gets openingDel
-  end   <- gets closingDel
+  end <- gets closingDel
   between (symbol $ start <> suffix) (string end) pKey
 {-# INLINE pTag #-}
 
 pClosingTag :: Key -> Parser ()
 pClosingTag key = do
   start <- gets openingDel
-  end   <- gets closingDel
+  end <- gets closingDel
   let str = keyToText key
   void $ between (symbol $ start <> "/") (string end) (symbol str)
 {-# INLINE pClosingTag #-}
@@ -157,19 +160,20 @@
 pKey = (fmap Key . lexeme . label "key") (implicit <|> other)
   where
     implicit = [] <$ char '.'
-    other    = sepBy1 (takeWhile1P (Just lbl) f) (char '.')
-    lbl      = "alphanumeric char or '-' or '_'"
-    f x      = isAlphaNum x || x == '-' || x == '_'
+    other = sepBy1 (takeWhile1P (Just lbl) f) (char '.')
+    lbl = "alphanumeric char or '-' or '_'"
+    f x = isAlphaNum x || x == '-' || x == '_'
 {-# INLINE pKey #-}
 
 pDelimiter :: Parser Text
 pDelimiter = takeWhile1P (Just "delimiter char") delChar <?> "delimiter"
-  where delChar x = not (isSpace x) && x /= '='
+  where
+    delChar x = not (isSpace x) && x /= '='
 {-# INLINE pDelimiter #-}
 
 pBol :: Parser ()
 pBol = do
-  o  <- getOffset
+  o <- getOffset
   o' <- gets newlineOffset
   unless (o == o') empty
 {-# INLINE pBol #-}
@@ -178,18 +182,16 @@
 -- Auxiliary types
 
 -- | Type of Mustache parser monad stack.
-
 type Parser = StateT St (Parsec Void Text)
 
 -- | State used in the parser.
-
 data St = St
-  { openingDel :: Text
-    -- ^ Opening delimiter
-  , closingDel :: Text
-    -- ^ Closing delimiter
-  , newlineOffset :: !Int
-    -- ^ The offset at which last newline character was parsed
+  { -- | Opening delimiter
+    openingDel :: Text,
+    -- | Closing delimiter
+    closingDel :: Text,
+    -- | The offset at which last newline character was parsed
+    newlineOffset :: !Int
   }
 
 ----------------------------------------------------------------------------
@@ -222,6 +224,6 @@
 eol' = do
   x <- eol
   o <- getOffset
-  modify' (\st -> st { newlineOffset = o } )
+  modify' (\st -> st {newlineOffset = o})
   return x
 {-# INLINE eol' #-}
diff --git a/Text/Mustache/Render.hs b/Text/Mustache/Render.hs
--- a/Text/Mustache/Render.hs
+++ b/Text/Mustache/Render.hs
@@ -1,3 +1,6 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
+
 -- |
 -- Module      :  Text.Mustache.Render
 -- Copyright   :  © 2016–present Stack Builders
@@ -10,32 +13,29 @@
 -- Functions for rendering Mustache templates. You don't usually need to
 -- import the module, because "Text.Mustache" re-exports everything you may
 -- need, import that module instead.
-
-{-# LANGUAGE CPP               #-}
-{-# LANGUAGE OverloadedStrings #-}
-
 module Text.Mustache.Render
-  ( renderMustache
-  , renderMustacheW )
+  ( renderMustache,
+    renderMustacheW,
+  )
 where
 
 import Control.Monad.Reader
-import Control.Monad.State.Strict (State, modify', execState)
+import Control.Monad.State.Strict (State, execState, modify')
 import Data.Aeson
 import Data.Foldable (asum)
+import qualified Data.HashMap.Strict as H
 import Data.List (tails)
 import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Map as M
 import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+import qualified Data.Text.Lazy.Builder as B
+import qualified Data.Text.Lazy.Encoding as TL
+import qualified Data.Vector as V
 import Text.Megaparsec.Pos (Pos, mkPos, unPos)
 import Text.Mustache.Type
-import qualified Data.HashMap.Strict     as H
-import qualified Data.List.NonEmpty      as NE
-import qualified Data.Map                as M
-import qualified Data.Text               as T
-import qualified Data.Text.Lazy          as TL
-import qualified Data.Text.Lazy.Builder  as B
-import qualified Data.Text.Lazy.Encoding as TL
-import qualified Data.Vector             as V
 
 #if !MIN_VERSION_base(4,13,0)
 import Data.Semigroup ((<>))
@@ -47,19 +47,22 @@
 -- | Synonym for the monad we use for rendering. It allows to share context
 -- and accumulate the result as 'B.Builder' data which is then turned into a
 -- lazy 'TL.Text'.
-
 type Render a = ReaderT RenderContext (State S) a
 
 data S = S ([MustacheWarning] -> [MustacheWarning]) B.Builder
 
 -- | The render monad context.
-
 data RenderContext = RenderContext
-  { rcIndent   :: Maybe Pos      -- ^ Actual indentation level
-  , rcContext  :: NonEmpty Value -- ^ The context stack
-  , rcPrefix   :: Key            -- ^ Prefix accumulated by entering sections
-  , rcTemplate :: Template       -- ^ The template to render
-  , rcLastNode :: Bool           -- ^ Is this last node in this partial?
+  { -- | Actual indentation level
+    rcIndent :: Maybe Pos,
+    -- | The context stack
+    rcContext :: NonEmpty Value,
+    -- | Prefix accumulated by entering sections
+    rcPrefix :: Key,
+    -- | The template to render
+    rcTemplate :: Template,
+    -- | Is this last node in this partial?
+    rcLastNode :: Bool
   }
 
 ----------------------------------------------------------------------------
@@ -67,20 +70,17 @@
 
 -- | Render a Mustache 'Template' using Aeson's 'Value' to get actual values
 -- for interpolation.
-
 renderMustache :: Template -> Value -> TL.Text
 renderMustache t = snd . renderMustacheW t
 
 -- | Like 'renderMustache', but also returns a collection of warnings.
 --
 -- @since 1.1.1
-
 renderMustacheW :: Template -> Value -> ([MustacheWarning], TL.Text)
 renderMustacheW t =
   runRender (renderPartial (templateActual t) Nothing renderNode) t
 
 -- | Render a single 'Node'.
-
 renderNode :: Node -> Render ()
 renderNode (TextBlock txt) = outputIndented txt
 renderNode (EscapedVar k) =
@@ -109,33 +109,31 @@
 
 -- | Run 'Render' monad given template to render and a 'Value' to take
 -- values from.
-
 runRender :: Render a -> Template -> Value -> ([MustacheWarning], TL.Text)
 runRender m t v = (ws [], B.toLazyText b)
   where
     S ws b = execState (runReaderT m rc) (S id mempty)
-    rc = RenderContext
-      { rcIndent   = Nothing
-      , rcContext  = v :| []
-      , rcPrefix   = mempty
-      , rcTemplate = t
-      , rcLastNode = True }
+    rc =
+      RenderContext
+        { rcIndent = Nothing,
+          rcContext = v :| [],
+          rcPrefix = mempty,
+          rcTemplate = t,
+          rcLastNode = True
+        }
 {-# INLINE runRender #-}
 
 -- | Output a piece of strict 'Text'.
-
 outputRaw :: Text -> Render ()
 outputRaw = tellBuilder . B.fromText
 {-# INLINE outputRaw #-}
 
 -- | Output indentation consisting of appropriate number of spaces.
-
 outputIndent :: Render ()
 outputIndent = asks rcIndent >>= outputRaw . buildIndent
 {-# INLINE outputIndent #-}
 
 -- | Output piece of strict 'Text' with added indentation.
-
 outputIndented :: Text -> Render ()
 outputIndented txt = do
   level <- asks rcIndent
@@ -147,24 +145,27 @@
 {-# INLINE outputIndented #-}
 
 -- | Render a partial.
-
-renderPartial
-  :: PName             -- ^ Name of partial to render
-  -> Maybe Pos         -- ^ Indentation level to use
-  -> (Node -> Render ()) -- ^ How to render nodes in that partial
-  -> Render ()
+renderPartial ::
+  -- | Name of partial to render
+  PName ->
+  -- | Indentation level to use
+  Maybe Pos ->
+  -- | How to render nodes in that partial
+  (Node -> Render ()) ->
+  Render ()
 renderPartial pname i f =
   local u (outputIndent >> getNodes >>= renderMany f)
   where
-    u rc = rc
-      { rcIndent   = addIndents i (rcIndent rc)
-      , rcPrefix   = mempty
-      , rcTemplate = (rcTemplate rc) { templateActual = pname }
-      , rcLastNode = True }
+    u rc =
+      rc
+        { rcIndent = addIndents i (rcIndent rc),
+          rcPrefix = mempty,
+          rcTemplate = (rcTemplate rc) {templateActual = pname},
+          rcLastNode = True
+        }
 {-# INLINE renderPartial #-}
 
 -- | Get collection of 'Node's for actual template.
-
 getNodes :: Render [Node]
 getNodes = do
   Template actual cache <- asks rcTemplate
@@ -172,21 +173,21 @@
 {-# INLINE getNodes #-}
 
 -- | Render many nodes.
-
-renderMany
-  :: (Node -> Render ()) -- ^ How to render a node
-  -> [Node]            -- ^ The collection of nodes to render
-  -> Render ()
+renderMany ::
+  -- | How to render a node
+  (Node -> Render ()) ->
+  -- | The collection of nodes to render
+  [Node] ->
+  Render ()
 renderMany _ [] = return ()
 renderMany f [n] = do
   ln <- asks rcLastNode
-  local (\rc -> rc { rcLastNode = ln && rcLastNode rc }) (f n)
-renderMany f (n:ns) = do
-  local (\rc -> rc { rcLastNode = False }) (f n)
+  local (\rc -> rc {rcLastNode = ln && rcLastNode rc}) (f n)
+renderMany f (n : ns) = do
+  local (\rc -> rc {rcLastNode = False}) (f n)
   renderMany f ns
 
 -- | Lookup a 'Value' by its 'Key'.
-
 lookupKey :: Key -> Render Value
 lookupKey (Key []) = NE.head <$> asks rcContext
 lookupKey k = do
@@ -201,85 +202,79 @@
 
 -- | Lookup a 'Value' by traversing another 'Value' using given 'Key' as
 -- “path”.
-
-simpleLookup
-  :: Bool
-     -- ^ At least one part of the path matched, in this case we are
-     -- “committed” to this lookup and cannot say “there is nothing, try
-     -- other level”. This is necessary to pass the “Dotted Names—Context
-     -- Precedence” test from the “interpolation.yml” spec.
-  -> Key               -- ^ The key to lookup
-  -> Value             -- ^ Source value
-  -> Maybe Value       -- ^ Looked-up value
-simpleLookup _ (Key [])     obj        = return obj
-simpleLookup c (Key (k:ks)) (Object m) =
+simpleLookup ::
+  -- | At least one part of the path matched, in this case we are
+  -- “committed” to this lookup and cannot say “there is nothing, try
+  -- other level”. This is necessary to pass the “Dotted Names—Context
+  -- Precedence” test from the “interpolation.yml” spec.
+  Bool ->
+  -- | The key to lookup
+  Key ->
+  -- | Source value
+  Value ->
+  -- | Looked-up value
+  Maybe Value
+simpleLookup _ (Key []) obj = return obj
+simpleLookup c (Key (k : ks)) (Object m) =
   case H.lookup k m of
     Nothing -> if c then Just Null else Nothing
-    Just  v -> simpleLookup True (Key ks) v
+    Just v -> simpleLookup True (Key ks) v
 simpleLookup _ _ _ = Nothing
 {-# INLINE simpleLookup #-}
 
 -- | Enter the section by adding given 'Key' prefix to current prefix.
-
 enterSection :: Key -> Render a -> Render a
 enterSection p =
-  local (\rc -> rc { rcPrefix = p <> rcPrefix rc })
+  local (\rc -> rc {rcPrefix = p <> rcPrefix rc})
 {-# INLINE enterSection #-}
 
 -- | Add new value on the top of context. The new value has the highest
 -- priority when lookup takes place.
-
 addToLocalContext :: Value -> Render a -> Render a
 addToLocalContext v =
-  local (\rc -> rc { rcContext = NE.cons v (rcContext rc) })
+  local (\rc -> rc {rcContext = NE.cons v (rcContext rc)})
 {-# INLINE addToLocalContext #-}
 
 ----------------------------------------------------------------------------
 -- Helpers
 
 -- | Register a warning.
-
 tellWarning :: MustacheWarning -> Render ()
-tellWarning w = modify' $ \(S ws b) -> S (ws . (w:)) b
+tellWarning w = modify' $ \(S ws b) -> S (ws . (w :)) b
 
 -- | Register a piece of output.
-
 tellBuilder :: B.Builder -> Render ()
 tellBuilder b' = modify' $ \(S ws b) -> S ws (b <> b')
 
 -- | Add two @'Maybe' 'Pos'@ values together.
-
 addIndents :: Maybe Pos -> Maybe Pos -> Maybe Pos
-addIndents Nothing  Nothing  = Nothing
-addIndents Nothing  (Just x) = Just x
-addIndents (Just x) Nothing  = Just x
+addIndents Nothing Nothing = Nothing
+addIndents Nothing (Just x) = Just x
+addIndents (Just x) Nothing = Just x
 addIndents (Just x) (Just y) = Just (mkPos $ unPos x + unPos y - 1)
 {-# INLINE addIndents #-}
 
 -- | Build indentation of specified length by repeating the space character.
-
 buildIndent :: Maybe Pos -> Text
 buildIndent Nothing = ""
 buildIndent (Just p) = let n = fromIntegral (unPos p) - 1 in T.replicate n " "
 {-# INLINE buildIndent #-}
 
 -- | Select invisible values.
-
 isBlank :: Value -> Bool
-isBlank Null         = True
+isBlank Null = True
 isBlank (Bool False) = True
-isBlank (Object   m) = H.null m
-isBlank (Array    a) = V.null a
-isBlank (String   s) = T.null s
-isBlank _            = False
+isBlank (Object m) = H.null m
+isBlank (Array a) = V.null a
+isBlank (String s) = T.null s
+isBlank _ = False
 {-# INLINE isBlank #-}
 
 -- | Render Aeson's 'Value' /without/ HTML escaping.
-
 renderValue :: Key -> Value -> Render Text
 renderValue k v =
   case v of
-    Null       -> return ""
+    Null -> return ""
     String str -> return str
     Object _ -> do
       tellWarning (MustacheDirectlyRenderedValue k)
@@ -293,12 +288,15 @@
 {-# INLINE renderValue #-}
 
 -- | Escape HTML represented as strict 'Text'.
-
 escapeHtml :: Text -> Text
-escapeHtml txt = foldr (uncurry T.replace) txt
-  [ ("\"", "&quot;")
-  , ("'",  "&#39;")
-  , ("<",  "&lt;")
-  , (">",  "&gt;")
-  , ("&",  "&amp;") ]
+escapeHtml txt =
+  foldr
+    (uncurry T.replace)
+    txt
+    [ ("\"", "&quot;"),
+      ("'", "&#39;"),
+      ("<", "&lt;"),
+      (">", "&gt;"),
+      ("&", "&amp;")
+    ]
 {-# INLINE escapeHtml #-}
diff --git a/Text/Mustache/Type.hs b/Text/Mustache/Type.hs
--- a/Text/Mustache/Type.hs
+++ b/Text/Mustache/Type.hs
@@ -1,3 +1,10 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
 -- |
 -- Module      :  Text.Mustache.Type
 -- Copyright   :  © 2016–present Stack Buliders
@@ -10,37 +17,31 @@
 -- Types used in the package. You don't usually need to import the module,
 -- because "Text.Mustache" re-exports everything you may need, import that
 -- module instead.
-
-{-# LANGUAGE DeriveDataTypeable         #-}
-{-# LANGUAGE DeriveGeneric              #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE OverloadedStrings          #-}
-{-# LANGUAGE TemplateHaskell            #-}
-
 module Text.Mustache.Type
-  ( Template (..)
-  , Node (..)
-  , Key (..)
-  , showKey
-  , PName (..)
-  , MustacheException (..)
-  , MustacheWarning (..)
-  , displayMustacheWarning )
+  ( Template (..),
+    Node (..),
+    Key (..),
+    showKey,
+    PName (..),
+    MustacheException (..),
+    MustacheWarning (..),
+    displayMustacheWarning,
+  )
 where
 
 import Control.DeepSeq
-import Control.Exception (Exception(..))
+import Control.Exception (Exception (..))
 import Data.Data (Data)
 import Data.Map (Map)
+import qualified Data.Map as M
 import Data.String (IsString (..))
 import Data.Text (Text)
+import qualified Data.Text as T
 import Data.Typeable (Typeable, cast)
 import Data.Void
 import GHC.Generics
-import Text.Megaparsec
-import qualified Data.Map  as M
-import qualified Data.Text as T
 import qualified Language.Haskell.TH.Syntax as TH
+import Text.Megaparsec
 
 -- | Mustache template as name of “top-level” template and a collection of
 -- all available templates (partials).
@@ -49,41 +50,51 @@
 -- (and their caches) using the @('<>')@ operator, the resulting 'Template'
 -- will have the same currently selected template as the left one. Union of
 -- caches is also left-biased.
-
 data Template = Template
-  { templateActual :: PName
-    -- ^ Name of currently “selected” template (top-level one).
-  , templateCache  :: Map PName [Node]
-    -- ^ Collection of all templates that are available for interpolation
+  { -- | Name of currently “selected” template (top-level one).
+    templateActual :: PName,
+    -- | Collection of all templates that are available for interpolation
     -- (as partials). The top-level one is also contained here and the
     -- “focus” can be switched easily by modifying 'templateActual'.
-  } deriving (Eq, Ord, Show, Data, Typeable, Generic)
+    templateCache :: Map PName [Node]
+  }
+  deriving (Eq, Ord, Show, Data, Typeable, Generic)
 
 instance Semigroup Template where
   (Template pname x) <> (Template _ y) = Template pname (M.union x y)
 
 -- | @since 2.1.0
-
 instance TH.Lift Template where
   lift = liftData
 
--- | Structural element of template.
+#if MIN_VERSION_template_haskell(2,16,0)
+  liftTyped = TH.unsafeTExpCoerce . TH.lift
+#endif
 
+-- | Structural element of template.
 data Node
-  = TextBlock       Text       -- ^ Plain text contained between tags
-  | EscapedVar      Key        -- ^ HTML-escaped variable
-  | UnescapedVar    Key        -- ^ Unescaped variable
-  | Section         Key [Node] -- ^ Mustache section
-  | InvertedSection Key [Node] -- ^ Inverted section
-  | Partial         PName (Maybe Pos)
-    -- ^ Partial with indentation level ('Nothing' means it was inlined)
+  = -- | Plain text contained between tags
+    TextBlock Text
+  | -- | HTML-escaped variable
+    EscapedVar Key
+  | -- | Unescaped variable
+    UnescapedVar Key
+  | -- | Mustache section
+    Section Key [Node]
+  | -- | Inverted section
+    InvertedSection Key [Node]
+  | -- | Partial with indentation level ('Nothing' means it was inlined)
+    Partial PName (Maybe Pos)
   deriving (Eq, Ord, Show, Data, Typeable, Generic)
 
 -- | @since 2.1.0
-
 instance TH.Lift Node where
   lift = liftData
 
+#if MIN_VERSION_template_haskell(2,16,0)
+  liftTyped = TH.unsafeTExpCoerce . TH.lift
+#endif
+
 -- | Identifier for values to interpolate.
 --
 -- The representation is the following:
@@ -91,30 +102,30 @@
 --     * @[]@—empty list means implicit iterators;
 --     * @[text]@—single key is a normal identifier;
 --     * @[text1, text2]@—multiple keys represent dotted names.
-
-newtype Key = Key { unKey :: [Text] }
+newtype Key = Key {unKey :: [Text]}
   deriving (Eq, Ord, Show, Semigroup, Monoid, Data, Typeable, Generic)
 
 instance NFData Key
 
 -- | @since 2.1.0
-
 instance TH.Lift Key where
   lift = liftData
 
+#if MIN_VERSION_template_haskell(2,16,0)
+  liftTyped = TH.unsafeTExpCoerce . TH.lift
+#endif
+
 -- | Pretty-print a key, this is helpful, for example, if you want to
 -- display an error message.
 --
 -- @since 0.2.0
-
 showKey :: Key -> Text
 showKey (Key []) = "<implicit>"
 showKey (Key xs) = T.intercalate "." xs
 
 -- | Identifier for partials. Note that with the @OverloadedStrings@
 -- extension you can use just string literals to create values of this type.
-
-newtype PName = PName { unPName :: Text }
+newtype PName = PName {unPName :: Text}
   deriving (Eq, Ord, Show, Data, Typeable, Generic)
 
 instance IsString PName where
@@ -123,20 +134,22 @@
 instance NFData PName
 
 -- | @since 2.1.0
-
 instance TH.Lift PName where
   lift = liftData
 
+#if MIN_VERSION_template_haskell(2,16,0)
+  liftTyped = TH.unsafeTExpCoerce . TH.lift
+#endif
+
 -- | Exception that is thrown when parsing of a template fails or referenced
 -- values are not provided.
-
 newtype MustacheException
-  = MustacheParserException (ParseErrorBundle Text Void)
-    -- ^ Template parser has failed. This contains the parse error.
+  = -- | Template parser has failed. This contains the parse error.
     --
     -- /Before version 0.2.0 it was called 'MustacheException'./
     --
     -- /The 'Text' field was added in version 1.0.0./
+    MustacheParserException (ParseErrorBundle Text Void)
   deriving (Eq, Show, Typeable, Generic)
 
 instance Exception MustacheException where
@@ -145,20 +158,18 @@
 -- | Warning that may be generated during rendering of a 'Template'.
 --
 -- @since 1.1.1
-
 data MustacheWarning
-  = MustacheVariableNotFound Key
-    -- ^ The template contained a variable for which there was no data
+  = -- | The template contained a variable for which there was no data
     -- counterpart in the current context.
-  | MustacheDirectlyRenderedValue Key
-    -- ^ A complex value such as an 'Object' or 'Array' was directly
+    MustacheVariableNotFound Key
+  | -- | A complex value such as an 'Object' or 'Array' was directly
     -- rendered into the template.
+    MustacheDirectlyRenderedValue Key
   deriving (Eq, Show, Typeable, Generic)
 
 -- | Pretty-print a 'MustacheWarning'.
 --
 -- @since 1.1.1
-
 displayMustacheWarning :: MustacheWarning -> String
 displayMustacheWarning (MustacheVariableNotFound key) =
   "Referenced value was not provided, key: " ++ T.unpack (showKey key)
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,135 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Main (main) where
+
+import Data.Aeson (Value (..))
+import qualified Data.Aeson as Aeson
+import qualified Data.HashMap.Strict as HM
+import Data.List (foldl')
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NE
+import Data.Semigroup (sconcat)
+import qualified Data.Text.Lazy.IO as T
+import Data.Version (showVersion)
+import qualified Data.Yaml as Yaml
+import Development.GitRev
+import Options.Applicative
+import Paths_stache (version)
+import System.Exit (exitFailure)
+import System.FilePath (takeExtension)
+import System.IO (hPutStrLn, stderr)
+import Text.Mustache
+
+main :: IO ()
+main = do
+  Opts {..} <- execParser optsParserInfo
+  template <-
+    sconcat
+      <$> mapM (compileMustacheDir optTarget) optTemplateDirs
+  context <-
+    foldl' mergeContexts emptyContext
+      <$> mapM loadContext optContextFiles
+  let rendered = renderMustache template context
+  case optOutputFile of
+    Nothing -> T.putStrLn rendered
+    Just ofile -> T.writeFile ofile rendered
+
+----------------------------------------------------------------------------
+-- Command line options parsing
+
+-- | Command line options.
+data Opts = Opts
+  { -- | Context files.
+    optContextFiles :: [FilePath],
+    -- | Where to save the result.
+    optOutputFile :: Maybe FilePath,
+    -- | Name of the template to render.
+    optTarget :: PName,
+    -- | Directories with templates.
+    optTemplateDirs :: NonEmpty FilePath
+  }
+
+optsParserInfo :: ParserInfo Opts
+optsParserInfo =
+  info (helper <*> ver <*> optsParser) . mconcat $
+    [ fullDesc,
+      progDesc "Command line interface to the Stache template processor",
+      header "stache—a simple implementation of Mustache templates"
+    ]
+  where
+    ver :: Parser (a -> a)
+    ver =
+      infoOption verStr . mconcat $
+        [ long "version",
+          short 'v',
+          help "Print version of the program"
+        ]
+    verStr =
+      unwords
+        [ "stache",
+          showVersion version,
+          $gitBranch,
+          $gitHash
+        ]
+
+optsParser :: Parser Opts
+optsParser =
+  Opts
+    <$> many
+      ( (strOption . mconcat)
+          [ long "context",
+            short 'c',
+            metavar "CONTEXT",
+            help "Context file in YAML or JSON format"
+          ]
+      )
+    <*> (optional . strOption . mconcat)
+      [ long "ofile",
+        short 'o',
+        metavar "OFILE",
+        help "Save the rendered document to this file (otherwise write to stdout)"
+      ]
+    <*> (argument str . mconcat)
+      [ metavar "TARGET",
+        help "Name of the template to render"
+      ]
+    <*> (fmap NE.fromList . some)
+      ( (argument str . mconcat)
+          [ metavar "DIR",
+            help "Template directories"
+          ]
+      )
+
+----------------------------------------------------------------------------
+-- Helpers
+
+-- | File context from a YAML or JSON file.
+loadContext :: FilePath -> IO Value
+loadContext file = do
+  let readYaml =
+        either
+          (Left . Yaml.prettyPrintParseException)
+          Right
+          <$> Yaml.decodeFileEither file
+  econtext <- case takeExtension file of
+    ".yml" -> readYaml
+    ".yaml" -> readYaml
+    _ -> Aeson.eitherDecodeFileStrict file
+  case econtext of
+    Left err -> spitErrorAndDie err
+    Right v@(Aeson.Object _) -> return v
+    Right _ -> spitErrorAndDie "context file should contain an object"
+
+mergeContexts :: Value -> Value -> Value
+mergeContexts (Aeson.Object m0) (Aeson.Object m1) =
+  Aeson.Object (HM.union m0 m1)
+mergeContexts _ _ = error "context merge failed"
+
+emptyContext :: Value
+emptyContext = Aeson.object []
+
+spitErrorAndDie :: String -> IO a
+spitErrorAndDie err = do
+  hPutStrLn stderr err
+  exitFailure
diff --git a/bench/Main.hs b/bench/Main.hs
--- a/bench/Main.hs
+++ b/bench/Main.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE OverloadedStrings    #-}
+{-# LANGUAGE OverloadedStrings #-}
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 
 module Main (main) where
@@ -7,88 +7,124 @@
 import Criterion.Main
 import Data.Aeson
 import Data.Text (Text)
+import qualified Data.Text.IO as T
 import Text.Megaparsec
 import Text.Mustache.Compile
 import Text.Mustache.Parser
 import Text.Mustache.Render
 import Text.Mustache.Type
-import qualified Data.Text.IO as T
 
 ----------------------------------------------------------------------------
 -- Benchmarks
 
 main :: IO ()
-main = defaultMain [ parserBench, renderBench ]
+main = defaultMain [parserBench, renderBench]
 
 parserBench :: Benchmark
-parserBench = bgroup "parser"
-  [ bparser "simple block of text"
-      "bench-data/lorem-ipsum.mustache"
-  , bparser "text with escaped var"
-      "bench-data/escaped-var.mustache"
-  , bparser "text with unescaped var"
-      "bench-data/unescaped-var.mustache"
-  , bparser "text with unescaped var special"
-      "bench-data/unescaped-var-spec.mustache"
-  , bparser "a section"
-      "bench-data/section.mustache"
-  , bparser "an inverted section"
-      "bench-data/inverted-section.mustache"
-  , bparser "nested sections"
-      "bench-data/nested-sections.mustache"
-  , bparser "text with partial"
-      "bench-data/partial.mustache"
-  , bparser "comprehensive template"
-      "bench-data/comprehensive.mustache"
-  ]
+parserBench =
+  bgroup
+    "parser"
+    [ bparser
+        "simple block of text"
+        "bench-data/lorem-ipsum.mustache",
+      bparser
+        "text with escaped var"
+        "bench-data/escaped-var.mustache",
+      bparser
+        "text with unescaped var"
+        "bench-data/unescaped-var.mustache",
+      bparser
+        "text with unescaped var special"
+        "bench-data/unescaped-var-spec.mustache",
+      bparser
+        "a section"
+        "bench-data/section.mustache",
+      bparser
+        "an inverted section"
+        "bench-data/inverted-section.mustache",
+      bparser
+        "nested sections"
+        "bench-data/nested-sections.mustache",
+      bparser
+        "text with partial"
+        "bench-data/partial.mustache",
+      bparser
+        "comprehensive template"
+        "bench-data/comprehensive.mustache"
+    ]
 
 renderBench :: Benchmark
-renderBench = bgroup "render"
-  [ brender "simple block of text"
-      "bench-data/lorem-ipsum.mustache"
-      Null
-  , brender "text with escaped var"
-      "bench-data/escaped-var.mustache"
-      (object ["escaped-variable" .= ("escaped variable" :: Text)])
-  , brender "text with unescaped var"
-      "bench-data/unescaped-var.mustache"
-      (object ["unescaped-variable" .= ("unescaped variable" :: Text)])
-  , brender "text with unescaped var special"
-      "bench-data/unescaped-var-spec.mustache"
-      (object ["unescaped-variable" .= ("unescaped variable" :: Text)])
-  , brender "a section"
-      "bench-data/section.mustache"
-      (object ["section" .= True])
-  , brender "an inverted section"
-      "bench-data/inverted-section.mustache"
-      (object ["section" .= False])
-  , brender "nested sections"
-      "bench-data/nested-sections.mustache"
-      (object [ "section-a" .= True
-              , "section-b" .= True
-              , "section-c" .= True ])
-  , brender' "text with partial"
-      "bench-data/" "partial"
-      Null
-  , brender' "comprehensive template"
-      "bench-data/" "comprehensive"
-      (object [ "first-thing"  .= ("the first thing" :: Text)
-              , "second-thing" .= ("the second thing" :: Text)
-              , "third-thing"  .= ("the third thing" :: Text)
-              , "items" .= [1..10 :: Int] ])
-  ]
+renderBench =
+  bgroup
+    "render"
+    [ brender
+        "simple block of text"
+        "bench-data/lorem-ipsum.mustache"
+        Null,
+      brender
+        "text with escaped var"
+        "bench-data/escaped-var.mustache"
+        (object ["escaped-variable" .= ("escaped variable" :: Text)]),
+      brender
+        "text with unescaped var"
+        "bench-data/unescaped-var.mustache"
+        (object ["unescaped-variable" .= ("unescaped variable" :: Text)]),
+      brender
+        "text with unescaped var special"
+        "bench-data/unescaped-var-spec.mustache"
+        (object ["unescaped-variable" .= ("unescaped variable" :: Text)]),
+      brender
+        "a section"
+        "bench-data/section.mustache"
+        (object ["section" .= True]),
+      brender
+        "an inverted section"
+        "bench-data/inverted-section.mustache"
+        (object ["section" .= False]),
+      brender
+        "nested sections"
+        "bench-data/nested-sections.mustache"
+        ( object
+            [ "section-a" .= True,
+              "section-b" .= True,
+              "section-c" .= True
+            ]
+        ),
+      brender'
+        "text with partial"
+        "bench-data/"
+        "partial"
+        Null,
+      brender'
+        "comprehensive template"
+        "bench-data/"
+        "comprehensive"
+        ( object
+            [ "first-thing" .= ("the first thing" :: Text),
+              "second-thing" .= ("the second thing" :: Text),
+              "third-thing" .= ("the third thing" :: Text),
+              "items" .= [1 .. 10 :: Int]
+            ]
+        )
+    ]
 
 bparser :: String -> FilePath -> Benchmark
-bparser desc path = env (T.readFile path)
-  (bench desc . nf (either (error . errorBundlePretty) id . parseMustache path))
+bparser desc path =
+  env
+    (T.readFile path)
+    (bench desc . nf (either (error . errorBundlePretty) id . parseMustache path))
 
 brender :: String -> FilePath -> Value -> Benchmark
-brender desc path value = env (compileMustacheFile path)
-  (bench desc . nf (`renderMustache` value))
+brender desc path value =
+  env
+    (compileMustacheFile path)
+    (bench desc . nf (`renderMustache` value))
 
 brender' :: String -> FilePath -> PName -> Value -> Benchmark
-brender' desc path pname value = env (compileMustacheDir pname path)
-  (bench desc . nf (`renderMustache` value))
+brender' desc path pname value =
+  env
+    (compileMustacheDir pname path)
+    (bench desc . nf (`renderMustache` value))
 
 ----------------------------------------------------------------------------
 -- Orphan instances
diff --git a/mustache-spec/Spec.hs b/mustache-spec/Spec.hs
--- a/mustache-spec/Spec.hs
+++ b/mustache-spec/Spec.hs
@@ -1,6 +1,6 @@
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
-{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TemplateHaskell #-}
 
 module Main (main) where
 
@@ -9,45 +9,45 @@
 import Data.ByteString (ByteString)
 import Data.FileEmbed (embedFile)
 import Data.Map (Map, (!))
+import qualified Data.Map as M
 import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
 import Data.Yaml
 import Test.Hspec
 import Text.Megaparsec
 import Text.Mustache
 import Text.Mustache.Parser
-import qualified Data.Map       as M
-import qualified Data.Text      as T
-import qualified Data.Text.Lazy as TL
 
 -- | Representation of information contained in a Mustache spec file.
-
 data SpecFile = SpecFile
-  { specOverview :: Text   -- ^ Textual overview of this spec file
-  , specTests    :: [Test] -- ^ The actual collection of tests
+  { -- | Textual overview of this spec file
+    specOverview :: Text,
+    -- | The actual collection of tests
+    specTests :: [Test]
   }
 
 instance FromJSON SpecFile where
   parseJSON = withObject "Mustache spec file" $ \o -> do
     specOverview <- o .: "overview"
-    specTests    <- o .: "tests"
+    specTests <- o .: "tests"
     return SpecFile {..}
 
 -- | Representation of a single test.
-
 data Test = Test
-  { testName     :: String
-  , testDesc     :: String
-  , testData     :: Value
-  , testTemplate :: Text
-  , testExpected :: Text
-  , testPartials :: Map Text Text
+  { testName :: String,
+    testDesc :: String,
+    testData :: Value,
+    testTemplate :: Text,
+    testExpected :: Text,
+    testPartials :: Map Text Text
   }
 
 instance FromJSON Test where
   parseJSON = withObject "Test" $ \o -> do
-    testName     <- o .: "name"
-    testDesc     <- o .: "desc"
-    testData     <- o .: "data"
+    testName <- o .: "name"
+    testDesc <- o .: "desc"
+    testData <- o .: "data"
     testTemplate <- o .: "template"
     testExpected <- o .: "expected"
     testPartials <- o .:? "partials" .!= M.empty
@@ -58,12 +58,12 @@
 
 spec :: Spec
 spec = do
-  specData "Comments"      $(embedFile "specification/comments.yml")
-  specData "Delimiters"    $(embedFile "specification/delimiters.yml")
+  specData "Comments" $(embedFile "specification/comments.yml")
+  specData "Delimiters" $(embedFile "specification/delimiters.yml")
   specData "Interpolation" $(embedFile "specification/interpolation.yml")
-  specData "Inverted"      $(embedFile "specification/inverted.yml")
-  specData "Partials"      $(embedFile "specification/partials.yml")
-  specData "Sections"      $(embedFile "specification/sections.yml")
+  specData "Inverted" $(embedFile "specification/inverted.yml")
+  specData "Partials" $(embedFile "specification/partials.yml")
+  specData "Sections" $(embedFile "specification/sections.yml")
 
 specData :: String -> ByteString -> Spec
 specData aspect bytes = describe aspect $ do
@@ -82,7 +82,7 @@
                 let pname = PName k
                 case parseMustache (T.unpack k) (testPartials ! k) of
                   Left perr -> handleError perr >> undefined
-                  Right ns  -> return (pname, ns)
+                  Right ns -> return (pname, ns)
               let ps2 = M.fromList ps1 `M.union` templateCache
               TL.toStrict (renderMustache (Template templateActual ps2) testData)
                 `shouldBe` testExpected
diff --git a/stache.cabal b/stache.cabal
--- a/stache.cabal
+++ b/stache.cabal
@@ -1,120 +1,163 @@
-name:                 stache
-version:              2.1.1
-cabal-version:        1.18
-tested-with:          GHC==8.4.4, GHC==8.6.5, GHC==8.8.3
-license:              BSD3
-license-file:         LICENSE.md
-author:               Mark Karpov <markkarpov92@gmail.com>
-maintainer:           Mark Karpov <markkarpov92@gmail.com>
-homepage:             https://github.com/stackbuilders/stache
-bug-reports:          https://github.com/stackbuilders/stache/issues
-category:             Text
-synopsis:             Mustache templates for Haskell
-build-type:           Simple
-description:          Mustache templates for Haskell.
-extra-doc-files:      CHANGELOG.md
-                    , README.md
-data-files:           bench-data/*.mustache
-                    , specification/*.yml
-                    , templates/*.mustache
+cabal-version:   1.18
+name:            stache
+version:         2.2.0
+license:         BSD3
+license-file:    LICENSE.md
+maintainer:      Mark Karpov <markkarpov92@gmail.com>
+author:          Mark Karpov <markkarpov92@gmail.com>
+tested-with:     ghc ==8.6.5 ghc ==8.8.3 ghc ==8.10.1
+homepage:        https://github.com/stackbuilders/stache
+bug-reports:     https://github.com/stackbuilders/stache/issues
+synopsis:        Mustache templates for Haskell
+description:     Mustache templates for Haskell.
+category:        Text
+build-type:      Simple
+data-files:
+    bench-data/*.mustache
+    specification/*.yml
+    templates/*.mustache
 
+extra-doc-files:
+    CHANGELOG.md
+    README.md
+
 source-repository head
-  type:               git
-  location:           https://github.com/stackbuilders/stache.git
+    type:     git
+    location: https://github.com/stackbuilders/stache.git
 
 flag dev
-  description:        Turn on development settings.
-  manual:             True
-  default:            False
+    description: Turn on development settings.
+    default:     False
+    manual:      True
 
 library
-  build-depends:      aeson            >= 0.11 && < 1.5
-                    , base             >= 4.11 && < 5.0
-                    , bytestring       >= 0.10 && < 0.11
-                    , containers       >= 0.5  && < 0.7
-                    , deepseq          >= 1.4  && < 1.5
-                    , directory        >= 1.2  && < 1.4
-                    , filepath         >= 1.2  && < 1.5
-                    , megaparsec       >= 7.0  && < 9.0
-                    , mtl              >= 2.1  && < 3.0
-                    , template-haskell >= 2.11 && < 2.16
-                    , text             >= 1.2  && < 1.3
-                    , unordered-containers >= 0.2.5 && < 0.3
-                    , vector           >= 0.11 && < 0.13
-  exposed-modules:    Text.Mustache
-                    , Text.Mustache.Compile
-                    , Text.Mustache.Compile.TH
-                    , Text.Mustache.Parser
-                    , Text.Mustache.Render
-                    , Text.Mustache.Type
-  if flag(dev)
-    ghc-options:      -O0 -Wall -Werror -fsimpl-tick-factor=150
-  else
-    ghc-options:      -O2 -Wall -fsimpl-tick-factor=150
-  if flag(dev)
-    ghc-options:      -Wcompat
-                      -Wincomplete-record-updates
-                      -Wincomplete-uni-patterns
-                      -Wnoncanonical-monad-instances
-  default-language:   Haskell2010
+    exposed-modules:
+        Text.Mustache
+        Text.Mustache.Compile
+        Text.Mustache.Compile.TH
+        Text.Mustache.Parser
+        Text.Mustache.Render
+        Text.Mustache.Type
 
+    default-language: Haskell2010
+    build-depends:
+        aeson >=0.11 && <1.6,
+        base >=4.12 && <5.0,
+        bytestring >=0.10 && <0.11,
+        containers >=0.5 && <0.7,
+        deepseq >=1.4 && <1.5,
+        directory >=1.2 && <1.4,
+        filepath >=1.2 && <1.5,
+        megaparsec >=7.0 && <9.0,
+        mtl >=2.1 && <3.0,
+        template-haskell >=2.11 && <2.17,
+        text >=1.2 && <1.3,
+        unordered-containers >=0.2.5 && <0.3,
+        vector >=0.11 && <0.13
+
+    if flag(dev)
+        ghc-options: -O0 -Wall -Werror -fsimpl-tick-factor=150
+
+    else
+        ghc-options: -O2 -Wall -fsimpl-tick-factor=150
+
+    if flag(dev)
+        ghc-options:
+            -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns
+            -Wnoncanonical-monad-instances
+
+executable stache
+    main-is:          Main.hs
+    hs-source-dirs:   app
+    other-modules:    Paths_stache
+    default-language: Haskell2010
+    build-depends:
+        aeson >=0.11 && <1.6,
+        base >=4.12 && <5.0,
+        gitrev >=1.3 && <1.4,
+        optparse-applicative >=0.14 && <0.16,
+        stache >=2.0 && <3.0,
+        text >=0.2 && <1.3,
+        unordered-containers >=0.2.5 && <0.3,
+        yaml >=0.8 && <0.12,
+        filepath >=1.2 && <1.5
+
+    if flag(dev)
+        ghc-options:
+            -Wall -Werror -Wcompat -Wincomplete-record-updates
+            -Wincomplete-uni-patterns -Wnoncanonical-monad-instances
+
+    else
+        ghc-options: -O2 -Wall
+
 test-suite tests
-  main-is:            Spec.hs
-  hs-source-dirs:     tests
-  type:               exitcode-stdio-1.0
-  build-depends:      aeson            >= 0.11 && < 1.5
-                    , base             >= 4.11 && < 5.0
-                    , containers       >= 0.5  && < 0.7
-                    , hspec            >= 2.0  && < 3.0
-                    , hspec-megaparsec >= 2.0  && < 3.0
-                    , megaparsec       >= 7.0  && < 9.0
-                    , stache
-                    , template-haskell >= 2.11 && < 2.16
-                    , text             >= 1.2  && < 1.3
-  other-modules:      Text.Mustache.Compile.THSpec
-                    , Text.Mustache.ParserSpec
-                    , Text.Mustache.RenderSpec
-                    , Text.Mustache.TypeSpec
-  build-tools:        hspec-discover   >= 2.0  && < 3.0
-  if flag(dev)
-    ghc-options:      -O0 -Wall -Werror
-  else
-    ghc-options:      -O2 -Wall
-  default-language:   Haskell2010
+    type:             exitcode-stdio-1.0
+    main-is:          Spec.hs
+    build-tools:      hspec-discover >=2.0 && <3.0
+    hs-source-dirs:   tests
+    other-modules:
+        Text.Mustache.Compile.THSpec
+        Text.Mustache.ParserSpec
+        Text.Mustache.RenderSpec
+        Text.Mustache.TypeSpec
 
+    default-language: Haskell2010
+    build-depends:
+        aeson >=0.11 && <1.6,
+        base >=4.12 && <5.0,
+        containers >=0.5 && <0.7,
+        hspec >=2.0 && <3.0,
+        hspec-megaparsec >=2.0 && <3.0,
+        megaparsec >=7.0 && <9.0,
+        stache -any,
+        template-haskell >=2.11 && <2.17,
+        text >=1.2 && <1.3
+
+    if flag(dev)
+        ghc-options: -O0 -Wall -Werror
+
+    else
+        ghc-options: -O2 -Wall
+
 test-suite mustache-spec
-  main-is:            Spec.hs
-  hs-source-dirs:     mustache-spec
-  type:               exitcode-stdio-1.0
-  build-depends:      aeson            >= 0.11 && < 1.5
-                    , base             >= 4.11 && < 5.0
-                    , bytestring       >= 0.10 && < 0.11
-                    , containers       >= 0.5  && < 0.7
-                    , file-embed
-                    , hspec            >= 2.0  && < 3.0
-                    , megaparsec       >= 7.0  && < 9.0
-                    , stache
-                    , text             >= 1.2  && < 1.3
-                    , yaml             >= 0.8  && < 0.12
-  if flag(dev)
-    ghc-options:      -Wall -Werror
-  else
-    ghc-options:      -O2 -Wall
-  default-language:   Haskell2010
+    type:             exitcode-stdio-1.0
+    main-is:          Spec.hs
+    hs-source-dirs:   mustache-spec
+    default-language: Haskell2010
+    build-depends:
+        aeson >=0.11 && <1.6,
+        base >=4.12 && <5.0,
+        bytestring >=0.10 && <0.11,
+        containers >=0.5 && <0.7,
+        file-embed -any,
+        hspec >=2.0 && <3.0,
+        megaparsec >=7.0 && <9.0,
+        stache -any,
+        text >=1.2 && <1.3,
+        yaml >=0.8 && <0.12
 
+    if flag(dev)
+        ghc-options: -Wall -Werror
+
+    else
+        ghc-options: -O2 -Wall
+
 benchmark bench
-  main-is:            Main.hs
-  hs-source-dirs:     bench
-  type:               exitcode-stdio-1.0
-  build-depends:      aeson            >= 0.11 && < 1.5
-                    , base             >= 4.10 && < 5.0
-                    , criterion        >= 0.6.2.1 && < 1.6
-                    , deepseq          >= 1.4  && < 1.5
-                    , megaparsec       >= 7.0  && < 9.0
-                    , stache
-                    , text             >= 1.2  && < 1.3
-  if flag(dev)
-    ghc-options:      -O2 -Wall -Werror
-  else
-    ghc-options:      -O2 -Wall
-  default-language:   Haskell2010
+    type:             exitcode-stdio-1.0
+    main-is:          Main.hs
+    hs-source-dirs:   bench
+    default-language: Haskell2010
+    build-depends:
+        aeson >=0.11 && <1.6,
+        base >=4.12 && <5.0,
+        criterion >=0.6.2.1 && <1.6,
+        deepseq >=1.4 && <1.5,
+        megaparsec >=7.0 && <9.0,
+        stache -any,
+        text >=1.2 && <1.3
+
+    if flag(dev)
+        ghc-options: -O2 -Wall -Werror
+
+    else
+        ghc-options: -O2 -Wall
diff --git a/tests/Text/Mustache/Compile/THSpec.hs b/tests/Text/Mustache/Compile/THSpec.hs
--- a/tests/Text/Mustache/Compile/THSpec.hs
+++ b/tests/Text/Mustache/Compile/THSpec.hs
@@ -1,18 +1,18 @@
-{-# LANGUAGE CPP               #-}
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes       #-}
-{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
 
 module Text.Mustache.Compile.THSpec
-  ( main
-  , spec )
+  ( main,
+    spec,
+  )
 where
 
+import qualified Data.Map as M
 import Test.Hspec
-
-import Text.Mustache.Type
-import qualified Data.Map                 as M
 import qualified Text.Mustache.Compile.TH as TH
+import Text.Mustache.Type
 
 #if !MIN_VERSION_base(4,13,0)
 import Data.Semigroup ((<>))
@@ -42,13 +42,16 @@
         `shouldBe` (fooTemplate <> barTemplate)
 
 qqTemplate :: Template
-qqTemplate = Template "quasi-quoted" $
-  M.singleton "quasi-quoted" [TextBlock "From Quasi-Quote!\n"]
+qqTemplate =
+  Template "quasi-quoted" $
+    M.singleton "quasi-quoted" [TextBlock "From Quasi-Quote!\n"]
 
 fooTemplate :: Template
-fooTemplate = Template "foo" $
-  M.singleton "foo" [TextBlock "This is the ‘foo’.\n"]
+fooTemplate =
+  Template "foo" $
+    M.singleton "foo" [TextBlock "This is the ‘foo’.\n"]
 
 barTemplate :: Template
-barTemplate = Template "bar" $
-  M.singleton "bar" [TextBlock "And this is the ‘bar’.\n"]
+barTemplate =
+  Template "bar" $
+    M.singleton "bar" [TextBlock "And this is the ‘bar’.\n"]
diff --git a/tests/Text/Mustache/ParserSpec.hs b/tests/Text/Mustache/ParserSpec.hs
--- a/tests/Text/Mustache/ParserSpec.hs
+++ b/tests/Text/Mustache/ParserSpec.hs
@@ -1,9 +1,10 @@
-{-# LANGUAGE CPP               #-}
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 module Text.Mustache.ParserSpec
-  ( main
-  , spec )
+  ( main,
+    spec,
+  )
 where
 
 import Test.Hspec
@@ -49,44 +50,47 @@
     it "parses empty section" $
       p "{{#section}}{{/section}}" `shouldParse` [Section (key "section") []]
     it "parses non-empty section" $
-      p "{{# section }}Hi, {{name}}!\n{{/section}}" `shouldParse`
-        [Section (key "section")
-         [ TextBlock "Hi, "
-         , EscapedVar (key "name")
-         , TextBlock "!\n"]]
+      p "{{# section }}Hi, {{name}}!\n{{/section}}"
+        `shouldParse` [ Section
+                          (key "section")
+                          [ TextBlock "Hi, ",
+                            EscapedVar (key "name"),
+                            TextBlock "!\n"
+                          ]
+                      ]
   context "when parsing an inverted section" $ do
     it "parses empty inverted section" $
-      p "{{^section}}{{/section}}" `shouldParse`
-        [InvertedSection (key "section") []]
+      p "{{^section}}{{/section}}"
+        `shouldParse` [InvertedSection (key "section") []]
     it "parses non-empty inverted section" $
-      p "{{^ section }}No one here?!\n{{/section}}" `shouldParse`
-        [InvertedSection (key "section") [TextBlock "No one here?!\n"]]
+      p "{{^ section }}No one here?!\n{{/section}}"
+        `shouldParse` [InvertedSection (key "section") [TextBlock "No one here?!\n"]]
   context "when parsing a partial" $ do
     it "parses a partial with white space" $
-      p "{{> that-s_my-partial }}" `shouldParse`
-        [Partial "that-s_my-partial" (Just $ mkPos 1)]
+      p "{{> that-s_my-partial }}"
+        `shouldParse` [Partial "that-s_my-partial" (Just $ mkPos 1)]
     it "parses a partial without white space" $
-      p "{{>that-s_my-partial}}" `shouldParse`
-        [Partial "that-s_my-partial" (Just $ mkPos 1)]
+      p "{{>that-s_my-partial}}"
+        `shouldParse` [Partial "that-s_my-partial" (Just $ mkPos 1)]
     it "handles indented partial correctly" $
-      p "   {{> next_one }}" `shouldParse`
-        [Partial "next_one" (Just $ mkPos 4)]
+      p "   {{> next_one }}"
+        `shouldParse` [Partial "next_one" (Just $ mkPos 4)]
   context "when running into delimiter change" $ do
     it "has effect" $
-      p "{{=<< >>=}}<<var>>{{var}}" `shouldParse`
-        [EscapedVar (key "var"), TextBlock "{{var}}"]
+      p "{{=<< >>=}}<<var>>{{var}}"
+        `shouldParse` [EscapedVar (key "var"), TextBlock "{{var}}"]
     it "handles whitespace just as well" $
-      p "{{=<<   >>=}}<<  var >>{{ var  }}" `shouldParse`
-        [EscapedVar (key "var"), TextBlock "{{ var  }}"]
+      p "{{=<<   >>=}}<<  var >>{{ var  }}"
+        `shouldParse` [EscapedVar (key "var"), TextBlock "{{ var  }}"]
     it "affects {{{s" $
-      p "{{=<< >>=}}<<{var}>>" `shouldParse`
-        [UnescapedVar (key "var")]
+      p "{{=<< >>=}}<<{var}>>"
+        `shouldParse` [UnescapedVar (key "var")]
     it "parses two subsequent delimiter changes" $
-      p "{{=(( ))=}}(( var ))((=-- $-=))--#section$---/section$-" `shouldParse`
-        [EscapedVar (key "var"), Section (key "section") []]
+      p "{{=(( ))=}}(( var ))((=-- $-=))--#section$---/section$-"
+        `shouldParse` [EscapedVar (key "var"), Section (key "section") []]
     it "propagates delimiter change from a nested scope" $
-      p "{{#section}}{{=<< >>=}}<</section>><<var>>" `shouldParse`
-        [Section (key "section") [], EscapedVar (key "var")]
+      p "{{#section}}{{=<< >>=}}<</section>><<var>>"
+        `shouldParse` [Section (key "section") [], EscapedVar (key "var")]
   context "when given malformed input" $ do
     it "rejects unclosed tags" $ do
       let s = "{{ name "
diff --git a/tests/Text/Mustache/RenderSpec.hs b/tests/Text/Mustache/RenderSpec.hs
--- a/tests/Text/Mustache/RenderSpec.hs
+++ b/tests/Text/Mustache/RenderSpec.hs
@@ -1,17 +1,18 @@
 {-# LANGUAGE OverloadedStrings #-}
 
 module Text.Mustache.RenderSpec
-  ( main
-  , spec )
+  ( main,
+    spec,
+  )
 where
 
-import Data.Aeson (object, KeyValue (..), Value (..))
+import Data.Aeson (KeyValue (..), Value (..), object)
+import qualified Data.Map as M
 import Data.Text (Text)
 import Test.Hspec
 import Text.Megaparsec
 import Text.Mustache.Render
 import Text.Mustache.Type
-import qualified Data.Map as M
 
 main :: IO ()
 main = hspec spec
@@ -20,30 +21,32 @@
 spec = describe "renderMustache" $ do
   let w ns value =
         let template = Template "test" (M.singleton "test" ns)
-        in fst (renderMustacheW template value)
+         in fst (renderMustacheW template value)
       r ns value =
         let template = Template "test" (M.singleton "test" ns)
-        in renderMustache template value
+         in renderMustache template value
       key = Key . pure
   it "leaves text block “as is”" $
     r [TextBlock "a text block"] Null `shouldBe` "a text block"
   it "renders escaped variables correctly" $
-    r [EscapedVar (key "foo")]
+    r
+      [EscapedVar (key "foo")]
       (object ["foo" .= ("<html>&\"'something'\"</html>" :: Text)])
       `shouldBe` "&lt;html&gt;&amp;&quot;&#39;something&#39;&quot;&lt;/html&gt;"
   it "renders unescaped variables “as is”" $
-    r [UnescapedVar (key "foo")]
+    r
+      [UnescapedVar (key "foo")]
       (object ["foo" .= ("<html>&\"something\"</html>" :: Text)])
       `shouldBe` "<html>&\"something\"</html>"
   context "when rendering a variable" $ do
     context "when variable does not exist" $
       it "generates correct warning" $
-        w [EscapedVar (key "foo")] (object []) `shouldBe`
-          [MustacheVariableNotFound (key "foo")]
+        w [EscapedVar (key "foo")] (object [])
+          `shouldBe` [MustacheVariableNotFound (key "foo")]
     context "when variable is not non-scalar" $
       it "generates correct warning" $
-        w [EscapedVar (key "foo")] (object ["foo" .= object []]) `shouldBe`
-          [MustacheDirectlyRenderedValue (key "foo")]
+        w [EscapedVar (key "foo")] (object ["foo" .= object []])
+          `shouldBe` [MustacheDirectlyRenderedValue (key "foo")]
   context "when rendering a section" $ do
     let nodes = [Section (key "foo") [UnescapedVar (key "bar"), TextBlock "*"]]
     context "when the key is not present" $ do
@@ -65,7 +68,8 @@
           r nodes (object ["foo" .= ("" :: Text)]) `shouldBe` ""
       context "when the key is a Boolean true" $
         it "renders the section without interpolation" $
-          r [Section (key "foo") [TextBlock "brr"]]
+          r
+            [Section (key "foo") [TextBlock "brr"]]
             (object ["foo" .= object ["bar" .= True]])
             `shouldBe` "brr"
       context "when the key is an object" $
@@ -78,29 +82,34 @@
             `shouldBe` "huh!*"
       context "when the key is a list of Boolean trues" $
         it "renders the section as many times as there are elements" $
-          r [Section (key "foo") [TextBlock "brr"]]
+          r
+            [Section (key "foo") [TextBlock "brr"]]
             (object ["foo" .= [True, True]])
             `shouldBe` "brrbrr"
       context "when the key is a list of objects" $
         it "renders the section many times changing context" $
-          r nodes (object ["foo" .= [object ["bar" .= x] | x <- [1..4] :: [Int]]])
+          r nodes (object ["foo" .= [object ["bar" .= x] | x <- [1 .. 4] :: [Int]]])
             `shouldBe` "1*2*3*4*"
       context "when the key is a number" $ do
         it "renders the section" $
-          r [Section (key "foo") [TextBlock "brr"]]
+          r
+            [Section (key "foo") [TextBlock "brr"]]
             (object ["foo" .= (5 :: Int)])
             `shouldBe` "brr"
         it "uses the key as context" $
-          r [Section (key "foo") [EscapedVar (Key [])]]
+          r
+            [Section (key "foo") [EscapedVar (Key [])]]
             (object ["foo" .= (5 :: Int)])
             `shouldBe` "5"
       context "when the key is a non-empty string" $ do
         it "renders the section" $
-          r [Section (key "foo") [TextBlock "brr"]]
+          r
+            [Section (key "foo") [TextBlock "brr"]]
             (object ["foo" .= ("x" :: Text)])
             `shouldBe` "brr"
         it "uses the key as context" $
-          r [Section (key "foo") [EscapedVar (Key [])]]
+          r
+            [Section (key "foo") [EscapedVar (Key [])]]
             (object ["foo" .= ("x" :: Text)])
             `shouldBe` "x"
   context "when rendering an inverted section" $ do
@@ -128,32 +137,48 @@
         it "skips non-empty list" $
           r nodes (object ["foo" .= [True]]) `shouldBe` ""
   context "when rendering a partial" $ do
-    let nodes = [ Partial "partial" (Just $ mkPos 4)
-                , TextBlock "*" ]
+    let nodes =
+          [ Partial "partial" (Just $ mkPos 4),
+            TextBlock "*"
+          ]
     it "skips missing partial" $
       r nodes Null `shouldBe` "   *"
     it "renders partial correctly" $
-      let template = Template "test" $
-            M.fromList [ ("test", nodes)
-                       , ("partial", [TextBlock "one\ntwo\nthree"]) ]
-      in renderMustache template Null `shouldBe`
-           "   one\n   two\n   three*"
+      let template =
+            Template "test" $
+              M.fromList
+                [ ("test", nodes),
+                  ("partial", [TextBlock "one\ntwo\nthree"])
+                ]
+       in renderMustache template Null
+            `shouldBe` "   one\n   two\n   three*"
   context "when rendering a nested partial" $
     it "renders outer partial correctly" $
-      let template = Template "outer" $
-            M.fromList [ ("inner",  [TextBlock "x"])
-                       , ("middle", [Partial "inner"  (Just $ mkPos 1)])
-                       , ("outer",  [Partial "middle" (Just $ mkPos 1)])
-                       ]
-      in renderMustache template Null `shouldBe` "x"
+      let template =
+            Template "outer" $
+              M.fromList
+                [ ("inner", [TextBlock "x"]),
+                  ("middle", [Partial "inner" (Just $ mkPos 1)]),
+                  ("outer", [Partial "middle" (Just $ mkPos 1)])
+                ]
+       in renderMustache template Null `shouldBe` "x"
   context "when using dotted keys inside a section" $
     it "it should be equivalent to access via one more section" $
-      r [ Section (key "things")
-          [ EscapedVar (Key ["atts", "color"])
-          , TextBlock " == "
-          , Section (key "atts") [EscapedVar (key "color")] ] ]
-        (object ["things" .=
-                 [object
-                  ["atts" .=
-                   object ["color" .= ("blue" :: Text)]]]])
+      r
+        [ Section
+            (key "things")
+            [ EscapedVar (Key ["atts", "color"]),
+              TextBlock " == ",
+              Section (key "atts") [EscapedVar (key "color")]
+            ]
+        ]
+        ( object
+            [ "things"
+                .= [ object
+                       [ "atts"
+                           .= object ["color" .= ("blue" :: Text)]
+                       ]
+                   ]
+            ]
+        )
         `shouldBe` "blue == blue"
diff --git a/tests/Text/Mustache/TypeSpec.hs b/tests/Text/Mustache/TypeSpec.hs
--- a/tests/Text/Mustache/TypeSpec.hs
+++ b/tests/Text/Mustache/TypeSpec.hs
@@ -1,14 +1,15 @@
-{-# LANGUAGE CPP               #-}
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 module Text.Mustache.TypeSpec
-  ( main
-  , spec )
+  ( main,
+    spec,
+  )
 where
 
+import qualified Data.Map as M
 import Test.Hspec
 import Text.Mustache.Type
-import qualified Data.Map as M
 
 #if !MIN_VERSION_base(4,13,0)
 import Data.Semigroup ((<>))
@@ -21,24 +22,25 @@
 spec = do
   describe "Template instances" $
     context "the Semigroup instance" $ do
-      it "the resulting template inherits focus of the left one" $
-        templateActual (templateA <> templateB)
-          `shouldBe` templateActual templateA
-      it "the resulting template merges caches with left bias" $
-        templateCache (templateA <> templateB)
-          `shouldBe` M.fromList
-            [ ("c", [TextBlock "foo"])
-            , ("d", [TextBlock "bar"])
-            , ("e", [TextBlock "baz"]) ]
+        it "the resulting template inherits focus of the left one" $
+          templateActual (templateA <> templateB)
+            `shouldBe` templateActual templateA
+        it "the resulting template merges caches with left bias" $
+          templateCache (templateA <> templateB)
+            `shouldBe` M.fromList
+              [ ("c", [TextBlock "foo"]),
+                ("d", [TextBlock "bar"]),
+                ("e", [TextBlock "baz"])
+              ]
   describe "showKey" $ do
     context "when the key has no elements in it" $
       it "is rendered correctly" $
         showKey (Key []) `shouldBe` "<implicit>"
     context "when the key has some elements" $
       it "is rendered correctly" $ do
-        showKey (Key ["boo"]) `shouldBe` "boo"
-        showKey (Key ["foo","bar"]) `shouldBe` "foo.bar"
-        showKey (Key ["baz","baz","quux"]) `shouldBe` "baz.baz.quux"
+          showKey (Key ["boo"]) `shouldBe` "boo"
+          showKey (Key ["foo", "bar"]) `shouldBe` "foo.bar"
+          showKey (Key ["baz", "baz", "quux"]) `shouldBe` "baz.baz.quux"
   describe "displayMustacheWarning" $ do
     it "renders “not found” warning correctly" $
       displayMustacheWarning (MustacheVariableNotFound (Key ["foo"]))
@@ -48,11 +50,17 @@
         `shouldBe` "Complex value rendered as such, key: foo"
 
 templateA :: Template
-templateA = Template "a" $ M.fromList
-  [ ("c", [TextBlock "foo"])
-  , ("d", [TextBlock "bar"]) ]
+templateA =
+  Template "a" $
+    M.fromList
+      [ ("c", [TextBlock "foo"]),
+        ("d", [TextBlock "bar"])
+      ]
 
 templateB :: Template
-templateB = Template "b" $ M.fromList
-  [ ("c", [TextBlock "bar"])
-  , ("e", [TextBlock "baz"]) ]
+templateB =
+  Template "b" $
+    M.fromList
+      [ ("c", [TextBlock "bar"]),
+        ("e", [TextBlock "baz"])
+      ]
