Skip to content

Commit

Permalink
Add support for binding JSON values to heist.
Browse files Browse the repository at this point in the history
  • Loading branch information
gregorycollins committed Dec 7, 2011
1 parent 51d2247 commit a3c9063
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 8 deletions.
9 changes: 8 additions & 1 deletion heist.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ extra-source-files:
test/templates/foo/b.tpl,
test/templates/index.tpl,
test/templates/ioc.tpl,
test/templates/json.tpl,
test/templates/json_object.tpl,
test/templates/json_snippet.tpl,
test/templates/markdown.tpl,
test/templates/noroot.tpl,
test/templates/page.tpl,
Expand All @@ -72,6 +75,7 @@ Library
Text.Templating.Heist.Splices.Cache,
Text.Templating.Heist.Splices.Html,
Text.Templating.Heist.Splices.Ignore,
Text.Templating.Heist.Splices.Json,
Text.Templating.Heist.Splices.Markdown,
Text.Templating.Heist.TemplateDirectory

Expand All @@ -80,9 +84,11 @@ Library
Text.Templating.Heist.Types

build-depends:
aeson >= 0.4 && < 0.5,
attoparsec >= 0.10 && < 0.11,
base >= 4 && < 5,
blaze-builder >= 0.2 && < 0.4,
blaze-html >= 0.4 && < 0.5,
bytestring,
containers >= 0.2 && < 0.5,
directory,
Expand All @@ -96,7 +102,8 @@ Library
time >= 1.1 && < 1.5,
transformers,
xmlhtml >= 0.1.6 && < 0.2,
unordered-containers >= 0.1.4 && < 0.2
unordered-containers >= 0.1.4 && < 0.2,
vector >= 0.9 && < 0.10


if impl(ghc >= 6.12.0)
Expand Down
222 changes: 222 additions & 0 deletions src/Text/Templating/Heist/Splices/Json.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}

module Text.Templating.Heist.Splices.Json (
bindJson
) where

------------------------------------------------------------------------------
import Control.Monad.Reader
import Data.Aeson
import Data.Attoparsec.Number
import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Lazy.Char8 as L
import qualified Data.HashMap.Strict as Map
import Data.Maybe
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import Text.Templating.Heist
import qualified Text.Blaze.Html5 as B
import Text.Blaze.Html5 ((!))
import Text.Blaze.Renderer.XmlHtml
import Text.XmlHtml
------------------------------------------------------------------------------

------------
-- public --
------------

------------------------------------------------------------------------------
-- | This splice binds convenience tags for the given JSON (or
-- JSON-convertible) value and runs the tag's child nodes using the new
-- bindings.
--
-- /Tags bound when you pass in an object/
--
-- Tags bound for an object looking like this:
--
-- > { "k_1": v_1, ..., "k_N": v_N }
--
-- @\<value:{k_i}\>@ -- treats v_i as text
-- @\<snippet:{k_i}\>@ -- treats v_i as HTML
-- @\<with:{k_i}\>@ -- explodes v_i and runs its children
--
-- @\<value var=\"foo.bar.baz\"\/>@ -- walks the JSON tree to find
-- \"foo.bar.baz\", and interprets it as a string
-- @\<snippet var=\"foo.bar.baz\"\/\>@
-- @\<with var=\"foo.bar.baz\"\>...\<with\>@
--
-- /Tags bound when you pass in anything else/
--
-- @\<value\/\>@ -- the given JSON value, as a string
-- @\<snippet\/\>@ -- the given JSON value, parsed and spliced in as HTML
--
bindJson :: (ToJSON a, Monad m) => a -> Splice m
bindJson = runReaderT explodeTag . toJSON


-------------
-- private --
-------------

------------------------------------------------------------------------------
errorMessage :: String -> [Node]
errorMessage s = renderHtmlNodes $
B.strong ! B.customAttribute "class" "error" $
B.toHtml s


------------------------------------------------------------------------------
type JsonMonad m a = ReaderT Value (HeistT m) a


------------------------------------------------------------------------------
withValue :: (Monad m) => Value -> JsonMonad m a -> HeistT m a
withValue = flip runReaderT


------------------------------------------------------------------------------
boolToText :: Bool -> Text
boolToText b = if b then "true" else "false"


------------------------------------------------------------------------------
numToText :: Number -> Text
numToText = T.decodeUtf8 . S.concat . L.toChunks . encode


------------------------------------------------------------------------------
findExpr :: Text -> Value -> Maybe Value
findExpr t = go (T.split (=='.') t)
where
go [] !value = Just value
go (x:xs) !value = findIn value >>= go xs
where
findIn (Object obj) = Map.lookup x obj
findIn _ = Nothing


