diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## Stache 0.1.0
+
+* Initial release.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,28 @@
+Copyright © 2016 Stack Builders
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+* Neither the name Mark Karpov nor the names of contributors may be used to
+  endorse or promote products derived from this software without specific
+  prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,83 @@
+# Stache
+
+[![License BSD3](https://img.shields.io/badge/license-BSD3-brightgreen.svg)](http://opensource.org/licenses/BSD-3-Clause)
+[![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)
+[![Coverage Status](https://coveralls.io/repos/github/stackbuilders/stache/badge.svg?branch=master)](https://coveralls.io/github/stackbuilders/stache?branch=master)
+
+This is a Haskell implementation of Mustache templates. The implementation
+conforms to the version 1.1.3 of official [Mustache specification]
+(https://github.com/mustache/spec). It's also faster than some alternative
+Mustache implementations in Haskell because it uses `Data.Text.Lazy.Builder`
+under the hood. It is extremely simple and straightforward to use with
+minimal but complete API — three functions to compile templates (from
+directory, from file, and from lazy text) and one to render them.
+
+The implementation uses the Megaparsec parsing library to parse the
+templates which is results in superior quality of error messages and is also
+faster than Parsec-based ones.
+
+For rendering you only need to create Aeson's `Value` where you put the data
+to interpolate. Since the library re-uses Aeson's instances and most data
+types in Haskell ecosystem are instances of classes like
+`Data.Aeson.ToJSON`, the whole process is very simple for end user.
+
+Template Haskell helpers for compilation of templates at compile time are
+available in the `Text.Mustache.Compile.TH` module. The helpers are
+currently available only for GHC 8 users though.
+
+One feature that is not currently supported is lambdas. The feature is
+marked as optional in the spec and can be emulated via processing of parsed
+template representation. The decision to drop lambdas is intentional, for
+the sake of simplicity and better integration with Aeson.
+
+## Quick start
+
+Here is an example of basic usage:
+
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main (main) where
+
+import Data.Aeson
+import Data.Text
+import Text.Megaparsec
+import Text.Mustache
+import qualified Data.Text.Lazy.IO as TIO
+
+main :: IO ()
+main = do
+  let res = compileMustacheText "foo"
+        "Hi, {{name}}! You have:\n{{#things}}\n  * {{.}}\n{{/things}}\n"
+  case res of
+    Left err -> putStrLn (parseErrorPretty err)
+    Right template -> TIO.putStr $ renderMustache template $ object
+      [ "name"   .= ("John" :: Text)
+      , "things" .= ["pen" :: Text, "candle", "egg"]
+      ]
+```
+
+If I run the program, it prints the following:
+
+```
+Hi, John! You have:
+  * pen
+  * candle
+  * egg
+```
+
+For more information about Mustache templates the following links may be
+helpful:
+
+* The official Mustache site: https://mustache.github.io/
+* The manual: https://mustache.github.io/mustache.5.html
+* The specification: https://github.com/mustache/spec
+
+## License
+
+Copyright © 2016 Stack Builders
+
+Distributed under BSD 3 clause license.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,6 @@
+module Main (main) where
+
+import Distribution.Simple
+
+main :: IO ()
+main = defaultMain
diff --git a/Text/Mustache.hs b/Text/Mustache.hs
new file mode 100644
--- /dev/null
+++ b/Text/Mustache.hs
@@ -0,0 +1,93 @@
+-- |
+-- Module      :  Text.Mustache
+-- Copyright   :  © 2016 Stack Builders
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- This is a Haskell implementation of Mustache templates. The
+-- implementation conforms to the version 1.1.3 of official Mustache
+-- specification <https://github.com/mustache/spec>. It's also faster than
+-- some alternative Mustache implementations in Haskell because it uses
+-- "Data.Text.Lazy.Builder" under the hood. It is extremely simple and
+-- straightforward to use with minimal but complete API — three functions to
+-- compile templates (from directory, from file, and from lazy text) and one
+-- to render them.
+--
+-- The implementation uses the Megaparsec parsing library to parse the
+-- templates which is results in superior quality of error messages and is
+-- also faster than Parsec-based ones.
+--
+-- For rendering you only need to create Aeson's 'Data.Aeson.Value' where
+-- you put the data to interpolate. Since the library re-uses Aeson's
+-- instances and most data types in Haskell ecosystem are instances of
+-- classes like 'Data.Aeson.ToJSON', the whole process is very simple for
+-- end user.
+--
+-- Template Haskell helpers for compilation of templates at compile time are
+-- available in the "Text.Mustache.Compile.TH" module. The helpers are
+-- currently available only for GHC 8 users though.
+--
+-- One feature that is not currently supported is lambdas. The feature is
+-- marked as optional in the spec and can be emulated via processing of
+-- parsed template representation. The decision to drop lambdas is
+-- intentional, for the sake of simplicity and better integration with
+-- Aeson.
+--
+-- Here is an example of basic usage:
+--
+-- > {-# LANGUAGE OverloadedStrings #-}
+-- >
+-- > module Main (main) where
+-- >
+-- > import Data.Aeson
+-- > import Data.Text
+-- > import Text.Megaparsec
+-- > import Text.Mustache
+-- > import qualified Data.Text.Lazy.IO as TIO
+-- >
+-- > main :: IO ()
+-- > main = do
+-- >   let res = compileMustacheText "foo"
+-- >         "Hi, {{name}}! You have:\n{{#things}}\n  * {{.}}\n{{/things}}\n"
+-- >   case res of
+-- >     Left err -> putStrLn (parseErrorPretty err)
+-- >     Right template -> TIO.putStr $ renderMustache template $ object
+-- >       [ "name"   .= ("John" :: Text)
+-- >       , "things" .= ["pen" :: Text, "candle", "egg"]
+-- >       ]
+--
+-- If I run the program, it prints the following:
+--
+-- > Hi, John! You have:
+-- >   * pen
+-- >   * candle
+-- >   * egg
+--
+-- For more information about Mustache templates the following links may be
+-- helpful:
+--
+--     * The official Mustache site: <https://mustache.github.io/>
+--     * The manual: <https://mustache.github.io/mustache.5.html>
+--     * The specification: <https://github.com/mustache/spec>
+
+module Text.Mustache
+  ( -- * Types
+    Template (..)
+  , Node (..)
+  , Key (..)
+  , PName (..)
+  , MustacheException (..)
+    -- * Compiling
+  , compileMustacheDir
+  , compileMustacheFile
+  , compileMustacheText
+    -- * Rendering
+  , renderMustache )
+where
+
+import Text.Mustache.Compile
+import Text.Mustache.Render
+import Text.Mustache.Type
diff --git a/Text/Mustache/Compile.hs b/Text/Mustache/Compile.hs
new file mode 100644
--- /dev/null
+++ b/Text/Mustache/Compile.hs
@@ -0,0 +1,103 @@
+-- |
+-- Module      :  Text.Mustache.Compile
+-- Copyright   :  © 2016 Stack Builders
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- 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.
+
+{-# LANGUAGE CPP #-}
+
+module Text.Mustache.Compile
+  ( compileMustacheDir
+  , compileMustacheFile
+  , compileMustacheText )
+where
+
+import Control.Monad.Catch (MonadThrow (..))
+import Control.Monad.Except
+import Data.Text.Lazy (Text)
+import System.Directory
+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.Lazy.IO as TL
+import qualified System.FilePath   as F
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative ((<$>))
+#endif
+
+-- | Compile all templates in specified directory and select one. Template
+-- files should have extension @mustache@, (e.g. @foo.mustache@) to be
+-- recognized. This function /does not/ scan the directory recursively.
+--
+-- The action can throw the same exceptions as 'getDirectoryContents', and
+-- 'T.readFile'.
+
+compileMustacheDir :: (MonadIO m, MonadThrow m)
+  => PName             -- ^ Which template to select after compiling
+  -> FilePath          -- ^ Directory with templates
+  -> m Template        -- ^ The resulting template
+compileMustacheDir pname path =
+  liftIO (getDirectoryContents path) >>=
+  filterM isMustacheFile . liftM (F.combine (F.takeDirectory path)) >>=
+  liftM selectKey . foldM f (Template undefined M.empty)
+  where
+    selectKey t = t { templateActual = pname }
+    f (Template _ old) fp = do
+      Template _ new <- compileMustacheFile fp
+      return (Template undefined (M.union new old))
+
+-- | Compile single Mustache template and select it.
+--
+-- The action can throw the same exceptions as 'T.readFile'.
+
+compileMustacheFile :: (MonadIO m, MonadThrow m)
+  => FilePath          -- ^ Location of the file
+  -> m Template
+compileMustacheFile path = liftIO (TL.readFile path) >>=
+  withException . compileMustacheText (pathToPName path)
+
+-- | 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 (ParseError Char Dec) Template -- ^ The result
+compileMustacheText pname txt =
+  Template pname . M.singleton pname <$> parseMustache "" txt
+
+----------------------------------------------------------------------------
+-- Helpers
+
+-- | Check if given 'FilePath' point to a mustache file.
+
+isMustacheFile :: MonadIO m => FilePath -> m Bool
+isMustacheFile path = do
+  exists <- liftIO (doesFileExist path)
+  let rightExtension = F.takeExtension path == ".mustache"
+  return (exists && rightExtension)
+
+-- | Build a 'PName' from given 'FilePath'.
+
+pathToPName :: FilePath -> PName
+pathToPName = PName . T.pack . F.takeBaseName
+{-# INLINE pathToPName #-}
+
+-- | Throw 'MustacheException' if argument is 'Left' or return the result
+-- inside 'Right'.
+
+withException :: MonadThrow m
+  => Either (ParseError Char Dec) Template -- ^ Value to process
+  -> m Template        -- ^ The result
+withException = either (throwM . MustacheException) return
+{-# INLINE withException #-}
diff --git a/Text/Mustache/Compile/TH.hs b/Text/Mustache/Compile/TH.hs
new file mode 100644
--- /dev/null
+++ b/Text/Mustache/Compile/TH.hs
@@ -0,0 +1,96 @@
+-- |
+-- Module      :  Text.Mustache.Compile.TH
+-- Copyright   :  © 2016 Stack Builders
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Template Haskell helpers to compile Mustache templates at compile time.
+-- This module is not imported as part of "Text.Mustache", so you need to
+-- import it yourself. Qualified import is recommended, but not necessary.
+--
+-- At the moment, functions in this module only work with GHC 8 (they
+-- require at least @template-haskell-2.11@).
+
+{-# LANGUAGE CPP             #-}
+{-# LANGUAGE RankNTypes      #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Text.Mustache.Compile.TH
+  ( compileMustacheDir
+  , compileMustacheFile
+  , compileMustacheText )
+where
+
+import Control.Monad.Catch (try)
+import Data.Text.Lazy (Text)
+import Data.Typeable (cast)
+import Language.Haskell.TH hiding (Dec)
+import Language.Haskell.TH.Syntax (lift)
+import Text.Megaparsec hiding (try)
+import Text.Mustache.Type
+import qualified Data.Text             as T
+import qualified Text.Mustache.Compile as C
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
+
+#if MIN_VERSION_template_haskell(2,11,0)
+import Language.Haskell.TH.Syntax (dataToExpQ)
+#else
+import Data.Data (Data)
+dataToExpQ :: Data a => (forall b. Data b => b -> Maybe (Q Exp)) -> a -> Q Exp
+dataToExpQ _ _ = fail "The feature requires at least GHC 8 to work"
+#endif
+
+-- | Compile all templates in specified directory and select one. Template
+-- files should have extension @mustache@, (e.g. @foo.mustache@) to be
+-- recognized. This function /does not/ scan the directory recursively.
+--
+-- This version compiles the templates at compile time.
+
+compileMustacheDir
+  :: PName             -- ^ Which template to select after compiling
+  -> FilePath          -- ^ Directory with templates
+  -> Q Exp             -- ^ The resulting template
+compileMustacheDir pname path =
+  (runIO . try) (C.compileMustacheDir pname path) >>= handleEither
+
+-- | Compile single Mustache template and select it.
+--
+-- This version compiles the template at compile time.
+
+compileMustacheFile
+  :: FilePath          -- ^ Location of the file
+  -> Q Exp
+compileMustacheFile path =
+  (runIO . try) (C.compileMustacheFile path) >>= handleEither
+
+-- | Compile Mustache template from 'Text' value. The cache will contain
+-- 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 pname text =
+  handleEither (C.compileMustacheText pname text)
+
+-- | Given an 'Either' result return 'Right' and signal pretty-printed error
+-- if we have a 'Left'.
+
+handleEither :: Either (ParseError Char Dec) Template -> Q Exp
+handleEither val =
+  case val of
+    Left err -> fail (parseErrorPretty err)
+    Right template -> dataToExpQ (fmap liftText . cast) template
+
+-- | Lift strict 'T.Text' to 'Q' 'Exp'.
+
+liftText :: T.Text -> Q Exp
+liftText txt = AppE (VarE 'T.pack) <$> lift (T.unpack txt)
diff --git a/Text/Mustache/Parser.hs b/Text/Mustache/Parser.hs
new file mode 100644
--- /dev/null
+++ b/Text/Mustache/Parser.hs
@@ -0,0 +1,197 @@
+-- |
+-- Module      :  Text.Mustache.Parser
+-- Copyright   :  © 2016 Stack Builders
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- 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.
+
+module Text.Mustache.Parser
+  ( parseMustache )
+where
+
+import Control.Applicative
+import Control.Monad
+import Control.Monad.State.Lazy
+import Data.Char (isSpace)
+import Data.List (intercalate)
+import Data.Maybe (catMaybes)
+import Data.Text.Lazy (Text)
+import Text.Megaparsec
+import Text.Mustache.Type
+import qualified Data.Text             as T
+import qualified Text.Megaparsec.Lexer as L
+
+----------------------------------------------------------------------------
+-- Parser
+
+-- | Parse given Mustache template.
+
+parseMustache
+  :: FilePath
+     -- ^ Location of file to parse
+  -> Text
+     -- ^ File contents (Mustache template)
+  -> Either (ParseError Char Dec) [Node]
+     -- ^ Parsed nodes or parse error
+parseMustache = parse $
+  evalStateT (pMustache eof) (Delimiters "{{" "}}")
+
+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 ]
+{-# INLINE pMustache #-}
+
+pTextBlock :: Parser Node
+pTextBlock = do
+  start <- gets openingDel
+  (void . notFollowedBy . string) start
+  let terminator = choice
+        [ (void . lookAhead . string) start
+        , pBol
+        , eof ]
+  TextBlock . T.pack <$> someTill anyChar terminator
+{-# INLINE pTextBlock #-}
+
+pUnescapedVariable :: Parser Node
+pUnescapedVariable = UnescapedVar <$> pTag "&"
+{-# INLINE pUnescapedVariable #-}
+
+pUnescapedSpecial :: Parser Node
+pUnescapedSpecial =
+  UnescapedVar <$> between (symbol "{{{") (string "}}}") pKey
+{-# INLINE pUnescapedSpecial #-}
+
+pSection :: String -> (Key -> [Node] -> Node) -> Parser Node
+pSection suffix f = do
+  key   <- withStandalone (pTag suffix)
+  nodes <- (pMustache . withStandalone . pClosingTag) key
+  return (f key nodes)
+{-# INLINE pSection #-}
+
+pPartial :: (Pos -> Maybe Pos) -> Parser Node
+pPartial f = do
+  pos <- f <$> L.indentLevel
+  key <- pTag ">"
+  let pname = PName $ T.intercalate (T.pack ".") (unKey key)
+  return (Partial pname pos)
+{-# INLINE pPartial #-}
+
+pComment :: Parser ()
+pComment = void $ do
+  start <- gets openingDel
+  end   <- gets closingDel
+  (void . symbol) (start ++ "!")
+  manyTill anyChar (string end)
+{-# INLINE pComment #-}
+
+pSetDelimiters :: Parser ()
+pSetDelimiters = void $ do
+  start <- gets openingDel
+  end   <- gets closingDel
+  (void . symbol) (start ++ "=")
+  start' <- pDelimiter <* scn
+  end'   <- pDelimiter <* scn
+  (void . string) ("=" ++ end)
+  put (Delimiters start' end')
+{-# INLINE pSetDelimiters #-}
+
+pEscapedVariable :: Parser Node
+pEscapedVariable = EscapedVar <$> pTag ""
+{-# INLINE pEscapedVariable #-}
+
+withStandalone :: Parser a -> Parser a
+withStandalone p = pStandalone p <|> p
+{-# INLINE withStandalone #-}
+
+pStandalone :: Parser a -> Parser a
+pStandalone p = pBol *> try (between sc (sc <* (void eol <|> eof)) p)
+{-# INLINE pStandalone #-}
+
+pTag :: String -> Parser Key
+pTag suffix = do
+  start <- gets openingDel
+  end   <- gets closingDel
+  between (symbol $ start ++ suffix) (string end) pKey
+{-# INLINE pTag #-}
+
+pClosingTag :: Key -> Parser ()
+pClosingTag key = do
+  start <- gets openingDel
+  end   <- gets closingDel
+  let str = keyToString key
+  void $ between (symbol $ start ++ "/") (string end) (symbol str)
+{-# INLINE pClosingTag #-}
+
+pKey :: Parser Key
+pKey = (fmap Key . lexeme . label "key") (implicit <|> other)
+  where
+    implicit = [] <$ char '.'
+    other    = sepBy1 (T.pack <$> some ch) (char '.')
+    ch       = alphaNumChar <|> oneOf "-_"
+{-# INLINE pKey #-}
+
+pDelimiter :: Parser String
+pDelimiter = some (satisfy delChar) <?> "delimiter"
+  where delChar x = not (isSpace x) && x /= '='
+{-# INLINE pDelimiter #-}
+
+pBol :: Parser ()
+pBol = do
+  level <- L.indentLevel
+  unless (level == unsafePos 1) empty
+{-# INLINE pBol #-}
+
+----------------------------------------------------------------------------
+-- Auxiliary types
+
+-- | Type of Mustache parser monad stack.
+
+type Parser = StateT Delimiters (Parsec Dec Text)
+
+-- | State used in Mustache parser. It includes currently set opening and
+-- closing delimiters.
+
+data Delimiters = Delimiters
+  { openingDel :: String
+  , closingDel :: String }
+
+----------------------------------------------------------------------------
+-- Lexer helpers and other
+
+scn :: Parser ()
+scn = L.space (void spaceChar) empty empty
+{-# INLINE scn #-}
+
+sc :: Parser ()
+sc = L.space (void $ oneOf " \t") empty empty
+{-# INLINE sc #-}
+
+lexeme :: Parser a -> Parser a
+lexeme = L.lexeme scn
+{-# INLINE lexeme #-}
+
+symbol :: String -> Parser String
+symbol = L.symbol scn
+{-# INLINE symbol #-}
+
+keyToString :: Key -> String
+keyToString (Key []) = "."
+keyToString (Key ks) = intercalate "." (T.unpack <$> ks)
+{-# INLINE keyToString #-}
diff --git a/Text/Mustache/Render.hs b/Text/Mustache/Render.hs
new file mode 100644
--- /dev/null
+++ b/Text/Mustache/Render.hs
@@ -0,0 +1,270 @@
+-- |
+-- Module      :  Text.Mustache.Render
+-- Copyright   :  © 2016 Stack Builders
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- 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 GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE RecordWildCards            #-}
+
+module Text.Mustache.Render
+  ( renderMustache )
+where
+
+import Control.Monad.Reader
+import Control.Monad.Writer.Lazy
+import Data.Aeson
+import Data.Foldable (asum)
+import Data.List (tails)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import Text.Megaparsec.Pos (Pos, unPos)
+import Text.Mustache.Type
+import qualified Data.ByteString.Lazy   as B
+import qualified Data.HashMap.Strict    as H
+import qualified Data.List.NonEmpty     as NE
+import qualified Data.Map               as M
+import qualified Data.Semigroup         as S
+import qualified Data.Text              as T
+import qualified Data.Text.Encoding     as T
+import qualified Data.Text.Lazy         as TL
+import qualified Data.Text.Lazy.Builder as B
+import qualified Data.Vector            as V
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative
+#endif
+
+----------------------------------------------------------------------------
+-- The rendering monad
+
+-- | 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
+-- lazy 'TL.Text'.
+
+type Render a = ReaderT RenderContext (Writer B.Builder) a
+
+-- | 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?
+  }
+
+----------------------------------------------------------------------------
+-- High-level interface
+
+-- | Render a Mustache 'Template' using Aeson's 'Value' to get actual values
+-- for interpolation.
+
+renderMustache :: Template -> Value -> TL.Text
+renderMustache t =
+  runRender (renderPartial (templateActual t) Nothing renderNode) t
+
+-- | Render a single 'Node'.
+
+renderNode :: Node -> Render ()
+renderNode (TextBlock txt) = outputIndented txt
+renderNode (EscapedVar k) =
+  lookupKey k >>= outputRaw . escapeHtml . renderValue
+renderNode (UnescapedVar k) =
+  lookupKey k >>= outputRaw . renderValue
+renderNode (Section k ns) = do
+  val <- lookupKey k
+  enterSection k $
+    unless (isBlank val) $
+      case val of
+        Object _ ->
+          addToLocalContext val (renderMany renderNode ns)
+        Array xs ->
+          forM_ (V.toList xs) $ \x ->
+            addToLocalContext x (renderMany renderNode ns)
+        Bool True ->
+          renderMany renderNode ns
+        String _  ->
+          renderMany renderNode ns
+        _ ->
+          return ()
+renderNode (InvertedSection k ns) = do
+  val <- lookupKey k
+  when (isBlank val) $
+    renderMany renderNode ns
+renderNode (Partial pname indent) =
+  renderPartial pname indent renderNode
+
+----------------------------------------------------------------------------
+-- The rendering monad vocabulary
+
+-- | Run 'Render' monad given template to render and a 'Value' to take
+-- values from.
+
+runRender :: Render a -> Template -> Value -> TL.Text
+runRender m t v = (B.toLazyText . execWriter) (runReaderT m rc)
+  where
+    rc = RenderContext
+      { rcIndent   = Nothing
+      , rcContext  = v :| []
+      , rcPrefix   = mempty
+      , rcTemplate = t
+      , rcLastNode = True }
+{-# INLINE runRender #-}
+
+-- | Output a piece of strict 'Text'.
+
+outputRaw :: Text -> Render ()
+outputRaw = tell . 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
+  lnode <- asks rcLastNode
+  let f x = outputRaw (T.replace "\n" ("\n" <> buildIndent level) x)
+  if lnode && T.isSuffixOf "\n" txt
+    then f (T.init txt) >> outputRaw "\n"
+    else f txt
+{-# 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 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 }
+{-# INLINE renderPartial #-}
+
+-- | Get collection of 'Node's for actual template.
+
+getNodes :: Render [Node]
+getNodes = do
+  Template actual cache <- asks rcTemplate
+  return (M.findWithDefault [] actual cache)
+{-# INLINE getNodes #-}
+
+-- | Render many nodes.
+
+renderMany
+  :: (Node -> Render ()) -- ^ How to render a node
+  -> [Node]            -- ^ The collection of nodes to render
+  -> 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)
+  renderMany f ns
+
+-- | Lookup a 'Value' by its 'Key'.
+
+lookupKey :: Key -> Render Value
+lookupKey (Key []) = NE.head <$> asks rcContext
+lookupKey k = do
+  v <- asks rcContext
+  p <- asks rcPrefix
+  return . fromMaybe Null $
+    if (null . drop 1 . unKey) k
+      then let f x = asum (simpleLookup (x <> k) <$> v)
+           in asum (fmap (f . Key) . reverse . tails $ unKey p)
+      else asum (simpleLookup (p <> k) <$> v)
+
+-- | Lookup a 'Value' by traversing another 'Value' using given 'Key' as
+-- “path”.
+
+simpleLookup :: Key -> Value -> Maybe Value
+simpleLookup (Key [])     obj        = return obj
+simpleLookup (Key (k:ks)) (Object m) = H.lookup k m >>= simpleLookup (Key ks)
+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 })
+{-# 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) })
+{-# INLINE addToLocalContext #-}
+
+----------------------------------------------------------------------------
+-- Helpers
+
+-- | 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 (Just x) (Just y) = Just (x S.<> y)
+{-# INLINE addIndents #-}
+
+-- | Build intentation 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 (Bool False) = True
+isBlank (Object   m) = H.null m
+isBlank (Array    a) = V.null a
+isBlank _            = False
+{-# INLINE isBlank #-}
+
+-- | Render Aeson's 'Value' /without/ HTML escaping.
+
+renderValue :: Value -> Text
+renderValue Null         = ""
+renderValue (String str) = str
+renderValue value        = (T.decodeUtf8 . B.toStrict . encode) value
+{-# INLINE renderValue #-}
+
+-- | Escape HTML represented as strict 'Text'.
+
+escapeHtml :: Text -> Text
+escapeHtml txt = foldr (uncurry T.replace) txt
+  [ ("\"", "&quot;")
+  , ("<",  "&lt;")
+  , (">",  "&gt;")
+  , ("&",  "&amp;") ]
+{-# INLINE escapeHtml #-}
diff --git a/Text/Mustache/Type.hs b/Text/Mustache/Type.hs
new file mode 100644
--- /dev/null
+++ b/Text/Mustache/Type.hs
@@ -0,0 +1,93 @@
+-- |
+-- Module      :  Text.Mustache.Type
+-- Copyright   :  © 2016 Stack Buliders
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Types used by 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 GeneralizedNewtypeDeriving #-}
+
+module Text.Mustache.Type
+  ( Template (..)
+  , Node (..)
+  , Key (..)
+  , PName (..)
+  , MustacheException (..) )
+where
+
+import Control.Monad.Catch (Exception)
+import Data.Data (Data)
+import Data.Map (Map)
+import Data.Semigroup
+import Data.String (IsString (..))
+import Data.Text (Text)
+import Data.Typeable (Typeable)
+import Text.Megaparsec
+import qualified Data.Map  as M
+import qualified Data.Text as T
+
+-- | Mustache template as name of “top-level” template and a collection of
+-- all available templates (partials).
+--
+-- 'Template' is a 'Semigroup'. This means that you can combine 'Template's
+-- (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
+    -- (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)
+
+instance Semigroup Template where
+  (Template pname x) <> (Template _ y) = Template pname (M.union x y)
+
+-- | 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)
+  deriving (Eq, Ord, Show, Data, Typeable)
+
+-- | Identifier for values to interpolate.
+--
+-- The representation is the following:
+--
+--     * @[]@ — empty list means implicit iterators;
+--     * @[text]@ — single key is a normal identifier;
+--     * @[text1, text2]@ — multiple keys represent dotted names.
+
+newtype Key = Key { unKey :: [Text] }
+  deriving (Eq, Ord, Show, Monoid, Data, Typeable)
+
+-- | 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 }
+  deriving (Eq, Ord, Show, Data, Typeable)
+
+instance IsString PName where
+  fromString = PName . T.pack
+
+-- | Exception that is thrown when parsing of a template has failed.
+
+data MustacheException = MustacheException (ParseError Char Dec)
+  deriving (Eq, Show, Typeable)
+
+instance Exception MustacheException
diff --git a/mustache-spec/Spec.hs b/mustache-spec/Spec.hs
new file mode 100644
--- /dev/null
+++ b/mustache-spec/Spec.hs
@@ -0,0 +1,88 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+
+module Main (main) where
+
+import Control.Monad
+import Data.Aeson
+import Data.ByteString (ByteString)
+import Data.FileEmbed (embedFile)
+import Data.Map (Map, (!))
+import Data.Text (Text)
+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
+  }
+
+instance FromJSON SpecFile where
+  parseJSON = withObject "Mustache spec file" $ \o -> do
+    specOverview <- o .: "overview"
+    specTests    <- o .: "tests"
+    return SpecFile {..}
+
+-- | Representation of a single test.
+
+data Test = Test
+  { testName     :: String
+  , testDesc     :: String
+  , testData     :: Value
+  , testTemplate :: TL.Text
+  , testExpected :: TL.Text
+  , testPartials :: Map Text TL.Text
+  }
+
+instance FromJSON Test where
+  parseJSON = withObject "Test" $ \o -> do
+    testName     <- o .: "name"
+    testDesc     <- o .: "desc"
+    testData     <- o .: "data"
+    testTemplate <- o .: "template"
+    testExpected <- o .: "expected"
+    testPartials <- o .:? "partials" .!= M.empty
+    return Test {..}
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec = do
+  specData "Comments"      $(embedFile "specs/comments.yml")
+  specData "Delimiters"    $(embedFile "specs/delimiters.yml")
+  specData "Interpolation" $(embedFile "specs/interpolation.yml")
+  specData "Inverted"      $(embedFile "specs/inverted.yml")
+  specData "Partials"      $(embedFile "specs/partials.yml")
+  specData "Sections"      $(embedFile "specs/sections.yml")
+
+specData :: String -> ByteString -> Spec
+specData aspect bytes = describe aspect $ do
+  let handleError = expectationFailure . parseErrorPretty
+  case decodeEither' bytes of
+    Left err ->
+      it "should load YAML specs first" $
+        expectationFailure (prettyPrintParseException err)
+    Right SpecFile {..} ->
+      forM_ specTests $ \Test {..} ->
+        it (testName ++ ": " ++ testDesc) $
+          case compileMustacheText (PName $ T.pack testName) testTemplate of
+            Left perr -> handleError perr
+            Right Template {..} -> do
+              ps1 <- forM (M.keys testPartials) $ \k -> do
+                let pname = PName k
+                case parseMustache (T.unpack k) (testPartials ! k) of
+                  Left perr -> handleError perr >> undefined
+                  Right ns  -> return (pname, ns)
+              let ps2 = M.fromList ps1 `M.union` templateCache
+              renderMustache (Template templateActual ps2) testData
+                `shouldBe` testExpected
diff --git a/specs/comments.yml b/specs/comments.yml
new file mode 100644
--- /dev/null
+++ b/specs/comments.yml
@@ -0,0 +1,103 @@
+overview: |
+  Comment tags represent content that should never appear in the resulting
+  output.
+
+  The tag's content may contain any substring (including newlines) EXCEPT the
+  closing delimiter.
+
+  Comment tags SHOULD be treated as standalone when appropriate.
+tests:
+  - name: Inline
+    desc: Comment blocks should be removed from the template.
+    data: { }
+    template: '12345{{! Comment Block! }}67890'
+    expected: '1234567890'
+
+  - name: Multiline
+    desc: Multiline comments should be permitted.
+    data: { }
+    template: |
+      12345{{!
+        This is a
+        multi-line comment...
+      }}67890
+    expected: |
+      1234567890
+
+  - name: Standalone
+    desc: All standalone comment lines should be removed.
+    data: { }
+    template: |
+      Begin.
+      {{! Comment Block! }}
+      End.
+    expected: |
+      Begin.
+      End.
+
+  - name: Indented Standalone
+    desc: All standalone comment lines should be removed.
+    data: { }
+    template: |
+      Begin.
+        {{! Indented Comment Block! }}
+      End.
+    expected: |
+      Begin.
+      End.
+
+  - name: Standalone Line Endings
+    desc: '"\r\n" should be considered a newline for standalone tags.'
+    data: { }
+    template: "|\r\n{{! Standalone Comment }}\r\n|"
+    expected: "|\r\n|"
+
+  - name: Standalone Without Previous Line
+    desc: Standalone tags should not require a newline to precede them.
+    data: { }
+    template: "  {{! I'm Still Standalone }}\n!"
+    expected: "!"
+
+  - name: Standalone Without Newline
+    desc: Standalone tags should not require a newline to follow them.
+    data: { }
+    template: "!\n  {{! I'm Still Standalone }}"
+    expected: "!\n"
+
+  - name: Multiline Standalone
+    desc: All standalone comment lines should be removed.
+    data: { }
+    template: |
+      Begin.
+      {{!
+      Something's going on here...
+      }}
+      End.
+    expected: |
+      Begin.
+      End.
+
+  - name: Indented Multiline Standalone
+    desc: All standalone comment lines should be removed.
+    data: { }
+    template: |
+      Begin.
+        {{!
+          Something's going on here...
+        }}
+      End.
+    expected: |
+      Begin.
+      End.
+
+  - name: Indented Inline
+    desc: Inline comments should not strip whitespace
+    data: { }
+    template: "  12 {{! 34 }}\n"
+    expected: "  12 \n"
+
+  - name: Surrounding Whitespace
+    desc: Comment removal should preserve surrounding whitespace.
+    data: { }
+    template: '12345 {{! Comment Block! }} 67890'
+    expected: '12345  67890'
diff --git a/specs/delimiters.yml b/specs/delimiters.yml
new file mode 100644
--- /dev/null
+++ b/specs/delimiters.yml
@@ -0,0 +1,158 @@
+overview: |
+  Set Delimiter tags are used to change the tag delimiters for all content
+  following the tag in the current compilation unit.
+
+  The tag's content MUST be any two non-whitespace sequences (separated by
+  whitespace) EXCEPT an equals sign ('=') followed by the current closing
+  delimiter.
+
+  Set Delimiter tags SHOULD be treated as standalone when appropriate.
+tests:
+  - name: Pair Behavior
+    desc: The equals sign (used on both sides) should permit delimiter changes.
+    data: { text: 'Hey!' }
+    template: '{{=<% %>=}}(<%text%>)'
+    expected: '(Hey!)'
+
+  - name: Special Characters
+    desc: Characters with special meaning regexen should be valid delimiters.
+    data: { text: 'It worked!' }
+    template: '({{=[ ]=}}[text])'
+    expected: '(It worked!)'
+
+  - name: Sections
+    desc: Delimiters set outside sections should persist.
+    data: { section: true, data: 'I got interpolated.' }
+    template: |
+      [
+      {{#section}}
+        {{data}}
+        |data|
+      {{/section}}
+
+      {{= | | =}}
+      |#section|
+        {{data}}
+        |data|
+      |/section|
+      ]
+    expected: |
+      [
+        I got interpolated.
+        |data|
+
+        {{data}}
+        I got interpolated.
+      ]
+
+  - name: Inverted Sections
+    desc: Delimiters set outside inverted sections should persist.
+    data: { section: false, data: 'I got interpolated.' }
+    template: |
+      [
+      {{^section}}
+        {{data}}
+        |data|
+      {{/section}}
+
+      {{= | | =}}
+      |^section|
+        {{data}}
+        |data|
+      |/section|
+      ]
+    expected: |
+      [
+        I got interpolated.
+        |data|
+
+        {{data}}
+        I got interpolated.
+      ]
+
+  - name: Partial Inheritence
+    desc: Delimiters set in a parent template should not affect a partial.
+    data: { value: 'yes' }
+    partials:
+      include: '.{{value}}.'
+    template: |
+      [ {{>include}} ]
+      {{= | | =}}
+      [ |>include| ]
+    expected: |
+      [ .yes. ]
+      [ .yes. ]
+
+  - name: Post-Partial Behavior
+    desc: Delimiters set in a partial should not affect the parent template.
+    data: { value: 'yes' }
+    partials:
+      include: '.{{value}}. {{= | | =}} .|value|.'
+    template: |
+      [ {{>include}} ]
+      [ .{{value}}.  .|value|. ]
+    expected: |
+      [ .yes.  .yes. ]
+      [ .yes.  .|value|. ]
+
+  # Whitespace Sensitivity
+
+  - name: Surrounding Whitespace
+    desc: Surrounding whitespace should be left untouched.
+    data: { }
+    template: '| {{=@ @=}} |'
+    expected: '|  |'
+
+  - name: Outlying Whitespace (Inline)
+    desc: Whitespace should be left untouched.
+    data: { }
+    template: " | {{=@ @=}}\n"
+    expected: " | \n"
+
+  - name: Standalone Tag
+    desc: Standalone lines should be removed from the template.
+    data: { }
+    template: |
+      Begin.
+      {{=@ @=}}
+      End.
+    expected: |
+      Begin.
+      End.
+
+  - name: Indented Standalone Tag
+    desc: Indented standalone lines should be removed from the template.
+    data: { }
+    template: |
+      Begin.
+        {{=@ @=}}
+      End.
+    expected: |
+      Begin.
+      End.
+
+  - name: Standalone Line Endings
+    desc: '"\r\n" should be considered a newline for standalone tags.'
+    data: { }
+    template: "|\r\n{{= @ @ =}}\r\n|"
+    expected: "|\r\n|"
+
+  - name: Standalone Without Previous Line
+    desc: Standalone tags should not require a newline to precede them.
+    data: { }
+    template: "  {{=@ @=}}\n="
+    expected: "="
+
+  - name: Standalone Without Newline
+    desc: Standalone tags should not require a newline to follow them.
+    data: { }
+    template: "=\n  {{=@ @=}}"
+    expected: "=\n"
+
+  # Whitespace Insensitivity
+
+  - name: Pair with Padding
+    desc: Superfluous in-tag whitespace should be ignored.
+    data: { }
+    template: '|{{= @   @ =}}|'
+    expected: '||'
diff --git a/specs/interpolation.yml b/specs/interpolation.yml
new file mode 100644
--- /dev/null
+++ b/specs/interpolation.yml
@@ -0,0 +1,238 @@
+overview: |
+  Interpolation tags are used to integrate dynamic content into the template.
+
+  The tag's content MUST be a non-whitespace character sequence NOT containing
+  the current closing delimiter.
+
+  This tag's content names the data to replace the tag.  A single period (`.`)
+  indicates that the item currently sitting atop the context stack should be
+  used; otherwise, name resolution is as follows:
+    1) Split the name on periods; the first part is the name to resolve, any
+    remaining parts should be retained.
+    2) Walk the context stack from top to bottom, finding the first context
+    that is a) a hash containing the name as a key OR b) an object responding
+    to a method with the given name.
+    3) If the context is a hash, the data is the value associated with the
+    name.
+    4) If the context is an object, the data is the value returned by the
+    method with the given name.
+    5) If any name parts were retained in step 1, each should be resolved
+    against a context stack containing only the result from the former
+    resolution.  If any part fails resolution, the result should be considered
+    falsey, and should interpolate as the empty string.
+  Data should be coerced into a string (and escaped, if appropriate) before
+  interpolation.
+
+  The Interpolation tags MUST NOT be treated as standalone.
+tests:
+  - name: No Interpolation
+    desc: Mustache-free templates should render as-is.
+    data: { }
+    template: |
+      Hello from {Mustache}!
+    expected: |
+      Hello from {Mustache}!
+
+  - name: Basic Interpolation
+    desc: Unadorned tags should interpolate content into the template.
+    data: { subject: "world" }
+    template: |
+      Hello, {{subject}}!
+    expected: |
+      Hello, world!
+
+  - name: HTML Escaping
+    desc: Basic interpolation should be HTML escaped.
+    data: { forbidden: '& " < >' }
+    template: |
+      These characters should be HTML escaped: {{forbidden}}
+    expected: |
+      These characters should be HTML escaped: &amp; &quot; &lt; &gt;
+
+  - name: Triple Mustache
+    desc: Triple mustaches should interpolate without HTML escaping.
+    data: { forbidden: '& " < >' }
+    template: |
+      These characters should not be HTML escaped: {{{forbidden}}}
+    expected: |
+      These characters should not be HTML escaped: & " < >
+
+  - name: Ampersand
+    desc: Ampersand should interpolate without HTML escaping.
+    data: { forbidden: '& " < >' }
+    template: |
+      These characters should not be HTML escaped: {{&forbidden}}
+    expected: |
+      These characters should not be HTML escaped: & " < >
+
+  - name: Basic Integer Interpolation
+    desc: Integers should interpolate seamlessly.
+    data: { mph: 85 }
+    template: '"{{mph}} miles an hour!"'
+    expected: '"85 miles an hour!"'
+
+  - name: Triple Mustache Integer Interpolation
+    desc: Integers should interpolate seamlessly.
+    data: { mph: 85 }
+    template: '"{{{mph}}} miles an hour!"'
+    expected: '"85 miles an hour!"'
+
+  - name: Ampersand Integer Interpolation
+    desc: Integers should interpolate seamlessly.
+    data: { mph: 85 }
+    template: '"{{&mph}} miles an hour!"'
+    expected: '"85 miles an hour!"'
+
+  - name: Basic Decimal Interpolation
+    desc: Decimals should interpolate seamlessly with proper significance.
+    data: { power: 1.210 }
+    template: '"{{power}} jiggawatts!"'
+    expected: '"1.21 jiggawatts!"'
+
+  - name: Triple Mustache Decimal Interpolation
+    desc: Decimals should interpolate seamlessly with proper significance.
+    data: { power: 1.210 }
+    template: '"{{{power}}} jiggawatts!"'
+    expected: '"1.21 jiggawatts!"'
+
+  - name: Ampersand Decimal Interpolation
+    desc: Decimals should interpolate seamlessly with proper significance.
+    data: { power: 1.210 }
+    template: '"{{&power}} jiggawatts!"'
+    expected: '"1.21 jiggawatts!"'
+
+  # Context Misses
+
+  - name: Basic Context Miss Interpolation
+    desc: Failed context lookups should default to empty strings.
+    data: { }
+    template: "I ({{cannot}}) be seen!"
+    expected: "I () be seen!"
+
+  - name: Triple Mustache Context Miss Interpolation
+    desc: Failed context lookups should default to empty strings.
+    data: { }
+    template: "I ({{{cannot}}}) be seen!"
+    expected: "I () be seen!"
+
+  - name: Ampersand Context Miss Interpolation
+    desc: Failed context lookups should default to empty strings.
+    data: { }
+    template: "I ({{&cannot}}) be seen!"
+    expected: "I () be seen!"
+
+  # Dotted Names
+
+  - name: Dotted Names - Basic Interpolation
+    desc: Dotted names should be considered a form of shorthand for sections.
+    data: { person: { name: 'Joe' } }
+    template: '"{{person.name}}" == "{{#person}}{{name}}{{/person}}"'
+    expected: '"Joe" == "Joe"'
+
+  - name: Dotted Names - Triple Mustache Interpolation
+    desc: Dotted names should be considered a form of shorthand for sections.
+    data: { person: { name: 'Joe' } }
+    template: '"{{{person.name}}}" == "{{#person}}{{{name}}}{{/person}}"'
+    expected: '"Joe" == "Joe"'
+
+  - name: Dotted Names - Ampersand Interpolation
+    desc: Dotted names should be considered a form of shorthand for sections.
+    data: { person: { name: 'Joe' } }
+    template: '"{{&person.name}}" == "{{#person}}{{&name}}{{/person}}"'
+    expected: '"Joe" == "Joe"'
+
+  - name: Dotted Names - Arbitrary Depth
+    desc: Dotted names should be functional to any level of nesting.
+    data:
+      a: { b: { c: { d: { e: { name: 'Phil' } } } } }
+    template: '"{{a.b.c.d.e.name}}" == "Phil"'
+    expected: '"Phil" == "Phil"'
+
+  - name: Dotted Names - Broken Chains
+    desc: Any falsey value prior to the last part of the name should yield ''.
+    data:
+      a: { }
+    template: '"{{a.b.c}}" == ""'
+    expected: '"" == ""'
+
+  - name: Dotted Names - Broken Chain Resolution
+    desc: Each part of a dotted name should resolve only against its parent.
+    data:
+      a: { b: { } }
+      c: { name: 'Jim' }
+    template: '"{{a.b.c.name}}" == ""'
+    expected: '"" == ""'
+
+  - name: Dotted Names - Initial Resolution
+    desc: The first part of a dotted name should resolve as any other name.
+    data:
+      a: { b: { c: { d: { e: { name: 'Phil' } } } } }
+      b: { c: { d: { e: { name: 'Wrong' } } } }
+    template: '"{{#a}}{{b.c.d.e.name}}{{/a}}" == "Phil"'
+    expected: '"Phil" == "Phil"'
+
+  - name: Dotted Names - Context Precedence
+    desc: Dotted names should be resolved against former resolutions.
+    data:
+      a: { b: { } }
+      b: { c: 'ERROR' }
+    template: '{{#a}}{{b.c}}{{/a}}'
+    expected: ''
+
+  # Whitespace Sensitivity
+
+  - name: Interpolation - Surrounding Whitespace
+    desc: Interpolation should not alter surrounding whitespace.
+    data: { string: '---' }
+    template: '| {{string}} |'
+    expected: '| --- |'
+
+  - name: Triple Mustache - Surrounding Whitespace
+    desc: Interpolation should not alter surrounding whitespace.
+    data: { string: '---' }
+    template: '| {{{string}}} |'
+    expected: '| --- |'
+
+  - name: Ampersand - Surrounding Whitespace
+    desc: Interpolation should not alter surrounding whitespace.
+    data: { string: '---' }
+    template: '| {{&string}} |'
+    expected: '| --- |'
+
+  - name: Interpolation - Standalone
+    desc: Standalone interpolation should not alter surrounding whitespace.
+    data: { string: '---' }
+    template: "  {{string}}\n"
+    expected: "  ---\n"
+
+  - name: Triple Mustache - Standalone
+    desc: Standalone interpolation should not alter surrounding whitespace.
+    data: { string: '---' }
+    template: "  {{{string}}}\n"
+    expected: "  ---\n"
+
+  - name: Ampersand - Standalone
+    desc: Standalone interpolation should not alter surrounding whitespace.
+    data: { string: '---' }
+    template: "  {{&string}}\n"
+    expected: "  ---\n"
+
+  # Whitespace Insensitivity
+
+  - name: Interpolation With Padding
+    desc: Superfluous in-tag whitespace should be ignored.
+    data: { string: "---" }
+    template: '|{{ string }}|'
+    expected: '|---|'
+
+  - name: Triple Mustache With Padding
+    desc: Superfluous in-tag whitespace should be ignored.
+    data: { string: "---" }
+    template: '|{{{ string }}}|'
+    expected: '|---|'
+
+  - name: Ampersand With Padding
+    desc: Superfluous in-tag whitespace should be ignored.
+    data: { string: "---" }
+    template: '|{{& string }}|'
+    expected: '|---|'
diff --git a/specs/inverted.yml b/specs/inverted.yml
new file mode 100644
--- /dev/null
+++ b/specs/inverted.yml
@@ -0,0 +1,193 @@
+overview: |
+  Inverted Section tags and End Section tags are used in combination to wrap a
+  section of the template.
+
+  These tags' content MUST be a non-whitespace character sequence NOT
+  containing the current closing delimiter; each Inverted Section tag MUST be
+  followed by an End Section tag with the same content within the same
+  section.
+
+  This tag's content names the data to replace the tag.  Name resolution is as
+  follows:
+    1) Split the name on periods; the first part is the name to resolve, any
+    remaining parts should be retained.
+    2) Walk the context stack from top to bottom, finding the first context
+    that is a) a hash containing the name as a key OR b) an object responding
+    to a method with the given name.
+    3) If the context is a hash, the data is the value associated with the
+    name.
+    4) If the context is an object and the method with the given name has an
+    arity of 1, the method SHOULD be called with a String containing the
+    unprocessed contents of the sections; the data is the value returned.
+    5) Otherwise, the data is the value returned by calling the method with
+    the given name.
+    6) If any name parts were retained in step 1, each should be resolved
+    against a context stack containing only the result from the former
+    resolution.  If any part fails resolution, the result should be considered
+    falsey, and should interpolate as the empty string.
+  If the data is not of a list type, it is coerced into a list as follows: if
+  the data is truthy (e.g. `!!data == true`), use a single-element list
+  containing the data, otherwise use an empty list.
+
+  This section MUST NOT be rendered unless the data list is empty.
+
+  Inverted Section and End Section tags SHOULD be treated as standalone when
+  appropriate.
+tests:
+  - name: Falsey
+    desc: Falsey sections should have their contents rendered.
+    data: { boolean: false }
+    template: '"{{^boolean}}This should be rendered.{{/boolean}}"'
+    expected: '"This should be rendered."'
+
+  - name: Truthy
+    desc: Truthy sections should have their contents omitted.
+    data: { boolean: true }
+    template: '"{{^boolean}}This should not be rendered.{{/boolean}}"'
+    expected: '""'
+
+  - name: Context
+    desc: Objects and hashes should behave like truthy values.
+    data: { context: { name: 'Joe' } }
+    template: '"{{^context}}Hi {{name}}.{{/context}}"'
+    expected: '""'
+
+  - name: List
+    desc: Lists should behave like truthy values.
+    data: { list: [ { n: 1 }, { n: 2 }, { n: 3 } ] }
+    template: '"{{^list}}{{n}}{{/list}}"'
+    expected: '""'
+
+  - name: Empty List
+    desc: Empty lists should behave like falsey values.
+    data: { list: [ ] }
+    template: '"{{^list}}Yay lists!{{/list}}"'
+    expected: '"Yay lists!"'
+
+  - name: Doubled
+    desc: Multiple inverted sections per template should be permitted.
+    data: { bool: false, two: 'second' }
+    template: |
+      {{^bool}}
+      * first
+      {{/bool}}
+      * {{two}}
+      {{^bool}}
+      * third
+      {{/bool}}
+    expected: |
+      * first
+      * second
+      * third
+
+  - name: Nested (Falsey)
+    desc: Nested falsey sections should have their contents rendered.
+    data: { bool: false }
+    template: "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |"
+    expected: "| A B C D E |"
+
+  - name: Nested (Truthy)
+    desc: Nested truthy sections should be omitted.
+    data: { bool: true }
+    template: "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |"
+    expected: "| A  E |"
+
+  - name: Context Misses
+    desc: Failed context lookups should be considered falsey.
+    data: { }
+    template: "[{{^missing}}Cannot find key 'missing'!{{/missing}}]"
+    expected: "[Cannot find key 'missing'!]"
+
+  # Dotted Names
+
+  - name: Dotted Names - Truthy
+    desc: Dotted names should be valid for Inverted Section tags.
+    data: { a: { b: { c: true } } }
+    template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == ""'
+    expected: '"" == ""'
+
+  - name: Dotted Names - Falsey
+    desc: Dotted names should be valid for Inverted Section tags.
+    data: { a: { b: { c: false } } }
+    template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == "Not Here"'
+    expected: '"Not Here" == "Not Here"'
+
+  - name: Dotted Names - Broken Chains
+    desc: Dotted names that cannot be resolved should be considered falsey.
+    data: { a: { } }
+    template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == "Not Here"'
+    expected: '"Not Here" == "Not Here"'
+
+  # Whitespace Sensitivity
+
+  - name: Surrounding Whitespace
+    desc: Inverted sections should not alter surrounding whitespace.
+    data: { boolean: false }
+    template: " | {{^boolean}}\t|\t{{/boolean}} | \n"
+    expected: " | \t|\t | \n"
+
+  - name: Internal Whitespace
+    desc: Inverted should not alter internal whitespace.
+    data: { boolean: false }
+    template: " | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n"
+    expected: " |  \n  | \n"
+
+  - name: Indented Inline Sections
+    desc: Single-line sections should not alter surrounding whitespace.
+    data: { boolean: false }
+    template: " {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n"
+    expected: " NO\n WAY\n"
+
+  - name: Standalone Lines
+    desc: Standalone lines should be removed from the template.
+    data: { boolean: false }
+    template: |
+      | This Is
+      {{^boolean}}
+      |
+      {{/boolean}}
+      | A Line
+    expected: |
+      | This Is
+      |
+      | A Line
+
+  - name: Standalone Indented Lines
+    desc: Standalone indented lines should be removed from the template.
+    data: { boolean: false }
+    template: |
+      | This Is
+        {{^boolean}}
+      |
+        {{/boolean}}
+      | A Line
+    expected: |
+      | This Is
+      |
+      | A Line
+
+  - name: Standalone Line Endings
+    desc: '"\r\n" should be considered a newline for standalone tags.'
+    data: { boolean: false }
+    template: "|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|"
+    expected: "|\r\n|"
+
+  - name: Standalone Without Previous Line
+    desc: Standalone tags should not require a newline to precede them.
+    data: { boolean: false }
+    template: "  {{^boolean}}\n^{{/boolean}}\n/"
+    expected: "^\n/"
+
+  - name: Standalone Without Newline
+    desc: Standalone tags should not require a newline to follow them.
+    data: { boolean: false }
+    template: "^{{^boolean}}\n/\n  {{/boolean}}"
+    expected: "^\n/\n"
+
+  # Whitespace Insensitivity
+
+  - name: Padding
+    desc: Superfluous in-tag whitespace should be ignored.
+    data: { boolean: false }
+    template: '|{{^ boolean }}={{/ boolean }}|'
+    expected: '|=|'
diff --git a/specs/partials.yml b/specs/partials.yml
new file mode 100644
--- /dev/null
+++ b/specs/partials.yml
@@ -0,0 +1,109 @@
+overview: |
+  Partial tags are used to expand an external template into the current
+  template.
+
+  The tag's content MUST be a non-whitespace character sequence NOT containing
+  the current closing delimiter.
+
+  This tag's content names the partial to inject.  Set Delimiter tags MUST NOT
+  affect the parsing of a partial.  The partial MUST be rendered against the
+  context stack local to the tag.  If the named partial cannot be found, the
+  empty string SHOULD be used instead, as in interpolations.
+
+  Partial tags SHOULD be treated as standalone when appropriate.  If this tag
+  is used standalone, any whitespace preceding the tag should treated as
+  indentation, and prepended to each line of the partial before rendering.
+tests:
+  - name: Basic Behavior
+    desc: The greater-than operator should expand to the named partial.
+    data: { }
+    template: '"{{>text}}"'
+    partials: { text: 'from partial' }
+    expected: '"from partial"'
+
+  - name: Failed Lookup
+    desc: The empty string should be used when the named partial is not found.
+    data: { }
+    template: '"{{>text}}"'
+    partials: { }
+    expected: '""'
+
+  - name: Context
+    desc: The greater-than operator should operate within the current context.
+    data: { text: 'content' }
+    template: '"{{>partial}}"'
+    partials: { partial: '*{{text}}*' }
+    expected: '"*content*"'
+
+  - name: Recursion
+    desc: The greater-than operator should properly recurse.
+    data: { content: "X", nodes: [ { content: "Y", nodes: [] } ] }
+    template: '{{>node}}'
+    partials: { node: '{{content}}<{{#nodes}}{{>node}}{{/nodes}}>' }
+    expected: 'X<Y<>>'
+
+  # Whitespace Sensitivity
+
+  - name: Surrounding Whitespace
+    desc: The greater-than operator should not alter surrounding whitespace.
+    data: { }
+    template: '| {{>partial}} |'
+    partials: { partial: "\t|\t" }
+    expected: "| \t|\t |"
+
+  - name: Inline Indentation
+    desc: Whitespace should be left untouched.
+    data: { data: '|' }
+    template: "  {{data}}  {{> partial}}\n"
+    partials: { partial: ">\n>" }
+    expected: "  |  >\n>\n"
+
+  - name: Standalone Line Endings
+    desc: '"\r\n" should be considered a newline for standalone tags.'
+    data: { }
+    template: "|\r\n{{>partial}}\r\n|"
+    partials: { partial: ">" }
+    expected: "|\r\n>|"
+
+  - name: Standalone Without Previous Line
+    desc: Standalone tags should not require a newline to precede them.
+    data: { }
+    template: "  {{>partial}}\n>"
+    partials: { partial: ">\n>"}
+    expected: "  >\n  >>"
+
+  - name: Standalone Without Newline
+    desc: Standalone tags should not require a newline to follow them.
+    data: { }
+    template: ">\n  {{>partial}}"
+    partials: { partial: ">\n>" }
+    expected: ">\n  >\n  >"
+
+  - name: Standalone Indentation
+    desc: Each line of the partial should be indented before rendering.
+    data: { content: "<\n->" }
+    template: |
+      \
+       {{>partial}}
+      /
+    partials:
+      partial: |
+        |
+        {{{content}}}
+        |
+    expected: |
+      \
+       |
+       <
+      ->
+       |
+      /
+
+  # Whitespace Insensitivity
+
+  - name: Padding Whitespace
+    desc: Superfluous in-tag whitespace should be ignored.
+    data: { boolean: true }
+    template: "|{{> partial }}|"
+    partials: { partial: "[]" }
+    expected: '|[]|'
diff --git a/specs/sections.yml b/specs/sections.yml
new file mode 100644
--- /dev/null
+++ b/specs/sections.yml
@@ -0,0 +1,263 @@
+overview: |
+  Section tags and End Section tags are used in combination to wrap a section
+  of the template for iteration
+
+  These tags' content MUST be a non-whitespace character sequence NOT
+  containing the current closing delimiter; each Section tag MUST be followed
+  by an End Section tag with the same content within the same section.
+
+  This tag's content names the data to replace the tag.  Name resolution is as
+  follows:
+    1) Split the name on periods; the first part is the name to resolve, any
+    remaining parts should be retained.
+    2) Walk the context stack from top to bottom, finding the first context
+    that is a) a hash containing the name as a key OR b) an object responding
+    to a method with the given name.
+    3) If the context is a hash, the data is the value associated with the
+    name.
+    4) If the context is an object and the method with the given name has an
+    arity of 1, the method SHOULD be called with a String containing the
+    unprocessed contents of the sections; the data is the value returned.
+    5) Otherwise, the data is the value returned by calling the method with
+    the given name.
+    6) If any name parts were retained in step 1, each should be resolved
+    against a context stack containing only the result from the former
+    resolution.  If any part fails resolution, the result should be considered
+    falsey, and should interpolate as the empty string.
+  If the data is not of a list type, it is coerced into a list as follows: if
+  the data is truthy (e.g. `!!data == true`), use a single-element list
+  containing the data, otherwise use an empty list.
+
+  For each element in the data list, the element MUST be pushed onto the
+  context stack, the section MUST be rendered, and the element MUST be popped
+  off the context stack.
+
+  Section and End Section tags SHOULD be treated as standalone when
+  appropriate.
+tests:
+  - name: Truthy
+    desc: Truthy sections should have their contents rendered.
+    data: { boolean: true }
+    template: '"{{#boolean}}This should be rendered.{{/boolean}}"'
+    expected: '"This should be rendered."'
+
+  - name: Falsey
+    desc: Falsey sections should have their contents omitted.
+    data: { boolean: false }
+    template: '"{{#boolean}}This should not be rendered.{{/boolean}}"'
+    expected: '""'
+
+  - name: Context
+    desc: Objects and hashes should be pushed onto the context stack.
+    data: { context: { name: 'Joe' } }
+    template: '"{{#context}}Hi {{name}}.{{/context}}"'
+    expected: '"Hi Joe."'
+
+  - name: Deeply Nested Contexts
+    desc: All elements on the context stack should be accessible.
+    data:
+      a: { one: 1 }
+      b: { two: 2 }
+      c: { three: 3 }
+      d: { four: 4 }
+      e: { five: 5 }
+    template: |
+      {{#a}}
+      {{one}}
+      {{#b}}
+      {{one}}{{two}}{{one}}
+      {{#c}}
+      {{one}}{{two}}{{three}}{{two}}{{one}}
+      {{#d}}
+      {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}
+      {{#e}}
+      {{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}
+      {{/e}}
+      {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}
+      {{/d}}
+      {{one}}{{two}}{{three}}{{two}}{{one}}
+      {{/c}}
+      {{one}}{{two}}{{one}}
+      {{/b}}
+      {{one}}
+      {{/a}}
+    expected: |
+      1
+      121
+      12321
+      1234321
+      123454321
+      1234321
+      12321
+      121
+      1
+
+  - name: List
+    desc: Lists should be iterated; list items should visit the context stack.
+    data: { list: [ { item: 1 }, { item: 2 }, { item: 3 } ] }
+    template: '"{{#list}}{{item}}{{/list}}"'
+    expected: '"123"'
+
+  - name: Empty List
+    desc: Empty lists should behave like falsey values.
+    data: { list: [ ] }
+    template: '"{{#list}}Yay lists!{{/list}}"'
+    expected: '""'
+
+  - name: Doubled
+    desc: Multiple sections per template should be permitted.
+    data: { bool: true, two: 'second' }
+    template: |
+      {{#bool}}
+      * first
+      {{/bool}}
+      * {{two}}
+      {{#bool}}
+      * third
+      {{/bool}}
+    expected: |
+      * first
+      * second
+      * third
+
+  - name: Nested (Truthy)
+    desc: Nested truthy sections should have their contents rendered.
+    data: { bool: true }
+    template: "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |"
+    expected: "| A B C D E |"
+
+  - name: Nested (Falsey)
+    desc: Nested falsey sections should be omitted.
+    data: { bool: false }
+    template: "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |"
+    expected: "| A  E |"
+
+  - name: Context Misses
+    desc: Failed context lookups should be considered falsey.
+    data: { }
+    template: "[{{#missing}}Found key 'missing'!{{/missing}}]"
+    expected: "[]"
+
+  # Implicit Iterators
+
+  - name: Implicit Iterator - String
+    desc: Implicit iterators should directly interpolate strings.
+    data:
+      list: [ 'a', 'b', 'c', 'd', 'e' ]
+    template: '"{{#list}}({{.}}){{/list}}"'
+    expected: '"(a)(b)(c)(d)(e)"'
+
+  - name: Implicit Iterator - Integer
+    desc: Implicit iterators should cast integers to strings and interpolate.
+    data:
+      list: [ 1, 2, 3, 4, 5 ]
+    template: '"{{#list}}({{.}}){{/list}}"'
+    expected: '"(1)(2)(3)(4)(5)"'
+
+  - name: Implicit Iterator - Decimal
+    desc: Implicit iterators should cast decimals to strings and interpolate.
+    data:
+      list: [ 1.10, 2.20, 3.30, 4.40, 5.50 ]
+    template: '"{{#list}}({{.}}){{/list}}"'
+    expected: '"(1.1)(2.2)(3.3)(4.4)(5.5)"'
+
+  - name: Implicit Iterator - Array
+    desc: Implicit iterators should allow iterating over nested arrays.
+    data:
+      list: [ [1, 2, 3], ['a', 'b', 'c'] ]
+    template: '"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}"'
+    expected: '"(123)(abc)"'
+
+  # Dotted Names
+
+  - name: Dotted Names - Truthy
+    desc: Dotted names should be valid for Section tags.
+    data: { a: { b: { c: true } } }
+    template: '"{{#a.b.c}}Here{{/a.b.c}}" == "Here"'
+    expected: '"Here" == "Here"'
+
+  - name: Dotted Names - Falsey
+    desc: Dotted names should be valid for Section tags.
+    data: { a: { b: { c: false } } }
+    template: '"{{#a.b.c}}Here{{/a.b.c}}" == ""'
+    expected: '"" == ""'
+
+  - name: Dotted Names - Broken Chains
+    desc: Dotted names that cannot be resolved should be considered falsey.
+    data: { a: { } }
+    template: '"{{#a.b.c}}Here{{/a.b.c}}" == ""'
+    expected: '"" == ""'
+
+  # Whitespace Sensitivity
+
+  - name: Surrounding Whitespace
+    desc: Sections should not alter surrounding whitespace.
+    data: { boolean: true }
+    template: " | {{#boolean}}\t|\t{{/boolean}} | \n"
+    expected: " | \t|\t | \n"
+
+  - name: Internal Whitespace
+    desc: Sections should not alter internal whitespace.
+    data: { boolean: true }
+    template: " | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n"
+    expected: " |  \n  | \n"
+
+  - name: Indented Inline Sections
+    desc: Single-line sections should not alter surrounding whitespace.
+    data: { boolean: true }
+    template: " {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n"
+    expected: " YES\n GOOD\n"
+
+  - name: Standalone Lines
+    desc: Standalone lines should be removed from the template.
+    data: { boolean: true }
+    template: |
+      | This Is
+      {{#boolean}}
+      |
+      {{/boolean}}
+      | A Line
+    expected: |
+      | This Is
+      |
+      | A Line
+
+  - name: Indented Standalone Lines
+    desc: Indented standalone lines should be removed from the template.
+    data: { boolean: true }
+    template: |
+      | This Is
+        {{#boolean}}
+      |
+        {{/boolean}}
+      | A Line
+    expected: |
+      | This Is
+      |
+      | A Line
+
+  - name: Standalone Line Endings
+    desc: '"\r\n" should be considered a newline for standalone tags.'
+    data: { boolean: true }
+    template: "|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|"
+    expected: "|\r\n|"
+
+  - name: Standalone Without Previous Line
+    desc: Standalone tags should not require a newline to precede them.
+    data: { boolean: true }
+    template: "  {{#boolean}}\n#{{/boolean}}\n/"
+    expected: "#\n/"
+
+  - name: Standalone Without Newline
+    desc: Standalone tags should not require a newline to follow them.
+    data: { boolean: true }
+    template: "#{{#boolean}}\n/\n  {{/boolean}}"
+    expected: "#\n/\n"
+
+  # Whitespace Insensitivity
+
+  - name: Padding
+    desc: Superfluous in-tag whitespace should be ignored.
+    data: { boolean: true }
+    template: '|{{# boolean }}={{/ boolean }}|'
+    expected: '|=|'
diff --git a/stache.cabal b/stache.cabal
new file mode 100644
--- /dev/null
+++ b/stache.cabal
@@ -0,0 +1,136 @@
+--
+-- Cabal configuration for ‘stache’ package.
+--
+-- Copyright © 2016 Stack Builders
+--
+-- Redistribution and use in source and binary forms, with or without
+-- modification, are permitted provided that the following conditions are
+-- met:
+--
+-- * Redistributions of source code must retain the above copyright notice,
+--   this list of conditions and the following disclaimer.
+--
+-- * Redistributions in binary form must reproduce the above copyright
+--   notice, this list of conditions and the following disclaimer in the
+--   documentation and/or other materials provided with the distribution.
+--
+-- * Neither the name Mark Karpov nor the names of contributors may be used
+--   to endorse or promote products derived from this software without
+--   specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY
+-- EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+-- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+-- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+-- STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+-- ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+-- POSSIBILITY OF SUCH DAMAGE.
+
+name:                 stache
+version:              0.1.0
+cabal-version:        >= 1.10
+license:              BSD3
+license-file:         LICENSE.md
+author:               Mark Karpov <markkarpov@openmailbox.org>
+maintainer:           Mark Karpov <markkarpov@openmailbox.org>
+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-source-files:   CHANGELOG.md
+                    , README.md
+                    , specs/comments.yml
+                    , specs/delimiters.yml
+                    , specs/interpolation.yml
+                    , specs/inverted.yml
+                    , specs/partials.yml
+                    , specs/sections.yml
+                    , templates/foo.mustache
+                    , templates/bar.mustache
+
+source-repository head
+  type:               git
+  location:           https://github.com/stackbuilders/stache.git
+
+flag dev
+  description:        Turn on development settings.
+  manual:             True
+  default:            False
+
+library
+  build-depends:      aeson            >= 0.11 && < 0.12
+                    , base             >= 4.7  && < 5.0
+                    , bytestring       >= 0.10 && < 0.11
+                    , containers       >= 0.5  && < 0.6
+                    , directory        >= 1.2  && < 1.3
+                    , exceptions       >= 0.8  && < 0.9
+                    , filepath         >= 1.2  && < 1.5
+                    , megaparsec       >= 5.0  && < 6.0
+                    , mtl              >= 2.1  && < 3.0
+                    , template-haskell >= 2.10 && < 2.12
+                    , text             >= 1.2  && < 1.3
+                    , unordered-containers >= 0.2.5 && < 0.3
+                    , vector           >= 0.11 && < 0.12
+  if !impl(ghc >= 8.0)
+    build-depends:    semigroups       == 0.18.*
+  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:      -Wall -Werror -fsimpl-tick-factor=150
+  else
+    ghc-options:      -O2 -Wall -fsimpl-tick-factor=150
+  default-language:   Haskell2010
+
+test-suite tests
+  main-is:            Spec.hs
+  hs-source-dirs:     tests
+  type:               exitcode-stdio-1.0
+  build-depends:      aeson            >= 0.11 && < 0.12
+                    , base             >= 4.7  && < 5.0
+                    , containers       >= 0.5  && < 0.6
+                    , hspec            >= 2.0  && < 3.0
+                    , hspec-megaparsec >= 0.2  && < 0.3
+                    , megaparsec       >= 5.0  && < 6.0
+                    , stache           >= 0.1.0
+                    , text             >= 1.2  && < 1.3
+  other-modules:      Text.Mustache.Compile.THSpec
+                    , Text.Mustache.ParserSpec
+                    , Text.Mustache.RenderSpec
+                    , Text.Mustache.TypeSpec
+  if !impl(ghc >= 8.0)
+    build-depends:    semigroups     == 0.18.*
+  if flag(dev)
+    ghc-options:      -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
+  default-language:   Haskell2010
+
+test-suite mustache-spec
+  main-is:            Spec.hs
+  hs-source-dirs:     mustache-spec
+  type:               exitcode-stdio-1.0
+  build-depends:      aeson            >= 0.11 && < 0.12
+                    , base             >= 4.7  && < 5.0
+                    , bytestring       >= 0.10 && < 0.11
+                    , containers       >= 0.5  && < 0.6
+                    , file-embed
+                    , hspec            >= 2.0  && < 3.0
+                    , megaparsec       >= 5.0  && < 6.0
+                    , stache           >= 0.1.0
+                    , text             >= 1.2  && < 1.3
+                    , yaml             >= 0.8  && < 0.9
+  if flag(dev)
+    ghc-options:      -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
+  default-language:   Haskell2010
diff --git a/templates/bar.mustache b/templates/bar.mustache
new file mode 100644
--- /dev/null
+++ b/templates/bar.mustache
@@ -0,0 +1,1 @@
+And this is the ‘bar’.
diff --git a/templates/foo.mustache b/templates/foo.mustache
new file mode 100644
--- /dev/null
+++ b/templates/foo.mustache
@@ -0,0 +1,1 @@
+This is the ‘foo’.
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/tests/Text/Mustache/Compile/THSpec.hs b/tests/Text/Mustache/Compile/THSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Text/Mustache/Compile/THSpec.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell   #-}
+
+module Text.Mustache.Compile.THSpec
+  ( main
+  , spec )
+where
+
+import Test.Hspec
+
+#if MIN_VERSION_template_haskell(2,11,0)
+import Data.Semigroup ((<>))
+import Text.Mustache.Type
+import qualified Data.Map                 as M
+import qualified Text.Mustache.Compile.TH as TH
+#endif
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+#if MIN_VERSION_template_haskell(2,11,0)
+spec = do
+  describe "compileMustacheText" $
+    it "compiles template from text at compile time" $
+      $(TH.compileMustacheText "foo" "This is the ‘foo’.\n")
+        `shouldBe` fooTemplate
+  describe "compileMustacheFile" $
+    it "compiles template from file at compile time" $
+      $(TH.compileMustacheFile "templates/foo.mustache")
+        `shouldBe` fooTemplate
+  describe "compileMustacheDir" $
+    it "compiles templates from directory at compile time" $
+      $(TH.compileMustacheDir "foo" "templates/")
+        `shouldBe` (fooTemplate <> barTemplate)
+
+fooTemplate :: Template
+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"]
+#else
+spec = return ()
+#endif
diff --git a/tests/Text/Mustache/ParserSpec.hs b/tests/Text/Mustache/ParserSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Text/Mustache/ParserSpec.hs
@@ -0,0 +1,104 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Mustache.ParserSpec
+  ( main
+  , spec )
+where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import Test.Hspec
+import Test.Hspec.Megaparsec
+import Text.Megaparsec
+import Text.Mustache.Parser
+import Text.Mustache.Type
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Set           as S
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative (pure)
+#endif
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec = describe "parseMustache" $ do
+  let p = parseMustache ""
+      key = Key . pure
+  it "parses text" $
+    p "test12356p0--=-34{}jnv,\n"
+      `shouldParse` [TextBlock "test12356p0--=-34{}jnv,\n"]
+  context "when parsing a variable" $ do
+    context "with white space" $ do
+      it "parses escaped {{ variable }}" $
+        p "{{ name }}" `shouldParse` [EscapedVar (key "name")]
+      it "parses unescaped {{{ variable }}}" $
+        p "{{{ name }}}" `shouldParse` [UnescapedVar (key "name")]
+      it "parses unescaped {{& variable }}" $
+        p "{{& name }}" `shouldParse` [UnescapedVar (key "name")]
+    context "without white space" $ do
+      it "parses escaped {{variable}}" $
+        p "{{name}}" `shouldParse` [EscapedVar (key "name")]
+      it "parses unescaped {{{variable}}}" $
+        p "{{{name}}}" `shouldParse` [UnescapedVar (key "name")]
+      it "parses unescaped {{& variable }}" $
+        p "{{&name}}" `shouldParse` [UnescapedVar (key "name")]
+    it "allows '-' in variable names" $
+      p "{{ var-name }}" `shouldParse` [EscapedVar (key "var-name")]
+    it "allows '_' in variable names" $
+      p "{{ var_name }}" `shouldParse` [EscapedVar (key "var_name")]
+  context "when parsing a section" $ do
+    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"]]
+  context "when parsing an inverted section" $ do
+    it "parses empty inverted 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"]]
+  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 $ unsafePos 1)]
+    it "parses a partial without white space" $
+      p "{{>that-s_my-partial}}" `shouldParse`
+        [Partial "that-s_my-partial" (Just $ unsafePos 1)]
+    it "handles indented partial correctly" $
+      p "   {{> next_one }}" `shouldParse`
+        [Partial "next_one" (Just $ unsafePos 4)]
+  context "when running into delimiter change" $ do
+    it "has effect" $
+      p "{{=<< >>=}}<<var>>{{var}}" `shouldParse`
+        [EscapedVar (key "var"), TextBlock "{{var}}"]
+    it "handles whitespace just as well" $
+      p "{{=<<   >>=}}<<  var >>{{ var  }}" `shouldParse`
+        [EscapedVar (key "var"), TextBlock "{{ var  }}"]
+    it "parses two subsequent delimiter changes" $
+      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")]
+  context "when given malformed input" $ do
+    let pos l c = SourcePos "" (unsafePos l) (unsafePos c) :| []
+        ne      = NE.fromList
+    it "rejects unclosed tags" $
+      p "{{ name " `shouldFailWith` ParseError
+        { errorPos        = pos 1 9
+        , errorUnexpected = S.singleton EndOfInput
+        , errorExpected   = S.singleton (Tokens $ ne "}}")
+        , errorCustom     = S.empty }
+    it "rejects unknown tags" $
+      p "{{? boo }}" `shouldFailWith` ParseError
+        { errorPos        = pos 1 3
+        , errorUnexpected = S.singleton (Tokens $ ne "?")
+        , errorExpected   = S.singleton (Label  $ ne "key")
+        , errorCustom     = S.empty }
diff --git a/tests/Text/Mustache/RenderSpec.hs b/tests/Text/Mustache/RenderSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Text/Mustache/RenderSpec.hs
@@ -0,0 +1,109 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Mustache.RenderSpec
+  ( main
+  , spec )
+where
+
+import Data.Aeson (object, KeyValue (..), Value (..))
+import Data.Text (Text)
+import Test.Hspec
+import Text.Megaparsec
+import Text.Mustache.Render
+import Text.Mustache.Type
+import qualified Data.Map as M
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative (pure)
+#endif
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec = describe "renderMustache" $ do
+  let r ns value =
+        let template = Template "test" (M.singleton "test" ns)
+        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")]
+      (object ["foo" .= ("<html>&\"something\"</html>" :: Text)])
+      `shouldBe` "&lt;html&gt;&amp;&quot;something&quot;&lt;/html&gt;"
+  it "renders unescaped variables “as is”" $
+    r [UnescapedVar (key "foo")]
+      (object ["foo" .= ("<html>&\"something\"</html>" :: Text)])
+      `shouldBe` "<html>&\"something\"</html>"
+  context "when rendering a section" $ do
+    let nodes = [Section (key "foo") [UnescapedVar (key "bar"), TextBlock "*"]]
+    context "when the key is not present" $
+      it "renders nothing" $
+        r nodes (object []) `shouldBe` ""
+    context "when the key is present" $ do
+      context "when the key is a “false” value" $ do
+        it "skips the Null value" $
+          r nodes (object ["foo" .= Null]) `shouldBe` ""
+        it "skips false Boolean" $
+          r nodes (object ["foo" .= False]) `shouldBe` ""
+        it "skips empty list" $
+          r nodes (object ["foo" .= ([] :: [Text])]) `shouldBe` ""
+        it "skips empty object" $
+          r nodes (object ["foo" .= object []]) `shouldBe` ""
+      context "when the key is a Boolean true" $
+        it "renders the section without interpolation" $
+          r [Section (key "foo") [TextBlock "brr"]]
+            (object ["foo" .= object ["bar" .= True]])
+            `shouldBe` "brr"
+      context "when the key is an object" $
+        it "uses it to render section once" $
+          r nodes (object ["foo" .= object ["bar" .= ("huh?" :: Text)]])
+            `shouldBe` "huh?*"
+      context "when the key is a singleton list" $
+        it "uses it to render section once" $
+          r nodes (object ["foo" .= object ["bar" .= ("huh!" :: Text)]])
+            `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"]]
+            (object ["foo" .= [True, True]])
+            `shouldBe` "brrbrr"
+      context "wehn 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]]])
+            `shouldBe` "1*2*3*4*"
+  context "when rendering an inverted section" $ do
+    let nodes = [InvertedSection (key "foo") [TextBlock "Here!"]]
+    context "when the key is not present" $
+      it "renders the inverse section" $
+        r nodes (object []) `shouldBe` "Here!"
+    context "when the key is present" $ do
+      context "when the key is a “false” value" $ do
+        it "renders with Null value" $
+          r nodes (object ["foo" .= Null]) `shouldBe` "Here!"
+        it "renders with false Boolean" $
+          r nodes (object ["foo" .= False]) `shouldBe` "Here!"
+        it "renders with empty list" $
+          r nodes (object ["foo" .= ([] :: [Text])]) `shouldBe` "Here!"
+        it "renders with empty object" $
+          r nodes (object ["foo" .= object []]) `shouldBe` "Here!"
+      context "when the key is a “true” value" $ do
+        it "skips true Boolean" $
+          r nodes (object ["foo" .= True]) `shouldBe` ""
+        it "skips non-empty object" $
+          r nodes (object ["foo" .= object ["bar" .= True]]) `shouldBe` ""
+        it "skips non-empty list" $
+          r nodes (object ["foo" .= [True]]) `shouldBe` ""
+  context "when rendering a partial" $ do
+    let nodes = [ Partial "partial" (Just $ unsafePos 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*"
diff --git a/tests/Text/Mustache/TypeSpec.hs b/tests/Text/Mustache/TypeSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Text/Mustache/TypeSpec.hs
@@ -0,0 +1,38 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Mustache.TypeSpec
+  ( main
+  , spec )
+where
+
+import Data.Semigroup ((<>))
+import Test.Hspec
+import Text.Mustache.Type
+import qualified Data.Map as M
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec =
+  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"]) ]
+
+templateA :: Template
+templateA = Template "a" $ M.fromList
+  [ ("c", [TextBlock "foo"])
+  , ("d", [TextBlock "bar"]) ]
+
+templateB :: Template
+templateB = Template "b" $ M.fromList
+  [ ("c", [TextBlock "bar"])
+  , ("e", [TextBlock "baz"]) ]
