diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+Copyright (c) 2015-2016, Orphid, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+3. Neither the name of the copyright holder nor the names of its
+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 AND CONTRIBUTORS
+"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
+HOLDER OR CONTRIBUTORS 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/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/benchmark/Benchmark.hs b/benchmark/Benchmark.hs
new file mode 100644
--- /dev/null
+++ b/benchmark/Benchmark.hs
@@ -0,0 +1,43 @@
+import           Criterion.Main
+import           Data.Aeson                 hiding (Result)
+import           Data.Attoparsec.Text
+import           Data.Text                  (Text)
+import           Text.Liquid.Parser
+import           Text.Liquid.Renderer
+import           Text.Liquid.Types
+import           Text.Liquid.VariableFinder
+
+template :: Text
+template = "some text prior {{ a.nested[2] | replace:\'foo\',\'bar\' | toUpper }}somesquishedtext{% if user == \'barry\' %} then this text {% else %} yo yo {% endif %}the end!{% case user %}{% when \'notbarry\' %} no barrys {% when \'barry\' %} t'is barry!{% else %} any old nonsense{% endcase %}"
+
+jsonCtx :: Value
+jsonCtx = object ["a" .= nested, "user" .= ("barry" :: Text)]
+  where nested = object ["nested" .= ([ "a", "b", "foobar" ] :: [Text])]
+
+parsedTemplate :: Maybe [Expr]
+parsedTemplate = maybeResult $ parseTemplate template
+
+{-# INLINE benchParseTemplate #-}
+benchParseTemplate :: Text -> Maybe [Expr]
+benchParseTemplate t = maybeResult $ parseTemplate t
+
+{-# INLINE benchRenderTemplate #-}
+benchRenderTemplate :: (Value, Maybe [Expr]) -> Maybe [Rendering Text]
+benchRenderTemplate (v, ts) = fmap (renderTemplate v) <$> ts
+
+{-#INLINE benchInterpret #-}
+benchInterpret :: (Value, Text) -> Rendering Text
+benchInterpret (j, t) = interpretWithJson j t
+
+{-#INLINE benchFindVariables #-}
+benchFindVariables :: Maybe [Expr] -> Maybe [(JsonVarPath, VType)]
+benchFindVariables xs = findAllVariables <$> xs
+
+main :: IO ()
+main = defaultMain [
+         bgroup "Text.Liquid" [ bench "parse-whnf" $ whnf benchParseTemplate template
+                              , bench "renderTemplate"  $ whnf benchRenderTemplate (jsonCtx, parsedTemplate)
+                              , bench "interpret" $ whnf benchInterpret (jsonCtx, template)
+                              , bench "findAllVariables" $ whnf benchFindVariables parsedTemplate
+                              ]]
+
diff --git a/liquid.cabal b/liquid.cabal
new file mode 100644
--- /dev/null
+++ b/liquid.cabal
@@ -0,0 +1,98 @@
+name:                  liquid
+version:               0.1.0.1
+synopsis:              Liquid template language library
+description:           Parser, interpreter and assorted functions for the Liquid template language
+license:               BSD3
+license-file:          LICENSE
+copyright:             Copyright (C) 2015-2016 Orphid, Inc.
+author:                James R. Thompson <james@projector.com>
+maintainer:            James R. Thompson <jamesthompsonoxford@gmail.com>
+homepage:              https://github.com/projectorhq/haskell-liquid
+build-type:            Simple
+category:              Template
+cabal-version:         >= 1.10
+stability:             Experimental
+description:
+  This package should be used by importing Text.Liquid.
+  Sundry nested packages can be used freely for other functionality. See the readme on Github for more details on usage.
+
+library
+  default-language:    Haskell2010
+
+  ghc-options:         -Wall -O2
+
+  hs-source-dirs:      src
+
+  exposed-modules:     Text.Liquid
+                       Text.Liquid.Helpers
+                       Text.Liquid.Parser
+                       Text.Liquid.Renderer
+                       Text.Liquid.Tokens
+                       Text.Liquid.Types
+                       Text.Liquid.VariableFinder
+
+  default-extensions:  OverloadedStrings
+
+  build-depends:       attoparsec             >=0.13.0.1  && <0.14,
+                       aeson                  >=0.11.2.0  && <0.12,
+                       base                   >=4.8       && <5,
+                       lens                   >=4.13      && <5,
+                       lens-aeson             >=1.0.0.5   && <1.1,
+                       mtl                    >=2.2.1     && <2.3,
+                       semigroups             >=0.18.0.1  && <0.19,
+                       scientific             >=0.3.4.2   && <0.4,
+                       text                   >=1.2.1.3   && <1.3,
+                       unordered-containers   >=0.2.5.1   && <0.3,
+                       validation             >=0.5.2     && <0.6,
+                       vector                 >=0.10.12.3 && <0.12
+
+test-suite test
+
+  default-language:    Haskell2010
+
+  type:                exitcode-stdio-1.0
+
+  hs-source-dirs:      test
+
+  main-is:             Main.hs
+
+  default-extensions:  OverloadedStrings
+
+  build-depends:       aeson                  >=0.11.2.0  && <0.12,
+                       attoparsec             >=0.13.0.1  && <0.14,
+                       base                   >=4.8       && <4.9,
+                       lens                   >=4.13      && <5,
+                       lens-aeson             >=1.0.0.5   && <1.1,
+                       liquid,
+                       mtl                    >=2.2.1     && <2.3,
+                       QuickCheck             >=2.8.1     && <2.9,
+                       scientific             >=0.3.4.2   && <0.4,
+                       semigroups             >=0.18.0.1  && <0.19,
+                       tasty                  >=0.10      && <0.12,
+                       tasty-hunit            >=0.9.2     && <0.10,
+                       tasty-th               >=0.1.3     && <0.2,
+                       tasty-quickcheck       >=0.8.3.2   && <0.9,
+                       text                   >=1.2.1.3   && <1.3,
+                       unordered-containers   >=0.2.5.1   && <0.3,
+                       validation             >=0.5.2     && <0.6,
+                       vector                 >=0.10.12.3 && <0.12
+
+benchmark bench
+  default-language:    Haskell2010
+
+  type:                exitcode-stdio-1.0
+
+  hs-source-dirs:      benchmark
+
+  main-is:             Benchmark.hs
+
+  default-extensions:  OverloadedStrings
+
+  ghc-options:         -O2
+
+  build-depends:       attoparsec             >=0.13.0.1 && <0.14,
+                       aeson                  >=0.11.2.0 && <0.12,
+                       base                   >=4.8      && <4.9,
+                       criterion              >=1.1.0    && <1.2,
+                       liquid,
+                       text                   >=1.2.1.3  && <1.3
diff --git a/src/Text/Liquid.hs b/src/Text/Liquid.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Liquid.hs
@@ -0,0 +1,18 @@
+module Text.Liquid (
+    Expr(..)
+  , JsonVarPath
+  , LiquidError(..)
+  , Rendering
+  , VarIndex(..)
+  , interpret
+  , interpretWithJson
+  , parseTemplate
+  , renderTemplate
+  , templateP
+  , templateParser
+  ) where
+
+import           Text.Liquid.Parser   (parseTemplate, templateP, templateParser)
+import           Text.Liquid.Renderer (interpret, interpretWithJson, renderTemplate)
+import           Text.Liquid.Types    (Expr(..), VarIndex(..), JsonVarPath, Rendering, LiquidError(..))
+
diff --git a/src/Text/Liquid/Helpers.hs b/src/Text/Liquid/Helpers.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Liquid/Helpers.hs
@@ -0,0 +1,125 @@
+{-# LANGUAGE GADTs          #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE RankNTypes     #-}
+
+module Text.Liquid.Helpers (
+    foldM'
+  , formatNum
+  , buildLens
+  , renderPath
+  , renderExpr
+  )where
+
+import           Data.Aeson.Lens   (key, nth)
+import           Data.Aeson.Types  (Value)
+import           Data.List         (intersperse)
+import           Data.Monoid
+import           Data.Scientific
+import           Data.Text         (Text)
+import qualified Data.Text         as T
+import           Text.Liquid.Types
+
+-- | Strict monadic foldl
+foldM' :: Monad m
+       => (a -> b -> m a)
+       -> a
+       -> [b]
+       -> m a
+foldM' _ z [] = return z
+foldM' f z (x:xs) = do
+  z' <- f z x
+  z' `seq` foldM' f z' xs
+
+-- | Format a number
+formatNum :: Scientific
+          -> Text
+formatNum s | isInteger s =
+  maybe T.empty (T.pack . show) (toBoundedInteger s :: Maybe Int)
+            | otherwise   =
+  T.pack $ formatScientific Fixed Nothing s
+
+-- | Compose a traversal into a JSON Value
+buildLens :: forall (f :: * -> *) . Applicative f
+          => JsonVarPath
+          -> ((Value -> f Value) -> Value -> f Value)
+buildLens xs = foldl1 (.) (matchKey <$> xs)
+  where matchKey (ObjectIndex i) = key i
+        matchKey (ArrayIndex i)  = nth i
+
+renderPath :: JsonVarPath
+           -> Text
+renderPath path = foldl conc T.empty path
+  where conc :: Text -> VarIndex -> Text
+        conc l (ObjectIndex r) | T.null l  = r
+                               | otherwise = l <> "." <> r
+        conc l (ArrayIndex r) | T.null l   = "[" <> (T.pack $ show r) <> "]"
+                              | otherwise  = l <> "[" <> (T.pack $ show r) <> "]"
+
+renderExpr :: Expr
+           -> Text
+renderExpr Noop                = mempty
+renderExpr (RawText t)         = t
+renderExpr (Num s)             = formatNum s
+renderExpr (Variable jp)       = renderPath jp
+renderExpr (QuoteString t)     = "\'" <> t <> "\'"
+renderExpr (Equal l r)         = (renderExpr l) <> " == " <> (renderExpr r)
+renderExpr (NotEqual l r)      = (renderExpr l) <> " != " <> (renderExpr r)
+renderExpr (GtEqual l r)       = (renderExpr l) <> " >= " <> (renderExpr r)
+renderExpr (LtEqual l r)       = (renderExpr l) <> " <= " <> (renderExpr r)
+renderExpr (Gt l r)            = (renderExpr l) <> " > " <> (renderExpr r)
+renderExpr (Lt l r)            = (renderExpr l) <> " < " <> (renderExpr r)
+renderExpr (Or l r)            = (renderExpr l) <> " or " <> (renderExpr r)
+renderExpr (And l r)           = (renderExpr l) <> " and " <> (renderExpr r)
+renderExpr (Contains l r)      = (renderExpr l) <> " contains " <> (renderExpr r)
+renderExpr Trueth              = "true"
+renderExpr Falseth             = "false"
+renderExpr Nil                 = "nil"
+renderExpr Null                = "null"
+renderExpr (Truthy x)          = renderExpr x
+renderExpr (IfClause x)        = "{% if " <> renderExpr x <> " %}"
+renderExpr (IfKeyClause x)     = "{% ifkey " <> renderExpr x <> " %}"
+renderExpr (ElsIfClause x)     = "{% elsif " <> renderExpr x <> " %}"
+renderExpr Else                = "{% else %}"
+renderExpr (FilterCell n [])   = n
+renderExpr (FilterCell n opts) = n <> ": " <> (mconcat $ intersperse ", " $ renderExpr <$> opts)
+renderExpr (Filter t fs)       = renderExpr t <> bar <> (mconcat $ intersperse bar $ renderExpr <$> fs)
+  where bar = " | "
+renderExpr (Output x)          = "{{ " <> renderExpr x <>  " }}"
+renderExpr (TrueStatements xs) = mconcat $ renderExpr <$> xs
+renderExpr (IfLogic i
+                    ts@(TrueStatements _)) =
+  renderExpr i  <>
+  renderExpr ts <>
+  "{% endif %}"
+renderExpr (IfLogic (IfLogic i ts1@(TrueStatements _))
+                    (IfLogic Else ts2@(TrueStatements _))) =
+  renderExpr i    <>
+  renderExpr ts1  <>
+  renderExpr Else <>
+  renderExpr ts2  <>
+  "{% endif %}"
+renderExpr (IfLogic (IfLogic i ts1@(TrueStatements _))
+                    (IfLogic ei@(ElsIfClause _) ts2@(TrueStatements _))) =
+  renderExpr i   <>
+  renderExpr ts1 <>
+  renderExpr ei  <>
+  renderExpr ts2 <>
+  "{% endif %}"
+renderExpr (IfLogic (IfLogic i ts1@(TrueStatements _))
+                    (IfLogic (IfLogic ei@(ElsIfClause _) ts2@(TrueStatements _))
+                             (IfLogic Else ts3@(TrueStatements _)))) =
+  renderExpr i    <>
+  renderExpr ts1  <>
+  renderExpr ei   <>
+  renderExpr ts2  <>
+  renderExpr Else <>
+  renderExpr ts3  <>
+  "{% endif %}"
+renderExpr (CaseLogic s ts)    = "{% case " <> renderExpr s <> " %}" <>
+                                 (mconcat $ renderCaseLogic <$> ts)  <>
+                                 "{% endcase %}"
+  where renderCaseLogic :: (Expr, Expr) -> Text
+        renderCaseLogic (Else, y) = "{% else %}" <> renderExpr y
+        renderCaseLogic (x, y)    = "{% when " <> renderExpr x <> " %}" <> renderExpr y
+renderExpr _                   = mempty
+
diff --git a/src/Text/Liquid/Parser.hs b/src/Text/Liquid/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Liquid/Parser.hs
@@ -0,0 +1,355 @@
+module Text.Liquid.Parser where
+
+import           Prelude                    hiding (and, null, or, takeWhile)
+
+import           Control.Applicative
+import           Control.Lens               (Prism', prism')
+import           Data.Attoparsec.Combinator (lookAhead)
+import           Data.Attoparsec.Text
+import           Data.Char                  (isAlpha)
+import           Data.List.NonEmpty         (NonEmpty(..), nonEmpty)
+import           Data.Scientific            (toBoundedInteger)
+import           Data.Semigroup             hiding (option)
+import           Data.Text                  (Text)
+import qualified Data.Text                  as T
+import           Text.Liquid.Helpers
+import           Text.Liquid.Tokens
+import           Text.Liquid.Types
+
+-- | Match middle parser, around explicit start and end parsers
+between :: Parser b -- ^ open tag parser
+        -> Parser b -- ^ close tag parser
+        -> Parser a -- ^ match middle parser
+        -> Parser a
+between open close p = do
+  _ <- open
+  x <- p
+  (close *> return x) <|> fail "Tag or output statement incomplete"
+
+-- | Match parser between whitespace
+stripped :: Parser a
+         -> Parser a
+stripped = between skipSpace skipSpace
+
+-- | Match given parser for a tag
+tag :: Parser a
+    -> Parser a
+tag p = between tagStart tagEnd (stripped p)
+
+-- | Match given parser for output block
+outputTag :: Parser a
+          -> Parser a
+outputTag p = between outputStart outputEnd (stripped p)
+
+-- | Match given tag name (e.g. for, case) with following parser
+tagWith :: Parser a -- ^ initial tag type, e.g. for
+        -> Parser b -- ^ follow on parser, e.g. variable
+        -> Parser b
+tagWith tg p = tag $ tg *> skipSpace >> p
+
+-- | Convert match into text
+mapT :: Parser [Char]
+     -> Parser Text
+mapT = fmap T.pack
+
+-- | Match variables (without indices, including underscore or hash)
+var :: Parser Text
+var = mapT $ many1 $ letter <|> satisfy (inClass "_-")
+
+-- | Parse a positive integer within square brackets, e.g. "[123]", NOT "[123.1]"
+parseBoxedInt :: Parser Int
+parseBoxedInt = do
+    sc <- between oBr cBr scientific
+    case toBoundedInteger sc of
+      Just i  -> if i >= 0 then return i else err
+      Nothing -> err
+  where err = fail "invalid variable (array) index, expecting a positive integer"
+
+-- | Parse a variable section with an optional indexing
+--   An array index MUST be preceded by an object index
+--   ...hence Maybe do comprehension
+varIndexSection :: Parser (NonEmpty VarIndex)
+varIndexSection = do
+  vs <- sepBy var dot
+  i  <- many parseBoxedInt
+  brokenChar <- oBr <|> return '~'
+  let ixs = do obs <- (nonEmpty (ObjectIndex <$> vs))
+               Just obs <> (nonEmpty (ArrayIndex <$> i))
+  if brokenChar == '[' then (fail "invalid array index - ill-typed") else case ixs of
+    Just nel -> return nel
+    Nothing  -> fail "invalid var index section"
+
+-- | Parse a variable
+variable :: Parser Expr
+variable = do
+  sections <- sepBy1 varIndexSection dot
+  return . Variable $ foldl1 (<>) sections
+
+-- | e.g. raw tag, comment tag
+rawBodyTag :: Parser Text -- ^ start tag matcher
+           -> Parser Text -- ^ end tag matcher
+           -> Parser Text
+rawBodyTag s e =
+  s >> skipSpace *> (mapT $ manyTill anyChar (skipSpace >> e))
+
+-- | Match interior of raw tag
+rawTag :: Parser Expr
+rawTag = RawText <$> rawBodyTag (tag rawStart) (tag rawEnd)
+
+-- | Match interior of comment tag
+commentTag :: Parser Expr
+commentTag = rawBodyTag (tag commentStart) (tag commentEnd) *> pure Noop
+
+-- | Match any raw text upto a tag/output start or the end of the input
+textPart :: Parser Expr
+textPart = RawText <$> (mapT $ manyTill1 (satisfy $ notInClass "{%") terminator)
+  where terminator = lookAhead $ tagStart <|> outputStart <|> (endOfInput *> pure T.empty)
+
+-- | Force the first character to be valid, otherwise fail miserably
+manyTill1 :: Alternative f => f a -> f b -> f [a]
+manyTill1 p e = (:) <$> p <*> s
+    where s = (e *> pure []) <|> ((:) <$> p <*> s)
+
+-- | Match an Ord comparison operator
+ordOperator :: Parser (Expr -> Expr -> Expr)
+ordOperator =
+  stripped $ eq *> pure Equal              <|>
+             neq *> pure NotEqual          <|>
+             gtEq *> pure GtEqual          <|>
+             ltEq *> pure LtEqual          <|>
+             (mapT . some $ gt) *> pure Gt <|>
+             (mapT . some $ lt) *> pure Lt <|>
+             contains *> pure Contains
+
+-- | Match an or, and or contains predicate
+ordCombinator :: Parser (Expr -> Expr -> Expr)
+ordCombinator =
+  stripped $ or  *> pure Or  <|>
+             and *> pure And
+
+-- | Match a quoted string
+quoteString :: Parser Expr
+quoteString = do
+  skipSpace
+  beginTick <- satisfy (inClass "\'\"")
+  qs        <- mapT $ manyTill anyChar (char beginTick)
+  return $ QuoteString qs
+
+-- | Match a binary predicate, e.g. a.b >= b.name
+binaryPredicate :: Parser Expr
+binaryPredicate = do
+  lhs <- quoteString                 <|>
+         (Num <$> scientific)        <|>
+         Null <$ (stripped null)     <|>
+         Nil <$ (stripped nil)       <|>
+         Falseth <$ (stripped false) <|>
+         Trueth  <$ (stripped true)  <|>
+         variable
+  op  <- ordOperator
+  rhs <- quoteString                 <|>
+         (Num <$> scientific)        <|>
+         Null <$ (stripped null)     <|>
+         Nil <$ (stripped nil)       <|>
+         Falseth <$ (stripped false) <|>
+         Trueth  <$ (stripped true)  <|>
+         variable
+  return $ op lhs rhs
+
+-- | Parse and evaluate truthiness
+truthy :: Parser Expr
+truthy =
+  Null    <$ (stripped null)              <|>
+  Nil     <$ (stripped nil)               <|>
+  Falseth <$ (stripped false)             <|>
+  Trueth  <$ (stripped true)              <|>
+  (Truthy . Num <$> stripped scientific)  <|>
+  (Truthy       <$> stripped quoteString) <|>
+  (Truthy       <$> stripped variable)
+
+-- | Match a binary predicate, e.g. a.b >= b.name or 'barry'
+predicate :: Parser Expr
+predicate = do
+  bpl <- stripped binaryPredicate
+  oc  <- ordCombinator
+  bpr <- stripped binaryPredicate
+  return $ oc bpl bpr
+
+-- | Match any predicate clause
+predicateClause :: Parser Expr
+predicateClause =
+  predicate <|> binaryPredicate <|> truthy
+
+-- | Match an if clause
+ifClause :: Parser Expr
+ifClause = IfClause <$> tagWith ifStart predicateClause
+
+-- | Match an ifkey clause
+ifKeyClause :: Parser Expr
+ifKeyClause = IfKeyClause <$> tagWith ifKeyStart variableOnly
+  where variableOnly = do res <- eitherP precheck variable
+                          case res of
+                            Left _   -> fail "Only variables as ifkey args allowed"
+                            Right ok -> return ok
+        precheck     = Null    <$ (stripped null)             <|>
+                       Nil     <$ (stripped nil)              <|>
+                       Falseth <$ (stripped false)            <|>
+                       Trueth  <$ (stripped true)             <|>
+                       (Truthy . Num <$> stripped scientific) <|>
+                       (Truthy       <$> stripped quoteString)
+
+-- | Match an elsif clause
+elsifClause :: Parser Expr
+elsifClause = ElsIfClause <$> tagWith elsIf predicateClause
+
+-- | Match an else clause
+elseClause :: Parser Expr
+elseClause = (tag els) *> pure Else
+
+-- | Match the end of an if clause
+endIfClause :: Parser Expr
+endIfClause = (tag endIf) *> pure Noop
+
+-- | Match a variable condition for a case clause
+caseClause :: Parser Expr
+caseClause = tagWith caseStart variable
+
+-- | Match a when clause, part of a case pattern match block
+whenClause :: Parser Expr
+whenClause = tagWith when (quoteString <|> (Num <$> scientific))
+
+-- | Match the end of a case pattern match block
+endCaseClause :: Parser Expr
+endCaseClause = (tag caseEnd) *> pure Noop
+
+-- | Match a filter fn name
+filterName :: Parser Text
+filterName = mapT $ skipSpace *> manyTill1 letter terminator
+  where terminator = colon <|> (skipSpace *> pipe) <|> (lookAhead $ satisfy (not . isAlpha))
+
+-- | Match the list of arguments for the filter fn
+filterArgs :: Parser [Expr]
+filterArgs = skipSpace *> sepBy numOrString comma
+  where numOrString = skipSpace *> (Num <$> scientific) <|> quoteString
+
+-- | Match a filter cell, fn and args
+filterCell :: Parser Expr
+filterCell = do
+  fnName <- filterName
+  args   <- filterArgs
+  typeCheckFilter fnName args
+
+-- | Type check the function args and check arity
+typeCheckFilter :: Text
+                -> [Expr]
+                -> Parser Expr
+typeCheckFilter "toUpper"             []                                   =
+  return $ FilterCell "toUpper" []
+typeCheckFilter "toUpper"             _                                    =
+  fail "toUpper filter takes no arguments"
+typeCheckFilter "toLower"             []                                   =
+  return $ FilterCell "toLower" []
+typeCheckFilter "toLower"             _                                    =
+  fail "toLower filter takes no arguments"
+typeCheckFilter "toTitle"             []                                   =
+  return $ FilterCell "toTitle" []
+typeCheckFilter "toTitle"             _                                    =
+  fail "toTitle filter takes no arguments"
+typeCheckFilter "replace"             a@[QuoteString _, QuoteString _]     =
+  return $ FilterCell "replace"       a
+typeCheckFilter "replace"             _                                    =
+  fail "replace filter requires find, replace strings as args"
+typeCheckFilter "first"               []                                   =
+  return $ FilterCell "first" []
+typeCheckFilter "first"               _                                    =
+  fail "first filter takes no arguments"
+typeCheckFilter "firstOrDefault"      a@(_:_)                              =
+  return $ FilterCell "firstOrDefault" a
+typeCheckFilter "firstOrDefault" _                                         =
+  fail "firstOrDefault requires a single default parameter"
+typeCheckFilter "last"                []                                   =
+  return $ FilterCell "last" []
+typeCheckFilter "last"           _                                         =
+  fail "last filter takes no arguments"
+typeCheckFilter "lastOrDefault"       a@(_:_)                              =
+  return $ FilterCell "lastOrDefault" a
+typeCheckFilter "lastOrDefault"       _                                    =
+  fail "lastOrDefault requires a single default parameter"
+typeCheckFilter "countElements"       []                                   =
+  return $ FilterCell "countElements" []
+typeCheckFilter "countElements"       _                                    =
+  fail "countElements takes no arguments"
+typeCheckFilter "renderWithSeparator" a@[QuoteString _]                    =
+  return $ FilterCell "renderWithSeparator" a
+typeCheckFilter "renderWithSeparator" _                                    =
+  fail "renderWithSeparator requires a separator argument with which to intersperse the target array"
+typeCheckFilter "toSentenceWithSeparator" a@[QuoteString _, QuoteString _] =
+  return $ FilterCell "toSentenceWithSeparator" a
+typeCheckFilter "toSentenceWithSeparator" _                                =
+  fail "toSentenceWithSeparator requires a separator argument and last element separator"
+typeCheckFilter l                     _                                    =
+  fail $ (show l) ++ ": function isn't supported"
+
+-- | Match multiple filter fns and args
+filterCells :: Parser [Expr]
+filterCells = many ((filterCell <* (skipSpace *> pipe)) <|> filterCell)
+
+-- | Match a lhs and a block of filters with their args
+filterBlock :: Parser Expr
+filterBlock = do
+  lhs   <- (quoteString <|> (skipSpace *> variable)) <* (skipSpace >> pipe)
+  cells <- filterCells
+  return $ Filter lhs cells
+
+-- | Output block, a variable, indexed variable, number or filter block
+output :: Parser Expr
+output = Output <$> outputTag (filterBlock <|> quoteString <|> variable)
+
+-- | If statement, optional elsif or else
+ifLogic :: Parser Expr
+ifLogic = do
+  start  <- ifClause <|> ifKeyClause <|> elsifClause <|> elseClause
+  iftrue <- TrueStatements <$>
+            manyTill (output <|> textPart)
+                     (lookAhead elsifClause <|> lookAhead elseClause <|> lookAhead endIfClause)
+  let sofar = IfLogic start iftrue
+  (endIfClause *> pure sofar) <|> (IfLogic sofar <$> ifLogic)
+
+-- | Case pattern match block
+caseLogic :: Parser Expr
+caseLogic = do
+    start    <- caseClause
+    patterns <- many1 whenBlock
+    _        <- endCaseClause
+    return $ CaseLogic start patterns
+  where whenBlock = do
+          pattern <- whenClause <|> elseClause
+          iftrue  <- TrueStatements <$>
+                     manyTill (output <|> textPart)
+                              (lookAhead whenClause <|> lookAhead elseClause <|> lookAhead endCaseClause)
+          return (pattern, iftrue)
+
+-- | Parse any block type
+block :: Parser Expr
+block = choice [ ifLogic
+               , caseLogic
+               , rawTag
+               , commentTag
+               , output
+               , textPart
+               ] <?> "Block Parsing"
+
+-- | Parse an entire template into chunks
+templateParser :: Parser [Expr]
+templateParser = manyTill1 (block <?> "Syntax Error") endOfInput
+
+-- | Run the templateParser on input text, force partial results to terminate with Failure
+parseTemplate :: Text
+              -> IResult Text [Expr]
+parseTemplate t =
+  feed (parse templateParser t) T.empty
+
+templateP :: Prism' Text [Expr]
+templateP = prism' back forw
+  where forw = maybeResult . parseTemplate
+        back = mconcat . fmap renderExpr
+
diff --git a/src/Text/Liquid/Renderer.hs b/src/Text/Liquid/Renderer.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Liquid/Renderer.hs
@@ -0,0 +1,416 @@
+module Text.Liquid.Renderer where
+
+import           Control.Applicative
+import           Control.Lens         hiding (op, (<|))
+import           Data.Aeson           hiding (Null)
+import           Data.Aeson.Lens
+import qualified Data.Aeson.Lens      as AL
+import           Data.Attoparsec.Text
+import           Data.Bifunctor       (second)
+import           Data.List            (intersperse)
+import           Data.List.NonEmpty   (NonEmpty (..), (<|))
+import           Data.Maybe           (fromMaybe)
+import           Data.Monoid          ((<>))
+import           Data.Scientific      (Scientific)
+import           Data.Text            (Text)
+import qualified Data.Text            as T
+import           Data.Validation
+import           Text.Liquid.Helpers
+import           Text.Liquid.Parser   (parseTemplate)
+import           Text.Liquid.Types
+
+-- | Interpret function - for use in testing the lib
+interpretWithJson :: Value -- ^ JSON context
+                  -> Text  -- ^ Raw template
+                  -> Rendering Text
+interpretWithJson ctx template = case parseRes of
+    (Fail brokenPart errCtxs _) ->
+      let (ok, bad) = T.breakOn brokenPart template
+      in _Failure # [ TemplateParsingError ok bad (T.pack <$> errCtxs) ]
+    (Partial _)    -> _Failure # [ LiquidError "Major parsing error! - Attoparsec Issue" ]
+    (Done _ ts)  -> interpret ctx ts
+  where parseRes = parseTemplate template
+
+-- | Interpret the raw data if it is ok
+interpret :: Value
+          -> [Expr]
+          -> Rendering Text
+interpret ctx template =
+  foldl (<>) T.empty <$> sequenceA (renderTemplate ctx <$> template)
+
+-- | Main template block rendering fn
+renderTemplate :: Value
+               -> Expr
+               -> Rendering Text
+
+-- | Rendering types
+renderTemplate j (Output f@(Filter _ _))      = applyFilter j f
+renderTemplate j (Output q@(QuoteString _))   = renderText j q
+renderTemplate j (Output v@(Variable _))      = renderText j v
+renderTemplate j (Output n@(Num _))           = renderText j n
+renderTemplate _ (RawText t)                  = pure t
+
+-- | If logic
+renderTemplate j (IfLogic (IfClause i)
+                          (TrueStatements ts)) =
+  evalLogic j (evalTruthiness j i) ts
+
+renderTemplate j (IfLogic (IfLogic (IfClause it) (TrueStatements ts))
+                          (IfLogic Else (TrueStatements ets)))
+    | ifTrue == (AccSuccess False) = evalLogic j (pure True) ets
+    | otherwise                    = evalLogic j ifTrue ts
+  where ifTrue = evalTruthiness j it
+
+renderTemplate j (IfLogic (IfLogic (IfClause it) (TrueStatements ts))
+                          (IfLogic (ElsIfClause eit) (TrueStatements eits)))
+    | ifTrue == (AccSuccess False) = evalLogic j ifElseTrue eits
+    | otherwise                    = evalLogic j ifTrue ts
+  where ifTrue     = evalTruthiness j it
+        ifElseTrue = evalTruthiness j eit
+
+renderTemplate j (IfLogic (IfLogic (IfClause it) (TrueStatements ts))
+                          (IfLogic (IfLogic (ElsIfClause eit) (TrueStatements eits))
+                                   (IfLogic Else (TrueStatements ets))))
+    | ifTrue == (AccSuccess False) && ifElseTrue == (AccSuccess False) = evalLogic j (pure True) ets
+    | ifTrue == (AccSuccess False)                                     = evalLogic j ifElseTrue eits
+    | otherwise                    = evalLogic j ifTrue ts
+    where ifTrue     = evalTruthiness j it
+          ifElseTrue = evalTruthiness j eit
+
+-- | Ifkey logic
+renderTemplate j (IfLogic (IfKeyClause i)
+                          (TrueStatements ts)) =
+  evalLogic j (evalKeyTruthiness j i) ts
+
+renderTemplate j (IfLogic (IfLogic (IfKeyClause it) (TrueStatements ts))
+                          (IfLogic Else (TrueStatements ets)))
+    | ifTrue == (AccSuccess False) = evalLogic j (pure True) ets
+    | otherwise                    = evalLogic j ifTrue ts
+  where ifTrue = evalKeyTruthiness j it
+
+renderTemplate j (IfLogic (IfLogic (IfKeyClause it) (TrueStatements ts))
+                          (IfLogic (ElsIfClause eit) (TrueStatements eits)))
+    | ifTrue == (AccSuccess False) = evalLogic j ifElseTrue eits
+    | otherwise                    = evalLogic j ifTrue ts
+  where ifTrue     = evalKeyTruthiness j it
+        ifElseTrue = evalTruthiness j eit
+
+renderTemplate j (IfLogic (IfLogic (IfKeyClause it) (TrueStatements ts))
+                          (IfLogic (IfLogic (ElsIfClause eit) (TrueStatements eits))
+                                   (IfLogic Else (TrueStatements ets))))
+    | ifTrue == (AccSuccess False) && ifElseTrue == (AccSuccess False) = evalLogic j (pure True) ets
+    | ifTrue == (AccSuccess False)                                     = evalLogic j ifElseTrue eits
+    | otherwise                    = evalLogic j ifTrue ts
+    where ifTrue     = evalKeyTruthiness j it
+          ifElseTrue = evalTruthiness j eit
+
+-- | Case logic
+renderTemplate j (CaseLogic (Variable v) patterns) =
+  evalCaseLogic j (extractValue j v) patterns
+
+-- | Catch all error - theoretically impossible.
+renderTemplate _ _ =
+  _Failure # [ LiquidError "Template rendering critical error!" ]
+
+-- | Evaluate predicate result and render
+evalLogic :: Value          -- ^ JSON Context
+          -> Rendering Bool -- ^ Predicate / logical expression result
+          -> [Expr]         -- ^ Expressions to evaluate if true
+          -> Rendering Text
+evalLogic j (AccSuccess True) ts =
+    foldl (<>) T.empty <$> tevals
+  where tevals = sequenceA $ (renderTemplate j <$> ts)
+evalLogic _ (AccSuccess False) _ = pure T.empty
+evalLogic _ failure _            =
+  second (const T.empty) failure
+
+-- | Evaluate case logic
+evalCaseLogic :: Value
+              -> Rendering Value -- ^ Extracted JSON value for case match
+              -> [(Expr, Expr)]
+              -> Rendering Text
+evalCaseLogic _ _ []                                      =
+  pure T.empty
+evalCaseLogic j v ((Num n, TrueStatements ts):xs)         =
+  case (==) (pure n) <$> preview _Number <$> v of
+    AccSuccess True  -> evalLogic j (pure True) ts
+    AccSuccess False -> evalCaseLogic j v xs
+    failure          -> second (const T.empty) failure
+evalCaseLogic j v ((QuoteString q, TrueStatements ts):xs) =
+  case (==) (pure q) <$> preview _String <$> v of
+    AccSuccess True  -> evalLogic j (pure True) ts
+    AccSuccess False -> evalCaseLogic j v xs
+    failure          -> second (const T.empty) failure
+evalCaseLogic j _ ((Else, TrueStatements ts):[])          =
+  evalLogic j (pure True) ts
+evalCaseLogic _ v ((Else, TrueStatements _):_)            =
+  _Failure # [ RenderingFailure "Multiple else blocks in a case statement" ] <*> v
+evalCaseLogic _ v _                                       =
+  _Failure # [ RenderingFailure "Impossible case pattern evaluation" ] <*> v
+
+-- | Render a renderable Expr as Text
+renderText :: Value
+           -> Expr
+           -> Rendering Text
+renderText _ Noop            = pure $ T.empty
+renderText _ (RawText t)     = pure t
+renderText _ (Num n)         = pure $ formatNum n
+renderText j (Variable xs)   =
+  numberOrTextFormat $ extractValue j xs
+renderText _ (QuoteString q) = pure q
+renderText _ e               =
+  _Failure # [ RenderingFailure $ "Can't render this type: " <> (renderExpr e) ]
+
+-- | Format variable as either number or Text
+numberOrTextFormat :: Rendering Value
+                   -> Rendering Text
+numberOrTextFormat rv =
+    fromMaybe (AccFailure [err]) (s <|> n)
+  where s = sequenceA $ preview _String <$> rv
+        n = sequenceA $ (fmap formatNum <$> preview _Number <$> rv)
+        err = NotAStringOrNumberJsonValue rv --TODO build better error
+
+-- | Eval key present & not null
+evalKeyTruthiness :: Value
+                  -> Expr
+                  -> Rendering Bool
+evalKeyTruthiness j (Variable i@(ObjectIndex "user" :| _)) =
+  maybe (pure False) (const (pure True)) (j ^? buildLens i.nonNull)
+evalKeyTruthiness j (Variable i@(ObjectIndex "event" :| _)) =
+  maybe (pure False) (const (pure True)) (j ^? buildLens i.nonNull)
+evalKeyTruthiness j (Variable i) =
+  maybe (pure False) (const (pure True)) $ (j ^? buildLens i.nonNull) <|> (j ^? buildLens (ObjectIndex "event" <| i).nonNull)
+evalKeyTruthiness _ _            =
+  _Failure # [ RenderingFailure "Can't evalulate if key on anything other than json context variables" ]
+
+-- | Eval truth
+evalTruthiness :: Value
+               -> Expr
+               -> Rendering Bool
+evalTruthiness j (Truthy (Variable i))                     =
+  case extractValue j i of
+    (AccSuccess v) -> maybe (pure True) pure (v ^? _Bool)
+    failure        -> second (const False) failure
+evalTruthiness _ (Truthy _)                                = pure True
+evalTruthiness _ Nil                                       = pure False
+evalTruthiness _ Null                                      = pure False
+evalTruthiness _ Falseth                                   = pure False
+evalTruthiness _ Trueth                                    = pure True
+evalTruthiness j (Equal l r)                               = bothSidesEqual j l r
+evalTruthiness j (NotEqual l r)                            = not <$> bothSidesEqual j l r
+evalTruthiness _ (GtEqual (Num l) (Num r))                 = pure $ l >= r
+evalTruthiness _ (LtEqual (Num l) (Num r))                 = pure $ l <= r
+evalTruthiness _ (Gt (Num l) (Num r))                      = pure $ l > r
+evalTruthiness _ (Lt (Num l) (Num r))                      = pure $ l < r
+evalTruthiness _ (GtEqual (QuoteString l) (QuoteString r)) = pure $ l >= r
+evalTruthiness _ (LtEqual (QuoteString l) (QuoteString r)) = pure $ l <= r
+evalTruthiness _ (Gt (QuoteString l) (QuoteString r))      = pure $ l > r
+evalTruthiness _ (Lt (QuoteString l) (QuoteString r))      = pure $ l < r
+evalTruthiness j (GtEqual l r)                             = varComparisons j (>=) l r
+evalTruthiness j (LtEqual l r)                             = varComparisons j (<=) l r
+evalTruthiness j (Gt l r)                                  = varComparisons j (>) l r
+evalTruthiness j (Lt l r)                                  = varComparisons j (<) l r
+evalTruthiness j (Contains l r)                            = containsCheck j l r
+evalTruthiness j (Or l r)                                  =
+  (||) <$> evalTruthiness j l <*> evalTruthiness j r
+evalTruthiness j (And l r)                                 =
+  (&&) <$> evalTruthiness j l <*> evalTruthiness j r
+evalTruthiness _ err                                       =
+  _Failure # [ ImpossibleTruthEvaluation err ]
+
+-- | Check if the variable contains the thing on the rhs
+containsCheck :: Value
+              -> Expr
+              -> Expr
+              -> Rendering Bool
+containsCheck j (Variable l) (QuoteString r) = elem r <$> v
+  where v = getStringsFromArray <$> extractValue j l
+containsCheck j (Variable l) (Num r)         = elem r <$> v
+  where v = getNumbersFromArray <$> extractValue j l
+containsCheck _ (Variable _) r               =
+  _Failure # [ ImpossibleComparison "Contains" (renderExpr r) ]
+containsCheck _ _ _                          =
+  _Failure # [ LiquidError "Contains checks can only be performed on arrays (i.e. Variables)" ]
+
+-- | Aggregate all the strings in the underlying array - if present
+getStringsFromArray :: Value
+                    -> [Text]
+getStringsFromArray v =
+  v ^.. values . _String
+
+-- | Aggregate all the numbers in the underlying array - if present
+getNumbersFromArray :: Value
+                    -> [Scientific]
+getNumbersFromArray v =
+  v ^.. values . _Number
+
+-- | Truth evaluation with variables
+--   ONLY numberic comparison allowed, text comparisons not supported
+varComparisons :: Value
+               -> (Maybe Scientific -> Maybe Scientific -> Bool)
+               -> Expr
+               -> Expr
+               -> Rendering Bool
+varComparisons j op (Num l)      (Variable r)         = op (pure l) <$> vr
+  where vr = preview _Number <$> extractValue j r
+varComparisons j op (Variable l) (Num r)              = op <$> vl <*> (pure $ pure r)
+  where vl = preview _Number <$> extractValue j l
+varComparisons j op lhs@(Variable l) rhs@(Variable r) = res
+  where vl = preview _Number <$> extractValue j l
+        vr = preview _Number <$> extractValue j r
+        res = case (vl, vr) of
+                (AccSuccess (Just _), AccSuccess (Just _)) -> op <$> vl <*> vr
+                (AccSuccess Nothing, AccSuccess (Just _))  ->
+                  _Failure # [ ImpossibleComparison ("Number not found at variable" <> renderExpr lhs) (renderExpr rhs) ]
+                (AccSuccess (Just _), AccSuccess Nothing)  ->
+                  _Failure # [ ImpossibleComparison (renderExpr lhs) ("Number not found at variable" <> renderExpr rhs)]
+                (_, _)                                     ->
+                  _Failure # [ ImpossibleComparison ("Number not found at variable" <> renderExpr lhs)
+                                                    ("Number not found at variable" <> renderExpr rhs) ]
+varComparisons _ _  l            r            =
+  _Failure # [ ImpossibleComparison (renderExpr l) (renderExpr r) ]
+
+-- | Evaluate a binary comparison
+bothSidesEqual :: Value -- ^ JSON context
+               -> Expr  -- ^ lhs
+               -> Expr  -- ^ rhs
+               -> Rendering Bool
+bothSidesEqual _ l r | l == r                      = pure True
+bothSidesEqual _ (QuoteString q1) (QuoteString q2) = pure $ q1 == q2
+bothSidesEqual j (Variable xs) (QuoteString q)     = (==) (pure q) <$> vl
+  where vl = preview _String <$> extractValue j xs
+bothSidesEqual j (QuoteString q) (Variable ys)     = (==) (pure q) <$> vr
+  where vr = preview _String <$> extractValue j ys
+bothSidesEqual j (Variable xs) (Variable ys)       = (==) <$> vl <*> vr
+  where vl = extractValue j xs
+        vr = extractValue j ys
+bothSidesEqual j (Variable xs) (Num n)             = (==) (pure n) <$> vl
+  where vl = preview _Number <$> extractValue j xs
+bothSidesEqual j (Num n) (Variable ys)             = (==) (pure n) <$> vr
+  where vr = preview _Number <$> extractValue j ys
+bothSidesEqual _ (Num l) (Num r)                   = pure $ l == r
+bothSidesEqual j (Variable xs) Trueth              = (==) (pure True) <$> vl
+  where vl = preview _Bool <$> extractValue j xs
+bothSidesEqual j Trueth (Variable xs)              = (==) (pure True) <$> vl
+  where vl = preview _Bool <$> extractValue j xs
+bothSidesEqual j (Variable xs) Falseth             = (==) (pure False) <$> vl
+  where vl = preview _Bool <$> extractValue j xs
+bothSidesEqual j Falseth (Variable xs)             = (==) (pure False) <$> vl
+  where vl = preview _Bool <$> extractValue j xs
+bothSidesEqual j (Variable xs) Null                = (==) (pure ()) <$> vl
+  where vl = preview AL._Null <$> extractValue j xs
+bothSidesEqual j Null (Variable xs)                = (==) (pure ()) <$> vl
+  where vl = preview AL._Null <$> extractValue j xs
+bothSidesEqual j (Variable xs) Nil                 = (==) (pure ()) <$> vl
+  where vl = preview AL._Null <$> extractValue j xs
+bothSidesEqual j Nil (Variable xs)                 = (==) (pure ()) <$> vl
+  where vl = preview AL._Null <$> extractValue j xs
+bothSidesEqual _ l r                               =
+  _Failure # [ ImpossibleComparison (renderExpr l) (renderExpr r) ]
+
+-- | Fold over multiple layers of variable syntax, and deal with future event nesting
+extractValue :: Value
+             -> JsonVarPath
+             -> Rendering Value
+extractValue j xz@(ObjectIndex "user" :| _) =
+  case j ^? buildLens xz of
+    Just v  -> _Success # v
+    Nothing -> _Failure # [ JsonValueNotFound xz ]
+extractValue j xz@(ObjectIndex "event" :| _) =
+  case (j ^? buildLens xz) of
+    Just v  -> _Success # v
+    Nothing -> _Failure # [ JsonValueNotFound xz ]
+extractValue j xz = -- ^ If template doesn't have context yet - add it after first attempting raw key
+  case (j ^? buildLens xz) <|>
+       (j ^? buildLens (ObjectIndex "event" <| xz)) of
+    Just v  -> _Success # v
+    Nothing -> _Failure # [ JsonValueNotFound xz ]
+
+-- | Apply a filter to the input
+applyFilter :: Value
+            -> Expr
+            -> Rendering Text
+applyFilter _ (Filter (QuoteString q) [])      = pure q
+applyFilter _ (Filter (QuoteString q) (c:fcs)) =
+  case applyFilterM q c >>= \i -> foldM' applyFilterM i fcs of
+    Just t -> AccSuccess t
+    _      -> _Failure # [ RenderingFailure "Filtration fn failure" ]
+applyFilter j (Filter (Variable vs) fcs)   =
+    case res of
+      (AccSuccess (Just t)) -> AccSuccess t
+      failure -> _Failure # [ RenderingFailure "Variable filtration fn failure" ] <*> failure
+  where res = (\v -> applyCellsM v fcs) <$> extractValue j vs
+applyFilter _ _                            =
+  _Failure # [ LiquidError "Filter Bug!" ]
+
+-- | Apply a chain of functions from l -> r
+applyCellsM :: Value
+            -> [Expr]
+            -> Maybe Text
+applyCellsM v []      = v ^? _String
+applyCellsM v (c:fcs) = arrayFilterM v c >>= \i -> foldM' applyFilterM i fcs
+
+-- | Apply a filter
+applyFilterM :: Text -- ^ LHS
+             -> Expr -- ^ FilterCell
+             -> Maybe Text
+applyFilterM i (FilterCell "toUpper" []) = pure $ T.toUpper i
+applyFilterM i (FilterCell "toLower" []) = pure $ T.toLower i
+applyFilterM i (FilterCell "toTitle" []) = pure $ T.toTitle i
+applyFilterM i (FilterCell "replace" [QuoteString find,
+                                      QuoteString rep ]) =
+  pure $ T.replace find rep i
+applyFilterM _ _                         = Nothing
+
+-- | Apply the array filter if the targeted value is an array, otherwise the reg filter
+arrayFilterM :: Value
+             -> Expr
+             -> Maybe Text
+arrayFilterM v fc | isn't _Nothing $ st = st >>= (flip applyFilterM) fc
+                  | otherwise           = applyArrayFilterM arr fc
+  where st  = v ^? _String
+        arr = v ^.. values
+
+-- | Apply an array filter to an array
+applyArrayFilterM :: [Value]
+                  -> Expr
+                  -> Maybe Text
+applyArrayFilterM [] (FilterCell "first" []) = pure ""
+applyArrayFilterM vs (FilterCell "first" []) = (vs ^? ix 0 . _String)               <|>
+                                               (formatNum <$> vs ^? ix 0 . _Number) <|>
+                                               (pure "")
+applyArrayFilterM [] (FilterCell "last" []) = pure ""
+applyArrayFilterM vs (FilterCell "last" []) = (vs ^? _last . _String)               <|>
+                                              (formatNum <$> vs ^? _last . _Number) <|>
+                                              (pure "")
+applyArrayFilterM [] (FilterCell "firstOrDefault" [QuoteString d]) = pure d
+applyArrayFilterM [] (FilterCell "firstOrDefault" [Num d])         = pure $ formatNum d
+applyArrayFilterM vs (FilterCell "firstOrDefault" _)               = (vs ^? ix 0 . _String) <|>
+                                                                     (formatNum <$> vs ^? ix 0 . _Number)
+applyArrayFilterM [] (FilterCell "lastOrDefault" [QuoteString d]) = pure d
+applyArrayFilterM [] (FilterCell "lastOrDefault" [Num d])         = pure $ formatNum d
+applyArrayFilterM vs (FilterCell "lastOrDefault" _)               = (vs ^? _last . _String) <|>
+                                                                    (formatNum <$> vs ^? _last . _Number)
+applyArrayFilterM [] (FilterCell "toSentenceWithSeparator" _)                 = pure ""
+applyArrayFilterM vs (FilterCell "toSentenceWithSeparator" [QuoteString sep, QuoteString fin]) = do
+  (upToLast, lastElem) <- vs^?_Snoc
+  case null upToLast of
+    True -> renderv lastElem
+    False -> do
+      text <- mconcat . intersperse sep <$> renderEachArrayElem upToLast
+      fmap (mappend $ text <> fin) $ renderv lastElem
+applyArrayFilterM [] (FilterCell "renderWithSeparator" _)                 = pure ""
+applyArrayFilterM vs (FilterCell "renderWithSeparator" [QuoteString sep]) =
+  mconcat . intersperse sep <$> renderEachArrayElem vs
+applyArrayFilterM [] (FilterCell "countElements" _) = pure "0"
+applyArrayFilterM vs (FilterCell "countElements" _) = pure . T.pack . show $ length vs
+applyArrayFilterM _  _ = Nothing
+
+renderv :: Value -> Maybe Text
+renderv v = v^?_String <|> (formatNum <$> v^?_Number)
+
+-- | Render each array element (can only contain strings or numbers!)
+renderEachArrayElem :: [Value]
+                    -> Maybe [Text]
+renderEachArrayElem = traverse renderv
+
diff --git a/src/Text/Liquid/Tokens.hs b/src/Text/Liquid/Tokens.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Liquid/Tokens.hs
@@ -0,0 +1,225 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Text.Liquid.Tokens (
+    and
+  , assign
+  , break
+  , cBr
+  , cPar
+  , captureEnd
+  , captureStart
+  , caseEnd
+  , caseStart
+  , colon
+  , comma
+  , commentEnd
+  , commentStart
+  , contains
+  , continue
+  , cycle
+  , dot
+  , dotDot
+  , els
+  , elsIf
+  , empty
+  , end
+  , endIf
+  , eq
+  , eqSign
+  , false
+  , forEnd
+  , forStart
+  , gt
+  , gtEq
+  , iN
+  , ifKeyStart
+  , ifStart
+  , include
+  , lt
+  , ltEq
+  , minus
+  , neq
+  , nil
+  , null
+  , oBr
+  , oPar
+  , or
+  , outputEnd
+  , outputStart
+  , pipe
+  , qMark
+  , rawEnd
+  , rawStart
+  , tableEnd
+  , tableStart
+  , tagEnd
+  , tagStart
+  , true
+  , underscore
+  , unlessEnd
+  , unlessStart
+  , when
+  , with
+  ) where
+
+import           Control.Applicative  ((<|>))
+import           Data.Attoparsec.Text
+import           Data.Text            (Text)
+import           Prelude              (Char)
+
+--------------------------------------------------------------------------------
+-- * Symbolic Tokens
+--------------------------------------------------------------------------------
+
+colon :: Parser Char
+colon = char ':'
+
+comma :: Parser Char
+comma = char ','
+
+dot :: Parser Char
+dot = char '.'
+
+dotDot :: Parser Text
+dotDot = ".."
+
+eq :: Parser Text
+eq = "=="
+
+eqSign :: Parser Char
+eqSign = char '='
+
+gt :: Parser Char
+gt = char '>'
+
+gtEq :: Parser Text
+gtEq = ">="
+
+lt :: Parser Char
+lt = char '<'
+
+ltEq :: Parser Text
+ltEq = "<="
+
+minus :: Parser Char
+minus = char '-'
+
+neq :: Parser Text
+neq = "!=" <|> "<>"
+
+oBr, cBr :: Parser Char
+oBr = char '['
+cBr = char ']'
+
+oPar, cPar :: Parser Char
+oPar = char '('
+cPar = char ')'
+
+outputStart, outputEnd :: Parser Text
+outputStart = "{{"
+outputEnd = "}}"
+
+pipe :: Parser Char
+pipe = char '|'
+
+qMark :: Parser Char
+qMark = char '?'
+
+tagStart, tagEnd :: Parser Text
+tagStart = "{%"
+tagEnd = "%}"
+
+underscore :: Parser Char
+underscore = char '_'
+
+--------------------------------------------------------------------------------
+-- * Textual Tokens
+--------------------------------------------------------------------------------
+
+and :: Parser Text
+and = "and"
+
+assign :: Parser Text
+assign = "assign"
+
+break, continue :: Parser Text
+break = "break"
+continue = "continue"
+
+captureStart, captureEnd :: Parser Text
+captureStart = "capture"
+captureEnd = "endcapture"
+
+caseStart, caseEnd :: Parser Text
+caseStart = "case"
+caseEnd = "endcase"
+
+commentStart, commentEnd :: Parser Text
+commentStart = "comment"
+commentEnd = "endcomment"
+
+contains :: Parser Text
+contains = "contains"
+
+cycle :: Parser Text
+cycle = "cycle"
+
+els :: Parser Text
+els = "else"
+
+elsIf :: Parser Text
+elsIf = "elsif"
+
+empty :: Parser Text
+empty = "empty"
+end :: Parser Text
+end = "end"
+
+endIf :: Parser Text
+endIf = "endif"
+
+forStart, forEnd :: Parser Text
+forStart = "for"
+forEnd = "endfor"
+
+iN :: Parser Text
+iN = "in"
+
+ifKeyStart :: Parser Text
+ifKeyStart = "ifkey"
+
+ifStart :: Parser Text
+ifStart = "if"
+
+include :: Parser Text
+include = "include"
+
+null, nil :: Parser Text
+null = "null"
+nil = "nil"
+
+or :: Parser Text
+or = "or"
+
+rawStart, rawEnd :: Parser Text
+rawStart = "raw"
+rawEnd = "endraw"
+
+tableStart, tableEnd :: Parser Text
+tableStart = "tablerow"
+tableEnd = "endtablerow"
+
+true, false :: Parser Text
+true = "true"
+false = "false"
+
+unlessStart, unlessEnd :: Parser Text
+unlessStart = "unless"
+unlessEnd = "endunless"
+
+when :: Parser Text
+when = "when"
+
+with :: Parser Text
+with = "with"
+
diff --git a/src/Text/Liquid/Types.hs b/src/Text/Liquid/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Liquid/Types.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Text.Liquid.Types where
+
+import           Control.Lens.TH      (makePrisms)
+import           Data.Aeson.Types     (Value)
+import           Data.List.NonEmpty   (NonEmpty)
+import           Data.Scientific      (Scientific)
+import           Data.Text            (Text)
+import           Data.Validation      (AccValidation)
+
+--------------------------------------------------------------------------------
+-- * Liquid Template Data
+--------------------------------------------------------------------------------
+
+type JsonVarPath = NonEmpty VarIndex
+
+data VarIndex
+  = ObjectIndex Text
+  | ArrayIndex Int
+  deriving (Eq, Ord, Show)
+
+data Expr
+  = Noop
+  | RawText Text
+  | Num Scientific
+  | Variable JsonVarPath
+  | QuoteString Text
+  | Equal Expr Expr
+  | NotEqual Expr Expr
+  | GtEqual Expr Expr
+  | LtEqual Expr Expr
+  | Gt Expr Expr
+  | Lt Expr Expr
+  | Or Expr Expr
+  | And Expr Expr
+  | Contains Expr Expr
+  | Nil
+  | Null
+  | Trueth
+  | Falseth
+  | Truthy Expr
+  | IfClause Expr
+  | IfKeyClause Expr
+  | ElsIfClause Expr
+  | Else
+  | FilterCell Text [Expr]
+  | Filter Expr [Expr]
+  | Output Expr
+  | TrueStatements [Expr]
+  | IfLogic Expr Expr
+  | CaseLogic Expr [(Expr, Expr)]
+  deriving (Eq, Show)
+makePrisms ''Expr
+
+--------------------------------------------------------------------------------
+-- * Error types
+--------------------------------------------------------------------------------
+
+type Rendering a = AccValidation [LiquidError] a
+
+data LiquidError
+  = TemplateParsingError Text Text [Text]
+  | JsonValueError Text
+  | JsonValueNotFound JsonVarPath
+  | NotAStringOrNumberJsonValue (Rendering Value)
+  | ImpossibleComparison Text Text
+  | ImpossibleTruthEvaluation Expr
+  | RenderingFailure Text
+  | LiquidError Text
+  deriving (Eq, Show)
+
diff --git a/src/Text/Liquid/VariableFinder.hs b/src/Text/Liquid/VariableFinder.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Liquid/VariableFinder.hs
@@ -0,0 +1,161 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TupleSections     #-}
+
+module Text.Liquid.VariableFinder where
+
+import           Control.Arrow
+import           Control.Monad.State.Lazy
+import qualified Data.List                as L
+import qualified Data.List.NonEmpty       as NEL
+import           Text.Liquid.Types
+
+-- | Allowed types
+data VType
+  = VString
+  | VNumber
+  | VStringOrNumber
+  | VBool
+  | VAny
+  deriving (Eq, Show)
+
+-- | Map findAllVariables across terms from parsed template and concatenate variable with inferred type
+findAllVariables :: [Expr]
+                 -> [(JsonVarPath, VType)]
+findAllVariables exps =
+  L.nub . join $ flip runStateT VStringOrNumber . findVariables mzero <$> exps
+
+-- | Find all the variables in a node's children, with a type scope
+findVariables :: StateT VType [] JsonVarPath
+              -> Expr
+              -> StateT VType [] JsonVarPath
+findVariables vs (Variable v)        =
+  return v `mplus` vs
+findVariables vs (Equal l r)         =
+  (findVariables mzero l) `mplus` (findVariables mzero r) `mplus` vs
+findVariables vs (NotEqual l r)      =
+  (findVariables mzero l) `mplus` (findVariables mzero r) `mplus` vs
+findVariables vs (GtEqual l r)       =
+  put VNumber >> (findVariables mzero l) `mplus` (findVariables mzero r) `mplus` vs
+findVariables vs (LtEqual l r)       =
+  put VNumber >> (findVariables mzero l) `mplus` (findVariables mzero r) `mplus` vs
+findVariables vs (Gt l r)            =
+  put VNumber >> (findVariables mzero l) `mplus` (findVariables mzero r) `mplus` vs
+findVariables vs (Lt l r)            =
+  put VNumber >> (findVariables mzero l) `mplus` (findVariables mzero r) `mplus` vs
+findVariables vs (Or l r)            =
+  (findVariables mzero l) `mplus` (findVariables mzero r) `mplus` vs
+findVariables vs (And l r)           =
+  (findVariables mzero l) `mplus` (findVariables mzero r) `mplus` vs
+findVariables vs (Contains l r)      =
+  (findVariables mzero l) `mplus` (findVariables mzero r) `mplus` vs
+findVariables vs (Truthy t)          =
+  put VBool >> findVariables vs t
+findVariables vs (IfClause i)        =
+  findVariables vs i
+findVariables vs (IfKeyClause i)        =
+  findVariables vs i
+findVariables vs (ElsIfClause i)     =
+  findVariables vs i
+findVariables vs (Filter f xs)       =
+  put VString >> (findVariables mzero f) `mplus` (msum $ findVariables mzero <$> xs) `mplus` vs
+findVariables vs (Output o)          =
+  findVariables vs o
+findVariables vs (TrueStatements xs) =
+  (msum $ findVariables mzero <$> xs) `mplus` vs
+findVariables vs (IfLogic l r)       =
+  (findVariables mzero l) `mplus` (findVariables mzero r) `mplus` vs
+findVariables vs (CaseLogic c xts)   =
+  let (ls, rs) = unzip xts
+  in (findVariables mzero c)             `mplus`
+     (msum $ findVariables mzero <$> ls) `mplus`
+     (msum $ findVariables mzero <$> rs) `mplus`
+     vs
+findVariables vs _                   =
+  vs
+
+-- | Find all context variables and add a sample filter.
+--   Designed to simulate required templates for aggregate contexts - see JsonTools
+makeAggregate :: Expr
+              -- ^ Aggregate sample filter to add
+              -> VarIndex
+              -- ^ Prefix to filter on - do not aggregate this prefix
+              -> [Expr]
+              -- ^ Parsed template to make `aggregate` style
+              -> [Expr]
+makeAggregate af pf xs =
+  aggregateElem af pf <$> xs
+
+aggregateElem :: Expr     -- ^ Aggregate sample filter to add
+              -> VarIndex -- ^ Prefix to filter on - do not aggregate this prefix
+              -> Expr     -- ^ Expression under modification
+              -> Expr
+aggregateElem _  _ Noop                             = Noop
+aggregateElem _  _ r@(RawText _)                    = r
+aggregateElem _  _ n@(Num _)                        = n
+aggregateElem _  _ v@(Variable _)                   = v
+aggregateElem _  _ q@(QuoteString _)                = q
+aggregateElem af pf (Equal l r)                     =
+  Equal (aggregateElem af pf l) (aggregateElem af pf r)
+aggregateElem af pf (NotEqual l r)                  =
+  NotEqual (aggregateElem af pf l) (aggregateElem af pf r)
+aggregateElem af pf (GtEqual l r)                   =
+  GtEqual (aggregateElem af pf l) (aggregateElem af pf r)
+aggregateElem af pf (LtEqual l r)                   =
+  LtEqual (aggregateElem af pf l) (aggregateElem af pf r)
+aggregateElem af pf (Gt l r)                        =
+  Gt (aggregateElem af pf l) (aggregateElem af pf r)
+aggregateElem af pf (Lt l r)                        =
+  Lt (aggregateElem af pf l) (aggregateElem af pf r)
+aggregateElem af pf (Or l r)                        =
+  Or (aggregateElem af pf l) (aggregateElem af pf r)
+aggregateElem af pf (And l r)                       =
+  And (aggregateElem af pf l) (aggregateElem af pf r)
+aggregateElem af pf (Contains l r)                  =
+  Contains (aggregateElem af pf l) (aggregateElem af pf r)
+aggregateElem af pf (Truthy x)                      =
+  Truthy (aggregateElem af pf x)
+aggregateElem af pf (IfClause x)                    =
+  IfClause (aggregateElem af pf x)
+aggregateElem af pf (IfKeyClause x)                 =
+  IfKeyClause (aggregateElem af pf x)
+aggregateElem af pf (ElsIfClause x)                 =
+  ElsIfClause (aggregateElem af pf x)
+aggregateElem _  _  Else                            = Else
+aggregateElem af pf (FilterCell x ys)               =
+  FilterCell x (aggregateElem af pf <$> ys)
+aggregateElem af pf (Filter (q@(QuoteString _)) fs) =
+  Filter q (aggregateElem af pf <$> fs)
+aggregateElem af pf (Filter (v@(Variable path)) fs)
+  | NEL.head path == pf = Filter v (aggregateElem af pf <$> fs)
+  | otherwise           = Filter v (aggregateElem af pf <$> updateFs af fs)
+aggregateElem af pf (Filter x fs)                   =
+  Filter x (aggregateElem af pf <$> fs)
+aggregateElem _  _ (Output q@(QuoteString _))       =
+  Output q
+aggregateElem af pf (Output v@(Variable path))
+  | NEL.head path == pf = Output v
+  | otherwise           = Output (Filter v [af])
+aggregateElem af pf (Output f)                      =
+  Output (aggregateElem af pf f)
+aggregateElem af pf (TrueStatements xs)             =
+  TrueStatements (aggregateElem af pf <$> xs)
+aggregateElem af pf (IfLogic l r)                   =
+  IfLogic (aggregateElem af pf l) (aggregateElem af pf r)
+aggregateElem af pf (CaseLogic x ys)                =
+  CaseLogic (aggregateElem af pf x) ((aggregateElem af pf *** aggregateElem af pf) <$> ys)
+aggregateElem _  _  x                               = x
+
+updateFs :: Expr   -- ^ Aggregate sample filter to prepend
+         -> [Expr] -- ^ List of filter cells
+         -> [Expr]
+updateFs _  []                                             = []
+updateFs _  f@((FilterCell "first" _):_)                   = f
+updateFs _  f@((FilterCell "firstOrDefault" _):_)          = f
+updateFs _  f@((FilterCell "last" _):_)                    = f
+updateFs _  f@((FilterCell "lastOrDefault" _):_)           = f
+updateFs _  f@((FilterCell "countElements" _):_)           = f
+updateFs _  f@((FilterCell "renderWithSeparator" _):_)     = f
+updateFs _  f@((FilterCell "toSentenceWithSeparator" _):_) = f
+updateFs af (f:fs)                                         = af:f:fs
+
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,16 @@
+import           Test.Tasty (defaultMain, TestTree, testGroup)
+import           Text.Liquid.HelperTests
+import           Text.Liquid.ParserTests
+import           Text.Liquid.RendererTests
+import           Text.Liquid.VariableFinderTests
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "Root"
+                  [ helperTests
+                  , parserTests
+                  , rendererTests
+                  , variableFinderTests
+                  ]