------------------------------------------------------------------------------
asHtml :: Monad m => Text -> m [Node]
asHtml t =
case (parseHTML "" $ T.encodeUtf8 t) of
Left e -> return $ errorMessage $
"Template error turning JSON into HTML: " ++ e
Right d -> return $! docContent d


------------------------------------------------------------------------------
snippetTag :: Monad m => JsonMonad m [Node]
snippetTag = ask >>= snip
where
txt t = lift $ asHtml t

snip Null = txt ""
snip (Bool b) = txt $ boolToText b
snip (Number n) = txt $ numToText n
snip (String t) = txt t
snip _ = lift $ do
node <- getParamNode
return $ errorMessage $ concat [
"error processing tag <"
, T.unpack $ fromMaybe "???" $ tagName node
, ">: can't interpret JSON arrays or objects as HTML."
]


------------------------------------------------------------------------------
valueTag :: Monad m => JsonMonad m [Node]
valueTag = ask >>= go
where
go Null = txt ""
go (Bool b) = txt $ boolToText b
go (Number n) = txt $ numToText n
go (String t) = txt t
go _ = lift $ do
node <- getParamNode
return $ errorMessage $ concat [
"error processing tag <"
, T.unpack $ fromMaybe "???" $ tagName node
, ">: can't interpret JSON arrays or objects as text."
]


txt t = return [TextNode t]


------------------------------------------------------------------------------
explodeTag :: (Monad m) => JsonMonad m [Node]
explodeTag = ask >>= go
where
--------------------------------------------------------------------------
go Null = goText ""
go (Bool b) = goText $ boolToText b
go (Number n) = goText $ numToText n
go (String t) = goText t
go (Array a) = goArray a
go (Object o) = goObject o

--------------------------------------------------------------------------
goText t = lift $ runChildrenWith [ ("value" , return [TextNode t])
, ("snippet" , asHtml t )
]

--------------------------------------------------------------------------
goArray :: (Monad m) => V.Vector Value -> JsonMonad m [Node]
goArray a = do
lift stopRecursion
dl <- V.foldM f id a
return $! dl []
where
f dl jsonValue = do
tags <- go jsonValue
return $! dl . (tags ++)

--------------------------------------------------------------------------
-- search the param node for attribute \"var=expr\", search the given JSON
-- object for the expression, and if it's found run the JsonMonad action m
-- using the restricted JSON object.
varAttrTag :: (Monad m) => Value -> (JsonMonad m [Node]) -> Splice m
varAttrTag v m = do
node <- getParamNode
maybe (noVar node) (hasVar node) $ getAttribute "var" node
where
noVar node = return $ errorMessage $
concat [ "expression error: no var attribute in <"
, T.unpack $ fromMaybe "???" $ tagName node
, "> tag"
]

hasVar node expr = maybe (return $ errorMessage $
concat [
"expression error: can't find \""
, T.unpack expr
, "\" in JSON object (<"
, T.unpack $ fromMaybe "???" $ tagName node
, "> tag)"
])
(runReaderT m)
(findExpr expr v)

--------------------------------------------------------------------------
genericBindings :: Monad m => JsonMonad m [(Text, Splice m)]
genericBindings = ask >>= \v -> return [ ("with", varAttrTag v explodeTag)
, ("snippet", varAttrTag v snippetTag)
, ("value", varAttrTag v valueTag )
]

--------------------------------------------------------------------------
goObject obj = do
start <- genericBindings
let bindings = Map.foldlWithKey' bindKvp start obj
lift $ runChildrenWith bindings

--------------------------------------------------------------------------
bindKvp bindings k v =
let newBindings = [ (T.append "with:" k , withValue v explodeTag)
, (T.append "snippet:" k, withValue v snippetTag)
, (T.append "value:" k , withValue v valueTag )
]
in newBindings ++ bindings
9 changes: 6 additions & 3 deletions test/heist-testsuite.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ Executable testsuite

build-depends:
QuickCheck >= 2,
aeson >= 0.4 && < 0.5,
attoparsec >= 0.10 && < 0.11,
base >= 4 && < 5,
blaze-builder >= 0.2 && <0.4,
blaze-builder >= 0.2 && < 0.4,
blaze-html >= 0.4 && < 0.5,
bytestring,
containers,
directory,
Expand All @@ -23,13 +25,14 @@ Executable testsuite
mtl >= 2,
random,
MonadCatchIO-transformers >= 0.2.1 && < 0.3,
test-framework >= 0.3.1 && <0.4,
test-framework >= 0.3.1 && < 0.4,
test-framework-hunit >= 0.2.5 && < 0.3,
test-framework-quickcheck2 >= 0.2.6 && < 0.3,
text >= 0.10 && < 0.12,
time,
transformers,
unordered-containers >= 0.1.4 && < 0.2
unordered-containers >= 0.1.4 && < 0.2,
vector >= 0.9 && < 0.10


