diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2015 Tobias Dammers
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/cli/GingerCLI.hs b/cli/GingerCLI.hs
new file mode 100644
--- /dev/null
+++ b/cli/GingerCLI.hs
@@ -0,0 +1,111 @@
+-- | An example Ginger CLI application.
+--
+-- Takes two optional arguments; the first one is a template file, the second
+-- one a file containing some context data in JSON format.
+{-#LANGUAGE OverloadedStrings #-}
+{-#LANGUAGE ScopedTypeVariables #-}
+module Main where
+
+import Text.Ginger
+import Text.Ginger.Html
+import Data.Text as Text
+import qualified Data.Aeson as JSON
+import Data.Maybe
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+import Control.Applicative
+import System.Environment ( getArgs )
+import System.IO
+import System.IO.Error
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as LBS
+import Control.Monad.Trans.Class ( lift )
+import Control.Monad.Trans.Maybe
+import Control.Monad
+import Data.Default ( def )
+
+loadFile fn = openFile fn ReadMode >>= hGetContents
+
+loadFileMay fn =
+    tryIOError (loadFile fn) >>= \e ->
+         case e of
+            Right contents -> return (Just contents)
+            Left err -> do
+                print err
+                return Nothing
+
+decodeFile :: (JSON.FromJSON v) => FilePath -> IO (Maybe v)
+decodeFile fn = JSON.decode <$> (openFile fn ReadMode >>= LBS.hGetContents)
+
+printF :: GVal (Run IO)
+printF = fromFunction $ go
+    where
+        go :: [(Maybe Text, GVal (Run IO))] -> Run IO (GVal (Run IO))
+        go args = forM_ args printArg >> return def
+        printArg (Nothing, v) = liftRun . putStrLn . Text.unpack . asText $ v
+        printArg (Just x, _) = return ()
+
+main = do
+    args <- getArgs
+    let (srcFn, scopeFn) = case args of
+            [] -> (Nothing, Nothing)
+            a:[] -> (Just a, Nothing)
+            a:b:[] -> (Just a, Just b)
+
+    scope <- case scopeFn of
+        Nothing -> return Nothing
+        Just fn -> (decodeFile fn :: IO (Maybe (HashMap Text JSON.Value)))
+
+    let scopeLookup key = toGVal (scope >>= HashMap.lookup key)
+        resolve = loadFileMay
+    let contextLookup :: Text -> Run IO (GVal (Run IO))
+        contextLookup key =
+            case key of
+                "print" -> return printF
+                _ -> return $ scopeLookup key
+
+    (tpl, src) <- case srcFn of
+            Just fn -> (,) <$> parseGingerFile resolve fn <*> return Nothing
+            Nothing -> getContents >>= \s -> (,) <$> parseGinger resolve Nothing s <*> return (Just s)
+
+    -- TODO: do some sort of arg parsing thing so that we can turn
+    -- template dumping on or off.
+    -- print tpl
+
+    case tpl of
+        Left err -> do
+            printParserError err
+            tplSource <- case src of
+                            Just s -> return s
+                            Nothing -> do
+                                let s = peSourceName err
+                                case s of
+                                    Nothing -> return ""
+                                    Just sn -> loadFile sn
+            displayParserError tplSource err
+        Right t -> runGingerT (makeContextM contextLookup (putStr . Text.unpack . htmlSource)) t
+
+printParserError :: ParserError -> IO ()
+printParserError = putStrLn . formatParserError
+
+formatParserError :: ParserError -> String
+formatParserError pe = Prelude.concat
+    [ fromMaybe "<<unknown source>>" . peSourceName $ pe
+    , ":"
+    , fromMaybe "" $ (++ ":") . show <$> peSourceLine pe
+    , fromMaybe "" $ (++ ":") . show <$> peSourceColumn pe
+    , peErrorMessage pe ]
+
+displayParserError :: String -> ParserError -> IO ()
+displayParserError src pe = do
+    case (peSourceLine pe, peSourceColumn pe) of
+        (Just l, cMay) -> do
+            let ln = Prelude.take 1 . Prelude.drop (l - 1) . Prelude.lines $ src
+            case ln of
+                [] -> return ()
+                x:_ -> do
+                    putStrLn x
+                    case cMay of
+                        Just c -> putStrLn $ Prelude.replicate (c - 1) ' ' ++ "^"
+                        _ -> return ()
+        _ -> return ()
diff --git a/ginger.cabal b/ginger.cabal
new file mode 100644
--- /dev/null
+++ b/ginger.cabal
@@ -0,0 +1,57 @@
+-- Initial ginger.cabal generated by cabal init.  For further 
+-- documentation, see http://haskell.org/cabal/users-guide/
+
+name:                ginger
+version:             0.1.0.0
+synopsis:            An implementation of the Jinja2 template language in Haskell
+-- description:         
+homepage:            https://bitbucket.org/tdammers/ginger
+license:             MIT
+license-file:        LICENSE
+author:              Tobias Dammers
+maintainer:          tdammers@gmail.com
+-- copyright:           
+category:            Text
+build-type:          Simple
+-- extra-source-files:  
+cabal-version:       >=1.10
+
+library
+  exposed-modules: Text.Ginger
+                 , Text.Ginger.AST
+                 , Text.Ginger.GVal
+                 , Text.Ginger.Html
+                 , Text.Ginger.Parse
+                 , Text.Ginger.Run
+  -- other-modules:       
+  -- other-extensions:    
+  build-depends: base >=4.5 && <5
+               , aeson
+               , bytestring
+               , data-default >= 0.5
+               , filepath >= 1.3
+               , mtl >= 2.2
+               , parsec >= 3.0
+               , safe >= 0.3
+               , scientific >= 0.3
+               , text
+               , transformers >= 0.3
+               , unordered-containers >= 0.2.5
+               , vector
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  GHC-options: -O0
+
+executable ginger
+    main-is: GingerCLI.hs
+    hs-source-dirs: cli
+    default-language:    Haskell2010
+    build-depends: base >= 4.5 && <5
+                 , bytestring
+                 , data-default >= 0.5
+                 , ginger
+                 , aeson
+                 , text
+                 , transformers
+                 , unordered-containers >= 0.2.5
+    GHC-options: -O0
diff --git a/src/Text/Ginger.hs b/src/Text/Ginger.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Ginger.hs
@@ -0,0 +1,239 @@
+{-|
+A Haskell implementation of the <http://jinja.pocoo.org/ Jinja2> template
+language.
+
+Ginger aims to be as close to the original Jinja language as possible, but
+avoiding blatant pythonisms and features that make little sense outside of
+an impure dynamic host language context, especially when this would require
+sacrificing runtime performance.
+-}
+
+module Text.Ginger
+(
+-- * Template Syntax
+-- ** Minimal example template
+-- | > <!DOCTYPE html>
+--   > <html>
+--   >     <head>
+--   >         <title>{{ title }}</title>
+--   >     </head>
+--   >     {# This is a comment. Comments are removed from the output. #}
+--   >     <body>
+--   >         <menu id="nav-main">
+--   >         {% for item in navigation %}
+--   >             <li><a href="{{ item.url }}">{{ item.label }}</a></li>
+--   >         {% endfor %}
+--   >         </menu>
+--   >         <div class="layout-content-main">
+--   >             <h1>{{ title }}</h1>
+--   >             {{ body }}
+--   >         </div>
+--   >     </body>
+--   > </html>
+
+-- There are two kinds of delimiters. `{% ... %}` and `{{ ... }}`. The first
+-- one is used to execute statements such as for-loops or assign values, the
+-- latter prints the result of an expression to the template.
+
+-- /Not implemented yet/: Jinja2 allows the programmer to override the default
+-- tags from @{% %}@ and @{{ }}@ to different tokens, e.g. @\<% %\>@ and @\<\<
+-- \>\>@.  Ginger does not currently support this.
+
+-- ** Variables
+-- | You can mess around with the variables in templates provided they are
+-- passed in by the application. Variables may have attributes or elements
+-- on them you can access too. What attributes a variable has depends
+-- heavily on the application providing that variable.
+
+-- | You can use a dot (@.@) to access attributes of a variable, but
+-- alternatively the so-called “subscript” syntax (@[]@) can be used. The
+-- following lines do the same thing:
+
+-- | @
+-- {{ foo.bar }}
+-- {{ foo['bar'] }}
+-- @
+
+-- | It’s important to know that the curly braces are /not/ part of the
+-- variable, but the print statement. If you access variables inside tags
+-- don’t put the braces around them.
+
+-- | If a variable or attribute does not exist you will get back an undefined
+-- value. What you can do with that kind of value depends on the
+-- application configuration: the default behavior is that it evaluates to
+-- an empty string if printed and that you can iterate over it, but every
+-- other operation fails.
+
+-- ** Expressions
+-- | Variables aren't the only thing that can go inside @{{ ... }}@; any valid
+-- Ginger expression can be used, and expressions can be constructed in many
+-- different ways. Note that all expressions are case sensitive: @null@ and
+-- @Null@ are not the same thing.
+-- Currently, the following constructs are available:
+
+-- *** Simple (\"Atomic\") Expressions
+-- **** Variable reference
+-- | Look up a variable in the current scope and return its value.
+
+-- | > {{ username }}
+
+-- **** String literals
+-- | A constant string. Strings can be single- or double-quoted.
+
+-- | > {{ "Hello, world!" }}
+
+-- **** Numeric literals
+-- | Numeric literals can be given in integer or decimal format:
+
+-- | @
+-- {{ 21 }}
+-- {{ 44.5 }}
+-- @
+
+-- **** Boolean literals
+-- | There are two boolean values, @true@ and @false@. You will not normally
+-- need to use them, as they are produced by boolean expressions such as
+-- comparisons, but they can be useful occasionally.
+
+-- | @
+-- {{ true }}
+-- {{ false }}
+-- @
+
+-- **** The Null literal
+-- | Represents the special /null/ value, which is used to signal the absence
+-- of a result or value. Looking up non-existent scope variables, for example,
+-- will produce Null.
+
+-- | > {{ null }}
+
+-- *** Data Structures
+-- **** List Literals
+-- | A list literal consists of a comma-separated list of expressions between
+-- square brackets:
+
+-- | > {{ [ foo, "bar", 1234 ] }}
+
+-- | Lists can contain any kind of expression, including other lists, so you can
+-- nest them:
+
+-- | > {{ [ foo, [ 1, 2, 3 ], "baz" ] }}
+
+-- **** Object Literals
+-- | An object is an unsorted key/value container, declared like this:
+
+-- | > {{ { "foo": "bar", "baz": "quux" } }}
+
+-- | Objects, like lists, can contain any kind of value. The keys, however, are
+-- restricted to strings; you can define them using any expression you like,
+-- but they are always converted to strings when inserting items into the
+-- container. This means that the following is valid:
+
+-- | > {{ { ["foo", 1]: "bar" } }}
+
+-- | It will, however, produce the same object as the following:
+
+-- | > {{ { "foo1": "bar" } }}
+
+-- | That's because converting @[\"foo\", 1]@ to a string produces @\"foo1\"@, and
+-- that is used as the key.
+
+-- *** Parentheses
+-- | Parentheses can be used to group expression constructs to override default
+-- precedence. Excess parentheses are simply ignored.
+
+-- *** Unary operators
+-- **** Function calls
+-- | When an expression evaluates to a function, you can call it using a list
+-- of arguments between parentheses. Most of the time, the function itself just
+-- comes from a scope variable, so a function call typically looks like this:
+
+-- | > {{ print(x, "hello!") }}
+
+-- | However, anything that returns a function can be called as a function:
+
+-- | > {{ system.console['log']("hello!") }}
+
+-- **** Filter expressions
+-- | Any function that takes at least one argument can be called through the
+-- alternative filter syntax instead:
+
+-- | > {{ x|print }}
+
+-- | This is equivalent to:
+
+-- | > {{ print(x) }}
+
+-- | Filters can take arguments:
+
+-- | > {{ x|append("foobar") }}
+
+-- | This is equivalent to:
+
+-- | > {{ append(x, "foobar") }}
+
+-- | /Deviation from Jinja2:/ Ginger does not distinguish between filters and
+-- functions at the semantics level; any function can be called as a filter,
+-- and vv., and the filter syntax is merely a syntactic variant of a function
+-- call.
+
+-- * Haskell API
+-- ** General
+-- | On the Haskell side of things, executing a template is a two-step process.
+-- First, template source code is parsed into a 'Template' data structure,
+-- which is then fed to 'runGinger' or 'runGingerT'.
+
+-- **  Parsing
+-- | Because Ginger templates can include other templates, the parser needs a way of
+-- resolving template names. Instead of hard-wiring the parser into 'IO' though,
+-- Ginger will work on any Monad type, but requires the caller to provide a
+-- suitable template resolver function. For 'IO', the resolver would typically
+-- load a file from a template directory, but other monads might have access to
+-- some sort of cache, or expose template compiled into a program, or simply
+-- return 'Nothing' unconditionally to disable any and all imports. A suitable
+-- example implementation for 'IO' would look like this:
+
+-- | > loadFile fn = openFile fn ReadMode >>= hGetContents
+-- > 
+-- > loadFileMay fn =
+-- >     tryIOError (loadFile fn) >>= \e ->
+-- >          case e of
+-- >             Right contents ->
+-- >                 return (Just contents)
+-- >             Left err -> do
+-- >                 print err -- remove this line if you want to fail silently
+-- >                 return Nothing
+
+-- | (Taken from @cli/GingerCLI.hs@). This interprets the template name as a
+-- filename relative to the CWD, and returns the file contents on success or
+-- 'Nothing' if there is any error.
+
+-- | If you don't need a monadic context for resolving includes (e.g. because you
+-- have pre-loaded all template sources), you can use the pure 'parseGinger'
+-- flavor, which does not rely on a host monad.
+  module Text.Ginger.Parse
+-- ** Running
+-- | The core function for running a template is 'runGinger' (or its monadic
+-- flavor 'runGingerT'); in order to pass an initial context to the template
+-- engine, pass a suitable 'GingerContext', which you can create using the
+-- 'makeContext' / 'makeContextM' functions.
+
+-- | An example call (for running a template in 'IO') would look something like
+-- this:
+
+-- > runGingerT (makeContextM scopeLookup (putStr . Text.unpack . htmlSource)) tpl
+, module Text.Ginger.Run
+-- ** Other concerns
+-- | Ginger's unitype value
+, module Text.Ginger.GVal
+-- | The data structures used to represent templates, statements and
+-- expressions internally.
+, module Text.Ginger.AST
+)
+where
+import Text.Ginger.Parse
+import Text.Ginger.AST
+import Text.Ginger.Run
+import Text.Ginger.GVal
+
+
diff --git a/src/Text/Ginger/AST.hs b/src/Text/Ginger/AST.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Ginger/AST.hs
@@ -0,0 +1,61 @@
+-- | Implements Ginger's Abstract Syntax Tree.
+module Text.Ginger.AST
+where
+
+import Data.Text (Text)
+import qualified Data.Text as Text
+import Text.Ginger.Html
+import Data.Scientific (Scientific)
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+
+
+-- | A context variable name.
+type VarName = Text
+
+-- | Top-level data structure, representing a fully parsed template.
+data Template
+    = Template
+        { templateBody :: Statement
+        , templateBlocks :: HashMap VarName Block
+        , templateParent :: Maybe Template
+        }
+        deriving (Show)
+
+-- | A macro definition ( {% macro %}
+data Macro
+    = Macro { macroArgs :: [VarName], macroBody :: Statement }
+    deriving (Show)
+
+-- | A block definition ( {% block %}
+data Block
+    = Block { blockBody :: Statement } -- TODO: scoped blocks
+    deriving (Show)
+
+-- | Ginger statements.
+data Statement
+    = MultiS [Statement] -- ^ A sequence of multiple statements
+    | ScopedS Statement -- ^ Run wrapped statement in a local scope
+    | LiteralS Html -- ^ Literal output (anything outside of any tag)
+    | InterpolationS Expression -- ^ {{ expression }}
+    | IfS Expression Statement Statement -- ^ {% if expression %}statement{% else %}statement{% endif %}
+    | ForS (Maybe VarName) VarName Expression Statement -- ^ {% for index, varname in expression %}statement{% endfor %}
+    | SetVarS VarName Expression -- ^ {% set varname = expr %}
+    | DefMacroS VarName Macro -- ^ {% macro varname %}statements{% endmacro %}
+    | BlockRefS VarName
+    | PreprocessedIncludeS Template -- ^ {% include "template" %}
+    | NullS -- ^ The do-nothing statement (NOP)
+    deriving (Show)
+
+-- | Expressions, building blocks for the expression minilanguage.
+data Expression
+    = StringLiteralE Text -- ^ String literal expression: "foobar"
+    | NumberLiteralE Scientific -- ^ Numeric literal expression: 123.4
+    | BoolLiteralE Bool -- ^ Boolean literal expression: true
+    | NullLiteralE -- ^ Literal null
+    | VarE VarName -- ^ Variable reference: foobar
+    | ListE [Expression] -- ^ List construct: [ expr, expr, expr ]
+    | ObjectE [(Expression, Expression)] -- ^ Object construct: { expr: expr, expr: expr, ... }
+    | MemberLookupE Expression Expression -- ^ foo[bar] (also dot access)
+    | CallE Expression [(Maybe Text, Expression)] -- ^ foo(bar=baz, quux)
+    deriving (Show)
diff --git a/src/Text/Ginger/GVal.hs b/src/Text/Ginger/GVal.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Ginger/GVal.hs
@@ -0,0 +1,341 @@
+{-#LANGUAGE OverloadedStrings #-}
+{-#LANGUAGE MultiParamTypeClasses #-}
+{-#LANGUAGE FlexibleInstances #-}
+
+-- | GVal is a generic unitype value, representing the kind of values that
+-- Ginger can understand.
+--
+-- Most of the types in this module are parametrized over an 'm' type, which
+-- is the host monad for template execution, as passed to 'runGingerT'.
+module Text.Ginger.GVal
+where
+
+import Prelude ( (.), ($), (==), (/=)
+               , (++), (+), (-), (*), (/), div
+               , (>>=), return
+               , undefined, otherwise, id, const
+               , Maybe (..)
+               , Bool (..)
+               , Either (..)
+               , Char
+               , Int
+               , Integer
+               , Double
+               , Show, show
+               , Integral
+               , fromIntegral, floor
+               , not
+               , fst, snd
+               , Monad
+               )
+import qualified Prelude
+import qualified Data.List as List
+import Data.Maybe ( fromMaybe, catMaybes, isJust )
+import Data.Text (Text)
+import Data.String (IsString, fromString)
+import qualified Data.Text as Text
+import qualified Data.Text.Lazy as LText
+import qualified Data.List as List
+import Safe (readMay, atMay)
+import Data.Monoid
+import Data.Scientific ( Scientific
+                       , floatingOrInteger
+                       , toBoundedInteger
+                       )
+import Control.Applicative
+import qualified Data.Aeson as JSON
+import qualified Data.HashMap.Strict as HashMap
+import Data.HashMap.Strict (HashMap)
+import qualified Data.Vector as Vector
+import Control.Monad ( forM, mapM )
+import Control.Monad.Trans (MonadTrans, lift)
+import Data.Default (Default, def)
+
+import Text.Ginger.Html
+
+-- | A function that can be called from within a template execution context.
+type Function m = [(Maybe Text, GVal m)] -> m (GVal m)
+
+-- | Match arguments passed to a function at runtime against a list of declared
+-- argument names.
+-- @matchFuncArgs argNames argsPassed@ returns @(matchedArgs, positionalArgs, namedArgs)@,
+-- where @matchedArgs@ is a list of arguments matched against declared names
+-- (by name or by position), @positionalArgs@ are the unused positional
+-- (unnamed) arguments, and @namedArgs@ are the unused named arguments.
+matchFuncArgs :: [Text] -> [(Maybe Text, GVal m)] -> (HashMap Text (GVal m), [GVal m], HashMap Text (GVal m))
+matchFuncArgs names args =
+    (matched, positional, named)
+    where
+        positionalRaw = [ v | (Nothing, v) <- args ]
+        namedRaw = HashMap.fromList [ (n, v) | (Just n, v) <- args ]
+        fromPositional = Prelude.zip names positionalRaw
+        numPositional = Prelude.length fromPositional
+        namesRemaining = Prelude.drop numPositional names
+        positional = Prelude.drop numPositional positionalRaw
+        fromNamed = catMaybes $ (List.map lookupName namesRemaining)
+        lookupName n = do
+            v <- HashMap.lookup n namedRaw
+            return (n, v)
+        matched = HashMap.fromList $ fromPositional ++ fromNamed
+        named = HashMap.difference namedRaw (HashMap.fromList fromNamed)
+
+-- | Ginger value.
+data GVal m =
+    GVal
+        { asList :: Maybe [GVal m]
+        , asDictItems :: Maybe [(Text, GVal m)]
+        , asLookup :: Maybe (Text -> Maybe (GVal m))
+        , asHtml :: Html
+        , asText :: Text
+        , asBoolean :: Bool
+        , asNumber :: Maybe Scientific
+        , asFunction :: Maybe (Function m)
+        , length :: Maybe Int
+        , isNull :: Bool
+        }
+
+isList :: GVal m -> Bool
+isList = isJust . asList
+
+isDict :: GVal m -> Bool
+isDict = isJust . asDictItems
+
+-- | The default 'GVal' is equivalent to NULL.
+instance Default (GVal m) where
+    def = GVal
+            { asList = Nothing
+            , asDictItems = Nothing
+            , asLookup = Nothing
+            , asHtml = unsafeRawHtml ""
+            , asText = ""
+            , asBoolean = False
+            , asNumber = Nothing
+            , asFunction = Nothing
+            , isNull = True
+            , length = Nothing
+            }
+
+-- | Types that implement conversion to 'GVal'
+class ToGVal m a where
+    toGVal :: a -> GVal m
+
+-- | Trivial instance for 'GVal' itself
+instance ToGVal m (GVal m) where
+    toGVal = id
+
+-- | For convenience, 'Show' is implemented in a way that looks similar to
+-- JavaScript / JSON
+instance Show (GVal m) where
+    show v
+        | isNull v = "null"
+        | isJust (asFunction v) = "<<function>>"
+        | isJust (asDictItems v) = "{" <> (mconcat . List.intersperse ", " $ [ show k <> ": " <> show v | (k, v) <- fromMaybe [] (asDictItems v) ]) <> "}"
+        | isJust (asList v) = "[" <> (mconcat . List.intersperse ", " . Prelude.map show $ fromMaybe [] (asList v)) <> "]"
+        | isJust (asNumber v) =
+            case floatingOrInteger <$> asNumber v :: Maybe (Either Double Integer) of
+                Just (Left x) -> show (asNumber v)
+                Just (Right x) -> show x
+                Nothing -> ""
+        | otherwise = show $ asText v
+
+-- | Converting to HTML hooks into the ToHtml instance for 'Text' for most tags.
+-- Tags that have no obvious textual representation render as empty HTML.
+instance ToHtml (GVal m) where
+    toHtml = asHtml
+
+-- | Treat a 'GVal' as a flat list and look up a value by integer index.
+-- If the value is not a List, or if the index exceeds the list length,
+-- return 'Nothing'.
+lookupIndex :: Int -> GVal m -> Maybe (GVal m)
+lookupIndex = lookupIndexMay . Just
+
+-- | Helper function; look up a value by an integer index when the index may or
+-- may not be available. If no index is given, return 'Nothing'.
+lookupIndexMay :: Maybe Int -> GVal m -> Maybe (GVal m)
+lookupIndexMay i v = do
+    index <- i
+    items <- asList v
+    atMay items index
+
+lookupKey :: Text -> GVal m -> Maybe (GVal m)
+lookupKey k v = do
+    lf <- asLookup v
+    lf k
+
+-- | Loosely-typed lookup: try dictionary-style lookup first (treat index as
+-- a string, and container as a dictionary), if that doesn't yield anything
+-- (either because the index is not string-ish, or because the container
+-- doesn't provide dictionary-style access), try index-based lookup.
+lookupLoose :: GVal m -> GVal m -> Maybe (GVal m)
+lookupLoose k v =
+    lookupKey (asText k) v <|> lookupIndexMay (floor <$> asNumber k) v
+
+-- | Treat a 'GVal' as a dictionary and list all the keys, with no particular
+-- ordering.
+keys :: GVal m -> Maybe [Text]
+keys v = Prelude.map fst <$> asDictItems v
+
+-- | Convert a 'GVal' to a number.
+toNumber :: GVal m -> Maybe Scientific
+toNumber = asNumber
+
+-- | Convert a 'GVal' to an 'Int'.
+-- The conversion will fail when the value is not numeric, and also if
+-- it is too large to fit in an 'Int'.
+toInt :: GVal m -> Maybe Int
+toInt x = toNumber x >>= toBoundedInteger
+
+-- | Loose cast to boolean.
+--
+-- Numeric zero, empty strings, empty lists, empty objects, 'Null', and boolean
+-- 'False' are considered falsy, anything else (including functions) is
+-- considered true-ish.
+toBoolean :: GVal m -> Bool
+toBoolean = asBoolean
+
+-- | Dynamically cast to a function.
+-- This yields 'Just' a 'Function' if the value is a function, 'Nothing' if
+-- it's not.
+toFunction :: GVal m -> Maybe (Function m)
+toFunction = asFunction
+
+-- | Turn a 'Function' into a 'GVal'
+fromFunction :: Function m -> GVal m
+fromFunction f =
+    def
+        { asHtml = html ""
+        , asText = ""
+        , asBoolean = True
+        , isNull = False
+        , asFunction = Just f
+        }
+
+instance ToGVal m v => ToGVal m (Maybe v) where
+    toGVal Nothing = def
+    toGVal (Just x) = toGVal x
+
+instance ToGVal m v => ToGVal m [v] where
+    toGVal xs = helper (Prelude.map toGVal xs)
+        where
+            helper :: [GVal m] -> GVal m
+            helper xs =
+                def
+                    { asHtml = mconcat . Prelude.map asHtml $ xs
+                    , asText = mconcat . Prelude.map asText $ xs
+                    , asBoolean = not . List.null $ xs
+                    , isNull = False
+                    , asList = Just $ Prelude.map toGVal xs
+                    , length = Just $ Prelude.length xs
+                    }
+
+instance ToGVal m v => ToGVal m (HashMap Text v) where
+    toGVal xs = helper (HashMap.map toGVal xs)
+        where
+            helper :: HashMap Text (GVal m) -> GVal m
+            helper xs =
+                def
+                    { asHtml = mconcat . Prelude.map asHtml . HashMap.elems $ xs
+                    , asText = mconcat . Prelude.map asText . HashMap.elems $ xs
+                    , asBoolean = not . HashMap.null $ xs
+                    , isNull = False
+                    , asLookup = Just (\v -> HashMap.lookup v xs)
+                    , asDictItems = Just $ HashMap.toList xs
+                    }
+
+instance ToGVal m Int where
+    toGVal x =
+        def
+            { asHtml = html . Text.pack . show $ x
+            , asText = Text.pack . show $ x
+            , asBoolean = x /= 0
+            , asNumber = Just . fromIntegral $ x
+            , isNull = False
+            }
+
+instance ToGVal m Integer where
+    toGVal x =
+        def
+            { asHtml = html . Text.pack . show $ x
+            , asText = Text.pack . show $ x
+            , asBoolean = x /= 0
+            , asNumber = Just . fromIntegral $ x
+            , isNull = False
+            }
+
+instance ToGVal m Scientific where
+    toGVal x =
+        def
+            { asHtml = html $ scientificToText x
+            , asText = scientificToText x
+            , asBoolean = x /= 0
+            , asNumber = Just x
+            , isNull = False
+            }
+
+scientificToText :: Scientific -> Text
+scientificToText x =
+    Text.pack $ case floatingOrInteger x of
+        Left x -> show x
+        Right x -> show x
+
+instance ToGVal m Bool where
+    toGVal x =
+        def
+            { asHtml = if x then html "1" else html ""
+            , asText = if x then "1" else ""
+            , asBoolean = x
+            , asNumber = Just $ if x then 1 else 0
+            , isNull = False
+            }
+
+instance IsString (GVal m) where
+    fromString x =
+        def
+            { asHtml = html . Text.pack $ x
+            , asText = Text.pack x
+            , asBoolean = not $ Prelude.null x
+            , asNumber = readMay x
+            , isNull = False
+            , length = Just . Prelude.length $ x
+            }
+
+instance ToGVal m Text where
+    toGVal x =
+        def
+            { asHtml = html x
+            , asText = x
+            , asBoolean = not $ Text.null x
+            , asNumber = readMay . Text.unpack $ x
+            , isNull = False
+            }
+
+instance ToGVal m LText.Text where
+    toGVal x =
+        def
+            { asHtml = html (LText.toStrict x)
+            , asText = LText.toStrict x
+            , asBoolean = not $ LText.null x
+            , asNumber = readMay . LText.unpack $ x
+            , isNull = False
+            }
+
+instance ToGVal m Html where
+    toGVal x =
+        def
+            { asHtml = x
+            , asText = htmlSource x
+            , asBoolean = not . Text.null . htmlSource $ x
+            , asNumber = readMay . Text.unpack . htmlSource $ x
+            , isNull = False
+            }
+
+-- | Convert Aeson 'Value's to 'GVal's over an arbitrary host monad. Because
+-- JSON cannot represent functions, this conversion will never produce a
+-- 'Function'.
+instance ToGVal m JSON.Value where
+    toGVal (JSON.Number n) = toGVal n
+    toGVal (JSON.String s) = toGVal s
+    toGVal (JSON.Bool b) = toGVal b
+    toGVal (JSON.Null) = def
+    toGVal (JSON.Array a) = toGVal $ Vector.toList a
+    toGVal (JSON.Object o) = toGVal o
diff --git a/src/Text/Ginger/Html.hs b/src/Text/Ginger/Html.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Ginger/Html.hs
@@ -0,0 +1,64 @@
+-- | A HTML type, useful for implementing type-safe conversion between plain
+-- text and HTML.
+-- The HTML representation used here assumed Unicode throughout, and UTF-8
+-- should be used as the encoding when sending @Html@ objects as responses to a
+-- HTTP client.
+{-#LANGUAGE GeneralizedNewtypeDeriving #-}
+{-#LANGUAGE OverloadedStrings #-}
+{-#LANGUAGE FlexibleInstances #-}
+module Text.Ginger.Html
+( Html
+, unsafeRawHtml
+, html
+, htmlSource
+, ToHtml (..)
+)
+where
+
+import Data.Text (Text)
+import qualified Data.Text as Text
+import Data.Monoid
+
+-- | A chunk of HTML source.
+newtype Html = Html { unHtml :: Text }
+    deriving (Monoid, Show, Eq, Ord)
+
+-- | Types that support conversion to HTML.
+class ToHtml s where
+    toHtml :: s -> Html
+
+-- | Text is automatically HTML-encoded
+instance ToHtml Text where
+    toHtml = html
+
+-- | String is automatically HTML-encoded and converted to 'Text'
+instance ToHtml [Char] where
+    toHtml = mconcat . map htmlEncodeChar
+
+-- | Html itself is a trivial instance
+instance ToHtml Html where
+    toHtml = id
+
+-- | Extract HTML source code from an @Html@ value.
+htmlSource :: Html -> Text
+htmlSource = unHtml
+
+-- | Convert a chunk of HTML source code into an @Html@ value as-is. Note that
+-- this bypasses any and all HTML encoding; the caller is responsible for
+-- taking appropriate measures against XSS and other potential vulnerabilities.
+-- In other words, the input to this function is considered pre-sanitized.
+unsafeRawHtml :: Text -> Html
+unsafeRawHtml = Html
+
+-- | Safely convert plain text to HTML.
+html :: Text -> Html
+html = mconcat . map htmlEncodeChar . Text.unpack
+
+-- | HTML-encode an individual character.
+htmlEncodeChar :: Char -> Html
+htmlEncodeChar '&' = Html "&amp;"
+htmlEncodeChar '"' = Html "&quot;"
+htmlEncodeChar '\'' = Html "&apos;"
+htmlEncodeChar '<' = Html "&lt;"
+htmlEncodeChar '>' = Html "&gt;"
+htmlEncodeChar c = Html $ Text.singleton c
diff --git a/src/Text/Ginger/Parse.hs b/src/Text/Ginger/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Ginger/Parse.hs
@@ -0,0 +1,633 @@
+{-#LANGUAGE TupleSections #-}
+{-#LANGUAGE OverloadedStrings #-}
+{-#LANGUAGE ScopedTypeVariables #-}
+-- | Ginger parser.
+module Text.Ginger.Parse
+( parseGinger
+, parseGingerFile
+, ParserError (..)
+, IncludeResolver
+, Source, SourceName
+)
+where
+
+import Text.Parsec ( ParseError
+                   , sourceLine
+                   , sourceColumn
+                   , sourceName
+                   , ParsecT
+                   , runParserT
+                   , try, lookAhead
+                   , manyTill, oneOf, string, notFollowedBy, between, sepBy
+                   , eof, spaces, anyChar, char
+                   , option
+                   , unexpected
+                   , digit
+                   , getState, modifyState
+                   , (<?>)
+                   )
+import Text.Parsec.Error ( errorMessages
+                         , errorPos
+                         , showErrorMessages
+                         )
+import Text.Ginger.AST
+import Text.Ginger.Html ( unsafeRawHtml )
+
+import Control.Monad.Reader ( ReaderT
+                            , runReaderT
+                            , ask, asks
+                            )
+import Control.Monad.Trans.Class ( lift )
+import Control.Applicative
+import Safe ( readMay )
+
+import Data.Text (Text)
+import Data.Maybe ( fromMaybe )
+import Data.Scientific ( Scientific )
+import qualified Data.Text as Text
+import Data.List ( foldr, nub, sort )
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+
+import System.FilePath ( takeDirectory, (</>) )
+
+-- | Input type for the parser (source code).
+type Source = String
+
+-- | A source identifier (typically a filename).
+type SourceName = String
+
+-- | Used to resolve includes. Ginger will call this function whenever it
+-- encounters an {% include %}, {% import %}, or {% extends %} directive.
+-- If the required source code is not available, the resolver should return
+-- @Nothing@, else @Just@ the source.
+type IncludeResolver m = SourceName -> m (Maybe Source)
+
+
+-- | Error information for Ginger parser errors.
+data ParserError =
+    ParserError
+        { peErrorMessage :: String -- ^ Human-readable error message
+        , peSourceName :: Maybe SourceName -- ^ Source name, if any
+        , peSourceLine :: Maybe Int -- ^ Line number, if available
+        , peSourceColumn :: Maybe Int -- ^ Column number, if available
+        }
+        deriving (Show)
+
+-- | Helper function to create a Ginger parser error from a Parsec error.
+fromParsecError :: ParseError -> ParserError
+fromParsecError e =
+    let pos = errorPos e
+        sourceFilename =
+            let sn = sourceName pos
+            in if null sn then Nothing else Just sn
+    in ParserError
+        (dropWhile (== '\n') .
+            showErrorMessages
+            "or"
+            "unknown parse error"
+            "expecting"
+            "unexpected"
+            "end of input"
+            $ errorMessages e)
+        sourceFilename
+        (Just $ sourceLine pos)
+        (Just $ sourceColumn pos)
+-- | Parse Ginger source from a file.
+parseGingerFile :: Monad m => IncludeResolver m -> SourceName -> m (Either ParserError Template)
+parseGingerFile resolve fn = do
+    srcMay <- resolve fn
+    case srcMay of
+        Nothing -> return . Left $
+            ParserError
+                { peErrorMessage = "Template source not found: " ++ fn
+                , peSourceName = Nothing
+                , peSourceLine = Nothing
+                , peSourceColumn = Nothing
+                }
+        Just src -> parseGinger resolve (Just fn) src
+
+
+data ParseContext m
+    = ParseContext
+        { pcResolve :: IncludeResolver m
+        , pcCurrentSource :: Maybe SourceName
+        }
+
+data ParseState
+    = ParseState
+        { psBlocks :: HashMap VarName Block
+        }
+
+defParseState :: ParseState
+defParseState =
+    ParseState
+        { psBlocks = HashMap.empty
+        }
+
+-- | Parse Ginger source from memory.
+parseGinger :: Monad m => IncludeResolver m -> Maybe SourceName -> Source -> m (Either ParserError Template)
+parseGinger resolve sn src = do
+    result <- runReaderT (runParserT (templateP `before` eof) defParseState (fromMaybe "<<unknown>>" sn) src) (ParseContext resolve sn)
+    case result of
+        Right t -> return . Right $ t
+        Left e -> return . Left $ fromParsecError e
+
+type Parser m a = ParsecT String ParseState (ReaderT (ParseContext m) m) a
+
+ignore :: Monad m => m a -> m ()
+ignore = (>> return ())
+
+getResolver :: Monad m => Parser m (IncludeResolver m)
+getResolver = asks pcResolve
+
+include :: Monad m => SourceName -> Parser m Statement
+include sourceName = PreprocessedIncludeS <$> includeTemplate sourceName
+
+-- include sourceName = templateBody <$> includeTemplate sourceName
+
+includeTemplate :: Monad m => SourceName -> Parser m Template
+includeTemplate sourceName = do
+    resolver <- getResolver
+    currentSource <- fromMaybe "" <$> asks pcCurrentSource
+    let includeSourceName = takeDirectory currentSource </> sourceName
+    pres <- lift . lift $ parseGingerFile resolver includeSourceName
+    case pres of
+        Right t -> return t
+        Left err -> fail (show err)
+
+reduceStatements :: [Statement] -> Statement
+reduceStatements [] = NullS
+reduceStatements (x:[]) = x
+reduceStatements xs = MultiS xs
+
+templateP :: Monad m => Parser m Template
+templateP = derivedTemplateP <|> baseTemplateP
+
+derivedTemplateP :: Monad m => Parser m Template
+derivedTemplateP = do
+    parentName <- try (spaces >> fancyTagP "extends" stringLiteralP)
+    parentTemplate <- includeTemplate parentName
+    blocks <- HashMap.fromList <$> many blockP
+    return $ Template { templateBody = NullS, templateParent = Just parentTemplate, templateBlocks = blocks }
+
+baseTemplateP :: Monad m => Parser m Template
+baseTemplateP = do
+    body <- statementsP
+    blocks <- psBlocks <$> getState
+    return $ Template { templateBody = body, templateParent = Nothing, templateBlocks = blocks }
+
+statementsP :: Monad m => Parser m Statement
+statementsP = do
+    reduceStatements . filter (not . isNullS) <$> many (try statementP)
+    where
+        isNullS NullS = True
+        isNullS _ = False
+
+statementP :: Monad m => Parser m Statement
+statementP = interpolationStmtP
+           <|> commentStmtP
+           <|> ifStmtP
+           <|> setStmtP
+           <|> forStmtP
+           <|> includeP
+           <|> macroStmtP
+           <|> blockStmtP
+           <|> callStmtP
+           <|> scopeStmtP
+           <|> literalStmtP
+
+interpolationStmtP :: Monad m => Parser m Statement
+interpolationStmtP = do
+    try $ string "{{"
+    spaces
+    expr <- expressionP
+    spaces
+    string "}}"
+    return $ InterpolationS expr
+
+literalStmtP :: Monad m => Parser m Statement
+literalStmtP = do
+    txt <- manyTill anyChar endOfLiteralP
+
+    case txt of
+        [] -> unexpected "{{"
+        _ -> return . LiteralS . unsafeRawHtml . Text.pack $ txt
+
+endOfLiteralP :: Monad m => Parser m ()
+endOfLiteralP =
+    (ignore . lookAhead . try . string $ "{{") <|>
+    (ignore . lookAhead $ openTagP) <|>
+    (ignore . lookAhead $ openCommentP) <|>
+    eof
+
+commentStmtP :: Monad m => Parser m Statement
+commentStmtP = do
+    try $ openCommentP
+    manyTill anyChar (try $ closeCommentP)
+    return NullS
+
+ifStmtP :: Monad m => Parser m Statement
+ifStmtP = do
+    condExpr <- fancyTagP "if" expressionP
+    trueStmt <- statementsP
+    falseStmt <- option NullS $ do
+        try $ simpleTagP "else"
+        statementsP
+    simpleTagP "endif"
+    return $ IfS condExpr trueStmt falseStmt
+
+setStmtP :: Monad m => Parser m Statement
+setStmtP = fancyTagP "set" setStmtInnerP
+
+setStmtInnerP :: Monad m => Parser m Statement
+setStmtInnerP = do
+    name <- identifierP
+    spaces
+    char '='
+    spaces
+    val <- expressionP
+    spaces
+    return $ SetVarS name val
+
+defineBlock :: VarName -> Block -> ParseState -> ParseState
+defineBlock name block s =
+    s { psBlocks = HashMap.insert name block (psBlocks s) }
+
+blockStmtP :: Monad m => Parser m Statement
+blockStmtP = do
+    (name, block) <- blockP
+    modifyState (defineBlock name block)
+    return $ BlockRefS name
+
+blockP :: Monad m => Parser m (VarName, Block)
+blockP = do
+    name <- fancyTagP "block" identifierP
+    body <- statementsP
+    fancyTagP "endblock" (optional $ string (Text.unpack name) >> spaces)
+    return (name, Block body)
+
+macroStmtP :: Monad m => Parser m Statement
+macroStmtP = do
+    (name, args) <- try $ fancyTagP "macro" macroHeadP
+    body <- statementsP
+    fancyTagP "endmacro" (optional $ string (Text.unpack name) >> spaces)
+    return $ DefMacroS name (Macro args body)
+
+macroHeadP :: Monad m => Parser m (VarName, [VarName])
+macroHeadP = do
+    name <- identifierP
+    spaces
+    args <- option [] $ groupP "(" ")" identifierP
+    spaces
+    return (name, args)
+
+-- {% call (foo) bar(baz) %}quux{% endcall %}
+--
+-- is the same as:
+--
+-- {% scope %}
+-- {% macro __lambda(foo) %}quux{% endmacro %}
+-- {% set caller = __lambda %}
+-- {{ bar(baz) }}
+-- {% endscope %]
+callStmtP :: Monad m => Parser m Statement
+callStmtP = do
+    (callerArgs, call) <- try $ fancyTagP "call" callHeadP
+    body <- statementsP
+    simpleTagP "endcall"
+    return (
+        ScopedS (
+            MultiS [ DefMacroS "caller" (Macro callerArgs body)
+                   , InterpolationS call
+                   ]))
+
+callHeadP :: Monad m => Parser m ([Text], Expression)
+callHeadP = do
+    callerArgs <- option [] $ groupP "(" ")" identifierP
+    spaces
+    call <- expressionP
+    spaces
+    return (callerArgs, call)
+
+scopeStmtP :: Monad m => Parser m Statement
+scopeStmtP =
+    ScopedS <$>
+        between
+            (simpleTagP "scope")
+            (simpleTagP "endscope")
+            statementsP
+
+forStmtP :: Monad m => Parser m Statement
+forStmtP = do
+    (iteree, varNameVal, varNameIndex) <- fancyTagP "for" forHeadP
+    body <- statementsP
+    simpleTagP "endfor"
+    return $ ForS varNameIndex varNameVal iteree body
+
+includeP :: Monad m => Parser m Statement
+includeP = do
+    sourceName <- fancyTagP "include" stringLiteralP
+    include sourceName
+
+forHeadP :: Monad m => Parser m (Expression, VarName, Maybe VarName)
+forHeadP = try forHeadInP <|> forHeadAsP
+
+forIteratorP :: Monad m => Parser m (VarName, Maybe VarName)
+forIteratorP = try forIndexedIteratorP <|> try forSimpleIteratorP <?> "iteration variables"
+
+forIndexedIteratorP :: Monad m => Parser m (VarName, Maybe VarName)
+forIndexedIteratorP = do
+    indexIdent <- identifierP
+    spaces
+    char ','
+    spaces
+    varIdent <- identifierP
+    spaces
+    return (varIdent, Just indexIdent)
+
+forSimpleIteratorP :: Monad m => Parser m (VarName, Maybe VarName)
+forSimpleIteratorP = do
+    varIdent <- identifierP
+    spaces
+    return (varIdent, Nothing)
+
+forHeadInP :: Monad m => Parser m (Expression, VarName, Maybe VarName)
+forHeadInP = do
+    (varIdent, indexIdent) <- forIteratorP
+    spaces
+    string "in"
+    notFollowedBy identCharP
+    spaces
+    iteree <- expressionP
+    return (iteree, varIdent, indexIdent)
+
+forHeadAsP :: Monad m => Parser m (Expression, VarName, Maybe VarName)
+forHeadAsP = do
+    iteree <- expressionP
+    spaces
+    string "as"
+    notFollowedBy identCharP
+    spaces
+    (varIdent, indexIdent) <- forIteratorP
+    return (iteree, varIdent, indexIdent)
+
+fancyTagP :: Monad m => String -> Parser m a -> Parser m a
+fancyTagP tagName inner =
+    between
+        (try $ do
+            openTagP
+            string tagName
+            spaces)
+        closeTagP
+        inner
+
+simpleTagP :: Monad m => String -> Parser m ()
+simpleTagP tagName = openTagP >> string tagName >> closeTagP
+
+openCommentP :: Monad m => Parser m ()
+openCommentP = openP '#'
+
+closeCommentP :: Monad m => Parser m ()
+closeCommentP = closeP '#'
+
+openTagP :: Monad m => Parser m ()
+openTagP = openP '%'
+
+closeTagP :: Monad m => Parser m ()
+closeTagP = closeP '%'
+
+openP :: Monad m => Char -> Parser m ()
+openP c = try (openWP c) <|> try (openNWP c)
+
+openWP :: Monad m => Char -> Parser m ()
+openWP c = ignore $ do
+    spaces
+    string [ '{', c, '-' ]
+    spaces
+
+openNWP :: Monad m => Char -> Parser m ()
+openNWP c = ignore $ do
+    string [ '{', c ]
+    spaces
+
+closeP :: Monad m => Char -> Parser m ()
+closeP c = try (closeWP c) <|> try (closeNWP c)
+
+closeWP :: Monad m => Char -> Parser m ()
+closeWP c = ignore $ do
+    spaces
+    string [ '-', c, '}' ]
+    spaces
+
+closeNWP :: Monad m => Char -> Parser m ()
+closeNWP c = ignore $ do
+    spaces
+    string [ c, '}' ]
+    optional . ignore . char $ '\n'
+
+expressionP :: Monad m => Parser m Expression
+expressionP = comparativeExprP
+
+operativeExprP :: forall m. Monad m => Parser m Expression -> [ (String, Text) ] -> Parser m Expression
+operativeExprP operandP operators = do
+    lhs <- operandP
+    spaces
+    tails <- many . try $ operativeTail
+    return $ foldl (flip ($)) lhs tails
+    where
+        opChars :: [Char]
+        opChars = nub . sort . concat . map fst $ operators
+        operativeTail :: Parser m (Expression -> Expression)
+        operativeTail = do
+            funcName <-
+                foldl (<|>) (fail "operator") $
+                [ try (string op >> notFollowedBy (oneOf opChars)) >> return fn | (op, fn) <- operators ]
+            spaces
+            rhs <- operandP
+            spaces
+            return (\lhs -> CallE (VarE funcName) [(Nothing, lhs), (Nothing, rhs)])
+
+comparativeExprP :: Monad m => Parser m Expression
+comparativeExprP =
+    operativeExprP
+        additiveExprP
+        [ ("==", "equals")
+        ]
+
+additiveExprP :: Monad m => Parser m Expression
+additiveExprP =
+    operativeExprP
+        multiplicativeExprP
+        [ ("+", "sum")
+        , ("-", "difference")
+        , ("~", "concat")
+        ]
+
+multiplicativeExprP :: Monad m => Parser m Expression
+multiplicativeExprP =
+    operativeExprP
+        postfixExprP
+        [ ("*", "product")
+        , ("//", "int_ratio")
+        , ("/", "ratio")
+        , ("%", "modulo")
+        ]
+
+postfixExprP :: Monad m => Parser m Expression
+postfixExprP = do
+    base <- atomicExprP
+    spaces
+    postfixes <- many . try $ postfixP `before` spaces
+    return $ foldl (flip ($)) base postfixes
+
+postfixP :: Monad m => Parser m (Expression -> Expression)
+postfixP = dotPostfixP
+         <|> arrayAccessP
+         <|> funcCallP
+         <|> filterP
+
+dotPostfixP :: Monad m => Parser m (Expression -> Expression)
+dotPostfixP = do
+    char '.'
+    spaces
+    i <- StringLiteralE <$> identifierP
+    return $ \e -> MemberLookupE e i
+
+arrayAccessP :: Monad m => Parser m (Expression -> Expression)
+arrayAccessP = do
+    i <- bracedP "[" "]" expressionP
+    return $ \e -> MemberLookupE e i
+
+funcCallP :: Monad m => Parser m (Expression -> Expression)
+funcCallP = do
+    args <- groupP "(" ")" funcArgP
+    return $ \e -> CallE e args
+
+funcArgP :: Monad m => Parser m (Maybe Text, Expression)
+funcArgP = namedFuncArgP <|> positionalFuncArgP
+
+namedFuncArgP :: Monad m => Parser m (Maybe Text, Expression)
+namedFuncArgP = do
+    name <- try $ identifierP `before` (between spaces spaces $ string "=")
+    expr <- expressionP
+    return (Just name, expr)
+
+positionalFuncArgP :: Monad m => Parser m (Maybe Text, Expression)
+positionalFuncArgP = try $ (Nothing,) <$> expressionP
+
+filterP :: Monad m => Parser m (Expression -> Expression)
+filterP = do
+    char '|'
+    spaces
+    func <- atomicExprP
+    args <- option [] $ groupP "(" ")" funcArgP
+    return $ \e -> CallE func ((Nothing, e):args)
+
+atomicExprP :: Monad m => Parser m Expression
+atomicExprP = parenthesizedExprP
+            <|> objectExprP
+            <|> listExprP
+            <|> stringLiteralExprP
+            <|> numberLiteralExprP
+            <|> varExprP
+
+parenthesizedExprP :: Monad m => Parser m Expression
+parenthesizedExprP =
+    between
+        (try . ignore $ char '(' >> spaces)
+        (ignore $ char ')' >> spaces)
+        expressionP
+
+listExprP :: Monad m => Parser m Expression
+listExprP = ListE <$> groupP "[" "]" expressionP
+
+objectExprP :: Monad m => Parser m Expression
+objectExprP = ObjectE <$> groupP "{" "}" expressionPairP
+
+expressionPairP :: Monad m => Parser m (Expression, Expression)
+expressionPairP = do
+    a <- expressionP
+    spaces
+    char ':'
+    spaces
+    b <- expressionP
+    spaces
+    return (a, b)
+
+groupP :: Monad m => String -> String -> Parser m a -> Parser m [a]
+groupP obr cbr inner =
+    bracedP obr cbr
+        (sepBy (inner `before` spaces) (try $ string "," `before` spaces))
+
+bracedP :: Monad m => String -> String -> Parser m a -> Parser m a
+bracedP obr cbr =
+    between
+        (try . ignore $ string obr >> spaces)
+        (ignore $ string cbr >> spaces)
+
+varExprP :: Monad m => Parser m Expression
+varExprP = do
+    litName <- identifierP
+    spaces
+    return $ case litName of
+        "true" -> BoolLiteralE True
+        "false" -> BoolLiteralE False
+        "null" -> NullLiteralE
+        _ -> VarE litName
+
+identifierP :: Monad m => Parser m Text
+identifierP =
+    Text.pack <$> (
+    (:)
+        <$> oneOf (['a'..'z'] ++ ['A'..'Z'] ++ ['_'])
+        <*> many identCharP)
+
+identCharP :: Monad m => Parser m Char
+identCharP = oneOf (['a'..'z'] ++ ['A'..'Z'] ++ ['_'] ++ ['0'..'9'])
+
+stringLiteralExprP :: Monad m => Parser m Expression
+stringLiteralExprP = do
+    StringLiteralE . Text.pack <$> stringLiteralP
+
+stringLiteralP :: Monad m => Parser m String
+stringLiteralP = do
+    d <- oneOf [ '\'', '\"' ]
+    manyTill stringCharP (char d)
+
+stringCharP :: Monad m => Parser m Char
+stringCharP = do
+    c1 <- anyChar
+    case c1 of
+        '\\' -> do
+            c2 <- anyChar
+            case c2 of
+                'n' -> return '\n'
+                'b' -> return '\b'
+                'v' -> return '\v'
+                '0' -> return '\0'
+                't' -> return '\t'
+                _ -> return c2
+        _ -> return c1
+
+numberLiteralExprP :: Monad m => Parser m Expression
+numberLiteralExprP = do
+    str <- numberLiteralP
+    let nMay :: Maybe Scientific
+        nMay = readMay str
+    case nMay of
+        Just n -> return . NumberLiteralE $ n
+        Nothing -> fail $ "Failed to parse " ++ str ++ " as a number"
+
+numberLiteralP :: Monad m => Parser m String
+numberLiteralP = do
+    sign <- option "" $ string "-"
+    integral <- string "0" <|> ((:) <$> oneOf ['1'..'9'] <*> many digit)
+    fractional <- option "" $ (:) <$> char '.' <*> many digit
+    return $ sign ++ integral ++ fractional
+
+followedBy :: Monad m => m b -> m a -> m a
+followedBy b a = a >>= \x -> (b >> return x)
+
+before :: Monad m => m a -> m b -> m a
+before = flip followedBy
diff --git a/src/Text/Ginger/Run.hs b/src/Text/Ginger/Run.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Ginger/Run.hs
@@ -0,0 +1,399 @@
+{-#LANGUAGE FlexibleContexts #-}
+{-#LANGUAGE FlexibleInstances #-}
+{-#LANGUAGE OverloadedStrings #-}
+{-#LANGUAGE TupleSections #-}
+{-#LANGUAGE TypeSynonymInstances #-}
+{-#LANGUAGE MultiParamTypeClasses #-}
+{-#LANGUAGE ScopedTypeVariables #-}
+-- | Execute Ginger templates in an arbitrary monad.
+module Text.Ginger.Run
+( runGingerT
+, runGinger
+, GingerContext
+, makeContext
+, makeContextM
+, Run, liftRun, liftRun2
+)
+where
+
+import Prelude ( (.), ($), (==), (/=)
+               , (+), (-), (*), (/), div
+               , (||), (&&)
+               , (++)
+               , Show, show
+               , undefined, otherwise
+               , Maybe (..)
+               , Bool (..)
+               , Int
+               , fromIntegral, floor
+               , not
+               , show
+               , uncurry
+               , seq
+               , snd
+               )
+import qualified Prelude
+import Data.Maybe (fromMaybe, isJust)
+import qualified Data.List as List
+import Text.Ginger.AST
+import Text.Ginger.Html
+import Text.Ginger.GVal
+
+import Data.Text (Text)
+import Data.String (fromString)
+import qualified Data.Text as Text
+import Control.Monad
+import Control.Monad.Identity
+import Control.Monad.Writer
+import Control.Monad.Reader
+import Control.Monad.State
+import Control.Applicative
+import qualified Data.HashMap.Strict as HashMap
+import Data.HashMap.Strict (HashMap)
+import Data.Scientific (Scientific)
+import Data.Scientific as Scientific
+import Data.Default (def)
+import Safe (readMay)
+
+-- | Execution context. Determines how to look up variables from the
+-- environment, and how to write out template output.
+data GingerContext m
+    = GingerContext
+        { contextLookup :: VarName -> Run m (GVal (Run m))
+        , contextWriteHtml :: Html -> Run m ()
+        }
+
+data RunState m
+    = RunState
+        { rsScope :: HashMap VarName (GVal (Run m))
+        , rsCapture :: Html
+        , rsCurrentTemplate :: Template -- the template we are currently running
+        , rsCurrentBlockName :: Maybe Text -- the name of the innermost block we're currently in
+        }
+
+unaryFunc :: forall m. (Monad m) => (GVal (Run m) -> GVal (Run m)) -> Function (Run m)
+unaryFunc f [] = return def
+unaryFunc f ((_, x):[]) = return (f x)
+
+ignoreArgNames :: ([a] -> b) -> ([(c, a)] -> b)
+ignoreArgNames f args = f (Prelude.map snd args)
+
+variadicNumericFunc :: Monad m => Scientific -> ([Scientific] -> Scientific) -> [(Maybe Text, GVal (Run m))] -> Run m (GVal (Run m))
+variadicNumericFunc zero f args =
+    return . toGVal . f $ args'
+    where
+        args' :: [Scientific]
+        args' = Prelude.map (fromMaybe zero . asNumber . snd) args
+
+variadicStringFunc :: Monad m => ([Text] -> Text) -> [(Maybe Text, GVal (Run m))] -> Run m (GVal (Run m))
+variadicStringFunc f args =
+    return . toGVal . f $ args'
+    where
+        args' :: [Text]
+        args' = Prelude.map (asText . snd) args
+
+defRunState :: forall m. Monad m => Template -> RunState m
+defRunState tpl =
+    RunState
+        { rsScope = HashMap.fromList scope
+        , rsCapture = html ""
+        , rsCurrentTemplate = tpl
+        , rsCurrentBlockName = Nothing
+        }
+    where
+        scope :: [(Text, GVal (Run m))]
+        scope =
+            [ ("raw", fromFunction gfnRawHtml)
+            , ("length", fromFunction . unaryFunc $ toGVal . length)
+            , ("str", fromFunction . unaryFunc $ toGVal . asText)
+            , ("num", fromFunction . unaryFunc $ toGVal . asNumber)
+            , ("iterable", fromFunction . unaryFunc $ toGVal . (\x -> isList x || isDict x))
+            , ("show", fromFunction . unaryFunc $ fromString . show)
+            , ("default", fromFunction gfnDefault)
+            , ("sum", fromFunction . variadicNumericFunc 0 $ Prelude.sum)
+            , ("difference", fromFunction . variadicNumericFunc 0 $ difference)
+            , ("concat", fromFunction . variadicStringFunc $ mconcat)
+            , ("product", fromFunction . variadicNumericFunc 1 $ Prelude.product)
+            , ("ratio", fromFunction . variadicNumericFunc 1 $ Scientific.fromFloatDigits . ratio . Prelude.map Scientific.toRealFloat)
+            , ("int_ratio", fromFunction . variadicNumericFunc 1 $ fromIntegral . intRatio . Prelude.map Prelude.floor)
+            , ("modulo", fromFunction . variadicNumericFunc 1 $ fromIntegral . modulo . Prelude.map Prelude.floor)
+            , ("equals", fromFunction gfnEquals)
+            ]
+
+        gfnRawHtml :: Function (Run m)
+        gfnRawHtml = unaryFunc (toGVal . unsafeRawHtml . asText)
+
+        gfnDefault :: Function (Run m)
+        gfnDefault [] = return def
+        gfnDefault ((_, x):xs)
+            | asBoolean x = return x
+            | otherwise = gfnDefault xs
+
+        gfnEquals :: Function (Run m)
+        gfnEquals [] = return $ toGVal True
+        gfnEquals (x:[]) = return $ toGVal True
+        gfnEquals (x:xs) =
+            return . toGVal $ Prelude.all ((snd x `looseEquals`) . snd) xs
+
+        looseEquals :: GVal (Run m) -> GVal (Run m) -> Bool
+        looseEquals a b
+            | isJust (asFunction a) || isJust (asFunction b) = False
+            | isJust (asList a) /= isJust (asList b) = False
+            | isJust (asDictItems a) /= isJust (asDictItems b) = False
+            -- Both numbers: do numeric comparison
+            | isJust (asNumber a) && isJust (asNumber b) = asNumber a == asNumber b
+            -- If either is NULL, the other must be falsy
+            | isNull a || isNull b = asBoolean a == asBoolean b
+            | otherwise = asText a == asText b
+
+        difference :: Prelude.Num a => [a] -> a
+        difference (x:xs) = x - Prelude.sum xs
+        difference [] = 0
+
+        ratio :: (Show a, Prelude.Fractional a, Prelude.Num a) => [a] -> a
+        ratio (x:xs) = x / Prelude.product xs
+        ratio [] = 0
+
+        intRatio :: (Prelude.Integral a, Prelude.Num a) => [a] -> a
+        intRatio (x:xs) = x `Prelude.div` Prelude.product xs
+        intRatio [] = 0
+
+        modulo :: (Prelude.Integral a, Prelude.Num a) => [a] -> a
+        modulo (x:xs) = x `Prelude.mod` Prelude.product xs
+        modulo [] = 0
+
+-- | Create an execution context for runGingerT.
+-- Takes a lookup function, which returns ginger values into the carrier monad
+-- based on a lookup key, and a writer function (outputting HTML by whatever
+-- means the carrier monad provides, e.g. @putStr@ for @IO@, or @tell@ for
+-- @Writer@s).
+makeContextM :: (Monad m, Functor m) => (VarName -> Run m (GVal (Run m))) -> (Html -> m ()) -> GingerContext m
+makeContextM l w = GingerContext l (liftRun2 w)
+
+liftLookup :: (Monad m, ToGVal (Run m) v) => (VarName -> m v) -> VarName -> Run m (GVal (Run m))
+liftLookup f k = do
+    v <- liftRun $ f k
+    return . toGVal $ v
+
+-- | Create an execution context for runGinger.
+-- The argument is a lookup function that maps top-level context keys to ginger
+-- values.
+makeContext :: (VarName -> GVal (Run (Writer Html))) -> GingerContext (Writer Html)
+makeContext l =
+    makeContextM
+        (return . l)
+        tell
+
+-- | Purely expand a Ginger template. @v@ is the type for Ginger values.
+runGinger :: GingerContext (Writer Html) -> Template -> Html
+runGinger context template = execWriter $ runGingerT context template
+
+-- | Monadically run a Ginger template. The @m@ parameter is the carrier monad,
+-- the @v@ parameter is the type for Ginger values.
+runGingerT :: (Monad m, Functor m) => GingerContext m -> Template -> m ()
+runGingerT context tpl = runReaderT (evalStateT (runTemplate tpl) (defRunState tpl)) context
+
+-- | Internal type alias for our template-runner monad stack.
+type Run m = StateT (RunState m) (ReaderT (GingerContext m) m)
+
+-- | Lift a value from the host monad @m@ into the 'Run' monad.
+liftRun :: Monad m => m a -> Run m a
+liftRun = lift . lift
+
+-- | Lift a function from the host monad @m@ into the 'Run' monad.
+liftRun2 :: Monad m => (a -> m b) -> a -> Run m b
+liftRun2 f x = liftRun $ f x
+
+-- | Find the effective base template of an inheritance chain
+baseTemplate :: Template -> Template
+baseTemplate t =
+    case templateParent t of
+        Nothing -> t
+        Just p -> baseTemplate p
+
+-- | Run a template.
+runTemplate :: (Monad m, Functor m) => Template -> Run m ()
+runTemplate = runStatement . templateBody . baseTemplate
+
+-- | Run an action within a different template context.
+withTemplate :: (Monad m, Functor m) => Template -> Run m a -> Run m a
+withTemplate tpl a = do
+    oldTpl <- gets rsCurrentTemplate
+    oldBlockName <- gets rsCurrentBlockName
+    modify (\s -> s { rsCurrentTemplate = tpl, rsCurrentBlockName = Nothing })
+    result <- a
+    modify (\s -> s { rsCurrentTemplate = oldTpl, rsCurrentBlockName = oldBlockName })
+    return result
+
+-- | Run an action within a block context
+withBlockName :: (Monad m, Functor m) => VarName -> Run m a -> Run m a
+withBlockName blockName a = do
+    oldBlockName <- gets rsCurrentBlockName
+    modify (\s -> s { rsCurrentBlockName = Just blockName })
+    result <- a
+    modify (\s -> s { rsCurrentBlockName = oldBlockName })
+    return result
+
+lookupBlock :: (Monad m, Functor m) => VarName -> Run m Block
+lookupBlock blockName = do
+    tpl <- gets rsCurrentTemplate
+    let blockMay = resolveBlock blockName tpl
+    case blockMay of
+        Nothing -> fail $ "Block " <> (Text.unpack blockName) <> " not defined"
+        Just block -> return block
+    where
+        resolveBlock :: VarName -> Template -> Maybe Block
+        resolveBlock name tpl =
+            case HashMap.lookup name (templateBlocks tpl) of
+                Just block ->
+                    return block -- Found it!
+                Nothing ->
+                    templateParent tpl >>= resolveBlock name
+
+-- | Run one statement.
+runStatement :: (Monad m, Functor m) => Statement -> Run m ()
+runStatement NullS = return ()
+runStatement (MultiS xs) = forM_ xs runStatement
+runStatement (LiteralS html) = echo html
+runStatement (InterpolationS expr) = runExpression expr >>= echo
+runStatement (IfS condExpr true false) = do
+    cond <- runExpression condExpr
+    runStatement $ if toBoolean cond then true else false
+
+runStatement (SetVarS name valExpr) = do
+    val <- runExpression valExpr
+    setVar name val
+
+runStatement (DefMacroS name macro) = do
+    let val = macroToGVal macro
+    setVar name val
+
+runStatement (BlockRefS blockName) = do
+    block <- lookupBlock blockName
+    withBlockName blockName $
+        runStatement (blockBody block)
+
+runStatement (ScopedS body) = withLocalScope runInner
+    where
+        runInner :: (Functor m, Monad m) => Run m ()
+        runInner = runStatement body
+
+runStatement (ForS varNameIndex varNameValue itereeExpr body) = do
+    iteree <- runExpression itereeExpr
+    let iterPairs =
+            if isJust (asDictItems iteree)
+                then [ (toGVal k, v) | (k, v) <- fromMaybe [] (asDictItems iteree) ]
+                else Prelude.zip (Prelude.map toGVal ([0..] :: [Int])) (fromMaybe [] (asList iteree))
+    withLocalScope $ forM_ iterPairs iteration
+    where
+        iteration (index, value) = do
+            setVar varNameValue value
+            case varNameIndex of
+                Nothing -> return ()
+                Just n -> setVar n index
+            runStatement body
+
+runStatement (PreprocessedIncludeS tpl) =
+    withTemplate tpl $ runTemplate tpl
+
+-- | Deeply magical function that converts a 'Macro' into a Function.
+macroToGVal :: forall m. (Functor m, Monad m) => Macro -> GVal (Run m)
+macroToGVal (Macro argNames body) =
+    fromFunction f
+    where
+        f :: Function (Run m)
+        -- Establish a local state to not contaminate the parent scope
+        -- with function arguments and local variables, and;
+        -- Establish a local context, where we override the HTML writer,
+        -- rewiring it to append any output to the state's capture.
+        f args =
+            withLocalState . local (\c -> c { contextWriteHtml = appendCapture }) $ do
+                clearCapture
+                forM (HashMap.toList matchedArgs) (uncurry setVar)
+                setVar "varargs" . toGVal $ positionalArgs
+                setVar "kwargs" . toGVal $ namedArgs
+                runStatement body
+                -- At this point, we're still inside the local state, so the
+                -- capture contains the macro's output; we now simply return
+                -- the capture as the function's return value.
+                toGVal <$> fetchCapture
+                where
+                    matchArgs' :: [(Maybe Text, GVal (Run m))] -> (HashMap Text (GVal (Run m)), [GVal (Run m)], HashMap Text (GVal (Run m)))
+                    matchArgs' = matchFuncArgs argNames
+                    (matchedArgs, positionalArgs, namedArgs) = matchArgs' args
+
+
+-- | Helper function to run a State action with a temporary state, reverting
+-- to the old state after the action has finished.
+withLocalState :: (Monad m, MonadState s m) => m a -> m a
+withLocalState a = do
+    s <- get
+    r <- a
+    put s
+    return r
+
+-- | Helper function to run a Scope action with a temporary scope, reverting
+-- to the old scope after the action has finished.
+withLocalScope :: (Monad m) => Run m a -> Run m a
+withLocalScope a = do
+    scope <- gets rsScope
+    r <- a
+    modify (\s -> s { rsScope = scope })
+    return r
+
+setVar :: Monad m => VarName -> GVal (Run m) -> Run m ()
+setVar name val = do
+    vars <- gets rsScope
+    let vars' = HashMap.insert name val vars
+    modify (\s -> s { rsScope = vars' })
+
+getVar :: Monad m => VarName -> Run m (GVal (Run m))
+getVar key = do
+    vars <- gets rsScope
+    case HashMap.lookup key vars of
+        Just val ->
+            return val
+        Nothing -> do
+            l <- asks contextLookup
+            l key
+
+clearCapture :: Monad m => Run m ()
+clearCapture = modify (\s -> s { rsCapture = unsafeRawHtml "" })
+
+appendCapture :: Monad m => Html -> Run m ()
+appendCapture h = modify (\s -> s { rsCapture = rsCapture s <> h })
+
+fetchCapture :: Monad m => Run m Html
+fetchCapture = gets rsCapture
+
+-- | Run (evaluate) an expression and return its value into the Run monad
+runExpression (StringLiteralE str) = return . toGVal $ str
+runExpression (NumberLiteralE n) = return . toGVal $ n
+runExpression (BoolLiteralE b) = return . toGVal $ b
+runExpression (NullLiteralE) = return def
+runExpression (VarE key) = getVar key
+runExpression (ListE xs) = toGVal <$> forM xs runExpression
+runExpression (ObjectE xs) = do
+    items <- forM xs $ \(a, b) -> do
+        l <- asText <$> runExpression a
+        r <- runExpression b
+        return (l, r)
+    return . toGVal . HashMap.fromList $ items
+runExpression (MemberLookupE baseExpr indexExpr) = do
+    base <- runExpression baseExpr
+    index <- runExpression indexExpr
+    return . fromMaybe def . lookupLoose index $ base
+runExpression (CallE funcE argsEs) = do
+    args <- forM argsEs $
+        \(argName, argE) -> (argName,) <$> runExpression argE
+    func <- toFunction <$> runExpression funcE
+    case func of
+        Nothing -> return def
+        Just f -> f args
+
+-- | Helper function to output a HTML value using whatever print function the
+-- context provides.
+echo :: (Monad m, Functor m, ToHtml h) => h -> Run m ()
+echo src = do
+    p <- asks contextWriteHtml
+    p (toHtml src)