ghc-options: -O2 -Wall -fhpc -fwarn-tabs -funbox-strict-fields -threaded
Expand Down
47 changes: 43 additions & 4 deletions test/suite/Text/Templating/Heist/Tests.hs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Text.Templating.Heist.Tests
Expand All @@ -12,6 +12,7 @@ module Text.Templating.Heist.Tests
------------------------------------------------------------------------------
import Blaze.ByteString.Builder
import Control.Monad.State
import Data.Aeson
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as L
Expand All @@ -36,6 +37,7 @@ import Text.Templating.Heist.Internal
import Text.Templating.Heist.Types
import Text.Templating.Heist.Splices.Apply
import Text.Templating.Heist.Splices.Ignore
import Text.Templating.Heist.Splices.Json
import Text.Templating.Heist.Splices.Markdown
import qualified Text.XmlHtml as X
import qualified Text.XmlHtml.Cursor as X
Expand Down Expand Up @@ -65,6 +67,8 @@ tests = [ testProperty "heist/simpleBind" simpleBindTest
, testCase "heist/ignore" ignoreTest
, testCase "heist/lookupTemplateContext" lookupTemplateTest
, testCase "heist/attrSpliceContext" attrSpliceContext
, testCase "heist/json/values" jsonValueTest
, testCase "heist/json/object" jsonObjectTest
]


Expand Down Expand Up @@ -228,6 +232,27 @@ markdownTest :: H.Assertion
markdownTest = renderTest "markdown" htmlExpected


------------------------------------------------------------------------------
jsonValueTest :: H.Assertion
jsonValueTest = do
renderTest "json" jsonExpected1
renderTest "json_snippet" jsonExpected2

where
jsonExpected1 = B.concat [ "<i>&lt;b&gt;ok&lt;/b&gt;</i><i>1</i>"
, "<i></i><i>false</i><i>foo</i>" ]
jsonExpected2 = "<i><b>ok</b></i><i>1</i><i></i><i>false</i><i>foo</i>"


------------------------------------------------------------------------------
jsonObjectTest :: H.Assertion
jsonObjectTest = do
renderTest "json_object" jsonExpected
where
jsonExpected = B.concat [ "<i>1</i><i><b>ok</b></i>12quuxquux1<b>ok</b>" ]


------------------------------------------------------------------------------
-- | Render a template and assert that it matches an expected result
renderTest :: ByteString -- ^ template name
-> ByteString -- ^ expected result
Expand All @@ -239,7 +264,21 @@ renderTest templateName expectedResult = do
check ts expectedResult

where
check ts str = do
bind txt = bindJson v
where
v :: Value
v = fromJust $ decode txt

check ts0 str = do
let ts = bindSplices [
("json", bind "[\"<b>ok</b>\", 1, null, false, \"foo\"]")
, ("jsonObject",
bind $ mconcat [
"{\"foo\": 1, \"bar\": \"<b>ok</b>\", "
, "\"baz\": { \"baz1\": 1, \"baz2\": 2 }, "
, "\"quux\": \"quux\" }"
])
] ts0
Just (doc, _) <- renderTemplate ts templateName
let result = B.filter (/= '\n') (toByteString doc)
H.assertEqual ("Should match " ++ (show str)) str result
Expand All @@ -264,13 +303,13 @@ divExpansion = renderTest "div_expansion" "<div>foo</div>"


------------------------------------------------------------------------------
-- | Handling of <content> and bound parameters in a bonud tag.
-- | Handling of <content> and bound parameters in a bound tag.
bindParam :: H.Assertion
bindParam = renderTest "bind_param" "<li>Hi there world</li>"


------------------------------------------------------------------------------
-- | Handling of <content> and bound parameters in a bonud tag.
-- | Handling of <content> and bound parameters in a bound tag.
attrSpliceContext :: H.Assertion
attrSpliceContext = renderTest "attrsubtest2" "<a href='asdf'>link</a>"

Expand Down
1 change: 1 addition & 0 deletions test/templates/json.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<json><i><value/></i></json>
1 change: 1 addition & 0 deletions test/templates/json_object.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<jsonObject><i><value:foo/></i><i><snippet:bar/></i><with:baz><value:baz1/><value:baz2/></with:baz><with:quux><value/></with:quux><with var="quux"><value/></with><value var="baz.baz1"/><snippet var="bar"/></jsonObject>
1 change: 1 addition & 0 deletions test/templates/json_snippet.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<json><i><snippet/></i></json>

0 comments on commit a3c9063

Please sign in to comment.