packages feed

heist 0.8.2 → 0.10.0

raw patch · 71 files changed

+4962/−2962 lines, 71 filesdep +containersdep +dlistdep +errorsdep ~basedep ~bytestringdep ~directoryPVP ok

version bump matches the API change (PVP)

Dependencies added: containers, dlist, errors, hashable

Dependency ranges changed: base, bytestring, directory, directory-tree, filepath, process, random

API changes (from Hackage documentation)

- Text.Templating.Heist: addOnLoadHook :: Monad m => (Template -> IO Template) -> HeistState m -> HeistState m
- Text.Templating.Heist: addPostRunHook :: Monad m => (Template -> m Template) -> HeistState m -> HeistState m
- Text.Templating.Heist: addPreRunHook :: Monad m => (Template -> m Template) -> HeistState m -> HeistState m
- Text.Templating.Heist: addTemplate :: Monad m => ByteString -> Template -> Maybe FilePath -> HeistState m -> HeistState m
- Text.Templating.Heist: addTemplatePathPrefix :: ByteString -> HeistState m -> HeistState m
- Text.Templating.Heist: addXMLTemplate :: Monad m => ByteString -> Template -> Maybe FilePath -> HeistState m -> HeistState m
- Text.Templating.Heist: bindSplice :: Monad m => Text -> Splice m -> HeistState m -> HeistState m
- Text.Templating.Heist: bindSplices :: Monad m => [(Text, Splice m)] -> HeistState m -> HeistState m
- Text.Templating.Heist: bindString :: Monad m => Text -> Text -> HeistState m -> HeistState m
- Text.Templating.Heist: bindStrings :: Monad m => [(Text, Text)] -> HeistState m -> HeistState m
- Text.Templating.Heist: callTemplate :: Monad m => ByteString -> [(Text, Splice m)] -> HeistT m Template
- Text.Templating.Heist: callTemplateWithText :: Monad m => ByteString -> [(Text, Text)] -> HeistT m Template
- Text.Templating.Heist: data HeistState m
- Text.Templating.Heist: data HeistT m a
- Text.Templating.Heist: defaultHeistState :: MonadIO m => HeistState m
- Text.Templating.Heist: evalHeistT :: Monad m => HeistT m a -> Node -> HeistState m -> m a
- Text.Templating.Heist: evalTemplate :: Monad m => ByteString -> HeistT m (Maybe Template)
- Text.Templating.Heist: getContext :: Monad m => HeistT m TPath
- Text.Templating.Heist: getDoc :: String -> IO (Either String DocumentFile)
- Text.Templating.Heist: getParamNode :: Monad m => HeistT m Node
- Text.Templating.Heist: getTS :: Monad m => HeistT m (HeistState m)
- Text.Templating.Heist: getTemplateFilePath :: Monad m => HeistT m (Maybe FilePath)
- Text.Templating.Heist: getXMLDoc :: String -> IO (Either String DocumentFile)
- Text.Templating.Heist: getsTS :: Monad m => (HeistState m -> r) -> HeistT m r
- Text.Templating.Heist: hasTemplate :: Monad m => ByteString -> HeistState m -> Bool
- Text.Templating.Heist: loadTemplates :: Monad m => FilePath -> HeistState m -> IO (Either String (HeistState m))
- Text.Templating.Heist: localParamNode :: Monad m => (Node -> Node) -> HeistT m a -> HeistT m a
- Text.Templating.Heist: localTS :: Monad m => (HeistState m -> HeistState m) -> HeistT m a -> HeistT m a
- Text.Templating.Heist: lookupSplice :: Monad m => Text -> HeistState m -> Maybe (Splice m)
- Text.Templating.Heist: mapSplices :: Monad m => (a -> Splice m) -> [a] -> Splice m
- Text.Templating.Heist: mkCacheTag :: MonadIO m => IO (HeistState m -> HeistState m, CacheTagState)
- Text.Templating.Heist: modifyTS :: Monad m => (HeistState m -> HeistState m) -> HeistT m ()
- Text.Templating.Heist: putTS :: Monad m => HeistState m -> HeistT m ()
- Text.Templating.Heist: renderTemplate :: Monad m => HeistState m -> ByteString -> m (Maybe (Builder, MIMEType))
- Text.Templating.Heist: renderWithArgs :: Monad m => [(Text, Text)] -> HeistState m -> ByteString -> m (Maybe (Builder, MIMEType))
- Text.Templating.Heist: restoreTS :: Monad m => HeistState m -> HeistT m ()
- Text.Templating.Heist: runChildren :: Monad m => Splice m
- Text.Templating.Heist: runChildrenWith :: Monad m => [(Text, Splice m)] -> Splice m
- Text.Templating.Heist: runChildrenWithTemplates :: Monad m => [(Text, Template)] -> Splice m
- Text.Templating.Heist: runChildrenWithText :: Monad m => [(Text, Text)] -> Splice m
- Text.Templating.Heist: runChildrenWithTrans :: Monad m => (b -> Splice m) -> [(Text, b)] -> Splice m
- Text.Templating.Heist: runNodeList :: Monad m => [Node] -> Splice m
- Text.Templating.Heist: setTemplates :: Monad m => TemplateMap -> HeistState m -> HeistState m
- Text.Templating.Heist: spliceNames :: HeistState m -> [Text]
- Text.Templating.Heist: stopRecursion :: Monad m => HeistT m ()
- Text.Templating.Heist: templateNames :: HeistState m -> [TPath]
- Text.Templating.Heist: textSplice :: Monad m => Text -> Splice m
- Text.Templating.Heist: type MIMEType = ByteString
- Text.Templating.Heist: type Splice m = HeistT m Template
- Text.Templating.Heist: type Template = [Node]
- Text.Templating.Heist.Splices.Apply: applyAttr :: Text
- Text.Templating.Heist.Splices.Apply: applyImpl :: Monad m => Splice m
- Text.Templating.Heist.Splices.Apply: applyNodes :: Monad m => Template -> Text -> Splice m
- Text.Templating.Heist.Splices.Apply: applyTag :: Text
- Text.Templating.Heist.Splices.Apply: rawApply :: Monad m => [Node] -> TPath -> [Node] -> HeistT m Template
- Text.Templating.Heist.Splices.Bind: bindAttr :: Text
- Text.Templating.Heist.Splices.Bind: bindImpl :: Monad m => Splice m
- Text.Templating.Heist.Splices.Bind: bindTag :: Text
- Text.Templating.Heist.Splices.BindStrict: bindStrictImpl :: Monad m => Splice m
- Text.Templating.Heist.Splices.BindStrict: bindStrictTag :: Text
- Text.Templating.Heist.Splices.Cache: clearCacheTagState :: CacheTagState -> IO ()
- Text.Templating.Heist.Splices.Cache: data CacheTagState
- Text.Templating.Heist.Splices.Cache: mkCacheTag :: MonadIO m => IO (HeistState m -> HeistState m, CacheTagState)
- Text.Templating.Heist.Splices.Html: extractHeads :: Node -> ([Node], Maybe Node)
- Text.Templating.Heist.Splices.Html: htmlImpl :: Monad m => Splice m
- Text.Templating.Heist.Splices.Html: htmlTag :: Text
- Text.Templating.Heist.Splices.Ignore: ignoreImpl :: Monad m => Splice m
- Text.Templating.Heist.Splices.Ignore: ignoreTag :: Text
- Text.Templating.Heist.Splices.Json: bindJson :: (ToJSON a, Monad m) => a -> Splice m
- Text.Templating.Heist.Splices.Markdown: MarkdownException :: ByteString -> MarkdownException
- Text.Templating.Heist.Splices.Markdown: NoMarkdownFileException :: NoMarkdownFileException
- Text.Templating.Heist.Splices.Markdown: PandocMissingException :: PandocMissingException
- Text.Templating.Heist.Splices.Markdown: data MarkdownException
- Text.Templating.Heist.Splices.Markdown: data NoMarkdownFileException
- Text.Templating.Heist.Splices.Markdown: data PandocMissingException
- Text.Templating.Heist.Splices.Markdown: instance Exception MarkdownException
- Text.Templating.Heist.Splices.Markdown: instance Exception NoMarkdownFileException
- Text.Templating.Heist.Splices.Markdown: instance Exception PandocMissingException
- Text.Templating.Heist.Splices.Markdown: instance Show MarkdownException
- Text.Templating.Heist.Splices.Markdown: instance Show NoMarkdownFileException
- Text.Templating.Heist.Splices.Markdown: instance Show PandocMissingException
- Text.Templating.Heist.Splices.Markdown: instance Typeable MarkdownException
- Text.Templating.Heist.Splices.Markdown: instance Typeable NoMarkdownFileException
- Text.Templating.Heist.Splices.Markdown: instance Typeable PandocMissingException
- Text.Templating.Heist.Splices.Markdown: markdownSplice :: MonadIO m => Splice m
- Text.Templating.Heist.Splices.Markdown: markdownTag :: Text
- Text.Templating.Heist.Splices.Markdown: pandoc :: FilePath -> FilePath -> FilePath -> IO ByteString
- Text.Templating.Heist.Splices.Markdown: pandocBS :: FilePath -> ByteString -> IO ByteString
- Text.Templating.Heist.Splices.Markdown: readProcessWithExitCode' :: FilePath -> [String] -> ByteString -> IO (ExitCode, ByteString, ByteString)
- Text.Templating.Heist.TemplateDirectory: data TemplateDirectory m
- Text.Templating.Heist.TemplateDirectory: getDirectoryTS :: (Monad m, MonadIO n) => TemplateDirectory m -> n (HeistState m)
- Text.Templating.Heist.TemplateDirectory: newTemplateDirectory :: (MonadIO m, MonadIO n) => FilePath -> HeistState m -> n (Either String (TemplateDirectory m))
- Text.Templating.Heist.TemplateDirectory: newTemplateDirectory' :: (MonadIO m, MonadIO n) => FilePath -> HeistState m -> n (TemplateDirectory m)
- Text.Templating.Heist.TemplateDirectory: reloadTemplateDirectory :: (MonadIO m, MonadIO n) => TemplateDirectory m -> n (Either String ())
+ Heist: DocumentFile :: Document -> Maybe FilePath -> DocumentFile
+ Heist: HeistConfig :: [(Text, Splice m)] -> [(Text, Splice IO)] -> [(Text, Splice m)] -> [(Text, AttrSplice m)] -> HashMap TPath DocumentFile -> HeistConfig m
+ Heist: HeistState :: HashMap Text (HeistT m m Template) -> HashMap TPath DocumentFile -> HashMap Text (HeistT m IO (DList (Chunk m))) -> !(HashMap TPath ([Chunk m], MIMEType)) -> HashMap Text (AttrSplice m) -> Bool -> TPath -> Int -> [DocType] -> Maybe FilePath -> KeyGen -> Bool -> HeistState m
+ Heist: _attrSpliceMap :: HeistState m -> HashMap Text (AttrSplice m)
+ Heist: _compiledSpliceMap :: HeistState m -> HashMap Text (HeistT m IO (DList (Chunk m)))
+ Heist: _compiledTemplateMap :: HeistState m -> !(HashMap TPath ([Chunk m], MIMEType))
+ Heist: _curContext :: HeistState m -> TPath
+ Heist: _curTemplateFile :: HeistState m -> Maybe FilePath
+ Heist: _doctypes :: HeistState m -> [DocType]
+ Heist: _keygen :: HeistState m -> KeyGen
+ Heist: _preprocessingMode :: HeistState m -> Bool
+ Heist: _recurse :: HeistState m -> Bool
+ Heist: _recursionDepth :: HeistState m -> Int
+ Heist: _spliceMap :: HeistState m -> HashMap Text (HeistT m m Template)
+ Heist: _templateMap :: HeistState m -> HashMap TPath DocumentFile
+ Heist: addTemplatePathPrefix :: ByteString -> HashMap TPath DocumentFile -> HashMap TPath DocumentFile
+ Heist: compiledTemplateNames :: HeistState m -> [TPath]
+ Heist: data Chunk m
+ Heist: data DocumentFile
+ Heist: data HeistConfig m
+ Heist: data HeistState m
+ Heist: data HeistT n m a
+ Heist: data RuntimeSplice m a
+ Heist: defaultLoadTimeSplices :: MonadIO m => [(Text, Splice m)]
+ Heist: dfDoc :: DocumentFile -> Document
+ Heist: dfFile :: DocumentFile -> Maybe FilePath
+ Heist: evalHeistT :: Monad m => HeistT n m a -> Node -> HeistState n -> m a
+ Heist: getContext :: Monad m => HeistT n m TPath
+ Heist: getDoc :: String -> IO (Either String DocumentFile)
+ Heist: getHS :: Monad m => HeistT n m (HeistState n)
+ Heist: getParamNode :: Monad m => HeistT n m Node
+ Heist: getTemplateFilePath :: Monad m => HeistT n m (Maybe FilePath)
+ Heist: getXMLDoc :: String -> IO (Either String DocumentFile)
+ Heist: getsHS :: Monad m => (HeistState n -> r) -> HeistT n m r
+ Heist: hasTemplate :: ByteString -> HeistState n -> Bool
+ Heist: hcAttributeSplices :: HeistConfig m -> [(Text, AttrSplice m)]
+ Heist: hcCompiledSplices :: HeistConfig m -> [(Text, Splice m)]
+ Heist: hcInterpretedSplices :: HeistConfig m -> [(Text, Splice m)]
+ Heist: hcLoadTimeSplices :: HeistConfig m -> [(Text, Splice IO)]
+ Heist: hcTemplates :: HeistConfig m -> HashMap TPath DocumentFile
+ Heist: initHeist :: Monad n => HeistConfig n -> EitherT [String] IO (HeistState n)
+ Heist: initHeistWithCacheTag :: MonadIO n => HeistConfig n -> EitherT [String] IO (HeistState n, CacheTagState)
+ Heist: instance Monoid (HeistConfig m)
+ Heist: loadTemplates :: FilePath -> EitherT [String] IO (HashMap TPath DocumentFile)
+ Heist: localHS :: Monad m => (HeistState n -> HeistState n) -> HeistT n m a -> HeistT n m a
+ Heist: localParamNode :: Monad m => (Node -> Node) -> HeistT n m a -> HeistT n m a
+ Heist: modifyHS :: Monad m => (HeistState n -> HeistState n) -> HeistT n m ()
+ Heist: orError :: Monad m => HeistT n m b -> String -> HeistT n m b
+ Heist: putHS :: Monad m => HeistState n -> HeistT n m ()
+ Heist: restoreHS :: Monad m => HeistState n -> HeistT n m ()
+ Heist: spliceNames :: HeistState m -> [Text]
+ Heist: templateNames :: HeistState m -> [TPath]
+ Heist: type AttrSplice m = Text -> m [(Text, Text)]
+ Heist: type MIMEType = ByteString
+ Heist: type TPath = [ByteString]
+ Heist: type Template = [Node]
+ Heist.Compiled: addSplices :: Monad m => [(Text, Splice n)] -> HeistT n m ()
+ Heist.Compiled: adjustPromise :: Monad m => Promise a -> (a -> a) -> RuntimeSplice m ()
+ Heist.Compiled: codeGen :: Monad m => [Chunk m] -> RuntimeSplice m Builder
+ Heist.Compiled: compileNode :: Monad n => Node -> Splice n
+ Heist.Compiled: data Promise a
+ Heist.Compiled: getPromise :: Monad m => Promise a -> RuntimeSplice m a
+ Heist.Compiled: mapPromises :: Monad n => (Promise a -> HeistT n IO (RuntimeSplice n Builder)) -> n [a] -> Splice n
+ Heist.Compiled: newEmptyPromise :: HeistT n IO (Promise a)
+ Heist.Compiled: promiseChildren :: Monad m => HeistT m IO (RuntimeSplice m Builder)
+ Heist.Compiled: promiseChildrenWith :: Monad n => [(Text, a -> Builder)] -> Promise a -> HeistT n IO (RuntimeSplice n Builder)
+ Heist.Compiled: promiseChildrenWithNodes :: Monad n => [(Text, a -> [Node])] -> Promise a -> HeistT n IO (RuntimeSplice n Builder)
+ Heist.Compiled: promiseChildrenWithText :: Monad n => [(Text, a -> Text)] -> Promise a -> HeistT n IO (RuntimeSplice n Builder)
+ Heist.Compiled: promiseChildrenWithTrans :: Monad n => (b -> Builder) -> [(Text, a -> b)] -> Promise a -> HeistT n IO (RuntimeSplice n Builder)
+ Heist.Compiled: putPromise :: Monad m => Promise a -> a -> RuntimeSplice m ()
+ Heist.Compiled: renderTemplate :: Monad n => HeistState n -> ByteString -> Maybe (n Builder, MIMEType)
+ Heist.Compiled: runNode :: Monad n => Node -> Splice n
+ Heist.Compiled: runNodeList :: Monad n => [Node] -> Splice n
+ Heist.Compiled: runSplice :: Monad n => Node -> HeistState n -> Splice n -> IO [Chunk n]
+ Heist.Compiled: type Splice n = HeistT n IO (DList (Chunk n))
+ Heist.Compiled: yieldLater :: Monad m => m Builder -> DList (Chunk m)
+ Heist.Compiled: yieldPure :: Builder -> DList (Chunk m)
+ Heist.Compiled: yieldPureText :: Text -> DList (Chunk m)
+ Heist.Compiled: yieldRuntime :: RuntimeSplice m Builder -> DList (Chunk m)
+ Heist.Compiled: yieldRuntimeEffect :: Monad m => RuntimeSplice m () -> DList (Chunk m)
+ Heist.Compiled: yieldRuntimeText :: Monad m => RuntimeSplice m Text -> DList (Chunk m)
+ Heist.Interpreted: addTemplate :: ByteString -> Template -> Maybe FilePath -> HeistState n -> HeistState n
+ Heist.Interpreted: addXMLTemplate :: ByteString -> Template -> Maybe FilePath -> HeistState n -> HeistState n
+ Heist.Interpreted: bindSplice :: Text -> Splice n -> HeistState n -> HeistState n
+ Heist.Interpreted: bindSplices :: [(Text, Splice n)] -> HeistState n -> HeistState n
+ Heist.Interpreted: bindString :: Monad n => Text -> Text -> HeistState n -> HeistState n
+ Heist.Interpreted: bindStrings :: Monad n => [(Text, Text)] -> HeistState n -> HeistState n
+ Heist.Interpreted: callTemplate :: Monad n => ByteString -> [(Text, Splice n)] -> HeistT n n Template
+ Heist.Interpreted: callTemplateWithText :: Monad n => ByteString -> [(Text, Text)] -> HeistT n n Template
+ Heist.Interpreted: evalTemplate :: Monad n => ByteString -> HeistT n n (Maybe Template)
+ Heist.Interpreted: lookupSplice :: Text -> HeistState n -> Maybe (Splice n)
+ Heist.Interpreted: mapSplices :: (Monad m, Monoid b) => (a -> m b) -> [a] -> m b
+ Heist.Interpreted: renderTemplate :: Monad n => HeistState n -> ByteString -> n (Maybe (Builder, MIMEType))
+ Heist.Interpreted: renderWithArgs :: Monad n => [(Text, Text)] -> HeistState n -> ByteString -> n (Maybe (Builder, MIMEType))
+ Heist.Interpreted: runChildren :: Monad n => Splice n
+ Heist.Interpreted: runChildrenWith :: Monad n => [(Text, Splice n)] -> Splice n
+ Heist.Interpreted: runChildrenWithTemplates :: Monad n => [(Text, Template)] -> Splice n
+ Heist.Interpreted: runChildrenWithText :: Monad n => [(Text, Text)] -> Splice n
+ Heist.Interpreted: runChildrenWithTrans :: Monad n => (b -> Splice n) -> [(Text, b)] -> Splice n
+ Heist.Interpreted: runNode :: Monad n => Node -> Splice n
+ Heist.Interpreted: runNodeList :: Monad n => [Node] -> Splice n
+ Heist.Interpreted: stopRecursion :: Monad m => HeistT n m ()
+ Heist.Interpreted: textSplice :: Monad n => Text -> Splice n
+ Heist.Interpreted: type Splice n = HeistT n n Template
+ Heist.Splices.Apply: applyAttr :: Text
+ Heist.Splices.Apply: applyImpl :: MonadIO n => Splice n
+ Heist.Splices.Apply: applyNodes :: MonadIO n => Template -> Text -> Splice n
+ Heist.Splices.Apply: applyTag :: Text
+ Heist.Splices.Apply: deprecatedContentCheck :: Monad m => Splice m
+ Heist.Splices.Apply: rawApply :: Monad n => Text -> [Node] -> Maybe FilePath -> TPath -> [Node] -> Splice n
+ Heist.Splices.Bind: bindAttr :: Text
+ Heist.Splices.Bind: bindImpl :: MonadIO n => Splice n
+ Heist.Splices.Bind: bindTag :: Text
+ Heist.Splices.BindStrict: bindStrictImpl :: MonadIO n => Splice n
+ Heist.Splices.BindStrict: bindStrictTag :: Text
+ Heist.Splices.Cache: cacheImpl :: MonadIO n => CacheTagState -> Splice n
+ Heist.Splices.Cache: cacheImplCompiled :: MonadIO n => CacheTagState -> Splice n
+ Heist.Splices.Cache: clearCacheTagState :: CacheTagState -> IO ()
+ Heist.Splices.Cache: data CacheTagState
+ Heist.Splices.Cache: mkCacheTag :: IO (Splice IO, CacheTagState)
+ Heist.Splices.Html: extractHeads :: Node -> ([Node], Maybe Node)
+ Heist.Splices.Html: htmlImpl :: Monad n => Splice n
+ Heist.Splices.Html: htmlTag :: Text
+ Heist.Splices.Ignore: ignoreImpl :: Monad m => Splice m
+ Heist.Splices.Ignore: ignoreTag :: Text
+ Heist.Splices.Json: bindJson :: (ToJSON a, Monad n) => a -> Splice n
+ Heist.Splices.Markdown: MarkdownException :: ByteString -> MarkdownException
+ Heist.Splices.Markdown: NoMarkdownFileException :: NoMarkdownFileException
+ Heist.Splices.Markdown: PandocMissingException :: PandocMissingException
+ Heist.Splices.Markdown: data MarkdownException
+ Heist.Splices.Markdown: data NoMarkdownFileException
+ Heist.Splices.Markdown: data PandocMissingException
+ Heist.Splices.Markdown: instance Exception MarkdownException
+ Heist.Splices.Markdown: instance Exception NoMarkdownFileException
+ Heist.Splices.Markdown: instance Exception PandocMissingException
+ Heist.Splices.Markdown: instance Show MarkdownException
+ Heist.Splices.Markdown: instance Show NoMarkdownFileException
+ Heist.Splices.Markdown: instance Show PandocMissingException
+ Heist.Splices.Markdown: instance Typeable MarkdownException
+ Heist.Splices.Markdown: instance Typeable NoMarkdownFileException
+ Heist.Splices.Markdown: instance Typeable PandocMissingException
+ Heist.Splices.Markdown: markdownSplice :: MonadIO m => Splice m
+ Heist.Splices.Markdown: markdownTag :: Text
+ Heist.Splices.Markdown: pandoc :: FilePath -> FilePath -> FilePath -> IO ByteString
+ Heist.Splices.Markdown: pandocBS :: FilePath -> ByteString -> IO ByteString
+ Heist.Splices.Markdown: readProcessWithExitCode' :: FilePath -> [String] -> ByteString -> IO (ExitCode, ByteString, ByteString)
+ Heist.TemplateDirectory: data TemplateDirectory n
+ Heist.TemplateDirectory: getDirectoryCTS :: TemplateDirectory n -> IO CacheTagState
+ Heist.TemplateDirectory: getDirectoryHS :: MonadIO n => TemplateDirectory n -> IO (HeistState n)
+ Heist.TemplateDirectory: newTemplateDirectory :: MonadIO n => FilePath -> HeistConfig n -> EitherT [String] IO (TemplateDirectory n)
+ Heist.TemplateDirectory: newTemplateDirectory' :: MonadIO n => FilePath -> HeistConfig n -> IO (TemplateDirectory n)
+ Heist.TemplateDirectory: reloadTemplateDirectory :: MonadIO n => TemplateDirectory n -> IO (Either String ())

Files

heist.cabal view
@@ -1,7 +1,27 @@ name:           heist-version:        0.8.2-synopsis:       An (x)html templating system-description:    An (x)html templating system+version:        0.10.0+synopsis:       An Haskell template system supporting both HTML5 and XML.+description:+    Heist is a powerful template system that supports both HTML5 and XML.+    Some of Heist's features are:+    .+    * Designer-friendly HTML5 (or XML) syntax+    .+    * Templates can be reloaded to make changes visible without recompiling+      your Haskell code+    .+    * Enforces near-perfect separation of business logic and view+    .+    * Powerful abstraction primitives allowing you to eliminate repetition+    .+    * Easy creation of domain-specific markup languages+    .+    * Built-in support for including JSON and Markdown content in templates+    .+    * Simple mechanism for designer-specified template caching+    .+    * Optional merging of multiple \<head\> tags defined anywhere in the+      document license:        BSD3 license-file:   LICENSE author:         Doug Beardsley, Gregory Collins@@ -36,30 +56,57 @@   test/heist-testsuite.cabal,   test/README,   test/runTestsAndCoverage.sh,+  test/suite/Benchmark.hs,+  test/suite/Heist/Compiled/Tests.hs,+  test/suite/Heist/Interpreted/Tests.hs,+  test/suite/Heist/TestCommon.hs,+  test/suite/Heist/Tests.hs,+  test/suite/Heist/Tutorial/AttributeSplices.lhs,+  test/suite/Heist/Tutorial/CompiledSplices.lhs,+  test/suite/Heist/Tutorial/Imports.hs,   test/suite/TestSuite.hs,-  test/suite/Text/Templating/Heist/Tests.hs,   test/templates/a.tpl,+  test/templates/attr_splice.tpl,   test/templates/attrs.tpl,+  test/templates/attrsubtest1.tpl,+  test/templates/attrsubtest2.tpl,   test/templates/bar/a.tpl,   test/templates/bar/index.tpl,+  test/templates/bind-apply-interaction/_outer.tpl,+  test/templates/bind-apply-interaction/caller.tpl,   test/templates/bind-attrs.tpl,-  test/templates/etc.tpl,+  test/templates/bind_param.tpl,+  test/templates/cache.tpl,+  test/templates/div_expansion.tpl,   test/templates/foo/a.tpl,   test/templates/foo/b.tpl,+  test/templates/foo/markdown-chdir.tpl,+  test/templates/foo/markdown-origdir.tpl,+  test/templates/foo/test2.md,+  test/templates/head_merge/index.tpl,+  test/templates/head_merge/nav.tpl,+  test/templates/head_merge/wrap.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,+  test/templates/people.tpl,+  test/templates/post.tpl,   test/templates/readme.txt,   test/templates/test.md,+  test/templates/textarea_expansion.tpl,+  test/templates/title_expansion.tpl,   test/templates/user/admin/main.tpl,   test/templates/user/admin/menu.tpl,   test/templates/user/main.tpl,   test/templates/user/menu.tpl,+  test/templates-bad/apply-missing-attr.tpl,+  test/templates-bad/apply-template-not-found.tpl,+  test/templates-bad/bind-infinite-loop.tpl,+  test/templates-bad/bind-missing-attr.tpl,   TODO  @@ -67,41 +114,50 @@   hs-source-dirs: src    exposed-modules:-    Text.Templating.Heist,-    Text.Templating.Heist.Splices,-    Text.Templating.Heist.Splices.Apply,-    Text.Templating.Heist.Splices.Bind,-    Text.Templating.Heist.Splices.BindStrict,-    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+    Heist,+    Heist.Compiled,+    Heist.Interpreted,+    Heist.Splices,+    Heist.Splices.Apply,+    Heist.Splices.Bind,+    Heist.Splices.BindStrict,+    Heist.Splices.Cache,+    Heist.Splices.Html,+    Heist.Splices.Ignore,+    Heist.Splices.Json,+    Heist.Splices.Markdown,+    Heist.TemplateDirectory    other-modules:-    Text.Templating.Heist.Internal,-    Text.Templating.Heist.Types+    Data.HeterogeneousEnvironment,+    Heist.Common,+    Heist.Compiled.Internal,+    Heist.Interpreted.Internal,+    Heist.Types    build-depends:-    aeson                     >= 0.6   && < 0.7,-    attoparsec                >= 0.10  && < 0.11,-    base                      >= 4     && < 5,-    blaze-builder             >= 0.2   && < 0.4,-    blaze-html                >= 0.4   && < 0.6,-    bytestring,-    directory,-    directory-tree,-    filepath,-    MonadCatchIO-transformers >= 0.2.1 && < 0.4,-    mtl                       >= 2.0   && < 2.2,-    process,-    random,-    text                      >= 0.10  && < 0.12,-    time                      >= 1.1   && < 1.5,-    xmlhtml                   >= 0.1.6 && < 0.3,-    unordered-containers      >= 0.1.4 && < 0.3,-    vector                    >= 0.9   && < 0.11+    MonadCatchIO-transformers  >= 0.2.1   && < 0.4,+    aeson                      >= 0.6     && < 0.7,+    attoparsec                 >= 0.10    && < 0.11,+    base                       >= 4       && < 5,+    blaze-builder              >= 0.2     && < 0.4,+    blaze-html                 >= 0.4     && < 0.6,+    bytestring                 >= 0.9     && < 0.11,+    containers                 >= 0.2     && < 0.6,+    directory                  >= 1.1     && < 1.3,+    directory-tree             >= 0.10    && < 0.12,+    dlist                      >= 0.5     && < 0.6,+    errors                     >= 1.3     && < 1.4,+    filepath                   >= 1.3     && < 1.4,+    hashable                   >= 1.1     && < 1.2,+    mtl                        >= 2.0     && < 2.2,+    process                    >= 1.1     && < 1.2,+    random                     >= 1.0.1.0 && < 1.1,+    text                       >= 0.10    && < 0.12,+    time                       >= 1.1     && < 1.5,+    unordered-containers       >= 0.1.4   && < 0.3,+    vector                     >= 0.9     && < 0.11,+    xmlhtml                    >= 0.1.6   && < 0.3    if impl(ghc >= 6.12.0)     ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2@@ -126,4 +182,5 @@  source-repository head   type:     git-  location: http://git.snapframework.com/heist.git+  location: https://github.com/snapframework/heist.git+
+ src/Data/HeterogeneousEnvironment.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE RankNTypes   #-}++------------------------------------------------------------------------------+module Data.HeterogeneousEnvironment+  ( KeyGen+  , HeterogeneousEnvironment+  , Key+  , newKeyGen+  , empty+  , makeKey+  , lookup+  , insert+  , delete+  , adjust+  , getKeyId+  ) where++------------------------------------------------------------------------------+import           Control.Monad+import           Data.IntMap   (IntMap)+import qualified Data.IntMap   as IM+import           Data.IORef+import           GHC.Exts+import           Prelude hiding (lookup)+import           Unsafe.Coerce++------------------------------------------------------------------------------+data HeterogeneousEnvironment = HeterogeneousEnvironment (IntMap Any)+newtype Key a = Key Int+newtype KeyGen = KeyGen (IORef Int)+++------------------------------------------------------------------------------+-- | If you use two different KeyGens to work with the same map, you deserve+-- what you get.+newKeyGen :: IO KeyGen+newKeyGen = liftM KeyGen $ newIORef 0+++------------------------------------------------------------------------------+getKeyId :: Key a -> Int+getKeyId (Key x) = x+++------------------------------------------------------------------------------+empty :: HeterogeneousEnvironment+empty = HeterogeneousEnvironment $ IM.empty+++------------------------------------------------------------------------------+makeKey :: KeyGen -> IO (Key a)+makeKey (KeyGen gen) = do+    k <- atomicModifyIORef gen nextKey+    return $ Key k+  where+    nextKey !x = if x >= maxBound-1+                   then error "too many keys generated"+                   else let !x' = x+1 in (x',x)+++------------------------------------------------------------------------------+lookup :: Key a -> HeterogeneousEnvironment -> Maybe a+lookup (Key k) (HeterogeneousEnvironment m) = fmap unsafeCoerce $ IM.lookup k m+++------------------------------------------------------------------------------+insert :: Key a -> a -> HeterogeneousEnvironment -> HeterogeneousEnvironment+insert (Key k) v (HeterogeneousEnvironment m) = HeterogeneousEnvironment $+                                                IM.insert k (unsafeCoerce v) m+++------------------------------------------------------------------------------+delete :: Key a -> HeterogeneousEnvironment -> HeterogeneousEnvironment+delete (Key k) (HeterogeneousEnvironment m) = HeterogeneousEnvironment $+                                              IM.delete k m+++------------------------------------------------------------------------------+adjust :: (a -> a) -> Key a -> HeterogeneousEnvironment -> HeterogeneousEnvironment+adjust f (Key k) (HeterogeneousEnvironment m) = HeterogeneousEnvironment $+                                                IM.adjust f' k m+  where+    f' = unsafeCoerce . f . unsafeCoerce
+ src/Heist.hs view
@@ -0,0 +1,227 @@+{-# LANGUAGE BangPatterns      #-}+{-# LANGUAGE OverloadedStrings #-}++{-|++This module defines the core data types used by Heist.  In practice you will+also want to import one or both of "Heist.Compiled" or "Heist.Interpreted" to+get functions needed for writing splices.++The Heist template system allows you to build custom HTML and XML based markup+languages.  With Heist you can define your own domain-specific tags+implemented with Haskell and use them in your templates.++-}++module Heist+  (+  -- * Primary Heist initialization functions+    loadTemplates+  , addTemplatePathPrefix+  , initHeist+  , initHeistWithCacheTag+  , defaultLoadTimeSplices++  -- * Core Heist data types+  , Template+  , TPath+  , HeistConfig(..)+  , MIMEType+  , DocumentFile(..)+  , AttrSplice+  , RuntimeSplice+  , Chunk+  , HeistState(..)+  , templateNames+  , compiledTemplateNames+  , hasTemplate+  , spliceNames+  , HeistT+  , evalHeistT+  , getParamNode+  , getContext+  , getTemplateFilePath+  , localParamNode+  , getsHS+  , getHS+  , putHS+  , modifyHS+  , restoreHS+  , localHS+  , getDoc+  , getXMLDoc+  , orError+  ) where++import           Control.Error+import           Control.Exception (SomeException)+import           Control.Monad.CatchIO+import           Control.Monad.Trans+import           Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.Foldable as F+import qualified Data.HeterogeneousEnvironment   as HE+import           Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as Map+import           Data.Monoid+import           Data.Text                       (Text)+import           System.Directory.Tree+import qualified Text.XmlHtml                    as X++import           Heist.Common+import qualified Heist.Compiled.Internal as C+import qualified Heist.Interpreted.Internal as I+import           Heist.Splices+import           Heist.Types+++data HeistConfig m = HeistConfig+    { hcInterpretedSplices :: [(Text, I.Splice m)]+    -- ^ Interpreted splices are the splices that Heist has always had.  They+    -- return a list of nodes and are processed at runtime.+    , hcLoadTimeSplices    :: [(Text, I.Splice IO)]+    -- ^ Load time splices are like interpreted splices because they return a+    -- list of nodes.  But they are like compiled splices because they are+    -- processed once at load time.  All of Heist's built-in splices should be+    -- used as load time splices.+    , hcCompiledSplices    :: [(Text, C.Splice m)]+    -- ^ Compiled splices return a DList of Chunks and are processed at load+    -- time to generate a runtime monad action that will be used to render the+    -- template.+    , hcAttributeSplices   :: [(Text, AttrSplice m)]+    -- ^ Attribute splices are bound to attribute names and return a list of+    -- attributes.+    , hcTemplates          :: HashMap TPath DocumentFile+    -- ^ Templates returned from the 'loadTemplates' function.+    }+++instance Monoid (HeistConfig m) where+    mempty = HeistConfig [] [] [] [] Map.empty+    mappend (HeistConfig a b c d e) (HeistConfig a' b' c' d' e') =+        HeistConfig (a `mappend` a')+                    (b `mappend` b')+                    (c `mappend` c')+                    (d `mappend` d')+                    (e `mappend` e')+++------------------------------------------------------------------------------+-- | The built-in set of static splices.  All the splices that used to be+-- enabled by default are included here.  To get the normal Heist behavior you+-- should include these in the hcLoadTimeSplices list in your HeistConfig.+defaultLoadTimeSplices :: MonadIO m => [(Text, (I.Splice m))]+defaultLoadTimeSplices =+    [ (applyTag, applyImpl)+    , (bindTag, bindImpl)+    , (ignoreTag, ignoreImpl)+    , (markdownTag, markdownSplice)+    , ("content", deprecatedContentCheck) -- To be removed in later versions+    ]+++------------------------------------------------------------------------------+-- | Loads templates from disk.  This function returns just a template map so+-- you can load multiple directories and combine the maps before initializing+-- your HeistState.+loadTemplates :: FilePath -> EitherT [String] IO (HashMap TPath DocumentFile)+loadTemplates dir = do+    d <- lift $ readDirectoryWith (loadTemplate dir) dir+    let tlist = F.fold (free d)+        errs = lefts tlist+    case errs of+        [] -> right $ Map.fromList $ rights tlist+        _  -> left errs+++------------------------------------------------------------------------------+-- | Adds a path prefix to a templates in a map returned by loadTemplates.  If+-- you want to add multiple levels of directories, separate them with slashes+-- as in "foo/bar".  Using an empty string as a path prefix will leave the+-- map unchanged.+addTemplatePathPrefix :: ByteString+                      -> HashMap TPath DocumentFile+                      -> HashMap TPath DocumentFile+addTemplatePathPrefix dir ts+  | B.null dir = ts+  | otherwise  = Map.fromList $+                 map (\(x,y) -> (f x, y)) $+                 Map.toList ts+  where+    f ps = ps++splitTemplatePath dir+++------------------------------------------------------------------------------+-- | This is the main Heist initialization function.  You pass in a map of all+-- templates and all of your splices and it constructs and returns a+-- HeistState.+--+-- We don't provide functions to add either type of loadtime splices to your+-- HeistState after initHeist because it doesn't make any sense unless you+-- re-initialize all templates with the new splices.  If you add any old-style+-- runtime heist splices after calling this function, they will still work+-- fine if you use Heist.Interpreted.renderTemplate.  If you add any templates+-- later, then those templates will be available for interpreted rendering,+-- but not for compiled rendering.+--+-- In the past you could add templates to your HeistState after initialization+-- using its Monoid instance.  Due to implementation details, this is no+-- longer possible.  All of your templates must be known when you call this+-- function.+initHeist :: Monad n+          => HeistConfig n+          -> EitherT [String] IO (HeistState n)+initHeist (HeistConfig i lt c a rawTemplates) = do+    keyGen <- lift HE.newKeyGen+    let empty = HeistState Map.empty Map.empty Map.empty Map.empty+                           Map.empty True [] 0 [] Nothing keyGen False+        hs0 = empty { _spliceMap = Map.fromList lt+                    , _templateMap = rawTemplates+                    , _preprocessingMode = True }+    tPairs <- lift $ evalHeistT+        (mapM preprocess $ Map.toList rawTemplates) (X.TextNode "") hs0+    let bad = lefts tPairs+        tmap = Map.fromList $ rights tPairs+        hs1 = empty { _spliceMap = Map.fromList i+                    , _templateMap = tmap+                    , _compiledSpliceMap = Map.fromList c+                    , _attrSpliceMap = Map.fromList a+                    }+    if not (null bad)+      then left bad+      else lift $ C.compileTemplates hs1+++------------------------------------------------------------------------------+-- | +preprocess :: (TPath, DocumentFile)+           -> HeistT IO IO (Either String (TPath, DocumentFile))+preprocess (tpath, docFile) = do+        let tname = tpathName tpath+        !emdoc <- try $ I.evalWithDoctypes tname+                  :: HeistT IO IO (Either SomeException (Maybe X.Document))+        let f !doc = (tpath, docFile { dfDoc = doc })+        return $! either (Left . show) (Right . maybe die f) emdoc+  where+    die = error "Preprocess didn't succeed!  This should never happen."+++------------------------------------------------------------------------------+-- | Wrapper around initHeist that also sets up a cache tag.  It sets up both+-- compiled and interpreted versions of the cache tag splices.  If you need to+-- do configure the cache tag differently than how this function does it, you+-- will still probably want to pattern your approach after this function's+-- implementation.+initHeistWithCacheTag :: MonadIO n+                      => HeistConfig n+                      -> EitherT [String] IO (HeistState n, CacheTagState)+initHeistWithCacheTag (HeistConfig i lt c a rawTemplates) = do+    (ss, cts) <- liftIO mkCacheTag+    let tag = "cache"+        hc' = HeistConfig ((tag, cacheImpl cts) : i)+                          ((tag, ss) : lt)+                          ((tag, cacheImplCompiled cts) : c)+                          a rawTemplates+    hs <- initHeist hc'+    return (hs, cts)+
+ src/Heist/Common.hs view
@@ -0,0 +1,288 @@+{-# LANGUAGE BangPatterns               #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE ScopedTypeVariables        #-}++module Heist.Common where++import           Control.Applicative+import           Control.Exception (SomeException)+import           Control.Monad+import           Control.Monad.CatchIO+import qualified Data.Attoparsec.Text            as AP+import           Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import           Data.Either+import           Data.Hashable+import           Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as Map+import           Data.List+import           Data.Maybe+import           Data.Monoid+import qualified Data.Text                       as T+import           Prelude hiding (catch)+import           System.FilePath+import           Heist.Types+import qualified Text.XmlHtml as X+++------------------------------------------------------------------------------+-- | If Heist is running in fail fast mode, then this function will throw an+-- exception with the second argument as the error message.  Otherwise, the+-- first argument will be executed to represent silent failure.+--+-- This behavior allows us to fail quickly if an error crops up during+-- load-time splice processing or degrade more gracefully if the error occurs+-- while a user request is being processed.+orError :: Monad m => HeistT n m b -> String -> HeistT n m b+orError silent msg = do+    hs <- getHS+    if _preprocessingMode hs+      then error $ (maybe "" (++": ") $ _curTemplateFile hs) ++ msg+      else silent+++------------------------------------------------------------------------------+-- | Function for showing a TPath.+showTPath :: TPath -> String+showTPath = BC.unpack . (`BC.append` ".tpl") . tpathName++tpathName :: TPath -> ByteString+tpathName = BC.intercalate "/" . reverse+++------------------------------------------------------------------------------+-- | Sets the current template file.+setCurTemplateFile :: Maybe FilePath -> HeistState n -> HeistState n+setCurTemplateFile Nothing ts = ts+setCurTemplateFile fp ts = ts { _curTemplateFile = fp }+++------------------------------------------------------------------------------+setCurContext :: TPath -> HeistState n -> HeistState n+setCurContext tp ts = ts { _curContext = tp }+++------------------------------------------------------------------------------+-- | Parser for attribute variable substitution.+attParser :: AP.Parser [AttAST]+attParser = liftM ($! []) (loop id)+  where+    append !dl !x = dl . (x:)++    loop !dl = go id+      where+        finish subDL = let !txt = T.concat $! subDL []+                           lit  = Literal $! T.concat $! subDL []+                       in return $! if T.null txt+                                      then dl+                                      else append dl lit++        go !subDL = (gobbleText >>= go . append subDL)+                    <|> (AP.endOfInput *> finish subDL)+                    <|> (do+                            res <- escSequence+                            dl' <- finish subDL+                            loop $! append dl' res)+                    <|> (do+                            idp <- identParser+                            dl' <- finish subDL+                            loop $! append dl' idp)++    gobbleText = AP.takeWhile1 (AP.notInClass "\\$")++    escSequence = AP.char '\\' *> (Escaped <$> AP.anyChar)++    identParser = AP.char '$' *> (ident <|> return (Literal "$"))+    ident = (AP.char '{' *> (Ident <$> AP.takeWhile (/='}')) <* AP.string "}")+++------------------------------------------------------------------------------+-- | Converts a path into an array of the elements in reverse order.  If the+-- path is absolute, we need to remove the leading slash so the split doesn't+-- leave @\"\"@ as the last element of the TPath.+--+-- FIXME @\"..\"@ currently doesn't work in paths, the solution is non-trivial+splitPathWith :: Char -> ByteString -> TPath+splitPathWith s p = if BC.null p then [] else (reverse $ BC.split s path)+  where+    path = if BC.head p == s then BC.tail p else p+++------------------------------------------------------------------------------+-- | Converts a path into an array of the elements in reverse order using the+-- path separator of the local operating system. See 'splitPathWith' for more+-- details.+splitLocalPath :: ByteString -> TPath+splitLocalPath = splitPathWith pathSeparator+++------------------------------------------------------------------------------+-- | Converts a path into an array of the elements in reverse order using a+-- forward slash (/) as the path separator. See 'splitPathWith' for more+-- details.+splitTemplatePath :: ByteString -> TPath+splitTemplatePath = splitPathWith '/'+++------------------------------------------------------------------------------+-- | Convenience function for looking up a template.+lookupTemplate :: ByteString+               -> HeistState n+               -> (HeistState n -> HashMap TPath t)+               -> Maybe (t, TPath)+lookupTemplate nameStr ts tm = f (tm ts) path name+  where+    (name:p) = case splitTemplatePath nameStr of+                   [] -> [""]+                   ps -> ps+    ctx = if B.isPrefixOf "/" nameStr then [] else _curContext ts+    path = p ++ ctx+    f = if '/' `BC.elem` nameStr+            then singleLookup+            else traversePath+++------------------------------------------------------------------------------+-- | Returns 'True' if the given template can be found in the heist state.+hasTemplate :: ByteString -> HeistState n -> Bool+hasTemplate nameStr ts =+    isJust $ lookupTemplate nameStr ts _templateMap+++------------------------------------------------------------------------------+-- | Does a single template lookup without cascading up.+singleLookup :: (Eq a, Hashable a)+             => HashMap [a] t -> [a] -> a -> Maybe (t, [a])+singleLookup tm path name = fmap (\a -> (a,path)) $ Map.lookup (name:path) tm+++------------------------------------------------------------------------------+-- | Searches for a template by looking in the full path then backing up into+-- each of the parent directories until the template is found.+traversePath :: (Eq a, Hashable a)+             => HashMap [a] t -> [a] -> a -> Maybe (t, [a])+traversePath tm [] name = fmap (\a -> (a,[])) (Map.lookup [name] tm)+traversePath tm path name =+    singleLookup tm path name `mplus`+    traversePath tm (tail path) name+++------------------------------------------------------------------------------+-- | Maps a splice generating function over a list and concatenates the+-- results.  This function now has a more general type signature so it works+-- with both compiled and interpreted splices.  The old type signature was+-- this:+--+-- > mapSplices :: (Monad n)+-- >         => (a -> Splice n n)+-- >         -> [a]+-- >         -> Splice n n+mapSplices :: (Monad m, Monoid b)+           => (a -> m b)+           -- ^ Splice generating function+           -> [a]+           -- ^ List of items to generate splices for+           -> m b+           -- ^ The result of all splices concatenated together.+mapSplices f vs = liftM mconcat $ mapM f vs+{-# INLINE mapSplices #-}+++------------------------------------------------------------------------------+-- | Gets the current context+getContext :: Monad m => HeistT n m TPath+getContext = getsHS _curContext+++------------------------------------------------------------------------------+-- | Gets the full path to the file holding the template currently being+-- processed.  Returns Nothing if the template is not associated with a file+-- on disk or if there is no template being processed.+getTemplateFilePath :: Monad m => HeistT n m (Maybe FilePath)+getTemplateFilePath = getsHS _curTemplateFile+++------------------------------------------------------------------------------+-- | Loads a template with the specified path and filename.  The+-- template is only loaded if it has a ".tpl" or ".xtpl" extension.+loadTemplate :: String -- ^ path of the template root+             -> String -- ^ full file path (includes the template root)+             -> IO [Either String (TPath, DocumentFile)] --TemplateMap+loadTemplate templateRoot fname+    | isHTMLTemplate = do+        c <- getDoc fname+        return [fmap (\t -> (splitLocalPath $ BC.pack tName, t)) c]+    | isXMLTemplate = do+        c <- getXMLDoc fname+        return [fmap (\t -> (splitLocalPath $ BC.pack tName, t)) c]+    | otherwise = return []+  where -- tName is path relative to the template root directory+        isHTMLTemplate = ".tpl"  `isSuffixOf` fname+        isXMLTemplate  = ".xtpl" `isSuffixOf` fname+        correction = if last templateRoot == '/' then 0 else 1+        extLen     = if isHTMLTemplate then 4 else 5+        tName = drop ((length templateRoot)+correction) $+                -- We're only dropping the template root, not the whole path+                take ((length fname) - extLen) fname+++------------------------------------------------------------------------------+-- | Type synonym for parsers.+type ParserFun = String -> ByteString -> Either String X.Document+++------------------------------------------------------------------------------+-- | Reads an HTML or XML template from disk.+getDocWith :: ParserFun -> String -> IO (Either String DocumentFile)+getDocWith parser f = do+    bs <- catch (liftM Right $ B.readFile f)+                (\(e::SomeException) -> return $ Left $ show e)++    let eitherDoc = either Left (parser f) bs+    return $ either (\s -> Left $ f ++ " " ++ s)+                    (\d -> Right $ DocumentFile d (Just f)) eitherDoc+++------------------------------------------------------------------------------+-- | Reads an HTML template from disk.+getDoc :: String -> IO (Either String DocumentFile)+getDoc = getDocWith X.parseHTML+++------------------------------------------------------------------------------+-- | Reads an XML template from disk.+getXMLDoc :: String -> IO (Either String DocumentFile)+getXMLDoc = getDocWith X.parseXML+++------------------------------------------------------------------------------+-- | Sets the templateMap in a HeistState.+setTemplates :: HashMap TPath DocumentFile -> HeistState n -> HeistState n+setTemplates m ts = ts { _templateMap = m }+++------------------------------------------------------------------------------+-- | Adds a template to the heist state.+insertTemplate :: TPath+               -> DocumentFile+               -> HeistState n+               -> HeistState n+insertTemplate p t st =+    setTemplates (Map.insert p t (_templateMap st)) st+++------------------------------------------------------------------------------+-- Gives the MIME type for a 'X.Document'+mimeType :: X.Document -> MIMEType+mimeType d = case d of+    (X.HtmlDocument e _ _) -> "text/html;charset=" `BC.append` enc e+    (X.XmlDocument  e _ _) -> "text/xml;charset="  `BC.append` enc e+  where+    enc X.UTF8    = "utf-8"+    -- Should not include byte order designation for UTF-16 since+    -- rendering will include a byte order mark. (RFC 2781, Sec. 3.3)+    enc X.UTF16BE = "utf-16"+    enc X.UTF16LE = "utf-16"++
+ src/Heist/Compiled.hs view
@@ -0,0 +1,61 @@+{-|++Compiled splices are similar to the original Heist (interpreted) splices, but+without the high performance costs of traversing a DOM at runtime.  Compiled+splices do all of their DOM processing at load time.  They are compiled to+produce a runtime computation that generates a ByteString Builder.  This+preserves the ability to write splices that access runtime information from+the HTTP request, database, etc.++If you import both this module and "Heist.Interpreted" in the same file, then+you will need to import them qualified.++-}++module Heist.Compiled+  (+  -- * High level compiled splice API+    Splice+  , renderTemplate+  , mapPromises+  , promiseChildren+  , promiseChildrenWith+  , promiseChildrenWithTrans+  , promiseChildrenWithText+  , promiseChildrenWithNodes++  -- * Constructing Chunks+  -- $yieldOverview+  , yieldPure+  , yieldRuntime+  , yieldRuntimeEffect+  , yieldPureText+  , yieldRuntimeText+  , yieldLater+  , addSplices++  -- * Lower level promise functions+  , Promise+  , newEmptyPromise++  -- * RuntimeSplice functions+  , getPromise+  , putPromise+  , adjustPromise+  , codeGen++  -- * Running nodes and splices+  , runNodeList+  , runNode+  , compileNode+  , runSplice++  ) where++import Heist.Compiled.Internal++-- $yieldOverview+-- The internals of the Chunk data type are deliberately not exported because+-- we want to hide the underlying implementation as much as possible.  The+-- @yield...@ functions give you lower lever construction of Chunks and DLists+-- of Chunks.
+ src/Heist/Compiled/Internal.hs view
@@ -0,0 +1,604 @@+{-# LANGUAGE BangPatterns               #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NoMonomorphismRestriction  #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE ScopedTypeVariables        #-}+{-# LANGUAGE TypeSynonymInstances       #-}++module Heist.Compiled.Internal where++import           Blaze.ByteString.Builder+import           Control.Arrow+import           Control.Monad.RWS.Strict+import           Control.Monad.State.Strict+import qualified Data.Attoparsec.Text            as AP+import           Data.ByteString (ByteString)+import           Data.DList                      (DList)+import qualified Data.DList                      as DL+import qualified Data.HashMap.Strict             as H+import qualified Data.HeterogeneousEnvironment   as HE+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           Prelude                         hiding (catch)+import qualified Text.XmlHtml                    as X++import           Heist.Common+import           Heist.Types++------------------------------------------------------------------------------+-- | A compiled Splice is a HeistT computation that returns a @DList+-- (Chunk m)@.+--+-- The more interesting part of the type signature is what comes before the+-- return value.  The first type parameter in @'HeistT' n IO@ is the runtime+-- monad.  This reveals that the Chunks know about the runtime monad.  The+-- second type parameter in @HeistT n IO@ is @IO@.  This tells is that the+-- compiled splices themselves are run in the IO monad, which will usually+-- mean at load time.  Compiled splices run at load time, and they return+-- computations that run at runtime.+type Splice n = HeistT n IO (DList (Chunk n))+++------------------------------------------------------------------------------+-- | Takes a promise function and a runtime action returning a list of items+-- that fit in the promise and returns a Splice that executes the promise+-- function for each item and concatenates the results.+--+-- This function works nicely with the 'promiseChildrenWith' family of+-- functions, much like the combination of 'mapSplices' and 'runChildrenWith'+-- for interpreted splices.+mapPromises :: Monad n+            => (Promise a -> HeistT n IO (RuntimeSplice n Builder))+            -- ^ Use 'promiseChildrenWith' or a variant to create this+            -- function.+            -> n [a]+            -- ^ Runtime computation returning a list of items+            -> Splice n+mapPromises f getList = do+    singlePromise <- newEmptyPromise+    runSingle <- f singlePromise+    return $ yieldRuntime $ do+        list <- lift getList+        htmls <- forM list $ \item ->+            putPromise singlePromise item >> runSingle+        return $ mconcat htmls+++------------------------------------------------------------------------------+-- | Returns a runtime computation that simply renders the node's children.+promiseChildren :: Monad m => HeistT m IO (RuntimeSplice m Builder)+promiseChildren = do+    res <- runNodeList . X.childNodes =<< getParamNode+    return $! codeGen $! consolidate res+{-# INLINE promiseChildren #-}+++------------------------------------------------------------------------------+-- | Binds a list of Builder splices before using the children of the spliced+-- node as a view.+promiseChildrenWith :: (Monad n)+                    => [(Text, a -> Builder)]+                    -> Promise a+                    -> HeistT n IO (RuntimeSplice n Builder)+promiseChildrenWith splices prom =+    localHS (bindSplices splices') promiseChildren+  where+    fieldSplice p f = return $ yieldRuntime $ liftM f $ getPromise p+    splices' = map (second (fieldSplice prom)) splices+++------------------------------------------------------------------------------+-- | Wrapper that composes a transformation function with the second item in+-- each of the tuples before calling promiseChildren.+promiseChildrenWithTrans :: Monad n+                         => (b -> Builder)+                         -> [(Text, a -> b)]+                         -> Promise a+                         -> HeistT n IO (RuntimeSplice n Builder)+promiseChildrenWithTrans f = promiseChildrenWith . map (second (f .))+++------------------------------------------------------------------------------+-- | Binds a list of Text splices before using the children of the spliced+-- node as a view.+promiseChildrenWithText :: (Monad n)+                        => [(Text, a -> Text)]+                        -> Promise a+                        -> HeistT n IO (RuntimeSplice n Builder)+promiseChildrenWithText =+    promiseChildrenWithTrans (fromByteString . T.encodeUtf8)+++------------------------------------------------------------------------------+-- | Binds a list of Node splices before using the children of the spliced+-- node as a view.  Note that this will slow down page generation because the+-- nodes generated by the splices must be traversed and rendered into a+-- ByteString at runtime.+promiseChildrenWithNodes :: (Monad n)+                         => [(Text, a -> [X.Node])]+                         -> Promise a+                         -> HeistT n IO (RuntimeSplice n Builder)+promiseChildrenWithNodes =+    promiseChildrenWithTrans (X.renderHtmlFragment X.UTF8)+++------------------------------------------------------------------------------+-- | Yields pure text known at load time.+pureTextChunk :: Text -> Chunk n+pureTextChunk t = Pure $ T.encodeUtf8 t+{-# INLINE pureTextChunk #-}+++------------------------------------------------------------------------------+-- | Yields a pure Builder known at load time.  You should use this and+-- 'yieldPureText' as much as possible to maximize the parts of your page that+-- can be compiled to static ByteStrings.+yieldPure :: Builder -> DList (Chunk m)+yieldPure = DL.singleton . Pure . toByteString+{-# INLINE yieldPure #-}+++------------------------------------------------------------------------------+-- | Yields a runtime action that returns a builder.+yieldRuntime :: RuntimeSplice m Builder -> DList (Chunk m)+yieldRuntime = DL.singleton . RuntimeHtml+{-# INLINE yieldRuntime #-}+++------------------------------------------------------------------------------+-- | Yields a runtime action that returns no value and is only needed for its+-- side effect.+yieldRuntimeEffect :: Monad m => RuntimeSplice m () -> DList (Chunk m)+yieldRuntimeEffect = DL.singleton . RuntimeAction+{-# INLINE yieldRuntimeEffect #-}+++------------------------------------------------------------------------------+-- | A convenience wrapper around yieldPure for working with Text.  Roughly+-- equivalent to 'textSplice' from Heist.Interpreted.+yieldPureText :: Text -> DList (Chunk m)+yieldPureText = DL.singleton . pureTextChunk+{-# INLINE yieldPureText #-}+++------------------------------------------------------------------------------+-- | Convenience wrapper around yieldRuntime allowing you to work with Text.+yieldRuntimeText :: Monad m => RuntimeSplice m Text -> DList (Chunk m)+yieldRuntimeText = yieldRuntime .  liftM (fromByteString . T.encodeUtf8)+{-# INLINE yieldRuntimeText #-}+++------------------------------------------------------------------------------+-- | This lets you turn a plain runtime monad function returning a Builder+-- into a compiled splice.+yieldLater :: Monad m => m Builder -> DList (Chunk m)+yieldLater = yieldRuntime . RuntimeSplice . lift+{-# INLINE yieldLater #-}+++------------------------------------------------------------------------------+-- | Returns a computation that performs load-time splice processing on the+-- supplied list of nodes.+runNodeList :: Monad n => [X.Node] -> Splice n+runNodeList = mapSplices runNode+++------------------------------------------------------------------------------+-- | Runs a single splice and returns the builder computation.+runSplice :: (Monad n)+          => X.Node+          -> HeistState n+          -> Splice n+          -> IO [Chunk n]+runSplice node hs splice = do+    (!a,_) <- runHeistT splice node hs+    return $! consolidate a+++------------------------------------------------------------------------------+-- | Runs a DocumentFile with the appropriate template context set.+runDocumentFile :: Monad n+                => TPath+                -> DocumentFile+                -> Splice n+runDocumentFile tpath df = do+    modifyHS (setCurTemplateFile curPath .  setCurContext tpath)+    runNodeList nodes+  where+    curPath     = dfFile df+    nodes       = X.docContent $! dfDoc df+++------------------------------------------------------------------------------+compileTemplate :: Monad n+                => HeistState n+                -> TPath+                -> DocumentFile+                -> IO [Chunk n]+compileTemplate hs tpath df = do+    !chunks <- runSplice nullNode hs $! runDocumentFile tpath df+    return chunks+  where+    -- This gets overwritten in runDocumentFile+    nullNode = X.TextNode ""+++------------------------------------------------------------------------------+compileTemplates :: Monad n => HeistState n -> IO (HeistState n)+compileTemplates hs = do+    ctm <- compileTemplates' hs+    return $! hs { _compiledTemplateMap = ctm }+--    let f = flip evalStateT HE.empty . unRT . codeGen+--    return $! hs { _compiledTemplateMap = H.map (first f) ctm }+++------------------------------------------------------------------------------+compileTemplates' :: Monad m+                  => HeistState m+                  -> IO (H.HashMap TPath ([Chunk m], MIMEType))+compileTemplates' hs = do+    ctm <- foldM runOne H.empty tpathDocfiles+    return $! ctm+  where+    tpathDocfiles :: [(TPath, DocumentFile)]+    tpathDocfiles = map (\(a,b) -> (a, b))+                        (H.toList $ _templateMap hs)++    runOne tmap (tpath, df) = do+        !mHtml <- compileTemplate hs tpath df+        return $! H.insert tpath (mHtml, mimeType $! dfDoc df) tmap+++------------------------------------------------------------------------------+-- | Consolidate consecutive Pure Chunks.+consolidate :: (Monad m) => DList (Chunk m) -> [Chunk m]+consolidate = consolidateL . DL.toList+  where+    consolidateL []     = []+    consolidateL (y:ys) = boilDown [] $! go [] y ys+      where+        ----------------------------------------------------------------------+        go soFar x [] = x : soFar+    +        go soFar (Pure a) ((Pure b) : xs) =+            go soFar (Pure $! a `mappend` b) xs+    +        go soFar (RuntimeHtml a) ((RuntimeHtml b) : xs) =+            go soFar (RuntimeHtml $! a `mappend` b) xs+    +        go soFar (RuntimeHtml a) ((RuntimeAction b) : xs) =+            go soFar (RuntimeHtml $! a >>= \x -> b >> return x) xs+    +        go soFar (RuntimeAction a) ((RuntimeHtml b) : xs) =+            go soFar (RuntimeHtml $! a >> b) xs+    +        go soFar (RuntimeAction a) ((RuntimeAction b) : xs) =+            go soFar (RuntimeAction $! a >> b) xs+    +        go soFar a (b : xs) = go (a : soFar) b xs+    +        ----------------------------------------------------------------------+        boilDown soFar []              = soFar+    +        boilDown soFar ((Pure h) : xs) = boilDown ((Pure $! h) : soFar) xs+    +        boilDown soFar (x : xs) = boilDown (x : soFar) xs+++------------------------------------------------------------------------------+-- | Given a list of output chunks, consolidate turns consecutive runs of+-- @Pure Html@ values into maximally-efficient pre-rendered strict+-- 'ByteString' chunks.+codeGen :: Monad m => [Chunk m] -> RuntimeSplice m Builder+codeGen l = V.foldr mappend mempty $! V.map toAct $! V.fromList l+  where+    toAct !(RuntimeHtml !m)   = m+    toAct !(Pure !h)          = return $! fromByteString h+    toAct !(RuntimeAction !m) = m >> return mempty+{-# INLINE codeGen #-}+++------------------------------------------------------------------------------+-- | Looks up a splice in the compiled splice map.+lookupSplice :: Text -> HeistT n IO (Maybe (Splice n))+lookupSplice nm = getsHS (H.lookup nm . _compiledSpliceMap)+++------------------------------------------------------------------------------+-- | Runs a single node.  If there is no splice referenced anywhere in the+-- subtree, then it is rendered as a pure chunk, otherwise it is compiled to+-- a runtime computation.+runNode :: Monad n => X.Node -> Splice n+runNode node = localParamNode (const node) $ do+    isStatic <- subtreeIsStatic node+    if isStatic+      then return $! yieldPure $!+             X.renderHtmlFragment X.UTF8 [parseAttrs node]+      else compileNode node+++parseAttrs :: X.Node -> X.Node+parseAttrs (X.Element nm attrs ch) = newAttrs `seq` X.Element nm newAttrs ch+  where+    newAttrs = map parseAttr attrs+parseAttrs !n = n++parseAttr :: (Text, Text) -> (Text, Text)+parseAttr (k,v) = (k, T.concat $! map cvt ast)+  where+    !ast = case AP.feed (AP.parse attParser v) "" of+            (AP.Done _ res) -> res+            (AP.Fail _ _ _) -> []+            (AP.Partial _ ) -> []+    cvt (Literal x) = x+    cvt (Escaped c) = T.singleton c+    cvt (Ident _) = error "parseAttrs: impossible case"++------------------------------------------------------------------------------+-- | Checks whether a node's subtree is static and can be rendered up front at+-- load time.+subtreeIsStatic :: X.Node -> HeistT n IO Bool+subtreeIsStatic (X.Element nm attrs ch) = do+    isNodeDynamic <- liftM isJust $ lookupSplice nm+    attrSplices <- getsHS _attrSpliceMap+    let hasSubstitutions (k,v) = hasAttributeSubstitutions v ||+                                 H.member k attrSplices+    if isNodeDynamic+      then return False+      else do+          let hasDynamicAttrs = any hasSubstitutions attrs+          if hasDynamicAttrs+            then return False+            else do+                staticSubtrees <- mapM subtreeIsStatic ch+                return $ and staticSubtrees++subtreeIsStatic _ = return True+++------------------------------------------------------------------------------+-- | Checks whether a string has any attribute substitutions.+hasAttributeSubstitutions :: Text -> Bool+hasAttributeSubstitutions txt = any isIdent ast+  where+    ast = case AP.feed (AP.parse attParser txt) "" of+            (AP.Done _ res) -> res+            (AP.Fail _ _ _) -> []+            (AP.Partial _ ) -> []+++------------------------------------------------------------------------------+-- |+parseAtt :: Monad n => (Text, Text) -> HeistT n IO (DList (Chunk n))+parseAtt (k,v) = do+    mas <- getsHS (H.lookup k . _attrSpliceMap)+    maybe doInline (return . doAttrSplice) mas++  where+    cvt (Literal x) = return $ yieldPureText x+    cvt (Escaped c) = return $ yieldPureText $ T.singleton c+    cvt (Ident x) =+        localParamNode (const $ X.Element x [] []) $ getAttributeSplice x++    -- Handles inline parsing of $() splice syntax in attributes+    doInline = do+        let ast = case AP.feed (AP.parse attParser v) "" of+                    (AP.Done _ res) -> res+                    (AP.Fail _ _ _) -> []+                    (AP.Partial _ ) -> []+        chunks <- mapM cvt ast+        let value = DL.concat chunks+        return $ attrToChunk k value++    -- Handles attribute splices+    doAttrSplice splice = DL.singleton $ RuntimeHtml $ lift $ do+        res <- splice v+        return $ mconcat $ map attrToBuilder res++    +------------------------------------------------------------------------------+-- | Given a 'X.Node' in the DOM tree, produces a \"runtime splice\" that will+-- generate html at runtime. Leaves the writer monad state untouched.+compileNode :: Monad n => X.Node -> Splice n+compileNode (X.Element nm attrs ch) =+    -- Is this node a splice, or does it merely contain splices?+    lookupSplice nm >>= fromMaybe compileStaticElement+  where+    tag0 = T.append "<" nm+    end = T.concat [ "</" , nm , ">"]+    -- If the tag is not a splice, but it contains dynamic children+    compileStaticElement = do+        -- Parse the attributes: we have Left for static and Right for runtime+        compiledAttrs <- mapM parseAtt attrs++        childHtml <- runNodeList ch++        return $! if null (DL.toList childHtml)+          then DL.concat [ DL.singleton $! pureTextChunk $! tag0+                         , DL.concat compiledAttrs+                         , DL.singleton $! pureTextChunk " />"+                         ]+          else DL.concat [ DL.singleton $! pureTextChunk $! tag0+                         , DL.concat compiledAttrs+                         , DL.singleton $! pureTextChunk ">"+                         , childHtml+                         , DL.singleton $! pureTextChunk $! end+                         ]+compileNode _ = error "impossible"+++attrToChunk :: Text -> DList (Chunk n) -> DList (Chunk n)+attrToChunk !k !v = do+    DL.concat+        [ DL.singleton $! pureTextChunk $! T.concat [" ", k, "=\""]+        , v, DL.singleton $! pureTextChunk "\"" ]+    ++attrToBuilder :: (Text, Text) -> Builder+attrToBuilder (k,v)+  | T.null v  = mconcat+    [ fromByteString $! T.encodeUtf8 " "+    , fromByteString $! T.encodeUtf8 k+    ]+  | otherwise = mconcat+    [ fromByteString $! T.encodeUtf8 " "+    , fromByteString $! T.encodeUtf8 k+    , fromByteString $! T.encodeUtf8 "=\""+    , fromByteString $! T.encodeUtf8 v+    , fromByteString $! T.encodeUtf8 "\""+    ]+++------------------------------------------------------------------------------+getAttributeSplice :: Text -> HeistT n IO (DList (Chunk n))+getAttributeSplice name =+    lookupSplice name >>= fromMaybe (return DL.empty)+{-# INLINE getAttributeSplice #-}+++------------------------------------------------------------------------------+-- | Promises are used for referencing the results of future runtime+-- computations during load time splice processing.+newtype Promise a = Promise (HE.Key a)+++------------------------------------------------------------------------------+-- | Gets the result of a promised runtime computation.+getPromise :: (Monad m) => Promise a -> RuntimeSplice m a+getPromise (Promise k) = do+    mb <- gets (HE.lookup k)+    return $ fromMaybe e mb++  where+    e = error $ "getPromise: dereferenced empty key (id "+                ++ show (HE.getKeyId k) ++ ")"+{-# INLINE getPromise #-}+++------------------------------------------------------------------------------+-- | Adds a promise to the runtime splice context.+putPromise :: (Monad m) => Promise a -> a -> RuntimeSplice m ()+putPromise (Promise k) x = modify (HE.insert k x)+{-# INLINE putPromise #-}+++------------------------------------------------------------------------------+-- | Modifies a promise.+adjustPromise :: Monad m => Promise a -> (a -> a) -> RuntimeSplice m ()+adjustPromise (Promise k) f = modify (HE.adjust f k)+{-# INLINE adjustPromise #-}+++------------------------------------------------------------------------------+-- | Creates an empty promise.+newEmptyPromise :: HeistT n IO (Promise a)+newEmptyPromise = do+    keygen <- getsHS _keygen+    key    <- liftIO $ HE.makeKey keygen+    return $! Promise key+{-# INLINE newEmptyPromise #-}+++-- ------------------------------------------------------------------------------+-- -- | Creates an empty promise with some error checking to help with debugging.+-- newEmptyPromiseWithError :: (Monad n)+--                          => String -> HeistT n IO (Promise a)+-- newEmptyPromiseWithError from = do+--     keygen <- getsHS _keygen+--     prom   <- liftM Promise $ liftIO $ HE.makeKey keygen+--     yieldRuntimeEffect $ putPromise prom+--                        $ error+--                        $ "deferenced empty promise created at" ++ from+--     return prom+-- {-# INLINE newEmptyPromiseWithError #-}+-- +-- +-- ------------------------------------------------------------------------------+-- -- | Creates a promise for a future runtime computation.+-- promise :: (Monad n) => n a -> HeistT n IO (Promise a)+-- promise act = runtimeSplicePromise (lift act)+-- {-# INLINE promise #-}+-- +-- +-- ------------------------------------------------------------------------------+-- -- | Turns a RuntimeSplice computation into a promise.+-- runtimeSplicePromise :: (Monad n)+--                      => RuntimeSplice n a+--                      -> HeistT n IO (Promise a)+-- runtimeSplicePromise act = do+--     prom <- newEmptyPromiseWithError "runtimeSplicePromise"+-- +--     let m = do+--         x <- act+--         putPromise prom x+--         return ()+-- +--     yieldRuntimeEffect m+--     return prom+-- {-# INLINE runtimeSplicePromise #-}+-- +-- +-- ------------------------------------------------------------------------------+-- -- | Sets up a runtime transformation on a 'Promise'.+-- withPromise :: (Monad n)+--             => Promise a+--             -> (a -> n b)+--             -> HeistT n IO (Promise b)+-- withPromise promA f = do+--     promB <- newEmptyPromiseWithError "withPromise"+-- +--     let m = do+--         a <- getPromise promA+--         b <- lift $ f a+--         putPromise promB b+--         return ()+-- +--     yieldRuntimeEffect m+--     return promB+-- {-# INLINE withPromise #-}+++------------------------------------------------------------------------------+-- | Binds a compiled splice.  This function should not be exported.+bindSplice :: Text             -- ^ tag name+           -> Splice n         -- ^ splice action+           -> HeistState n     -- ^ source state+           -> HeistState n+bindSplice n v ts =+    ts { _compiledSpliceMap = H.insert n v (_compiledSpliceMap ts) }+++------------------------------------------------------------------------------+-- | Binds a list of compiled splices.  This function should not be exported.+bindSplices :: [(Text, Splice n)]  -- ^ splices to bind+            -> HeistState n        -- ^ source state+            -> HeistState n+bindSplices ss ts = foldr (uncurry bindSplice) ts ss+++------------------------------------------------------------------------------+-- | Adds a list of compiled splices to the splice map.  This function is+-- useful because it allows compiled splices to bind other compiled splices+-- during load-time splice processing.+addSplices :: Monad m => [(Text, Splice n)] -> HeistT n m ()+addSplices ss = modifyHS (bindSplices ss)+++------------------------------------------------------------------------------+-- | Looks up a compiled template and returns a runtime monad computation that+-- constructs a builder.+renderTemplate :: Monad n+               => HeistState n+               -> ByteString+               -> Maybe (n Builder, MIMEType)+renderTemplate hs nm =+    fmap (first interpret . fst) $! lookupTemplate nm hs _compiledTemplateMap++interpret :: Monad m => [Chunk m] -> m Builder+interpret = flip evalStateT HE.empty . unRT . codeGen++
+ src/Heist/Interpreted.hs view
@@ -0,0 +1,80 @@+{-|++This module defines the API for writing and working with interpreted splices.+It exports some of the same symbols as "Heist.Compiled", so you will probably+want to import it qualified.++Interpreted splices can be thought of as a function @Node -> m [Node]@.  Heist+then substitutes the resulting list of nodes into your template in place of+the input node.  'Splice' is implemented as a type synonym @type Splice m =+HeistT m [Node]@, and 'HeistT' has a function 'getParamNode' that lets you get+the input node.++Suppose you have a place on your page where you want to display a link with+the text \"Logout username\" if the user is currently logged in or a link to+the login page if no user is logged in.  Assume you have a function+@getUser :: MyAppMonad (Maybe Text)@ that gets the current user.+You can implement this functionality with a 'Splice' as follows:++> import           Blaze.ByteString.Builder+> import           Data.ByteString.Char8 (ByteString)+> import qualified Data.ByteString.Char8 as B+> import           Data.Text (Text)+> import qualified Data.Text as T+> import qualified Text.XmlHtml as X+>+> import qualified Heist.Interpreted as I+>+> link :: Text -> Text -> X.Node+> link target text = X.Element "a" [("href", target)] [X.TextNode text]+>+> loginLink :: X.Node+> loginLink = link "/login" "Login"+>+> logoutLink :: Text -> X.Node+> logoutLink user = link "/logout" (T.append "Logout " user)+>+> loginLogoutSplice :: I.Splice MyAppMonad+> loginLogoutSplice = do+>     user <- lift getUser+>     return [maybe loginLink logoutLink user]+>++-}++module Heist.Interpreted+  (+    Splice++  -- * HeistState Functions+  , addTemplate+  , addXMLTemplate+  , lookupSplice+  , bindSplice+  , bindSplices++  -- * Functions for creating splices+  , textSplice+  , runChildren+  , runChildrenWith+  , runChildrenWithTrans+  , runChildrenWithTemplates+  , runChildrenWithText+  , mapSplices++  -- * HeistT functions+  , stopRecursion+  , runNode+  , runNodeList+  , evalTemplate+  , bindStrings+  , bindString+  , callTemplate+  , callTemplateWithText+  , renderTemplate+  , renderWithArgs+  ) where++import Heist.Interpreted.Internal+import Heist.Common (mapSplices)+
+ src/Heist/Interpreted/Internal.hs view
@@ -0,0 +1,427 @@+{-# LANGUAGE BangPatterns               #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE PackageImports             #-}+{-# LANGUAGE ScopedTypeVariables        #-}++module Heist.Interpreted.Internal where++------------------------------------------------------------------------------+import           Blaze.ByteString.Builder+import           Control.Arrow hiding (loop)+import           Control.Monad+import           Control.Monad.Trans+import qualified Data.Attoparsec.Text as AP+import           Data.ByteString (ByteString)+import           Data.List+import qualified Data.HashMap.Strict as Map+import           Data.Maybe+import           Data.Monoid+import qualified Data.Text as T+import           Data.Text (Text)+import           Prelude hiding (catch)+import qualified Text.XmlHtml as X++------------------------------------------------------------------------------+import           Heist.Common+import           Heist.Types+++type Splice n = HeistT n n Template+++------------------------------------------------------------------------------+-- | Mappends a doctype to the state.+addDoctype :: Monad m => [X.DocType] -> HeistT n m ()+addDoctype dt = do+    modifyHS (\s -> s { _doctypes = _doctypes s `mappend` dt })+++------------------------------------------------------------------------------+-- HeistState functions+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | Binds a new splice declaration to a tag name within a 'HeistState'.+bindSplice :: Text            -- ^ tag name+           -> Splice n        -- ^ splice action+           -> HeistState n    -- ^ source state+           -> HeistState n+bindSplice n v ts = ts {_spliceMap = Map.insert n v (_spliceMap ts)}+++------------------------------------------------------------------------------+-- | Binds a set of new splice declarations within a 'HeistState'.+bindSplices :: [(Text, Splice n)] -- ^ splices to bind+            -> HeistState n       -- ^ start state+            -> HeistState n+bindSplices ss ts = foldl' (flip id) ts acts+  where+    acts = map (uncurry bindSplice) ss+++------------------------------------------------------------------------------+-- | Converts 'Text' to a splice returning a single 'TextNode'.+textSplice :: Monad n => Text -> Splice n+textSplice t = return [X.TextNode t]+++------------------------------------------------------------------------------+-- | Runs the parameter node's children and returns the resulting node list.+-- By itself this function is a simple passthrough splice that makes the+-- spliced node disappear.  In combination with locally bound splices, this+-- function makes it easier to pass the desired view into your splices.+runChildren :: Monad n => Splice n+runChildren = runNodeList . X.childNodes =<< getParamNode+++------------------------------------------------------------------------------+-- | Binds a list of splices before using the children of the spliced node as+-- a view.+runChildrenWith :: (Monad n)+                => [(Text, Splice n)]+                -- ^ List of splices to bind before running the param nodes.+                -> Splice n+                -- ^ Returns the passed in view.+runChildrenWith splices = localHS (bindSplices splices) runChildren+++------------------------------------------------------------------------------+-- | Wrapper around runChildrenWith that applies a transformation function to+-- the second item in each of the tuples before calling runChildrenWith.+runChildrenWithTrans :: (Monad n)+          => (b -> Splice n)+          -- ^ Splice generating function+          -> [(Text, b)]+          -- ^ List of tuples to be bound+          -> Splice n+runChildrenWithTrans f = runChildrenWith . map (second f)+++------------------------------------------------------------------------------+-- | Like runChildrenWith but using constant templates rather than dynamic+-- splices.+runChildrenWithTemplates :: (Monad n) => [(Text, Template)] -> Splice n+runChildrenWithTemplates = runChildrenWithTrans return+++------------------------------------------------------------------------------+-- | Like runChildrenWith but using literal text rather than dynamic splices.+runChildrenWithText :: (Monad n) => [(Text, Text)] -> Splice n+runChildrenWithText = runChildrenWithTrans textSplice+++------------------------------------------------------------------------------+-- | Convenience function for looking up a splice.+lookupSplice :: Text+             -> HeistState n+             -> Maybe (Splice n)+lookupSplice nm ts = Map.lookup nm $ _spliceMap ts+{-# INLINE lookupSplice #-}+++------------------------------------------------------------------------------+-- | Adds an HTML format template to the heist state.+addTemplate :: ByteString+            -- ^ Path that the template will be referenced by+            -> Template+            -- ^ The template's DOM nodes+            -> Maybe FilePath+            -- ^ An optional path to the actual file on disk where the+            -- template is stored+            -> HeistState n+            -> HeistState n+addTemplate n t mfp st =+    insertTemplate (splitTemplatePath n) doc st+  where+    doc = DocumentFile (X.HtmlDocument X.UTF8 Nothing t) mfp+++------------------------------------------------------------------------------+-- | Adds an XML format template to the heist state.+addXMLTemplate :: ByteString+               -- ^ Path that the template will be referenced by+               -> Template+               -- ^ The template's DOM nodes+               -> Maybe FilePath+               -- ^ An optional path to the actual file on disk where the+               -- template is stored+               -> HeistState n+               -> HeistState n+addXMLTemplate n t mfp st =+    insertTemplate (splitTemplatePath n) doc st+  where+    doc = DocumentFile (X.XmlDocument X.UTF8 Nothing t) mfp+++------------------------------------------------------------------------------+-- | Stops the recursive processing of splices.  Consider the following+-- example:+--+--   > <foo>+--   >   <bar>+--   >     ...+--   >   </bar>+--   > </foo>+--+-- Assume that @\"foo\"@ is bound to a splice procedure. Running the @foo@+-- splice will result in a list of nodes @L@.  Normally @foo@ will recursively+-- scan @L@ for splices and run them.  If @foo@ calls @stopRecursion@, @L@+-- will be included in the output verbatim without running any splices.+stopRecursion :: Monad m => HeistT n m ()+stopRecursion = modifyHS (\st -> st { _recurse = False })+++------------------------------------------------------------------------------+-- | Performs splice processing on a single node.+runNode :: Monad n => X.Node -> Splice n+runNode (X.Element nm at ch) = do+    newAtts <- (return . concat) =<< mapM runAttrSplice at+    let n = X.Element nm newAtts ch+    s <- liftM (lookupSplice nm) getHS+    maybe (runKids newAtts) (recurseSplice n) s+  where+    runKids newAtts = do+        newKids <- runNodeList ch+        return [X.Element nm newAtts newKids]+runNode n                    = return [n]+++------------------------------------------------------------------------------+-- | Runs the attribute splice if it exists, otherwise it does inline $()+-- substitution.+runAttrSplice :: (Monad n) => (Text, Text) -> HeistT n n [(Text, Text)]+runAttrSplice a@(k,v) = do+    splice <- getsHS (Map.lookup k . _attrSpliceMap)+    maybe (liftM (:[]) $ attSubst a) (lift . ($v)) splice+++------------------------------------------------------------------------------+-- | Helper function for substituting a parsed attribute into an attribute+-- tuple.+attSubst :: (Monad n) => (t, Text) -> HeistT n n (t, Text)+attSubst (n,v) = do+    v' <- parseAtt v+    return (n,v')+++------------------------------------------------------------------------------+-- | Parses an attribute for any identifier expressions and performs+-- appropriate substitution.+parseAtt :: (Monad n) => Text -> HeistT n n Text+parseAtt bs = do+    let ast = case AP.feed (AP.parse attParser bs) "" of+                (AP.Done _ res) -> res+                (AP.Fail _ _ _) -> []+                (AP.Partial _)  -> []+    chunks <- mapM cvt ast+    return $ T.concat chunks+  where+    cvt (Literal x) = return x+    cvt (Escaped c) = renderEscaped c+    cvt (Ident x)   =+        localParamNode (const $ X.Element x [] []) $ getAttributeSplice x++    renderEscaped c = do+        hs <- getHS+        if _preprocessingMode hs+          then return $ T.snoc "\\" c+          else return $ T.singleton c+++------------------------------------------------------------------------------+-- | Gets the attribute value.  If the splice's result list contains non-text+-- nodes, this will translate them into text nodes with nodeText and+-- concatenate them together.+--+-- Originally, this only took the first node from the splices's result list,+-- and only if it was a text node. This caused problems when the splice's+-- result contained HTML entities, as they would split a text node. This was+-- then fixed to take the first consecutive bunch of text nodes, and return+-- their concatenation. This was seen as more useful than throwing an error,+-- and more intuitive than trying to render all the nodes as text.+--+-- However, it was decided in the end to render all the nodes as text, and+-- then concatenate them. If a splice returned+-- \"some \<b\>text\<\/b\> foobar\", the user would almost certainly want+-- \"some text foobar\" to be rendered, and Heist would probably seem+-- annoyingly limited for not being able to do this. If the user really did+-- want it to render \"some \", it would probably be easier for them to+-- accept that they were silly to pass more than that to be substituted than+-- it would be for the former user to accept that+-- \"some \<b\>text\<\/b\> foobar\" is being rendered as \"some \" because+-- it's \"more intuitive\".+getAttributeSplice :: Monad n => Text -> HeistT n n Text+getAttributeSplice name = do+    hs <- getHS+    let noSplice = if _preprocessingMode hs+                     then return $ T.concat ["${", name, "}"]+                     else return ""+    let s = lookupSplice name hs+    maybe noSplice (liftM (T.concat . map X.nodeText)) s++------------------------------------------------------------------------------+-- | Performs splice processing on a list of nodes.+runNodeList :: Monad n => [X.Node] -> Splice n+runNodeList = mapSplices runNode+{-# INLINE runNodeList #-}+++------------------------------------------------------------------------------+-- | The maximum recursion depth.  (Used to prevent infinite loops.)+mAX_RECURSION_DEPTH :: Int+mAX_RECURSION_DEPTH = 50+++------------------------------------------------------------------------------+-- | Checks the recursion flag and recurses accordingly.  Does not recurse+-- deeper than mAX_RECURSION_DEPTH to avoid infinite loops.+recurseSplice :: Monad n => X.Node -> Splice n -> Splice n+recurseSplice node splice = do+    result <- localParamNode (const node) splice+    hs <- getHS+    if _recurse hs+        then if _recursionDepth hs < mAX_RECURSION_DEPTH+               then do modRecursionDepth (+1)+                       res <- runNodeList result+                       restoreHS hs+                       return res+               else return result `orError` err+        else return result+  where+    err = unwords+        ["Recursion limit reached in node"+        ,"<"++(T.unpack $ X.elementTag node)++">.  You"+        ,"probably have infinite splice recursion!"+        ]+               +++------------------------------------------------------------------------------+-- | Looks up a template name runs a 'HeistT' computation on it.+lookupAndRun :: Monad m+             => ByteString+             -> ((DocumentFile, TPath) -> HeistT n m (Maybe a))+             -> HeistT n m (Maybe a)+lookupAndRun name k = do+    ts <- getHS+    let mt = lookupTemplate name ts _templateMap+    let curPath = join $ fmap (dfFile . fst) mt+    modifyHS (setCurTemplateFile curPath)+    maybe (return Nothing) k mt+++------------------------------------------------------------------------------+-- | Looks up a template name evaluates it by calling runNodeList.+evalTemplate :: Monad n+             => ByteString+             -> HeistT n n (Maybe Template)+evalTemplate name = lookupAndRun name+    (\(t,ctx) -> localHS (\ts -> ts {_curContext = ctx})+                         (liftM Just $ runNodeList $ X.docContent $ dfDoc t))+++------------------------------------------------------------------------------+-- | Sets the document type of a 'X.Document' based on the 'HeistT'+-- value.+fixDocType :: Monad m => X.Document -> HeistT n m X.Document+fixDocType d = do+    dts <- getsHS _doctypes+    return $ d { X.docType = listToMaybe dts }+++------------------------------------------------------------------------------+-- | Runs a template and sets the doctype properly.  This is the right thing+-- to do if we are starting at the top level.+evalWithDoctypes :: Monad n+                 => ByteString+                 -> HeistT n n (Maybe X.Document)+evalWithDoctypes name = lookupAndRun name $ \(t,ctx) -> do+    addDoctype $ maybeToList $ X.docType $ dfDoc t+    ts <- getHS+    let nodes = X.docContent $ dfDoc t+    putHS (ts {_curContext = ctx})+    newNodes <- runNodeList nodes+    restoreHS ts+    newDoc   <- fixDocType $ (dfDoc t) { X.docContent = newNodes }+    return (Just newDoc)+++------------------------------------------------------------------------------+-- | Binds a list of constant string splices.+bindStrings :: Monad n+            => [(Text, Text)]+            -> HeistState n+            -> HeistState n+bindStrings pairs ts = foldr (uncurry bindString) ts pairs+++------------------------------------------------------------------------------+-- | Binds a single constant string splice.+bindString :: Monad n+           => Text+           -> Text+           -> HeistState n+           -> HeistState n+bindString n = bindSplice n . textSplice+++------------------------------------------------------------------------------+-- | Renders a template with the specified parameters.  This is the function+-- to use when you want to "call" a template and pass in parameters from+-- inside a splice.  If the template does not exist, this version simply+-- returns an empty list.+callTemplate :: Monad n+             => ByteString         -- ^ The name of the template+             -> [(Text, Splice n)] -- ^ Association list of+                                   -- (name,value) parameter pairs+             -> HeistT n n Template+callTemplate name params = do+    modifyHS $ bindSplices params+    liftM (maybe [] id) $ evalTemplate name+++------------------------------------------------------------------------------+-- | Like callTemplate except the splices being bound are constant text+-- splices.+callTemplateWithText :: Monad n+                     => ByteString     -- ^ The name of the template+                     -> [(Text, Text)] -- ^ Association list of+                                       -- (name,value) parameter pairs+                     -> HeistT n n Template+callTemplateWithText name params = do+    modifyHS $ bindStrings params+    liftM (maybe [] id) $ evalTemplate name+++------------------------------------------------------------------------------+-- | Renders a template from the specified HeistState to a 'Builder'.  The+-- MIME type returned is based on the detected character encoding, and whether+-- the root template was an HTML or XML format template.  It will always be+-- @text/html@ or @text/xml@.  If a more specific MIME type is needed for a+-- particular XML application, it must be provided by the application.+renderTemplate :: Monad n+               => HeistState n+               -> ByteString+               -> n (Maybe (Builder, MIMEType))+renderTemplate ts name = evalHeistT tpl (X.TextNode "") ts+  where tpl = do mt <- evalWithDoctypes name+                 case mt of+                    Nothing  -> return Nothing+                    Just doc -> return $ Just $ (X.render doc, mimeType doc)+++------------------------------------------------------------------------------+-- | Renders a template with the specified arguments passed to it.  This is a+-- convenience function for the common pattern of calling renderTemplate after+-- using bindString, bindStrings, or bindSplice to set up the arguments to the+-- template.+renderWithArgs :: Monad n+               => [(Text, Text)]+               -> HeistState n+               -> ByteString+               -> n (Maybe (Builder, MIMEType))+renderWithArgs args ts = renderTemplate (bindStrings args ts)++
+ src/Heist/Splices.hs view
@@ -0,0 +1,16 @@+module Heist.Splices+  ( module Heist.Splices.Apply+  , module Heist.Splices.Bind+  , module Heist.Splices.Cache+  , module Heist.Splices.Html+  , module Heist.Splices.Ignore+  , module Heist.Splices.Markdown+  ) where++import Heist.Splices.Apply+import Heist.Splices.Bind+import Heist.Splices.Cache+import Heist.Splices.Html+import Heist.Splices.Ignore+import Heist.Splices.Markdown+
+ src/Heist/Splices/Apply.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+module Heist.Splices.Apply where++------------------------------------------------------------------------------+import           Control.Monad.Trans+import           Data.Maybe+import           Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Text.XmlHtml as X++------------------------------------------------------------------------------+import           Heist.Common+import           Heist.Interpreted.Internal+import           Heist.Types+++------------------------------------------------------------------------------+-- | Default name for the apply splice.+applyTag :: Text+applyTag = "apply"+++------------------------------------------------------------------------------+-- | Default attribute name for the apply tag.+applyAttr :: Text+applyAttr = "template"+++------------------------------------------------------------------------------+-- | +rawApply :: (Monad n)+         => Text+         -> [X.Node]+         -> Maybe FilePath+         -> TPath+         -> [X.Node]+         -> Splice n+rawApply paramTag calledNodes templateFile newContext paramNodes = do+    hs <- getHS  -- Can't use localHS here because the modifier is not pure+    processedParams <- runNodeList paramNodes++    -- apply should do a bottom-up traversal, so we run the called nodes+    -- before doing <content/> substitution.+    modifyHS (setCurContext newContext . setCurTemplateFile templateFile)++    let process = concatMap (treeMap processedParams)+    if _recursionDepth hs < mAX_RECURSION_DEPTH+      then do modRecursionDepth (+1)+              res <- runNodeList calledNodes+              restoreHS hs+              return $! process res+      else do restoreHS hs+              (return []) `orError` err+  where+    err = "template recursion exceeded max depth, "+++          "you probably have infinite splice recursion!"+    treeMap :: [X.Node] -> X.Node -> [X.Node]+    treeMap ns n@(X.Element nm _ cs)+      | nm == paramTag = ns+      | otherwise = [n { X.elementChildren = cs' }]+      where+        !cs' = concatMap (treeMap ns) cs+    treeMap _ n = [n]+++------------------------------------------------------------------------------+-- | Applies a template as if the supplied nodes were the children of the+-- <apply> tag.+applyNodes :: MonadIO n => Template -> Text -> Splice n+applyNodes nodes template = do+    hs <- getHS+    maybe (return [] `orError` err)+          (\(t,ctx) -> do+              addDoctype $ maybeToList $ X.docType $ dfDoc t+              rawApply "apply-content" (X.docContent $ dfDoc t)+                       (dfFile t) ctx nodes)+          (lookupTemplate (T.encodeUtf8 template) hs _templateMap)+  where+    err = "apply tag cannot find template \""++(T.unpack template)++"\""+++------------------------------------------------------------------------------+-- | Implementation of the apply splice.+applyImpl :: MonadIO n => Splice n+applyImpl = do+    node <- getParamNode+    let err = "must supply \"" ++ T.unpack applyAttr +++              "\" attribute in <" ++ T.unpack (X.elementTag node) ++ ">"+    case X.getAttribute applyAttr node of+        Nothing   -> return [] `orError` err+        Just template -> applyNodes (X.childNodes node) template+++deprecatedContentCheck :: Monad m => Splice m+deprecatedContentCheck =+    return [] `orError` unwords+      ["<content> tag deprecated.  Use"+      ,"<apply-content> or <bind-content>"+      ]+
+ src/Heist/Splices/Bind.hs view
@@ -0,0 +1,43 @@+module Heist.Splices.Bind where++------------------------------------------------------------------------------+import           Control.Monad.Trans+import           Data.Text (Text)+import qualified Data.Text as T+import qualified Text.XmlHtml as X++------------------------------------------------------------------------------+import           Heist.Common+import           Heist.Interpreted.Internal+import           Heist.Splices.Apply+import           Heist.Types++-- | Default name for the bind splice.+bindTag :: Text+bindTag = "bind"+++------------------------------------------------------------------------------+-- | Default attribute name for the bind tag.+bindAttr :: Text+bindAttr = "tag"+++------------------------------------------------------------------------------+-- | Implementation of the bind splice.+bindImpl :: MonadIO n => Splice n+bindImpl = do+    node <- getParamNode+    let err = "must supply \"" ++ T.unpack bindAttr +++              "\" attribute in <" ++ T.unpack (X.elementTag node) ++ ">"+    maybe (return () `orError` err)+          (add node)+          (X.getAttribute bindAttr node)+    return []+  where+    add node nm = modifyHS $ bindSplice nm $ do+        caller <- getParamNode+        ctx <- getContext+        rawApply "bind-content" (X.childNodes node)+                 Nothing ctx (X.childNodes caller)+
+ src/Heist/Splices/BindStrict.hs view
@@ -0,0 +1,37 @@+module Heist.Splices.BindStrict where++------------------------------------------------------------------------------+import           Control.Monad.Trans+import           Data.Text (Text)+import qualified Data.Text as T+import qualified Text.XmlHtml as X++------------------------------------------------------------------------------+import           Heist.Common+import           Heist.Interpreted.Internal+import           Heist.Splices.Apply+import           Heist.Splices.Bind+import           Heist.Types++-- | Default name for the bind splice.+bindStrictTag :: Text+bindStrictTag = "bindStrict"+++------------------------------------------------------------------------------+-- | Implementation of the bind splice.+bindStrictImpl :: MonadIO n => Splice n+bindStrictImpl = do+    node <- getParamNode+    cs <- runChildren+    let err = "must supply \"" ++ T.unpack bindAttr +++              "\" attribute in <" ++ T.unpack (X.elementTag node) ++ ">"+    maybe (return () `orError` err) (add cs)+          (X.getAttribute bindAttr node)+    return []++  where+    add cs nm = modifyHS $ bindSplice nm $ do+        caller <- getParamNode+        ctx <- getContext+        rawApply "bindstrict-content" cs Nothing ctx (X.childNodes caller)
+ src/Heist/Splices/Cache.hs view
@@ -0,0 +1,202 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}++-- | The \"cache\" splice ensures that its contents are cached and only+-- evaluated periodically.  The cached contents are returned every time the+-- splice is referenced.+--+-- Use the ttl attribute to set the amount of time between reloads.  The ttl+-- value should be a positive integer followed by a single character+-- specifying the units.  Valid units are a single letter abbreviation for one+-- of seconds, minutes, hours, days, and weeks.  If the ttl string is invalid+-- or the ttl attribute is not specified, the cache is never refreshed unless+-- explicitly cleared with clearCacheTagState.  The compiled splice version of+-- the cache tag does not require a cache tag state, so clearCacheTagState+-- will not work for compiled cache tags.++module Heist.Splices.Cache+  ( CacheTagState+  , cacheImpl+  , cacheImplCompiled+  , mkCacheTag+  , clearCacheTagState+  ) where++------------------------------------------------------------------------------+import           Blaze.ByteString.Builder+import           Control.Concurrent+import           Control.Monad+import           Control.Monad.Trans+import           Data.IORef+import qualified Data.HashMap.Strict as H+import           Data.HashMap.Strict (HashMap)+import qualified Data.HashSet as Set+import           Data.Text (Text)+import qualified Data.Text as T+import           Data.Text.Read+import           Data.Time.Clock+import           Data.Word+import           System.Random+import           Text.XmlHtml+++------------------------------------------------------------------------------+import qualified Heist.Compiled.Internal as C+import           Heist.Interpreted.Internal+import           Heist.Types+++------------------------------------------------------------------------------+cacheTagName :: Text+cacheTagName = "cache"+++------------------------------------------------------------------------------+-- | State for storing cache tag information+newtype CacheTagState =+    CTS (MVar ([IORef (Maybe (UTCTime, Builder))], HashMap Text (UTCTime, Template)))+++addCompiledRef :: IORef (Maybe (UTCTime, Builder)) -> CacheTagState -> IO ()+addCompiledRef ref (CTS mv) = do+    modifyMVar_ mv (\(a,b) -> return (ref:a, b))++    +------------------------------------------------------------------------------+-- | Clears the cache tag state.+clearCacheTagState :: CacheTagState -> IO ()+clearCacheTagState (CTS cacheMVar) = do+    refs <- modifyMVar cacheMVar (\(a,_) -> return ((a, H.empty), a))+    mapM_ (\ref -> writeIORef ref Nothing) refs+++------------------------------------------------------------------------------+-- | Converts a TTL string into an integer number of seconds.+parseTTL :: Text -> Int+parseTTL s = value * multiplier+  where+    (value,rest) = either (const (0::Int,"s")) id $ decimal s+    multiplier = case T.take 1 rest of+        "s" -> 1 :: Int+        "m" -> 60+        "h" -> 3600+        "d" -> 86400+        "w" -> 604800+        _   -> 1+++getTTL :: Node -> NominalDiffTime+getTTL tree = fromIntegral $ maybe 0 parseTTL $ getAttribute "ttl" tree+{-# INLINE getTTL #-}+++------------------------------------------------------------------------------+-- | This is the splice that actually does the work.  You should bind it to+-- the same tag name as you bound the splice returned by mkCacheTag otherwise+-- it won't work and you'll get runtime errors.+cacheImpl :: (MonadIO n) => CacheTagState -> Splice n+cacheImpl (CTS mv) = do+    tree <- getParamNode+    let err = error $ unwords ["cacheImpl is bound to a tag"+                              ,"that didn't get an id attribute."+                              ," This should never happen."]+    let i = maybe err id $ getAttribute "id" tree+        !ttl = getTTL tree+    mp <- liftIO $ readMVar mv++    ns <- do+        cur <- liftIO getCurrentTime+        let mbn = H.lookup i $ snd mp+            reload = do+                nodes' <- runNodeList $ childNodes tree+                let newMap = H.insert i (cur, nodes') $ snd mp+                liftIO $ modifyMVar_ mv (\(a,_) -> return (a, newMap))+                return $! nodes'+        case mbn of+            Nothing -> reload+            (Just (lastUpdate,n)) -> do+                if ttl > 0 && tagName tree == Just cacheTagName &&+                   diffUTCTime cur lastUpdate > ttl+                  then reload+                  else do+                      stopRecursion+                      return $! n++    return ns+++------------------------------------------------------------------------------+-- | This is the compiled splice version of cacheImpl.+cacheImplCompiled :: (MonadIO n) => CacheTagState -> C.Splice n+cacheImplCompiled cts = do+    tree <- getParamNode+    let !ttl = getTTL tree++    compiled <- C.runNodeList $ childNodes tree+    ref <- liftIO $ newIORef Nothing+    liftIO $ addCompiledRef ref cts+    let reload curTime = do+            builder <- C.codeGen $! C.consolidate compiled+            let out = fromByteString $! toByteString $! builder+            liftIO $ writeIORef ref (Just (curTime, out))+            return $! out+    return $ C.yieldRuntime $ do+        mbn <- liftIO $ readIORef ref+        cur <- liftIO getCurrentTime+        case mbn of+            Nothing -> reload cur+            (Just (lastUpdate,bs)) -> do+                if (ttl > 0 && diffUTCTime cur lastUpdate > ttl)+                  then reload cur+                  else return $! bs+++------------------------------------------------------------------------------+-- | Returns items necessary to set up a \"cache\" tag.  The cache tag cannot+-- be bound automatically with the other default Heist tags.  This is because+-- this function also returns CacheTagState, so the user will be able to clear+-- it with the 'clearCacheTagState' function.+--+-- This function returns a splice and a CacheTagState.  The splice is of type+-- @Splice IO@ because it has to be bound as a load time preprocessing splice.+-- Haskell's type system won't allow you to screw up and pass this splice as+-- the wrong argument to initHeist.+mkCacheTag :: IO (Splice IO, CacheTagState)+mkCacheTag = do+    sr <- newIORef $ Set.empty+    mv <- liftM CTS $ newMVar ([], H.empty)++    return $ (setupSplice sr, mv)+++------------------------------------------------------------------------------+-- | Explicit type signature to avoid the Show polymorphism problem.+generateId :: IO Word+generateId = getStdRandom random+++------------------------------------------------------------------------------+-- | Gets a unique ID for use in the cache tags.+getId :: IORef (Set.HashSet Text) -> IO Text+getId setref = do+    i <- liftM (T.pack . show) generateId+    _set <- readIORef setref+    if Set.member i _set+      then getId setref+      else do+          writeIORef setref $ Set.insert i _set+          return $ T.append "cache-id-" i+++------------------------------------------------------------------------------+-- | A splice that sets the id attribute so that nodes can be cache-aware.+setupSplice :: IORef (Set.HashSet Text) -> Splice IO+setupSplice setref = do+    i <- liftIO $ getId setref+    node <- getParamNode++    newChildren <- runNodeList $ childNodes node+    stopRecursion+    return $ [setAttribute "id" i $ node { elementChildren = newChildren }]++
+ src/Heist/Splices/Html.hs view
@@ -0,0 +1,47 @@+module Heist.Splices.Html where++------------------------------------------------------------------------------+import           Data.Maybe+import           Data.Text (Text)+import qualified Text.XmlHtml as X++------------------------------------------------------------------------------+import           Heist.Interpreted.Internal+import           Heist.Types+++------------------------------------------------------------------------------+-- | Name for the html splice.+htmlTag :: Text+htmlTag = "html"+++------------------------------------------------------------------------------+-- | The html splice runs all children and then traverses the returned node+-- forest removing all head nodes.  Then it merges them all and prepends it to+-- the html tag's child list.+htmlImpl :: Monad n => Splice n+htmlImpl = do+    node <- getParamNode+    children <- runNodeList $ X.childNodes node+    let (heads, mnode) = extractHeads $ node { X.elementChildren = children }+        new (X.Element t a c) = X.Element t a $+            X.Element "head" [] heads : c+        new n = n+    stopRecursion+    return [maybe node new mnode]++------------------------------------------------------------------------------+-- | Extracts all heads from a node tree.+extractHeads :: X.Node+             -- ^ The root (html) node+             -> ([X.Node], Maybe X.Node)+             -- ^ A tuple of a list of head nodes and the original tree with+             --   heads removed.+extractHeads (X.Element t a c)+  | t == "head" = (c, Nothing)+  | otherwise   = (concat heads, Just $ X.Element t a (catMaybes mcs))+  where+    (heads, mcs) = unzip $ map extractHeads c+extractHeads n = ([], Just n)+
+ src/Heist/Splices/Ignore.hs view
@@ -0,0 +1,22 @@+module Heist.Splices.Ignore where++------------------------------------------------------------------------------+import           Data.Text (Text)++------------------------------------------------------------------------------+import           Heist.Interpreted.Internal+++------------------------------------------------------------------------------+-- | Default name for the ignore splice.+ignoreTag :: Text+ignoreTag = "ignore"+++------------------------------------------------------------------------------+-- | The ignore tag and everything it surrounds disappears in the+-- rendered output.+ignoreImpl :: Monad m => Splice m+ignoreImpl = return []++
+ src/Heist/Splices/Json.hs view
@@ -0,0 +1,224 @@+{-# LANGUAGE BangPatterns      #-}+{-# LANGUAGE OverloadedStrings #-}++module 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 qualified Text.Blaze.Html5            as B+import           Text.Blaze.Html5            ((!))+import           Text.Blaze.Renderer.XmlHtml+import           Text.XmlHtml+------------------------------------------------------------------------------++import           Heist.Interpreted.Internal+import           Heist.Types++                                 ------------+                                 -- 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 n) => a -> Splice n+bindJson = runReaderT explodeTag . toJSON+++                                 -------------+                                 -- private --+                                 -------------++------------------------------------------------------------------------------+errorMessage :: String -> [Node]+errorMessage s = renderHtmlNodes $+                     B.strong ! B.customAttribute "class" "error" $+                     B.toHtml s+++------------------------------------------------------------------------------+type JsonMonad n m a = ReaderT Value (HeistT n m) a+++------------------------------------------------------------------------------+withValue :: (Monad m) => Value -> JsonMonad n m a -> HeistT n 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 n 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 n 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 n) => JsonMonad n n [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 n) => V.Vector Value -> JsonMonad n n [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 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 n => JsonMonad n n [(Text, Splice n)]+    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
+ src/Heist/Splices/Markdown.hs view
@@ -0,0 +1,184 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-| The \"markdown\" splice formats markdown content as HTML and inserts+it into the document.++If the file attribute is present the contents of the tag is ignored and+the file specified is converted to HTML.++Otherwise the non-markup children of the tag are processed as markdown+and converted to HTML.++This splice requires that the \"pandoc\" executable is in your path.+-}+module Heist.Splices.Markdown where++------------------------------------------------------------------------------+import           Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import           Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import           Data.Maybe+import           Control.Concurrent+import           Control.Exception (throwIO)+import           Control.Monad+import           Control.Monad.CatchIO+import           Control.Monad.Trans+import           Data.Typeable+import           Prelude hiding (catch)+import           System.Directory+import           System.Exit+import           System.FilePath.Posix+import           System.IO+import           System.Process+import           Text.XmlHtml++------------------------------------------------------------------------------+import           Heist.Common+import           Heist.Interpreted.Internal+import           Heist.Types++data PandocMissingException = PandocMissingException+   deriving (Typeable)++instance Show PandocMissingException where+    show PandocMissingException =+        "Cannot find the \"pandoc\" executable; is it on your $PATH?"++instance Exception PandocMissingException+++data MarkdownException = MarkdownException ByteString+   deriving (Typeable)++instance Show MarkdownException where+    show (MarkdownException e) =+        "Markdown error: pandoc replied:\n\n" ++ BC.unpack e++instance Exception MarkdownException+++data NoMarkdownFileException = NoMarkdownFileException+    deriving (Typeable)++instance Show NoMarkdownFileException where+    show NoMarkdownFileException =+        "Markdown error: no file or template in context" +++        " during processing of markdown tag"++instance Exception NoMarkdownFileException where++------------------------------------------------------------------------------+-- | Default name for the markdown splice.+markdownTag :: Text+markdownTag = "markdown"++------------------------------------------------------------------------------+-- | Implementation of the markdown splice.+markdownSplice :: MonadIO m => Splice m+markdownSplice = do+    templateDir <- liftM (fmap takeDirectory) getTemplateFilePath+    pdMD <- liftIO $ findExecutable "pandoc"++    when (isNothing pdMD) $ liftIO $ throwIO PandocMissingException++    tree <- getParamNode+    (source,markup) <- liftIO $+        case getAttribute "file" tree of+            Just f  -> do+                m <- maybe (liftIO $ throwIO NoMarkdownFileException )+                           (\tp -> pandoc (fromJust pdMD) tp $ T.unpack f)+                           templateDir+                return (T.unpack f,m)+            Nothing -> do+                m <- pandocBS (fromJust pdMD) $ T.encodeUtf8 $ nodeText tree+                return ("inline_splice",m)++    let ee = parseHTML source markup+    case ee of+      Left e  -> throw $ MarkdownException+                       $ BC.pack ("Error parsing markdown output: " ++ e)+      Right d -> return (docContent d)+++pandoc :: FilePath -> FilePath -> FilePath -> IO ByteString+pandoc pandocPath templateDir inputFile = do+    (ex, sout, serr) <- readProcessWithExitCode' pandocPath args ""++    when (isFail ex) $ throw $ MarkdownException serr+    return $ BC.concat [ "<div class=\"markdown\">\n"+                         , sout+                         , "\n</div>" ]++  where+    isFail ExitSuccess = False+    isFail _           = True++    args = [ "-S", "--no-wrap", templateDir </> inputFile ]+++pandocBS :: FilePath -> ByteString -> IO ByteString+pandocBS pandocPath s = do+    -- using the crummy string functions for convenience here+    (ex, sout, serr) <- readProcessWithExitCode' pandocPath args s++    when (isFail ex) $ throw $ MarkdownException serr+    return $ BC.concat [ "<div class=\"markdown\">\n"+                       , sout+                       , "\n</div>" ]++  where+    isFail ExitSuccess = False+    isFail _           = True+    args = [ "-S", "--no-wrap" ]+++-- a version of readProcessWithExitCode that does I/O properly+readProcessWithExitCode'+    :: FilePath                 -- ^ command to run+    -> [String]                 -- ^ any arguments+    -> ByteString               -- ^ standard input+    -> IO (ExitCode,ByteString,ByteString) -- ^ exitcode, stdout, stderr+readProcessWithExitCode' cmd args input = do+    (Just inh, Just outh, Just errh, pid) <-+        createProcess (proc cmd args){ std_in  = CreatePipe,+                                       std_out = CreatePipe,+                                       std_err = CreatePipe }+    outMVar <- newEmptyMVar++    outM <- newEmptyMVar+    errM <- newEmptyMVar++    -- fork off a thread to start consuming stdout+    _ <- forkIO $ do+        out <- B.hGetContents outh+        putMVar outM out+        putMVar outMVar ()++    -- fork off a thread to start consuming stderr+    _ <- forkIO $ do+        err  <- B.hGetContents errh+        putMVar errM err+        putMVar outMVar ()++    -- now write and flush any input+    when (not (B.null input)) $ do B.hPutStr inh input; hFlush inh+    hClose inh -- done with stdin++    -- wait on the output+    takeMVar outMVar+    takeMVar outMVar+    hClose outh++    -- wait on the process+    ex <- waitForProcess pid++    out <- readMVar outM+    err <- readMVar errM++    return (ex, out, err)++++
+ src/Heist/TemplateDirectory.hs view
@@ -0,0 +1,100 @@+{-|++This module defines a TemplateDirectory data structure for convenient+interaction with templates within web apps.++-}++module Heist.TemplateDirectory+    ( TemplateDirectory+    , newTemplateDirectory+    , newTemplateDirectory'++    , getDirectoryHS+    , getDirectoryCTS+    , reloadTemplateDirectory+    ) where++------------------------------------------------------------------------------+import           Control.Concurrent+import           Control.Error+import           Control.Monad+import           Control.Monad.Trans+import           Heist+import           Heist.Splices.Cache+++------------------------------------------------------------------------------+-- | Structure representing a template directory.+data TemplateDirectory n+    = TemplateDirectory+        FilePath+        (HeistConfig n)+        (MVar (HeistState n))+        (MVar CacheTagState)+++------------------------------------------------------------------------------+-- | Creates and returns a new 'TemplateDirectory' wrapped in an Either for+-- error handling.+newTemplateDirectory :: MonadIO n+                     => FilePath+                     -> HeistConfig n+                     -> EitherT [String] IO (TemplateDirectory n)+newTemplateDirectory dir hc = do+    templates <- loadTemplates dir+    let hc' = hc { hcTemplates = templates }+    (hs,cts) <- initHeistWithCacheTag hc'+    tsMVar <- liftIO $ newMVar hs+    ctsMVar <- liftIO $ newMVar cts+    return $ TemplateDirectory dir hc' tsMVar ctsMVar+++------------------------------------------------------------------------------+-- | Creates and returns a new 'TemplateDirectory', using the monad's fail+-- function on error.+newTemplateDirectory' :: MonadIO n+                      => FilePath+                      -> HeistConfig n+                      -> IO (TemplateDirectory n)+newTemplateDirectory' dir hc = do+    res <- runEitherT $ newTemplateDirectory dir hc+    either (error . concat) return res+++------------------------------------------------------------------------------+-- | Gets the 'HeistState' from a TemplateDirectory.+getDirectoryHS :: (MonadIO n)+               => TemplateDirectory n+               -> IO (HeistState n)+getDirectoryHS (TemplateDirectory _ _ tsMVar _) =+    liftIO $ readMVar $ tsMVar+++------------------------------------------------------------------------------+-- | Clears the TemplateDirectory's cache tag state.+getDirectoryCTS :: TemplateDirectory n -> IO CacheTagState+getDirectoryCTS (TemplateDirectory _ _ _ ctsMVar) = readMVar ctsMVar+++------------------------------------------------------------------------------+-- | Clears cached content and reloads templates from disk.+reloadTemplateDirectory :: (MonadIO n)+                        => TemplateDirectory n+                        -> IO (Either String ())+reloadTemplateDirectory (TemplateDirectory p hc tsMVar ctsMVar) = do+    ehs <- runEitherT $ do+        templates <- loadTemplates p+        initHeistWithCacheTag (hc { hcTemplates = templates })+    leftPass ehs $ \(hs,cts) -> do+        modifyMVar_ tsMVar (const $ return hs)+        modifyMVar_ ctsMVar (const $ return cts)+++------------------------------------------------------------------------------+-- | Prepends an error onto a Left.+leftPass :: Monad m => Either [String] b -> (b -> m c) -> m (Either String c)+leftPass e m = either (return . Left . loadError . concat)+                      (liftM Right . m) e+  where+    loadError = (++) "Error loading templates: "
+ src/Heist/Types.hs view
@@ -0,0 +1,489 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UndecidableInstances #-}++{-|++This module contains the core Heist data types.  ++Edward Kmett wrote most of the HeistT monad code and associated instances,+liberating us from the unused writer portion of RWST.++-}++module Heist.Types where++------------------------------------------------------------------------------+import           Blaze.ByteString.Builder+import           Control.Applicative+import           Control.Arrow+import           Control.Monad.CatchIO+import           Control.Monad.Cont+import           Control.Monad.Error+import           Control.Monad.Reader+import           Control.Monad.State.Strict+import           Data.ByteString.Char8 (ByteString)+import           Data.DList                      (DList)+import qualified Data.HashMap.Strict as H+import           Data.HashMap.Strict (HashMap)+import           Data.HeterogeneousEnvironment   (HeterogeneousEnvironment)+import qualified Data.HeterogeneousEnvironment as HE+import           Data.Monoid+import           Data.Text (Text)+import qualified Data.Text as T+import           Data.Text.Encoding+import           Data.Typeable+import           Prelude hiding (catch)+import qualified Text.XmlHtml as X++import Debug.Trace++tr :: Show a => String -> a -> a+tr s x = trace (s++show x) x++------------------------------------------------------------------------------+-- | A 'Template' is a forest of XML nodes.  Here we deviate from the \"single+-- root node\" constraint of well-formed XML because we want to allow+-- templates to contain document fragments that may not have a single root.+type Template = [X.Node]+++------------------------------------------------------------------------------+-- | MIME Type.  The type alias is here to make the API clearer.+type MIMEType = ByteString+++------------------------------------------------------------------------------+-- | Reversed list of directories.  This holds the path to the template+-- currently being processed.+type TPath = [ByteString]+++------------------------------------------------------------------------------+-- | Holds data about templates read from disk.+data DocumentFile = DocumentFile+    { dfDoc  :: X.Document+    , dfFile :: Maybe FilePath+    } deriving (Eq)+++------------------------------------------------------------------------------+-- | Monad used for runtime splice execution.+newtype RuntimeSplice m a = RuntimeSplice {+      unRT :: StateT HeterogeneousEnvironment m a+    } deriving ( Applicative+               , Functor+               , Monad+               , MonadIO+               , MonadState HeterogeneousEnvironment+               , MonadTrans )+++------------------------------------------------------------------------------+instance (Monad m, Monoid a) => Monoid (RuntimeSplice m a) where+    mempty = return mempty++    a `mappend` b = do+        !x <- a+        !y <- b+        return $! x `mappend` y+++------------------------------------------------------------------------------+-- | Opaque type representing pieces of output from compiled splices.+data Chunk m = Pure !ByteString+               -- ^ output known at load time+             | RuntimeHtml !(RuntimeSplice m Builder)+               -- ^ output computed at run time+             | RuntimeAction !(RuntimeSplice m ())+               -- ^ runtime action used only for its side-effect+++instance Show (Chunk m) where+    show (Pure _) = "Pure"+    show (RuntimeHtml _) = "RuntimeHtml"+    show (RuntimeAction _) = "RuntimeAction"+++showChunk :: Chunk m -> String+showChunk (Pure b) = T.unpack $ decodeUtf8 b+showChunk (RuntimeHtml _) = "RuntimeHtml"+showChunk (RuntimeAction _) = "RuntimeAction"+++isPureChunk :: Chunk m -> Bool+isPureChunk (Pure _) = True+isPureChunk _ = False+++------------------------------------------------------------------------------+-- | Type alias for attribute splices.  The function parameter is the value of+-- the bound attribute splice.  The return value is a list of attribute+-- key/value pairs that get substituted in the place of the bound attribute.+type AttrSplice m = Text -> m [(Text, Text)]+++------------------------------------------------------------------------------+-- | Holds all the state information needed for template processing.  You will+-- build a @HeistState@ using 'initHeist' and any of Heist's @HeistState ->+-- HeistState@ \"filter\" functions.  Then you use the resulting @HeistState@+-- in calls to 'renderTemplate'.+--+-- m is the runtime monad+data HeistState m = HeistState {+    -- | A mapping of splice names to splice actions+      _spliceMap           :: HashMap Text (HeistT m m Template)+    -- | A mapping of template names to templates+    , _templateMap         :: HashMap TPath DocumentFile++    -- | A mapping of splice names to splice actions+    , _compiledSpliceMap   :: HashMap Text (HeistT m IO (DList (Chunk m)))+    -- | A mapping of template names to templates+    --, _compiledTemplateMap :: HashMap TPath (m Builder, MIMEType)+    , _compiledTemplateMap :: !(HashMap TPath ([Chunk m], MIMEType))++    , _attrSpliceMap       :: HashMap Text (AttrSplice m)++    -- | A flag to control splice recursion+    , _recurse             :: Bool+    -- | The path to the template currently being processed.+    , _curContext          :: TPath+    -- | A counter keeping track of the current recursion depth to prevent+    -- infinite loops.+    , _recursionDepth      :: Int+    -- | The doctypes encountered during template processing.+    , _doctypes            :: [X.DocType]+    -- | The full path to the current template's file on disk.+    , _curTemplateFile     :: Maybe FilePath+    -- | A key generator used to produce new unique Promises.+    , _keygen              :: HE.KeyGen++    -- | Flag indicating whether we're in preprocessing mode.  During+    -- preprocessing, errors should stop execution and be reported.  During+    -- template rendering, it's better to skip the errors and render the page.+    , _preprocessingMode   :: Bool+}+++-- NOTE: We got rid of the Monoid instance because it is absolutely not safe+-- to combine two compiledTemplateMaps.  All compiled templates must be known+-- at load time and processed in a single call to initHeist/loadTemplates or+-- whatever we end up calling it..++instance (Typeable1 m) => Typeable (HeistState m) where+    typeOf _ = mkTyConApp templateStateTyCon [typeOf1 (undefined :: m ())]+++------------------------------------------------------------------------------+-- | HeistT is the monad transformer used for splice processing.  HeistT+-- intentionally does not expose any of its functionality via MonadState or+-- MonadReader functions.  We define passthrough instances for the most common+-- types of monads.  These instances allow the user to use HeistT in a monad+-- stack without needing calls to `lift`.+--+-- @n@ is the runtime monad (the parameter to HeistState).+--+-- @m@ is the monad being run now.  In this case, \"now\" is a variable+-- concept.  The type @HeistT n n@ means that \"now\" is runtime.  The type+-- @HeistT n IO@ means that \"now\" is @IO@, and more importantly it is NOT+-- runtime. In Heist, the rule of thumb is that @IO@ means load time and @n@+-- means runtime.+newtype HeistT n m a = HeistT {+    runHeistT :: X.Node+              -> HeistState n+              -> m (a, HeistState n)+}+++------------------------------------------------------------------------------+-- | Gets the names of all the templates defined in a HeistState.+templateNames :: HeistState m -> [TPath]+templateNames ts = H.keys $ _templateMap ts+++------------------------------------------------------------------------------+-- | Gets the names of all the templates defined in a HeistState.+compiledTemplateNames :: HeistState m -> [TPath]+compiledTemplateNames ts = H.keys $ _compiledTemplateMap ts+++------------------------------------------------------------------------------+-- | Gets the names of all the splices defined in a HeistState.+spliceNames :: HeistState m -> [Text]+spliceNames ts = H.keys $ _spliceMap ts+++------------------------------------------------------------------------------+-- | The Typeable instance is here so Heist can be dynamically executed with+-- Hint.+templateStateTyCon :: TyCon+templateStateTyCon = mkTyCon "Heist.HeistState"+{-# NOINLINE templateStateTyCon #-}++------------------------------------------------------------------------------+-- | Evaluates a template monad as a computation in the underlying monad.+evalHeistT :: (Monad m)+           => HeistT n m a+           -> X.Node+           -> HeistState n+           -> m a+evalHeistT m r s = do+    (a, _) <- runHeistT m r s+    return a+{-# INLINE evalHeistT #-}+++------------------------------------------------------------------------------+-- | Functor instance+instance Functor m => Functor (HeistT n m) where+    fmap f (HeistT m) = HeistT $ \r s -> first f <$> m r s+++------------------------------------------------------------------------------+-- | Applicative instance+instance (Monad m, Functor m) => Applicative (HeistT n m) where+    pure = return+    (<*>) = ap+++------------------------------------------------------------------------------+-- | Monad instance+instance Monad m => Monad (HeistT n m) where+    return a = HeistT (\_ s -> return (a, s))+    {-# INLINE return #-}+    HeistT m >>= k = HeistT $ \r s -> do+        (a, s') <- m r s+        runHeistT (k a) r s'+    {-# INLINE (>>=) #-}+++------------------------------------------------------------------------------+-- | MonadIO instance+instance MonadIO m => MonadIO (HeistT n m) where+    liftIO = lift . liftIO+++------------------------------------------------------------------------------+-- | MonadTrans instance+instance MonadTrans (HeistT n) where+    lift m = HeistT $ \_ s -> do+        a <- m+        return (a, s)+++------------------------------------------------------------------------------+-- | MonadCatchIO instance+instance MonadCatchIO m => MonadCatchIO (HeistT n m) where+    catch (HeistT a) h = HeistT $ \r s -> do+       let handler e = runHeistT (h e) r s+       catch (a r s) handler+    block (HeistT m) = HeistT $ \r s -> block (m r s)+    unblock (HeistT m) = HeistT $ \r s -> unblock (m r s)+++------------------------------------------------------------------------------+-- | MonadFix passthrough instance+instance MonadFix m => MonadFix (HeistT n m) where+    mfix f = HeistT $ \r s ->+        mfix $ \ (a, _) -> runHeistT (f a) r s+++------------------------------------------------------------------------------+-- | Alternative passthrough instance+instance (Functor m, MonadPlus m) => Alternative (HeistT n m) where+    empty = mzero+    (<|>) = mplus+++------------------------------------------------------------------------------+-- | MonadPlus passthrough instance+instance MonadPlus m => MonadPlus (HeistT n m) where+    mzero = lift mzero+    m `mplus` n = HeistT $ \r s ->+        runHeistT m r s `mplus` runHeistT n r s+++------------------------------------------------------------------------------+-- | MonadState passthrough instance+instance MonadState s m => MonadState s (HeistT n m) where+    get = lift get+    {-# INLINE get #-}+    put = lift . put+    {-# INLINE put #-}+++------------------------------------------------------------------------------+-- | MonadReader passthrough instance+instance MonadReader r m => MonadReader r (HeistT n m) where+    ask = HeistT $ \_ s -> do+            r <- ask+            return (r,s)+    local f (HeistT m) =+        HeistT $ \r s -> local f (m r s)+++------------------------------------------------------------------------------+-- | Helper for MonadError instance.+liftCatch :: (m (a,HeistState n)+              -> (e -> m (a,HeistState n))+              -> m (a,HeistState n))+          -> HeistT n m a+          -> (e -> HeistT n m a)+          -> HeistT n m a+liftCatch ce m h =+    HeistT $ \r s ->+        (runHeistT m r s `ce`+        (\e -> runHeistT (h e) r s))+++------------------------------------------------------------------------------+-- | MonadError passthrough instance+instance (MonadError e m) => MonadError e (HeistT n m) where+    throwError = lift . throwError+    catchError = liftCatch catchError+++------------------------------------------------------------------------------+-- | Helper for MonadCont instance.+liftCallCC :: ((((a,HeistState n) -> m (b, HeistState n))+                  -> m (a, HeistState n))+                -> m (a, HeistState n))+           -> ((a -> HeistT n m b) -> HeistT n m a)+           -> HeistT n m a+liftCallCC ccc f = HeistT $ \r s ->+    ccc $ \c ->+    runHeistT (f (\a -> HeistT $ \_ _ -> c (a, s))) r s+++------------------------------------------------------------------------------+-- | MonadCont passthrough instance+instance (MonadCont m) => MonadCont (HeistT n m) where+    callCC = liftCallCC callCC+++------------------------------------------------------------------------------+-- | The Typeable instance is here so Heist can be dynamically executed with+-- Hint.+templateMonadTyCon :: TyCon+templateMonadTyCon = mkTyCon "Heist.HeistT"+{-# NOINLINE templateMonadTyCon #-}++instance (Typeable1 m) => Typeable1 (HeistT n m) where+    typeOf1 _ = mkTyConApp templateMonadTyCon [typeOf1 (undefined :: m ())]+++------------------------------------------------------------------------------+-- Functions for our monad.+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | Gets the node currently being processed.+--+--   > <speech author="Shakespeare">+--   >   To sleep, perchance to dream.+--   > </speech>+--+-- When you call @getParamNode@ inside the code for the @speech@ splice, it+-- returns the Node for the @speech@ tag and its children.  @getParamNode >>=+-- childNodes@ returns a list containing one 'TextNode' containing part of+-- Hamlet's speech.  @liftM (getAttribute \"author\") getParamNode@ would+-- return @Just "Shakespeare"@.+getParamNode :: Monad m => HeistT n m X.Node+getParamNode = HeistT $ \r s -> return (r,s)+{-# INLINE getParamNode #-}+++------------------------------------------------------------------------------+-- | HeistT's 'local'.+localParamNode :: Monad m+               => (X.Node -> X.Node)+               -> HeistT n m a+               -> HeistT n m a+localParamNode f m = HeistT $ \r s -> runHeistT m (f r) s+{-# INLINE localParamNode #-}+++------------------------------------------------------------------------------+-- | HeistT's 'gets'.+getsHS :: Monad m => (HeistState n -> r) -> HeistT n m r+getsHS f = HeistT $ \_ s -> return (f s, s)+{-# INLINE getsHS #-}+++------------------------------------------------------------------------------+-- | HeistT's 'get'.+getHS :: Monad m => HeistT n m (HeistState n)+getHS = HeistT $ \_ s -> return (s, s)+{-# INLINE getHS #-}+++------------------------------------------------------------------------------+-- | HeistT's 'put'.+putHS :: Monad m => HeistState n -> HeistT n m ()+putHS s = HeistT $ \_ _ -> return ((), s)+{-# INLINE putHS #-}+++------------------------------------------------------------------------------+-- | HeistT's 'modify'.+modifyHS :: Monad m+         => (HeistState n -> HeistState n)+         -> HeistT n m ()+modifyHS f = HeistT $ \_ s -> return ((), f s)+{-# INLINE modifyHS #-}+++------------------------------------------------------------------------------+-- | Restores the HeistState.  This function is almost like putHS except it+-- preserves the current doctypes.  You should use this function instead of+-- @putHS@ to restore an old state.  This was needed because doctypes needs to+-- be in a "global scope" as opposed to the template call "local scope" of+-- state items such as recursionDepth, curContext, and spliceMap.+restoreHS :: Monad m => HeistState n -> HeistT n m ()+restoreHS old = modifyHS (\cur -> old { _doctypes = _doctypes cur })+{-# INLINE restoreHS #-}+++------------------------------------------------------------------------------+-- | Abstracts the common pattern of running a HeistT computation with+-- a modified heist state.+localHS :: Monad m+        => (HeistState n -> HeistState n)+        -> HeistT n m a+        -> HeistT n m a+localHS f k = do+    ts <- getHS+    putHS $ f ts+    res <- k+    restoreHS ts+    return res+{-# INLINE localHS #-}+++------------------------------------------------------------------------------+-- | Modifies the recursion depth.+modRecursionDepth :: Monad m => (Int -> Int) -> HeistT n m ()+modRecursionDepth f =+    modifyHS (\st -> st { _recursionDepth = f (_recursionDepth st) })+++------------------------------------------------------------------------------+-- | AST to hold attribute parsing structure.  This is necessary because+-- attoparsec doesn't support parsers running in another monad.+data AttAST = Literal Text+            | Ident   Text+            | Escaped Char+  deriving (Show)+++------------------------------------------------------------------------------+isIdent :: AttAST -> Bool+isIdent (Ident _) = True+isIdent _         = False++
− src/Text/Templating/Heist.hs
@@ -1,176 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}--{-|--  This module contains the core definitions for the Heist template system.--  The Heist template system is based on HTML and XML.  It allows you to build-  custom HTML and XML based markup languages.  With Heist you can define your-  own domain-specific HTML and XML tags implemented with Haskell and use them-  in your templates.--  The most important concept in Heist is the 'Splice'.  Splices can be thought-  of as functions that transform a node into a list of nodes.  Heist then-  substitutes the resulting list of nodes into your template in place of the-  input node.  'Splice' is implemented as a type synonym @type Splice m =-  HeistT m [Node]@, and 'HeistT' has a function 'getParamNode'-  that lets you get the input node.--  Suppose you have a place on your page where you want to display a link with-  the text \"Logout username\" if the user is currently logged in or a link to-  the login page if no user is logged in.  Assume you have a function-  @getUser :: MyAppMonad (Maybe Text)@ that gets the current user.-  You can implement this functionality with a 'Splice' as follows:--  > import             Blaze.ByteString.Builder-  > import             Data.ByteString.Char8 (ByteString)-  > import qualified   Data.ByteString.Char8 as B-  > import             Data.Text (Text)-  > import qualified   Data.Text as T-  > import qualified   Text.XmlHtml as X-  >-  > import             Text.Templating.Heist-  >-  > link :: Text -> Text -> X.Node-  > link target text = X.Element "a" [("href", target)] [X.TextNode text]-  >-  > loginLink :: X.Node-  > loginLink = link "/login" "Login"-  >-  > logoutLink :: Text -> X.Node-  > logoutLink user = link "/logout" (T.append "Logout " user)-  >-  > loginLogoutSplice :: Splice MyAppMonad-  > loginLogoutSplice = do-  >     user <- lift getUser-  >     return [maybe loginLink logoutLink user]-  >--  Next, you need to bind that splice to a tag.  Heist stores information-  about splices and templates in the 'HeistState' data structure.  The-  following code demonstrates how this splice would be used.--  > mySplices = [ ("loginLogout", loginLogoutSplice) ]-  >-  > main = do-  >     ets <- loadTemplates "templates" $-  >            bindSplices mySplices defaultHeistState-  >     let ts = either error id ets-  >     t <- runMyAppMonad $ renderTemplate ts "index"-  >     print $ maybe "Page not found" (toByteString . fst) t--  Here we build up our 'HeistState' by starting with defaultHeistState and-  applying bindSplice for all the splices we want to add.  Then we pass this-  to loadTemplates our final 'HeistState' wrapped in an Either to handle-  errors.  Then we use this 'HeistState' to render our templates.---}--module Text.Templating.Heist-  (-    -- * Types-    Template-  , MIMEType-  , Splice-  , HeistT-  , HeistState-  , evalHeistT-  , templateNames-  , spliceNames--    -- * Functions and declarations on HeistState values-  , addTemplate-  , addXMLTemplate-  , defaultHeistState-  , bindSplice-  , bindSplices-  , lookupSplice-  , setTemplates-  , loadTemplates-  , hasTemplate-  , addTemplatePathPrefix--    -- * Hook functions-    -- $hookDoc-  , addOnLoadHook-  , addPreRunHook-  , addPostRunHook--    -- * HeistT functions-  , stopRecursion-  , getParamNode-  , runNodeList-  , getContext-  , getTemplateFilePath--  , localParamNode-  , getsTS-  , getTS-  , putTS-  , modifyTS-  , restoreTS-  , localTS--    -- * Functions for running splices and templates-  , evalTemplate-  , callTemplate-  , callTemplateWithText-  , renderTemplate-  , renderWithArgs-  , bindStrings-  , bindString--    -- * Functions for creating splices-  , textSplice-  , runChildren-  , runChildrenWith-  , runChildrenWithTrans-  , runChildrenWithTemplates-  , runChildrenWithText-  , mapSplices--    -- * Misc functions-  , getDoc-  , getXMLDoc-  , mkCacheTag-  ) where--import           Control.Monad.Trans-import qualified Data.HashMap.Strict as Map-import           Text.Templating.Heist.Internal-import           Text.Templating.Heist.Splices-import           Text.Templating.Heist.Types------------------------------------------------------------------------------------ | The default set of built-in splices.-defaultSpliceMap :: MonadIO m => SpliceMap m-defaultSpliceMap = Map.fromList-    [(applyTag, applyImpl)-    ,(bindTag, bindImpl)-    ,(ignoreTag, ignoreImpl)-    ,(markdownTag, markdownSplice)-    ]------------------------------------------------------------------------------------ | An empty heist state, with Heist's default splices (@\<apply\>@,--- @\<bind\>@, @\<ignore\>@, and @\<markdown\>@) mapped.  The cache tag is--- not mapped here because it must be mapped manually in your application.-defaultHeistState :: MonadIO m => HeistState m-defaultHeistState =-    HeistState (defaultSpliceMap) Map.empty True [] 0-               return return return [] Nothing----- $hookDoc--- Heist hooks allow you to modify templates when they are loaded and before--- and after they are run.  Every time you call one of the addAbcHook--- functions the hook is added to onto the processing pipeline.  The hooks--- processes the template in the order that they were added to the--- HeistState.------ The pre-run and post-run hooks are run before and after every template is--- run/rendered.  You should be careful what code you put in these hooks--- because it can significantly affect the performance of your site.-
− src/Text/Templating/Heist/Internal.hs
@@ -1,743 +0,0 @@-{-# LANGUAGE BangPatterns               #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings          #-}-{-# LANGUAGE PackageImports             #-}-{-# LANGUAGE ScopedTypeVariables        #-}--module Text.Templating.Heist.Internal where---------------------------------------------------------------------------------import             Blaze.ByteString.Builder-import             Control.Applicative-import             Control.Arrow hiding (loop)-import             Control.Exception (SomeException)-import             Control.Monad-import             Control.Monad.CatchIO-import             Control.Monad.Trans-import qualified   Data.Attoparsec.Text as AP-import             Data.ByteString (ByteString)-import qualified   Data.ByteString as B-import qualified   Data.ByteString.Char8 as BC-import             Data.Either-import qualified   Data.Foldable as F-import             Data.List-import qualified   Data.HashMap.Strict as Map-import             Data.Maybe-import             Data.Monoid-import qualified   Data.Text as T-import             Data.Text (Text)-import             Prelude hiding (catch)-import             System.Directory.Tree hiding (name)-import             System.FilePath-import qualified   Text.XmlHtml as X---------------------------------------------------------------------------------import             Text.Templating.Heist.Types------------------------------------------------------------------------------------ | Mappends a doctype to the state.-addDoctype :: Monad m => [X.DocType] -> HeistT m ()-addDoctype dt = do-    modifyTS (\s -> s { _doctypes = _doctypes s `mappend` dt })------------------------------------------------------------------------------------ HeistState functions------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Adds an on-load hook to a `HeistState`.-addOnLoadHook :: (Monad m) =>-                 (Template -> IO Template)-              -> HeistState m-              -> HeistState m-addOnLoadHook hook ts = ts { _onLoadHook = _onLoadHook ts >=> hook }------------------------------------------------------------------------------------ | Adds a pre-run hook to a `HeistState`.-addPreRunHook :: (Monad m) =>-                 (Template -> m Template)-              -> HeistState m-              -> HeistState m-addPreRunHook hook ts = ts { _preRunHook = _preRunHook ts >=> hook }------------------------------------------------------------------------------------ | Adds a post-run hook to a `HeistState`.-addPostRunHook :: (Monad m) =>-                  (Template -> m Template)-               -> HeistState m-               -> HeistState m-addPostRunHook hook ts = ts { _postRunHook = _postRunHook ts >=> hook }------------------------------------------------------------------------------------ | Binds a new splice declaration to a tag name within a 'HeistState'.-bindSplice :: Monad m =>-              Text              -- ^ tag name-           -> Splice m          -- ^ splice action-           -> HeistState m      -- ^ source state-           -> HeistState m-bindSplice n v ts = ts {_spliceMap = Map.insert n v (_spliceMap ts)}------------------------------------------------------------------------------------ | Binds a set of new splice declarations within a 'HeistState'.-bindSplices :: Monad m =>-               [(Text, Splice m)] -- ^ splices to bind-            -> HeistState m       -- ^ start state-            -> HeistState m-bindSplices ss ts = foldl' (flip id) ts acts-  where-    acts = map (uncurry bindSplice) ss------------------------------------------------------------------------------------ | Sets the current template file.-setCurTemplateFile :: Monad m-                   => Maybe FilePath -> HeistState m -> HeistState m-setCurTemplateFile fp ts = ts { _curTemplateFile = fp }------------------------------------------------------------------------------------ | Converts 'Text' to a splice returning a single 'TextNode'.-textSplice :: (Monad m) => Text -> Splice m-textSplice t = return [X.TextNode t]------------------------------------------------------------------------------------ | Runs the parameter node's children and returns the resulting node list.--- By itself this function is a simple passthrough splice that makes the--- spliced node disappear.  In combination with locally bound splices, this--- function makes it easier to pass the desired view into your splices.-runChildren :: Monad m => Splice m-runChildren = runNodeList . X.childNodes =<< getParamNode------------------------------------------------------------------------------------ | Binds a list of splices before using the children of the spliced node as--- a view.-runChildrenWith :: (Monad m)-                => [(Text, Splice m)]-                -- ^ List of splices to bind before running the param nodes.-                -> Splice m-                -- ^ Returns the passed in view.-runChildrenWith splices = localTS (bindSplices splices) runChildren------------------------------------------------------------------------------------ | Wrapper around runChildrenWith that applies a transformation function to--- the second item in each of the tuples before calling runChildrenWith.-runChildrenWithTrans :: (Monad m)-          => (b -> Splice m)-          -- ^ Splice generating function-          -> [(Text, b)]-          -- ^ List of tuples to be bound-          -> Splice m-runChildrenWithTrans f = runChildrenWith . map (second f)------------------------------------------------------------------------------------ | Like runChildrenWith but using constant templates rather than dynamic--- splices.-runChildrenWithTemplates :: (Monad m) => [(Text, Template)] -> Splice m-runChildrenWithTemplates = runChildrenWithTrans return------------------------------------------------------------------------------------ | Like runChildrenWith but using literal text rather than dynamic splices.-runChildrenWithText :: (Monad m) => [(Text, Text)] -> Splice m-runChildrenWithText = runChildrenWithTrans textSplice------------------------------------------------------------------------------------ | Maps a splice generating function over a list and concatenates the--- results.-mapSplices :: (Monad m)-        => (a -> Splice m)-        -- ^ Splice generating function-        -> [a]-        -- ^ List of items to generate splices for-        -> Splice m-        -- ^ The result of all splices concatenated together.-mapSplices f vs = liftM concat $ mapM f vs-{-# INLINE mapSplices #-}------------------------------------------------------------------------------------ | Convenience function for looking up a splice.-lookupSplice :: Monad m =>-                Text-             -> HeistState m-             -> Maybe (Splice m)-lookupSplice nm ts = Map.lookup nm $ _spliceMap ts-{-# INLINE lookupSplice #-}------------------------------------------------------------------------------------ | Converts a path into an array of the elements in reverse order.  If the--- path is absolute, we need to remove the leading slash so the split doesn't--- leave @\"\"@ as the last element of the TPath.------ FIXME @\"..\"@ currently doesn't work in paths, the solution is non-trivial-splitPathWith :: Char -> ByteString -> TPath-splitPathWith s p = if BC.null p then [] else (reverse $ BC.split s path)-  where-    path = if BC.head p == s then BC.tail p else p---- | Converts a path into an array of the elements in reverse order using the--- path separator of the local operating system. See 'splitPathWith' for more--- details.-splitLocalPath :: ByteString -> TPath-splitLocalPath = splitPathWith pathSeparator---- | Converts a path into an array of the elements in reverse order using a--- forward slash (/) as the path separator. See 'splitPathWith' for more--- details.-splitTemplatePath :: ByteString -> TPath-splitTemplatePath = splitPathWith '/'------------------------------------------------------------------------------------ | Does a single template lookup without cascading up.-singleLookup :: TemplateMap-             -> TPath-             -> ByteString-             -> Maybe (DocumentFile, TPath)-singleLookup tm path name = fmap (\a -> (a,path)) $ Map.lookup (name:path) tm------------------------------------------------------------------------------------ | Searches for a template by looking in the full path then backing up into--- each of the parent directories until the template is found.-traversePath :: TemplateMap-             -> TPath-             -> ByteString-             -> Maybe (DocumentFile, TPath)-traversePath tm [] name = fmap (\a -> (a,[])) (Map.lookup [name] tm)-traversePath tm path name =-    singleLookup tm path name `mplus`-    traversePath tm (tail path) name------------------------------------------------------------------------------------ | Returns 'True' if the given template can be found in the heist state.-hasTemplate :: Monad m =>-               ByteString-            -> HeistState m-            -> Bool-hasTemplate nameStr ts = isJust $ lookupTemplate nameStr ts------------------------------------------------------------------------------------ | Convenience function for looking up a template.-lookupTemplate :: Monad m =>-                  ByteString-               -> HeistState m-               -> Maybe (DocumentFile, TPath)-lookupTemplate nameStr ts =-    f (_templateMap ts) path name-  where (name:p) = case splitTemplatePath nameStr of-                       [] -> [""]-                       ps -> ps-        ctx = if B.isPrefixOf "/" nameStr then [] else _curContext ts-        path = p ++ ctx-        f = if '/' `BC.elem` nameStr-                then singleLookup-                else traversePath------------------------------------------------------------------------------------ | Sets the templateMap in a HeistState.-setTemplates :: Monad m => TemplateMap -> HeistState m -> HeistState m-setTemplates m ts = ts { _templateMap = m }------------------------------------------------------------------------------------ | Adds a template to the heist state.-insertTemplate :: Monad m =>-               TPath-            -> DocumentFile-            -> HeistState m-            -> HeistState m-insertTemplate p t st =-    setTemplates (Map.insert p t (_templateMap st)) st------------------------------------------------------------------------------------ | Adds an HTML format template to the heist state.-addTemplate :: Monad m-            => ByteString-            -- ^ Path that the template will be referenced by-            -> Template-            -- ^ The template's DOM nodes-            -> Maybe FilePath-            -- ^ An optional path to the actual file on disk where the-            -- template is stored-            -> HeistState m-            -> HeistState m-addTemplate n t mfp st =-    insertTemplate (splitTemplatePath n) doc st-  where-    doc = DocumentFile (X.HtmlDocument X.UTF8 Nothing t) mfp------------------------------------------------------------------------------------ | Adds an XML format template to the heist state.-addXMLTemplate :: Monad m-               => ByteString-               -- ^ Path that the template will be referenced by-               -> Template-               -- ^ The template's DOM nodes-               -> Maybe FilePath-               -- ^ An optional path to the actual file on disk where the-               -- template is stored-               -> HeistState m-               -> HeistState m-addXMLTemplate n t mfp st =-    insertTemplate (splitTemplatePath n) doc st-  where-    doc = DocumentFile (X.XmlDocument X.UTF8 Nothing t) mfp------------------------------------------------------------------------------------ | Stops the recursive processing of splices.  Consider the following--- example:------   > <foo>---   >   <bar>---   >     ...---   >   </bar>---   > </foo>------ Assume that @\"foo\"@ is bound to a splice procedure. Running the @foo@--- splice will result in a list of nodes @L@.  Normally @foo@ will recursively--- scan @L@ for splices and run them.  If @foo@ calls @stopRecursion@, @L@--- will be included in the output verbatim without running any splices.-stopRecursion :: Monad m => HeistT m ()-stopRecursion = modifyTS (\st -> st { _recurse = False })------------------------------------------------------------------------------------ | Sets the current context-setContext :: Monad m => TPath -> HeistT m ()-setContext c = modifyTS (\st -> st { _curContext = c })------------------------------------------------------------------------------------ | Gets the current context-getContext :: Monad m => HeistT m TPath-getContext = getsTS _curContext------------------------------------------------------------------------------------ | Gets the full path to the file holding the template currently being--- processed.  Returns Nothing if the template is not associated with a file--- on disk or if there is no template being processed.-getTemplateFilePath :: Monad m => HeistT m (Maybe FilePath)-getTemplateFilePath = getsTS _curTemplateFile------------------------------------------------------------------------------------ | Performs splice processing on a single node.-runNode :: Monad m => X.Node -> Splice m-runNode (X.Element nm at ch) = do-    newAtts <- mapM attSubst at-    let n = X.Element nm newAtts ch-    s <- liftM (lookupSplice nm) getTS-    maybe (runKids newAtts) (recurseSplice n) s-  where-    runKids newAtts = do-        newKids <- runNodeList ch-        return [X.Element nm newAtts newKids]-runNode n                    = return [n]------------------------------------------------------------------------------------ | Helper function for substituting a parsed attribute into an attribute--- tuple.-attSubst :: (Monad m) => (t, Text) -> HeistT m (t, Text)-attSubst (n,v) = do-    v' <- parseAtt v-    return (n,v')------------------------------------------------------------------------------------ | Parses an attribute for any identifier expressions and performs--- appropriate substitution.-parseAtt :: (Monad m) => Text -> HeistT m Text-parseAtt bs = do-    let ast = case AP.feed (AP.parse attParser bs) "" of-                (AP.Done _ res) -> res-                (AP.Fail _ _ _) -> []-                (AP.Partial _)  -> []-    chunks <- mapM cvt ast-    return $ T.concat chunks-  where-    cvt (Literal x) = return x-    cvt (Ident x)   =-        localParamNode (const $ X.Element x [] []) $ getAttributeSplice x------------------------------------------------------------------------------------ | AST to hold attribute parsing structure.  This is necessary because--- attoparsec doesn't support parsers running in another monad.-data AttAST = Literal Text-            | Ident   Text-  deriving (Show)------------------------------------------------------------------------------------ | Parser for attribute variable substitution.-attParser :: AP.Parser [AttAST]-attParser = liftM ($! []) (loop id)-  where-    append !dl !x = dl . (x:)--    loop !dl = go id-      where-        finish subDL = let !txt = T.concat $! subDL []-                           lit  = Literal $! T.concat $! subDL []-                       in return $! if T.null txt-                                      then dl-                                      else append dl lit--        go !subDL = (gobbleText >>= go . append subDL)-                    <|> (AP.endOfInput *> finish subDL)-                    <|> (escChar >>= go . append subDL)-                    <|> (do-                            idp <- identParser-                            dl' <- finish subDL-                            loop $! append dl' idp)--    gobbleText = AP.takeWhile1 (AP.notInClass "\\$")--    escChar = AP.char '\\' *> (T.singleton <$> AP.anyChar)--    identParser = AP.char '$' *> (ident <|> return (Literal "$"))-    ident = (AP.char '{' *> (Ident <$> AP.takeWhile (/='}')) <* AP.string "}")------------------------------------------------------------------------------------ | Gets the attribute value.  If the splice's result list contains non-text--- nodes, this will translate them into text nodes with nodeText and--- concatenate them together.------ Originally, this only took the first node from the splices's result list,--- and only if it was a text node. This caused problems when the splice's--- result contained HTML entities, as they would split a text node. This was--- then fixed to take the first consecutive bunch of text nodes, and return--- their concatenation. This was seen as more useful than throwing an error,--- and more intuitive than trying to render all the nodes as text.------ However, it was decided in the end to render all the nodes as text, and--- then concatenate them. If a splice returned--- \"some \<b\>text\<\/b\> foobar\", the user would almost certainly want--- \"some text foobar\" to be rendered, and Heist would probably seem--- annoyingly limited for not being able to do this. If the user really did--- want it to render \"some \", it would probably be easier for them to--- accept that they were silly to pass more than that to be substituted than--- it would be for the former user to accept that--- \"some \<b\>text\<\/b\> foobar\" is being rendered as \"some \" because--- it's \"more intuitive\".-getAttributeSplice :: Monad m => Text -> HeistT m Text-getAttributeSplice name = do-    s <- liftM (lookupSplice name) getTS-    nodes <- maybe (return []) id s-    return $ T.concat $ map X.nodeText nodes----------------------------------------------------------------------------------- | Performs splice processing on a list of nodes.-runNodeList :: Monad m => [X.Node] -> Splice m-runNodeList = mapSplices runNode-{-# INLINE runNodeList #-}------------------------------------------------------------------------------------ | The maximum recursion depth.  (Used to prevent infinite loops.)-mAX_RECURSION_DEPTH :: Int-mAX_RECURSION_DEPTH = 50------------------------------------------------------------------------------------ | Checks the recursion flag and recurses accordingly.  Does not recurse--- deeper than mAX_RECURSION_DEPTH to avoid infinite loops.-recurseSplice :: Monad m => X.Node -> Splice m -> Splice m-recurseSplice node splice = do-    result <- localParamNode (const node) splice-    ts' <- getTS-    if _recurse ts' && _recursionDepth ts' < mAX_RECURSION_DEPTH-        then do modRecursionDepth (+1)-                res <- runNodeList result-                restoreTS ts'-                return res-        else return result-  where-    modRecursionDepth :: Monad m => (Int -> Int) -> HeistT m ()-    modRecursionDepth f =-        modifyTS (\st -> st { _recursionDepth = f (_recursionDepth st) })------------------------------------------------------------------------------------ | Looks up a template name runs a 'HeistT' computation on it.-lookupAndRun :: Monad m-             => ByteString-             -> ((DocumentFile, TPath) -> HeistT m (Maybe a))-             -> HeistT m (Maybe a)-lookupAndRun name k = do-    ts <- getTS-    let mt = lookupTemplate name ts-    let curPath = join $ fmap (dfFile . fst) mt-    modifyTS (setCurTemplateFile curPath)-    maybe (return Nothing) k mt------------------------------------------------------------------------------------ | Looks up a template name evaluates it by calling runNodeList.-evalTemplate :: Monad m-            => ByteString-            -> HeistT m (Maybe Template)-evalTemplate name = lookupAndRun name-    (\(t,ctx) -> localTS (\ts -> ts {_curContext = ctx})-                         (liftM Just $ runNodeList $ X.docContent $ dfDoc t))------------------------------------------------------------------------------------ | Sets the document type of a 'X.Document' based on the 'HeistT'--- value.-fixDocType :: Monad m => X.Document -> HeistT m X.Document-fixDocType d = do-    dts <- getsTS _doctypes-    return $ d { X.docType = listToMaybe dts }------------------------------------------------------------------------------------ | Same as evalWithHooks, but returns the entire 'X.Document' rather than--- just the nodes.  This is the right thing to do if we are starting at the--- top level.-evalWithHooksInternal :: Monad m-                      => ByteString-                      -> HeistT m (Maybe X.Document)-evalWithHooksInternal name = lookupAndRun name $ \(t,ctx) -> do-    addDoctype $ maybeToList $ X.docType $ dfDoc t-    ts <- getTS-    nodes <- lift $ _preRunHook ts $ X.docContent $ dfDoc t-    putTS (ts {_curContext = ctx})-    res <- runNodeList nodes-    restoreTS ts-    newNodes <- lift (_postRunHook ts res)-    newDoc   <- fixDocType $ (dfDoc t) { X.docContent = newNodes }-    return (Just newDoc)------------------------------------------------------------------------------------ | Looks up a template name evaluates it by calling runNodeList.  This also--- executes pre- and post-run hooks and adds the doctype.-evalWithHooks :: Monad m-            => ByteString-            -> HeistT m (Maybe Template)-evalWithHooks name = liftM (liftM X.docContent) (evalWithHooksInternal name)------------------------------------------------------------------------------------ | Binds a list of constant string splices.-bindStrings :: Monad m-            => [(Text, Text)]-            -> HeistState m-            -> HeistState m-bindStrings pairs ts = foldr (uncurry bindString) ts pairs------------------------------------------------------------------------------------ | Binds a single constant string splice.-bindString :: Monad m-            => Text-            -> Text-            -> HeistState m-            -> HeistState m-bindString n = bindSplice n . textSplice------------------------------------------------------------------------------------ | Renders a template with the specified parameters.  This is the function--- to use when you want to "call" a template and pass in parameters from--- inside a splice.  If the template does not exist, this version simply--- returns an empty list.-callTemplate :: Monad m-             => ByteString         -- ^ The name of the template-             -> [(Text, Splice m)] -- ^ Association list of-                                   -- (name,value) parameter pairs-             -> HeistT m Template-callTemplate name params = do-    modifyTS $ bindSplices params-    liftM (maybe [] id) $ evalTemplate name------------------------------------------------------------------------------------ | Like callTemplate except the splices being bound are constant text--- splices.-callTemplateWithText :: Monad m-                     => ByteString     -- ^ The name of the template-                     -> [(Text, Text)] -- ^ Association list of-                                       -- (name,value) parameter pairs-                     -> HeistT m Template-callTemplateWithText name params = do-    modifyTS $ bindStrings params-    liftM (maybe [] id) $ evalTemplate name------------------------------------------------------------------------------------ Gives the MIME type for a 'X.Document'-mimeType :: X.Document -> ByteString-mimeType d = case d of-    (X.HtmlDocument e _ _) -> "text/html;charset=" `BC.append` enc e-    (X.XmlDocument  e _ _) -> "text/xml;charset="  `BC.append` enc e-  where-    enc X.UTF8    = "utf-8"-    -- Should not include byte order designation for UTF-16 since-    -- rendering will include a byte order mark. (RFC 2781, Sec. 3.3)-    enc X.UTF16BE = "utf-16"-    enc X.UTF16LE = "utf-16"------------------------------------------------------------------------------------ | Renders a template from the specified HeistState to a 'Builder'.  The--- MIME type returned is based on the detected character encoding, and whether--- the root template was an HTML or XML format template.  It will always be--- @text/html@ or @text/xml@.  If a more specific MIME type is needed for a--- particular XML application, it must be provided by the application.-renderTemplate :: Monad m-               => HeistState m-               -> ByteString-               -> m (Maybe (Builder, MIMEType))-renderTemplate ts name = evalHeistT tpl (X.TextNode "") ts-  where tpl = do mt <- evalWithHooksInternal name-                 case mt of-                    Nothing  -> return Nothing-                    Just doc -> return $ Just $ (X.render doc, mimeType doc)------------------------------------------------------------------------------------ | Renders a template with the specified arguments passed to it.  This is a--- convenience function for the common pattern of calling renderTemplate after--- using bindString, bindStrings, or bindSplice to set up the arguments to the--- template.-renderWithArgs :: Monad m-                   => [(Text, Text)]-                   -> HeistState m-                   -> ByteString-                   -> m (Maybe (Builder, MIMEType))-renderWithArgs args ts = renderTemplate (bindStrings args ts)------------------------------------------------------------------------------------ Template loading------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Type synonym for parsers.-type ParserFun = String -> ByteString -> Either String X.Document------------------------------------------------------------------------------------ | Reads an HTML or XML template from disk.-getDocWith :: ParserFun -> String -> IO (Either String DocumentFile)-getDocWith parser f = do-    bs <- catch (liftM Right $ B.readFile f)-                (\(e::SomeException) -> return $ Left $ show e)--    let eitherDoc = either Left (parser f) bs-    return $ either (\s -> Left $ f ++ " " ++ s)-                    (\d -> Right $ DocumentFile d (Just f)) eitherDoc------------------------------------------------------------------------------------ | Reads an HTML template from disk.-getDoc :: String -> IO (Either String DocumentFile)-getDoc = getDocWith X.parseHTML------------------------------------------------------------------------------------ | Reads an XML template from disk.-getXMLDoc :: String -> IO (Either String DocumentFile)-getXMLDoc = getDocWith X.parseXML------------------------------------------------------------------------------------ | Loads a template with the specified path and filename.  The--- template is only loaded if it has a ".tpl" or ".xtpl" extension.-loadTemplate :: String -- ^ path of the template root-             -> String -- ^ full file path (includes the template root)-             -> IO [Either String (TPath, DocumentFile)] --TemplateMap-loadTemplate templateRoot fname-    | isHTMLTemplate = do-        c <- getDoc fname-        return [fmap (\t -> (splitLocalPath $ BC.pack tName, t)) c]-    | isXMLTemplate = do-        c <- getXMLDoc fname-        return [fmap (\t -> (splitLocalPath $ BC.pack tName, t)) c]-    | otherwise = return []-  where -- tName is path relative to the template root directory-        isHTMLTemplate = ".tpl"  `isSuffixOf` fname-        isXMLTemplate  = ".xtpl" `isSuffixOf` fname-        correction = if last templateRoot == '/' then 0 else 1-        extLen     = if isHTMLTemplate then 4 else 5-        tName = drop ((length templateRoot)+correction) $-                -- We're only dropping the template root, not the whole path-                take ((length fname) - extLen) fname------------------------------------------------------------------------------------ | Traverses the specified directory structure and builds a HeistState by--- loading all the files with a ".tpl" or ".xtpl" extension.-loadTemplates :: Monad m => FilePath -> HeistState m-              -> IO (Either String (HeistState m))-loadTemplates dir ts = do-    d <- readDirectoryWith (loadTemplate dir) dir-    let tlist = F.fold (free d)-        errs = lefts tlist-    case errs of-        [] -> liftM Right $ foldM loadHook ts $ rights tlist-        _  -> return $ Left $ unlines errs------------------------------------------------------------------------------------ | Runs a template modifying function on a DocumentFile.-runHook :: Monad m => (Template -> m Template)-        -> DocumentFile-        -> m DocumentFile-runHook f t = do-    n <- f $ X.docContent $ dfDoc t-    return $ t { dfDoc = (dfDoc t) { X.docContent = n } }------------------------------------------------------------------------------------ | Runs the onLoad hook on the template and returns the 'HeistState'--- with the result inserted.-loadHook :: Monad m => HeistState m -> (TPath, DocumentFile)-         -> IO (HeistState m)-loadHook ts (tp, t) = do-    t' <- runHook (_onLoadHook ts) t-    return $ insertTemplate tp t' ts------------------------------------------------------------------------------------ | Adds a path prefix to all the templates in the 'HeistState'.  If you--- want to add multiple levels of directories, separate them with slashes as--- in "foo/bar".  Using an empty string as a path prefix will leave the--- 'HeistState' unchanged.-addTemplatePathPrefix :: ByteString -> HeistState m -> HeistState m-addTemplatePathPrefix dir ts-  | B.null dir = ts-  | otherwise  = ts { _templateMap = Map.fromList $-                                     map (\(x,y) -> (f x, y)) $-                                     Map.toList $-                                     _templateMap ts-                    }-  where-    f ps = ps++splitTemplatePath dir-
− src/Text/Templating/Heist/Splices.hs
@@ -1,16 +0,0 @@-module Text.Templating.Heist.Splices-  ( module Text.Templating.Heist.Splices.Apply-  , module Text.Templating.Heist.Splices.Bind-  , module Text.Templating.Heist.Splices.Cache-  , module Text.Templating.Heist.Splices.Html-  , module Text.Templating.Heist.Splices.Ignore-  , module Text.Templating.Heist.Splices.Markdown-  ) where--import Text.Templating.Heist.Splices.Apply-import Text.Templating.Heist.Splices.Bind-import Text.Templating.Heist.Splices.Cache-import Text.Templating.Heist.Splices.Html-import Text.Templating.Heist.Splices.Ignore-import Text.Templating.Heist.Splices.Markdown-
− src/Text/Templating/Heist/Splices/Apply.hs
@@ -1,65 +0,0 @@-module Text.Templating.Heist.Splices.Apply where---------------------------------------------------------------------------------import           Data.Maybe-import           Data.Text (Text)-import qualified Data.Text.Encoding as T-import qualified Text.XmlHtml as X---------------------------------------------------------------------------------import           Text.Templating.Heist.Internal-import           Text.Templating.Heist.Types----------------------------------------------------------------------------------- | Default name for the apply splice.-applyTag :: Text-applyTag = "apply"------------------------------------------------------------------------------------ | Default attribute name for the apply tag.-applyAttr :: Text-applyAttr = "template"------------------------------------------------------------------------------------ | Raw core of apply functionality.  This is abstracted for use in other--- places like an enhanced (from the original) bind-rawApply :: (Monad m)-         => [X.Node]-         -> TPath-         -> [X.Node]-         -> HeistT m Template-rawApply calledNodes newContext paramNodes = do-    st <- getTS  -- Can't use localTS here because the modifier is not pure-    processedParams <- runNodeList paramNodes-    modifyTS (bindSplice "content" $ return processedParams)-    setContext newContext-    result <- runNodeList calledNodes-    restoreTS st-    return result------------------------------------------------------------------------------------ | Applies a template as if the supplied nodes were the children of the--- <apply> tag.-applyNodes :: Monad m => Template -> Text -> Splice m-applyNodes nodes template = do-    st <- getTS-    maybe (return []) -- TODO: error handling-          (\(t,ctx) -> do-              addDoctype $ maybeToList $ X.docType $ dfDoc t-              rawApply (X.docContent $ dfDoc t) ctx nodes)-          (lookupTemplate (T.encodeUtf8 template) st)------------------------------------------------------------------------------------ | Implementation of the apply splice.-applyImpl :: Monad m => Splice m-applyImpl = do-    node <- getParamNode-    case X.getAttribute applyAttr node of-        Nothing   -> return [] -- TODO: error handling-        Just template -> applyNodes (X.childNodes node) template--
− src/Text/Templating/Heist/Splices/Bind.hs
@@ -1,39 +0,0 @@-module Text.Templating.Heist.Splices.Bind where---------------------------------------------------------------------------------import           Data.Text (Text)-import qualified Text.XmlHtml as X---------------------------------------------------------------------------------import           Text.Templating.Heist.Internal-import           Text.Templating.Heist.Splices.Apply-import           Text.Templating.Heist.Types---- | Default name for the bind splice.-bindTag :: Text-bindTag = "bind"------------------------------------------------------------------------------------ | Default attribute name for the bind tag.-bindAttr :: Text-bindAttr = "tag"------------------------------------------------------------------------------------ | Implementation of the bind splice.-bindImpl :: Monad m => Splice m-bindImpl = do-    node <- getParamNode-    maybe (return ())-          (add node)-          (X.getAttribute bindAttr node)-    return []--  where-    add node nm = modifyTS $ bindSplice nm $ do-        caller <- getParamNode-        ctx <- getContext-        rawApply (X.childNodes node) ctx (X.childNodes caller)--
− src/Text/Templating/Heist/Splices/BindStrict.hs
@@ -1,32 +0,0 @@-module Text.Templating.Heist.Splices.BindStrict where---------------------------------------------------------------------------------import           Data.Text (Text)-import qualified Text.XmlHtml as X---------------------------------------------------------------------------------import           Text.Templating.Heist.Internal-import           Text.Templating.Heist.Splices.Apply-import           Text.Templating.Heist.Splices.Bind-import           Text.Templating.Heist.Types---- | Default name for the bind splice.-bindStrictTag :: Text-bindStrictTag = "bindStrict"------------------------------------------------------------------------------------ | Implementation of the bind splice.-bindStrictImpl :: Monad m => Splice m-bindStrictImpl = do-    node <- getParamNode-    cs <- runChildren-    maybe (return ()) (add cs)-          (X.getAttribute bindAttr node)-    return []--  where-    add cs nm = modifyTS $ bindSplice nm $ do-        caller <- getParamNode-        ctx <- getContext-        rawApply cs ctx (X.childNodes caller)
− src/Text/Templating/Heist/Splices/Cache.hs
@@ -1,152 +0,0 @@--- | The \"cache\" splice ensures that its contents are cached and only--- evaluated periodically.  The cached contents are returned every time the--- splice is referenced.------ Use the ttl attribute to set the amount of time between reloads.  The ttl--- value should be a positive integer followed by a single character--- specifying the units.  Valid units are seconds, minutes, hours, days, and--- weeks.  If the ttl string is invalid or the ttl attribute is not specified,--- the cache is never refreshed unless explicitly cleared with--- clearCacheTagState.-module Text.Templating.Heist.Splices.Cache-  ( CacheTagState-  , mkCacheTag-  , clearCacheTagState-  ) where---------------------------------------------------------------------------------import           Control.Concurrent-import           Control.Monad-import           Control.Monad.Trans-import           Data.IORef-import qualified Data.HashMap.Strict as H-import           Data.HashMap.Strict (HashMap)-import qualified Data.HashSet as Set-import           Data.Text (Text)-import qualified Data.Text as T-import           Data.Text.Read-import           Data.Time.Clock-import           System.Random-import           Text.XmlHtml.Cursor-import           Text.XmlHtml hiding (Node)----------------------------------------------------------------------------------import           Text.Templating.Heist.Internal-import           Text.Templating.Heist.Types----------------------------------------------------------------------------------cacheTagName :: Text-cacheTagName = "cache"------------------------------------------------------------------------------------ | State for storing cache tag information-newtype CacheTagState = CTS (MVar (HashMap Text (UTCTime, Template)))------------------------------------------------------------------------------------ | Clears the cache tag state.-clearCacheTagState :: CacheTagState -> IO ()-clearCacheTagState (CTS cacheMVar) =-    modifyMVar_ cacheMVar (const $ return H.empty)------------------------------------------------------------------------------------ | Converts a TTL string into an integer number of seconds.-parseTTL :: Text -> Int-parseTTL s = value * multiplier-  where-    value = either (const 0) fst $ decimal s-    multiplier = case T.last s of-        's' -> 1-        'm' -> 60-        'h' -> 3600-        'd' -> 86400-        'w' -> 604800-        _   -> 0---------------------------------------------------------------------------------cacheImpl :: (MonadIO m)-           => CacheTagState-           -> HeistT m Template-cacheImpl (CTS mv) = do-    tree <- getParamNode-    let err = error $ unwords ["cacheImpl is bound to a tag"-                              ,"that didn't get an id attribute."-                              ," This should never happen."]-    let i = maybe err id $ getAttribute "id" tree-        ttl = maybe 0 parseTTL $ getAttribute "ttl" tree-    mp <- liftIO $ readMVar mv--    (mp',ns) <- do-                   cur <- liftIO getCurrentTime-                   let mbn = H.lookup i mp-                       reload = do-                           nodes' <- runNodeList $ childNodes tree-                           return $! (H.insert i (cur,nodes') mp, nodes')-                   case mbn of-                       Nothing -> reload-                       (Just (lastUpdate,n)) -> do-                           if ttl > 0 && tagName tree == Just cacheTagName &&-                              diffUTCTime cur lastUpdate > fromIntegral ttl-                             then reload-                             else do-                                 stopRecursion-                                 return $! (mp,n)--    liftIO $ modifyMVar_ mv (const $ return mp')--    return ns------------------------------------------------------------------------------------ | Returns a function that modifies a HeistState to include a \"cache\"--- tag.  The cache tag is not bound automatically with the other default Heist--- tags.  This is because this function also returns CacheTagState, so the--- user will be able to clear it with the 'clearCacheTagState' function.-mkCacheTag :: MonadIO m-           => IO (HeistState m -> HeistState m, CacheTagState)-mkCacheTag = do-    sr <- newIORef $ Set.empty-    mv <- liftM CTS $ newMVar H.empty--    return $ ( addOnLoadHook (assignIds sr) .-               -- The cache tag allows the ttl attribute.-               bindSplice cacheTagName (cacheImpl mv) .-               -- Like the old static tag...does not allow ttl-               bindSplice "static" (cacheImpl mv)-             , mv)--  where-    generateId :: IO Int-    generateId = getStdRandom random--    assignIds setref = mapM f-        where-          f node = g $ fromNode node--          getId = do-              i  <- liftM (T.pack . show) generateId-              st <- readIORef setref-              if Set.member i st-                then getId-                else do-                    writeIORef setref $ Set.insert i st-                    return $ T.append "cache-id-" i--          g curs = do-              let node = current curs-              curs' <- if tagName node == Just cacheTagName ||-                          tagName node == Just "static"-                         then do-                             i <- getId-                             return $ modifyNode (setAttribute "id" i) curs-                         else return curs-              let mbc = nextDF curs'-              maybe (return $ topNode curs') g mbc----
− src/Text/Templating/Heist/Splices/Html.hs
@@ -1,47 +0,0 @@-module Text.Templating.Heist.Splices.Html where---------------------------------------------------------------------------------import           Data.Maybe-import           Data.Text (Text)-import qualified Text.XmlHtml as X---------------------------------------------------------------------------------import           Text.Templating.Heist.Internal-import           Text.Templating.Heist.Types------------------------------------------------------------------------------------ | Name for the html splice.-htmlTag :: Text-htmlTag = "html"------------------------------------------------------------------------------------ | The html splice runs all children and then traverses the returned node--- forest removing all head nodes.  Then it merges them all and prepends it to--- the html tag's child list.-htmlImpl :: Monad m => Splice m-htmlImpl = do-    node <- getParamNode-    children <- runNodeList $ X.childNodes node-    let (heads, mnode) = extractHeads $ node { X.elementChildren = children }-        new (X.Element t a c) = X.Element t a $-            X.Element "head" [] heads : c-        new n = n-    stopRecursion-    return [maybe node new mnode]----------------------------------------------------------------------------------- | Extracts all heads from a node tree.-extractHeads :: X.Node-             -- ^ The root (html) node-             -> ([X.Node], Maybe X.Node)-             -- ^ A tuple of a list of head nodes and the original tree with-             --   heads removed.-extractHeads (X.Element t a c)-  | t == "head" = (c, Nothing)-  | otherwise   = (concat heads, Just $ X.Element t a (catMaybes mcs))-  where-    (heads, mcs) = unzip $ map extractHeads c-extractHeads n = ([], Just n)-
− src/Text/Templating/Heist/Splices/Ignore.hs
@@ -1,22 +0,0 @@-module Text.Templating.Heist.Splices.Ignore where---------------------------------------------------------------------------------import           Data.Text (Text)---------------------------------------------------------------------------------import           Text.Templating.Heist.Types------------------------------------------------------------------------------------ | Default name for the ignore splice.-ignoreTag :: Text-ignoreTag = "ignore"------------------------------------------------------------------------------------ | The ignore tag and everything it surrounds disappears in the--- rendered output.-ignoreImpl :: Monad m => Splice m-ignoreImpl = return []--
− src/Text/Templating/Heist/Splices/Json.hs
@@ -1,222 +0,0 @@-{-# 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
− src/Text/Templating/Heist/Splices/Markdown.hs
@@ -1,183 +0,0 @@-{-# LANGUAGE DeriveDataTypeable #-}-{-| The \"markdown\" splice formats markdown content as HTML and inserts-it into the document.--If the file attribute is present the contents of the tag is ignored and-the file specified is converted to HTML.--Otherwise the non-markup children of the tag are processed as markdown-and converted to HTML.--This splice requires that the \"pandoc\" executable is in your path.--}-module Text.Templating.Heist.Splices.Markdown where---------------------------------------------------------------------------------import           Data.ByteString (ByteString)-import qualified Data.ByteString as B-import qualified Data.ByteString.Char8 as BC-import           Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import           Data.Maybe-import           Control.Concurrent-import           Control.Exception (throwIO)-import           Control.Monad-import           Control.Monad.CatchIO-import           Control.Monad.Trans-import           Data.Typeable-import           Prelude hiding (catch)-import           System.Directory-import           System.Exit-import           System.FilePath.Posix-import           System.IO-import           System.Process-import           Text.XmlHtml---------------------------------------------------------------------------------import           Text.Templating.Heist.Internal-import           Text.Templating.Heist.Types--data PandocMissingException = PandocMissingException-   deriving (Typeable)--instance Show PandocMissingException where-    show PandocMissingException =-        "Cannot find the \"pandoc\" executable; is it on your $PATH?"--instance Exception PandocMissingException---data MarkdownException = MarkdownException ByteString-   deriving (Typeable)--instance Show MarkdownException where-    show (MarkdownException e) =-        "Markdown error: pandoc replied:\n\n" ++ BC.unpack e--instance Exception MarkdownException---data NoMarkdownFileException = NoMarkdownFileException-    deriving (Typeable)--instance Show NoMarkdownFileException where-    show NoMarkdownFileException =-        "Markdown error: no file or template in context" ++-        " during processing of markdown tag"--instance Exception NoMarkdownFileException where----------------------------------------------------------------------------------- | Default name for the markdown splice.-markdownTag :: Text-markdownTag = "markdown"----------------------------------------------------------------------------------- | Implementation of the markdown splice.-markdownSplice :: MonadIO m => Splice m-markdownSplice = do-    templateDir <- liftM (fmap takeDirectory) getTemplateFilePath-    pdMD <- liftIO $ findExecutable "pandoc"--    when (isNothing pdMD) $ liftIO $ throwIO PandocMissingException--    tree <- getParamNode-    (source,markup) <- liftIO $-        case getAttribute "file" tree of-            Just f  -> do-                m <- maybe (liftIO $ throwIO NoMarkdownFileException )-                           (\tp -> pandoc (fromJust pdMD) tp $ T.unpack f)-                           templateDir-                return (T.unpack f,m)-            Nothing -> do-                m <- pandocBS (fromJust pdMD) $ T.encodeUtf8 $ nodeText tree-                return ("inline_splice",m)--    let ee = parseHTML source markup-    case ee of-      Left e  -> throw $ MarkdownException-                       $ BC.pack ("Error parsing markdown output: " ++ e)-      Right d -> return (docContent d)---pandoc :: FilePath -> FilePath -> FilePath -> IO ByteString-pandoc pandocPath templateDir inputFile = do-    (ex, sout, serr) <- readProcessWithExitCode' pandocPath args ""--    when (isFail ex) $ throw $ MarkdownException serr-    return $ BC.concat [ "<div class=\"markdown\">\n"-                         , sout-                         , "\n</div>" ]--  where-    isFail ExitSuccess = False-    isFail _           = True--    args = [ "-S", "--no-wrap", templateDir </> inputFile ]---pandocBS :: FilePath -> ByteString -> IO ByteString-pandocBS pandocPath s = do-    -- using the crummy string functions for convenience here-    (ex, sout, serr) <- readProcessWithExitCode' pandocPath args s--    when (isFail ex) $ throw $ MarkdownException serr-    return $ BC.concat [ "<div class=\"markdown\">\n"-                       , sout-                       , "\n</div>" ]--  where-    isFail ExitSuccess = False-    isFail _           = True-    args = [ "-S", "--no-wrap" ]----- a version of readProcessWithExitCode that does I/O properly-readProcessWithExitCode'-    :: FilePath                 -- ^ command to run-    -> [String]                 -- ^ any arguments-    -> ByteString               -- ^ standard input-    -> IO (ExitCode,ByteString,ByteString) -- ^ exitcode, stdout, stderr-readProcessWithExitCode' cmd args input = do-    (Just inh, Just outh, Just errh, pid) <--        createProcess (proc cmd args){ std_in  = CreatePipe,-                                       std_out = CreatePipe,-                                       std_err = CreatePipe }-    outMVar <- newEmptyMVar--    outM <- newEmptyMVar-    errM <- newEmptyMVar--    -- fork off a thread to start consuming stdout-    forkIO $ do-        out <- B.hGetContents outh-        putMVar outM out-        putMVar outMVar ()--    -- fork off a thread to start consuming stderr-    forkIO $ do-        err  <- B.hGetContents errh-        putMVar errM err-        putMVar outMVar ()--    -- now write and flush any input-    when (not (B.null input)) $ do B.hPutStr inh input; hFlush inh-    hClose inh -- done with stdin--    -- wait on the output-    takeMVar outMVar-    takeMVar outMVar-    hClose outh--    -- wait on the process-    ex <- waitForProcess pid--    out <- readMVar outM-    err <- readMVar errM--    return (ex, out, err)----
− src/Text/Templating/Heist/TemplateDirectory.hs
@@ -1,85 +0,0 @@-{-|--This module defines a TemplateDirectory data structure for convenient-interaction with templates within web apps.---}--module Text.Templating.Heist.TemplateDirectory-    ( TemplateDirectory-    , newTemplateDirectory-    , newTemplateDirectory'--    , getDirectoryTS-    , reloadTemplateDirectory-    ) where---------------------------------------------------------------------------------import           Control.Concurrent-import           Control.Monad-import           Control.Monad.Trans-import           Text.Templating.Heist-import           Text.Templating.Heist.Splices.Cache------------------------------------------------------------------------------------ | Structure representing a template directory.-data TemplateDirectory m-    = TemplateDirectory-        FilePath-        (HeistState m)-        (MVar (HeistState m))-        CacheTagState------------------------------------------------------------------------------------ | Creates and returns a new 'TemplateDirectory' wrapped in an Either for--- error handling.-newTemplateDirectory :: (MonadIO m, MonadIO n)-                     => FilePath-                     -> HeistState m-                     -> n (Either String (TemplateDirectory m))-newTemplateDirectory dir templateState = liftIO $ do-    (modTs,cts) <- mkCacheTag-    let origTs = modTs templateState-    ets <- loadTemplates dir origTs-    leftPass ets $ \ts -> do-        tsMVar <- newMVar $ ts-        return $ TemplateDirectory dir origTs tsMVar cts------------------------------------------------------------------------------------ | Creates and returns a new 'TemplateDirectory', using the monad's fail--- function on error.-newTemplateDirectory' :: (MonadIO m, MonadIO n)-                      => FilePath-                      -> HeistState m-                      -> n (TemplateDirectory m)-newTemplateDirectory' = ((either fail return =<<) .) . newTemplateDirectory------------------------------------------------------------------------------------ | Gets the 'HeistState' from a TemplateDirectory.-getDirectoryTS :: (Monad m, MonadIO n)-               => TemplateDirectory m-               -> n (HeistState m)-getDirectoryTS (TemplateDirectory _ _ tsMVar _) = liftIO $ readMVar $ tsMVar------------------------------------------------------------------------------------ | Clears cached content and reloads templates from disk.-reloadTemplateDirectory :: (MonadIO m, MonadIO n)-                        => TemplateDirectory m-                        -> n (Either String ())-reloadTemplateDirectory (TemplateDirectory p origTs tsMVar cts) = liftIO $ do-    clearCacheTagState cts-    ets <- loadTemplates p origTs-    leftPass ets $ \ts -> modifyMVar_ tsMVar (const $ return ts)------------------------------------------------------------------------------------ | Prepends an error onto a Left.-leftPass :: Monad m => Either String b -> (b -> m c) -> m (Either String c)-leftPass e m = either (return . Left . loadError) (liftM Right . m) e-  where-    loadError = (++) "Error loading templates: "
− src/Text/Templating/Heist/Types.hs
@@ -1,406 +0,0 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE PackageImports #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE UndecidableInstances #-}--{-|--This module contains the core Heist data types.  HeistT intentionally-does not expose any of its functionality via MonadState or MonadReader-functions.  We define passthrough instances for the most common types of-monads.  These instances allow the user to use HeistT in a monad stack-without needing calls to `lift`.--Edward Kmett wrote most of the HeistT code and associated instances,-liberating us from the unused writer portion of RWST.---}--module Text.Templating.Heist.Types where---------------------------------------------------------------------------------import             Control.Applicative-import             Control.Arrow-import             Control.Monad.CatchIO-import             Control.Monad.Cont-import             Control.Monad.Error-import             Control.Monad.Reader-import             Control.Monad.State-import             Data.ByteString.Char8 (ByteString)-import qualified   Data.HashMap.Strict as H-import             Data.HashMap.Strict (HashMap)-import             Data.Monoid-import             Data.Text (Text)-import             Data.Typeable-import             Prelude hiding (catch)-import qualified   Text.XmlHtml as X------------------------------------------------------------------------------------ | A 'Template' is a forest of XML nodes.  Here we deviate from the "single--- root node" constraint of well-formed XML because we want to allow templates--- to contain fragments of a document that may not have a single root.-type Template = [X.Node]------------------------------------------------------------------------------------ | MIME Type.  The type alias is here to make the API clearer.-type MIMEType = ByteString------------------------------------------------------------------------------------ | Reversed list of directories.  This holds the path to the template--- currently being processed.-type TPath = [ByteString]---data DocumentFile = DocumentFile-    { dfDoc  :: X.Document-    , dfFile :: Maybe FilePath-    } deriving (Eq)----------------------------------------------------------------------------------- | All documents representing templates are stored in a map.-type TemplateMap = HashMap TPath DocumentFile------------------------------------------------------------------------------------ | A Splice is a HeistT computation that returns a 'Template'.-type Splice m = HeistT m Template------------------------------------------------------------------------------------ | SpliceMap associates a name and a Splice.-type SpliceMap m = HashMap Text (Splice m)------------------------------------------------------------------------------------ | Holds all the state information needed for template processing.  You will--- build a @HeistState@ using any of Heist's @HeistState m -> HeistState m@--- \"filter\" functions.  Then you use the resulting @HeistState@ in calls to--- @renderTemplate@.-data HeistState m = HeistState {-    -- | A mapping of splice names to splice actions-      _spliceMap       :: SpliceMap m-    -- | A mapping of template names to templates-    , _templateMap     :: TemplateMap-    -- | A flag to control splice recursion-    , _recurse         :: Bool-    -- | The path to the template currently being processed.-    , _curContext      :: TPath-    -- | A counter keeping track of the current recursion depth to prevent-    -- infinite loops.-    , _recursionDepth  :: Int-    -- | A hook run on all templates at load time.-    , _onLoadHook      :: Template -> IO Template-    -- | A hook run on all templates just before they are rendered.-    , _preRunHook      :: Template -> m Template-    -- | A hook run on all templates just after they are rendered.-    , _postRunHook     :: Template -> m Template-    -- | The doctypes encountered during template processing.-    , _doctypes        :: [X.DocType]-    -- | The full path to the current template's file on disk.-    , _curTemplateFile :: Maybe FilePath-}------------------------------------------------------------------------------------ | Gets the names of all the templates defined in a HeistState.-templateNames :: HeistState m -> [TPath]-templateNames ts = H.keys $ _templateMap ts------------------------------------------------------------------------------------ | Gets the names of all the splices defined in a HeistState.-spliceNames :: HeistState m -> [Text]-spliceNames ts = H.keys $ _spliceMap ts----------------------------------------------------------------------------------instance (Monad m) => Monoid (HeistState m) where-    mempty = HeistState H.empty H.empty True [] 0-                           return return return [] Nothing--    (HeistState s1 t1 r1 _ d1 o1 b1 a1 dt1 ctf1) `mappend`-        (HeistState s2 t2 r2 c2 d2 o2 b2 a2 dt2 ctf2) =-        HeistState s t r c2 d (o1 >=> o2) (b1 >=> b2) (a1 >=> a2)-            (dt1 `mappend` dt2) ctf-      where-        s = s1 `mappend` s2-        t = t1 `mappend` t2-        r = r1 && r2-        d = max d1 d2-        ctf = getLast $ Last ctf1 `mappend` Last ctf2----------------------------------------------------------------------------------instance Eq (HeistState m) where-    a == b = (_recurse a == _recurse b) &&-             (_templateMap a == _templateMap b) &&-             (_curContext a == _curContext b)------------------------------------------------------------------------------------ | The Typeable instance is here so Heist can be dynamically executed with--- Hint.-templateStateTyCon :: TyCon-templateStateTyCon = mkTyCon "Text.Templating.Heist.HeistState"-{-# NOINLINE templateStateTyCon #-}--instance (Typeable1 m) => Typeable (HeistState m) where-    typeOf _ = mkTyConApp templateStateTyCon [typeOf1 (undefined :: m ())]------------------------------------------------------------------------------------ | HeistT is the monad used for 'Splice' processing.  HeistT provides--- \"passthrough\" instances for many of the monads you might use in the inner--- monad.-newtype HeistT m a = HeistT {-    runHeistT :: X.Node-              -> HeistState m-              -> m (a, HeistState m)-}------------------------------------------------------------------------------------ | Evaluates a template monad as a computation in the underlying monad.-evalHeistT :: Monad m-           => HeistT m a-           -> X.Node-           -> HeistState m-           -> m a-evalHeistT m r s = do-    (a, _) <- runHeistT m r s-    return a-{-# INLINE evalHeistT #-}------------------------------------------------------------------------------------ | Functor instance-instance Functor m => Functor (HeistT m) where-    fmap f (HeistT m) = HeistT $ \r s -> first f <$> m r s------------------------------------------------------------------------------------ | Applicative instance-instance (Monad m, Functor m) => Applicative (HeistT m) where-    pure = return-    (<*>) = ap------------------------------------------------------------------------------------ | Monad instance-instance Monad m => Monad (HeistT m) where-    return a = HeistT (\_ s -> return (a, s))-    {-# INLINE return #-}-    HeistT m >>= k = HeistT $ \r s -> do-        (a, s') <- m r s-        runHeistT (k a) r s'-    {-# INLINE (>>=) #-}------------------------------------------------------------------------------------ | MonadIO instance-instance MonadIO m => MonadIO (HeistT m) where-    liftIO = lift . liftIO------------------------------------------------------------------------------------ | MonadTrans instance-instance MonadTrans HeistT where-    lift m = HeistT $ \_ s -> do-        a <- m-        return (a, s)------------------------------------------------------------------------------------ | MonadCatchIO instance-instance MonadCatchIO m => MonadCatchIO (HeistT m) where-    catch (HeistT a) h = HeistT $ \r s -> do-       let handler e = runHeistT (h e) r s-       catch (a r s) handler-    block (HeistT m) = HeistT $ \r s -> block (m r s)-    unblock (HeistT m) = HeistT $ \r s -> unblock (m r s)------------------------------------------------------------------------------------ | MonadFix passthrough instance-instance MonadFix m => MonadFix (HeistT m) where-    mfix f = HeistT $ \r s ->-        mfix $ \ (a, _) -> runHeistT (f a) r s------------------------------------------------------------------------------------ | Alternative passthrough instance-instance (Functor m, MonadPlus m) => Alternative (HeistT m) where-    empty = mzero-    (<|>) = mplus------------------------------------------------------------------------------------ | MonadPlus passthrough instance-instance MonadPlus m => MonadPlus (HeistT m) where-    mzero = lift mzero-    m `mplus` n = HeistT $ \r s ->-        runHeistT m r s `mplus` runHeistT n r s------------------------------------------------------------------------------------ | MonadState passthrough instance-instance MonadState s m => MonadState s (HeistT m) where-    get = lift get-    {-# INLINE get #-}-    put = lift . put-    {-# INLINE put #-}------------------------------------------------------------------------------------ | MonadReader passthrough instance-instance MonadReader r m => MonadReader r (HeistT m) where-    ask = HeistT $ \_ s -> do-            r <- ask-            return (r,s)-    local f (HeistT m) =-        HeistT $ \r s -> local f (m r s)------------------------------------------------------------------------------------ | Helper for MonadError instance.-liftCatch :: (m (a,HeistState m)-              -> (e -> m (a,HeistState m))-              -> m (a,HeistState m))-          -> HeistT m a-          -> (e -> HeistT m a)-          -> HeistT m a-liftCatch ce m h =-    HeistT $ \r s ->-        (runHeistT m r s `ce`-        (\e -> runHeistT (h e) r s))------------------------------------------------------------------------------------ | MonadError passthrough instance-instance (MonadError e m) => MonadError e (HeistT m) where-    throwError = lift . throwError-    catchError = liftCatch catchError------------------------------------------------------------------------------------ | Helper for MonadCont instance.-liftCallCC :: ((((a,HeistState m) -> m (b, HeistState m))-                  -> m (a, HeistState m))-                -> m (a, HeistState m))-           -> ((a -> HeistT m b) -> HeistT m a)-           -> HeistT m a-liftCallCC ccc f = HeistT $ \r s ->-    ccc $ \c ->-    runHeistT (f (\a -> HeistT $ \_ _ -> c (a, s))) r s------------------------------------------------------------------------------------ | MonadCont passthrough instance-instance (MonadCont m) => MonadCont (HeistT m) where-    callCC = liftCallCC callCC------------------------------------------------------------------------------------ | The Typeable instance is here so Heist can be dynamically executed with--- Hint.-templateMonadTyCon :: TyCon-templateMonadTyCon = mkTyCon "Text.Templating.Heist.HeistT"-{-# NOINLINE templateMonadTyCon #-}--instance (Typeable1 m) => Typeable1 (HeistT m) where-    typeOf1 _ = mkTyConApp templateMonadTyCon [typeOf1 (undefined :: m ())]------------------------------------------------------------------------------------ Functions for our monad.------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Gets the node currently being processed.------   > <speech author="Shakespeare">---   >   To sleep, perchance to dream.---   > </speech>------ When you call @getParamNode@ inside the code for the @speech@ splice, it--- returns the Node for the @speech@ tag and its children.  @getParamNode >>=--- childNodes@ returns a list containing one 'TextNode' containing part of--- Hamlet's speech.  @liftM (getAttribute \"author\") getParamNode@ would--- return @Just "Shakespeare"@.-getParamNode :: Monad m => HeistT m X.Node-getParamNode = HeistT $ \r s -> return (r,s)-{-# INLINE getParamNode #-}------------------------------------------------------------------------------------ | HeistT's 'local'.-localParamNode :: Monad m-               => (X.Node -> X.Node)-               -> HeistT m a-               -> HeistT m a-localParamNode f m = HeistT $ \r s -> runHeistT m (f r) s-{-# INLINE localParamNode #-}------------------------------------------------------------------------------------ | HeistT's 'gets'.-getsTS :: Monad m => (HeistState m -> r) -> HeistT m r-getsTS f = HeistT $ \_ s -> return (f s, s)-{-# INLINE getsTS #-}------------------------------------------------------------------------------------ | HeistT's 'get'.-getTS :: Monad m => HeistT m (HeistState m)-getTS = HeistT $ \_ s -> return (s, s)-{-# INLINE getTS #-}------------------------------------------------------------------------------------ | HeistT's 'put'.-putTS :: Monad m => HeistState m -> HeistT m ()-putTS s = HeistT $ \_ _ -> return ((), s)-{-# INLINE putTS #-}------------------------------------------------------------------------------------ | HeistT's 'modify'.-modifyTS :: Monad m-                    => (HeistState m -> HeistState m)-                    -> HeistT m ()-modifyTS f = HeistT $ \_ s -> return ((), f s)-{-# INLINE modifyTS #-}------------------------------------------------------------------------------------ | Restores the HeistState.  This function is almost like putTS except it--- preserves the current doctypes.  You should use this function instead of--- @putTS@ to restore an old state.  This was needed because doctypes needs to--- be in a "global scope" as opposed to the template call "local scope" of--- state items such as recursionDepth, curContext, and spliceMap.-restoreTS :: Monad m => HeistState m -> HeistT m ()-restoreTS old = modifyTS (\cur -> old { _doctypes = _doctypes cur })-{-# INLINE restoreTS #-}------------------------------------------------------------------------------------ | Abstracts the common pattern of running a HeistT computation with--- a modified heist state.-localTS :: Monad m-        => (HeistState m -> HeistState m)-        -> HeistT m a-        -> HeistT m a-localTS f k = do-    ts <- getTS-    putTS $ f ts-    res <- k-    restoreTS ts-    return res-{-# INLINE localTS #-}-
test/heist-testsuite.cabal view
@@ -8,33 +8,89 @@   main-is:         TestSuite.hs    build-depends:-    QuickCheck                 >= 2,-    aeson                      >= 0.4      && < 0.7,-    attoparsec                 >= 0.10     && < 0.11,-    base                       >= 4        && < 5,-    blaze-builder              >= 0.2      && < 0.4,-    blaze-html                 >= 0.4      && < 0.6,-    bytestring,-    containers,-    directory,-    directory-tree,-    process,-    filepath,-    xmlhtml                    >= 0.1.6    && < 0.3,     HUnit                      >= 1.2      && < 2,-    mtl                        >= 2,-    random,-    MonadCatchIO-transformers  >= 0.2.1    && < 0.3,+    QuickCheck                 >= 2,+    MonadCatchIO-transformers  >= 0.2.1    && < 0.4,     test-framework             >= 0.6      && <0.7,     test-framework-hunit       >= 0.2.7    && <0.3,     test-framework-quickcheck2 >= 0.2.12.1 && <0.3,-    text                       >= 0.10     && < 0.12,-    time,-    transformers,-    unordered-containers       >= 0.1.4    && < 0.3,-    vector                     >= 0.9      && < 0.10+    aeson                      >= 0.6     && < 0.7,+    attoparsec                 >= 0.10    && < 0.11,+    base                       >= 4       && < 5,+    blaze-builder              >= 0.2     && < 0.4,+    blaze-html                 >= 0.4     && < 0.6,+    bytestring                 >= 0.9     && < 0.11,+    containers                 >= 0.2     && < 0.6,+    directory                  >= 1.1     && < 1.3,+    directory-tree             >= 0.10    && < 0.12,+    dlist                      >= 0.5     && < 0.6,+    errors                     >= 1.3     && < 1.4,+    filepath                   >= 1.3     && < 1.4,+    hashable                   >= 1.1     && < 1.2,+    mtl                        >= 2.0     && < 2.2,+    process                    >= 1.1     && < 1.2,+    random                     >= 1.0.1.0 && < 1.1,+    text                       >= 0.10    && < 0.12,+    time                       >= 1.1     && < 1.5,+    transformers               >= 0.2     && < 0.4,+    unordered-containers       >= 0.1.4   && < 0.3,+    vector                     >= 0.9     && < 0.11,+    xmlhtml                    >= 0.1.6   && < 0.3     ghc-options: -O2 -Wall -fhpc -fwarn-tabs -funbox-strict-fields -threaded   Extensions: OverloadedStrings++Executable benchmark+  hs-source-dirs: ../src suite+  main-is: Benchmark.hs++  build-depends:+    MonadCatchIO-transformers >= 0.3     && < 0.4,+    HUnit                     >= 1.2     && < 1.3,+    criterion                 >= 0.6     && < 0.7,+    test-framework            >= 0.4     && < 0.7,+    test-framework-hunit      >= 0.2     && < 0.3,++    -- Copied from regular dependencies:++    aeson                      >= 0.6     && < 0.7,+    attoparsec                 >= 0.10    && < 0.11,+    base                       >= 4       && < 5,+    blaze-builder              >= 0.2     && < 0.4,+    blaze-html                 >= 0.4     && < 0.6,+    bytestring                 >= 0.9     && < 0.11,+    containers                 >= 0.2     && < 0.6,+    directory                  >= 1.1     && < 1.3,+    directory-tree             >= 0.10    && < 0.12,+    dlist                      >= 0.5     && < 0.6,+    errors                     >= 1.3     && < 1.4,+    filepath                   >= 1.3     && < 1.4,+    hashable                   >= 1.1     && < 1.2,+    mtl                        >= 2.0     && < 2.2,+    process                    >= 1.1     && < 1.2,+    random                     >= 1.0.1.0 && < 1.1,+    text                       >= 0.10    && < 0.12,+    time                       >= 1.1     && < 1.5,+    transformers               >= 0.2     && < 0.4,+    unordered-containers       >= 0.1.4   && < 0.3,+    vector                     >= 0.9     && < 0.11,+    xmlhtml                    >= 0.1.6   && < 0.3++  ghc-options: -O2 -Wall -fwarn-tabs -funbox-strict-fields -threaded+               -fno-warn-unused-do-bind -rtsopts++  ghc-prof-options: -prof -auto-all++  Extensions:+    GeneralizedNewtypeDeriving,+    PackageImports,+    ScopedTypeVariables,+    DeriveDataTypeable,+    FlexibleInstances,+    MultiParamTypeClasses,+    UndecidableInstances,+    OverloadedStrings,+    TypeSynonymInstances,+    NoMonomorphismRestriction 
test/runTestsAndCoverage.sh view
@@ -24,13 +24,13 @@ mkdir -p $DIR  EXCLUDES='Main-Data.CIByteString-Snap.Internal.Http.Parser.Tests-Snap.Internal.Http.Server.Tests-Snap.Internal.Http.Types.Tests-Snap.Internal.Iteratee.Tests-Text.Snap.Templates.Tests-Snap.Test.Common'+Heist.Compiled.Tests+Heist.Interpreted.Tests+Heist.Tutorial.AttributeSplices+Heist.Tutorial.CompiledSplices+Heist.Tutorial.Imports+Heist.TestCommon+Heist.Tests'  EXCL="" 
+ test/suite/Benchmark.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE BangPatterns      #-}+{-# LANGUAGE OverloadedStrings #-}++module Main where++------------------------------------------------------------------------------+import           Blaze.ByteString.Builder+import           Criterion+import           Criterion.Main+import           Criterion.Measurement hiding (getTime)+import           Control.Concurrent+import           Control.Error+import           Control.Exception (evaluate)+import           Control.Monad+import qualified Data.ByteString as B+import qualified Data.DList as DL+import qualified Data.Text as T+import           Data.Text.Encoding+import           Data.Time.Clock+import           Data.Maybe+import qualified Text.XmlHtml as X+import           System.Environment++import Heist+import Heist.Common+import qualified Heist.Compiled as C+import qualified Heist.Compiled.Internal as CI+import qualified Heist.Interpreted as I+import Heist.TestCommon+import Heist.Types++loadWithCache baseDir = do+    etm <- runEitherT $ do+        templates <- loadTemplates baseDir+        let hc = HeistConfig [] defaultLoadTimeSplices [] [] templates+        initHeistWithCacheTag hc+    either (error . unlines) (return . fst) etm++main = do+    (dir:file:_) <- getArgs+    applyComparison dir file++justRender dir = do+    let page = "faq"+        pageStr = T.unpack $ decodeUtf8 page+    hs <- loadWithCache dir+    let !compiledTemplate = fst $! fromJust $! C.renderTemplate hs page+        compiledAction = do+            res <- compiledTemplate+            return $! toByteString $! res+    out <- compiledAction+    putStrLn $ "Rendered ByteString of length "++(show $ B.length out)+    B.writeFile (pageStr++".out.compiled."++dir) $ out++    defaultMain+       [ bench (pageStr++"-compiled (just render)") (whnfIO compiledAction)+       ]++------------------------------------------------------------------------------+applyComparison :: FilePath -> String -> IO ()+applyComparison dir pageStr = do+    let page = encodeUtf8 $ T.pack pageStr+    hs <- loadWithCache dir+    let compiledAction = do+            res <- fst $ fromJust $ C.renderTemplate hs page+            return $! toByteString $! res+    out <- compiledAction+    B.writeFile (pageStr++".out.compiled."++dir) $ out++    let interpretedAction = do+            res <- I.renderTemplate hs page+            return $! toByteString $! fst $! fromJust res+    out2 <- interpretedAction+    B.writeFile (pageStr++".out.interpreted."++dir) $ out++    defaultMain+       [ bench (pageStr++"-compiled") (whnfIO compiledAction)+       , bench (pageStr++"-interpreted") (whnfIO interpretedAction)+       ]++cmdLineTemplate :: String -> String -> IO ()+cmdLineTemplate dir page = do+--    args <- getArgs+--    let page = head args+--    let dir = "test/snap-website"+    hs <- loadHS dir+    let action = fst $ fromJust $ C.renderTemplate hs+            (encodeUtf8 $ T.pack page)+    out <- action+    B.writeFile (page++".out.cur") $ toByteString out++--    reference <- B.readFile "faq.out"+--    if False+--      then do+--        putStrLn "Template didn't render properly"+--        error "Aborting"+--      else+--        putStrLn "Template rendered correctly"++    defaultMain [+         bench (page++"-speed") action+       ]+++testNode =+  X.Element "div" [("foo", "aoeu"), ("bar", "euid")] +    [X.Element "b" [] [X.TextNode "bolded text"]+    ,X.TextNode " not bolded"+    ,X.Element "a" [("href", "/path/to/page")] [X.TextNode "link"]+    ]++getChunks templateName = do+    hs <- loadHS "snap-website-nocache"+    let (Just t) = lookupTemplate templateName hs _compiledTemplateMap+    return $! fst $! fst t+
+ test/suite/Heist/Compiled/Tests.hs view
@@ -0,0 +1,38 @@+module Heist.Compiled.Tests+  ( tests+  ) where++import           Test.Framework (Test)+import           Test.Framework.Providers.HUnit+import qualified Test.HUnit as H+++------------------------------------------------------------------------------+import           Heist.Tutorial.CompiledSplices++-- NOTE: We can't test compiled templates on the templates directory as it+-- stands today because that directory contains some error conditions such as+-- infinite bind loops, apply tags with no template attribute, and apply tags+-- with ".." in the tag path (which doesn't currently work).++tests :: [Test]+tests = [ testCase     "compiled/simple"       simpleCompiledTest+        , testCase     "compiled/people"       peopleTest+        ]++simpleCompiledTest :: IO ()+simpleCompiledTest = do+    res <- runWithStateSplice "templates"+    H.assertEqual "compiled state splice" expected res+  where+    expected =+      "&#10;<html>&#10;3&#10;</html>&#10;"++peopleTest :: IO ()+peopleTest = do+    res <- personListTest "templates"+    H.assertEqual "people splice" expected res+  where+    expected =+      "&#10;<p>Doe, John: 42&#32;years old</p>&#10;&#10;<p>Smith, Jane: 21&#32;years old</p>&#10;&#10;"+
+ test/suite/Heist/Interpreted/Tests.hs view
@@ -0,0 +1,658 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NoMonomorphismRestriction  #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE TypeSynonymInstances       #-}+{-# OPTIONS_GHC -fno-warn-orphans       #-}++module Heist.Interpreted.Tests+  ( tests+  , quickRender+  ) where++------------------------------------------------------------------------------+import           Blaze.ByteString.Builder+import           Control.Error+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+import qualified Data.HashMap.Strict as Map+import           Data.Maybe+import           Data.Monoid+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import           Data.Text (Text)+import           System.IO.Unsafe+import           Test.Framework (Test)+import           Test.Framework.Providers.HUnit+import           Test.Framework.Providers.QuickCheck2+import qualified Test.HUnit as H+import           Test.QuickCheck+import           Test.QuickCheck.Monadic+++------------------------------------------------------------------------------+import           Heist+import           Heist.Common+import           Heist.Interpreted.Internal+import           Heist.Splices.Apply+import           Heist.Splices.Ignore+import           Heist.Splices.Json+import           Heist.Splices.Markdown+import           Heist.TestCommon+import           Heist.Types+import qualified Text.XmlHtml        as X+import qualified Text.XmlHtml.Cursor as X+++------------------------------------------------------------------------------+tests :: [Test]+tests = [ testProperty "heist/simpleBind"            simpleBindTest+        , testProperty "heist/simpleApply"           simpleApplyTest+        , testCase     "heist/templateAdd"           addTest+        , testCase     "heist/hasTemplate"           hasTemplateTest+        , testCase     "heist/getDoc"                getDocTest+        , testCase     "heist/load"                  loadTest+        , testCase     "heist/fsLoad"                fsLoadTest+        , testCase     "heist/renderNoName"          renderNoNameTest+        , testCase     "heist/doctype"               doctypeTest+        , testCase     "heist/attributeSubstitution" attrSubstTest+        , testCase     "heist/bindAttribute"         bindAttrTest+        , testCase     "heist/markdown"              markdownTest+        , testCase     "heist/title_expansion"       titleExpansion+        , testCase     "heist/textarea_expansion"    textareaExpansion+        , testCase     "heist/div_expansion"         divExpansion+        , testCase     "heist/bind_param"            bindParam+        , testCase     "heist/markdownText"          markdownTextTest+        , testCase     "heist/apply"                 applyTest+        , testCase     "heist/ignore"                ignoreTest+        , testCase     "heist/lookupTemplateContext" lookupTemplateTest+        , testCase     "heist/attrSpliceContext"     attrSpliceContext+        , testCase     "heist/json/values"           jsonValueTest+        , testCase     "heist/json/object"           jsonObjectTest+        ]+++------------------------------------------------------------------------------+simpleBindTest :: Property+simpleBindTest = monadicIO $ forAllM arbitrary prop+  where+    prop :: Bind -> PropertyM IO ()+    prop bind = do+        let template = buildBindTemplate bind+        let result   = buildResult bind++        spliceResult <- run $ do+            hs <- loadEmpty defaultLoadTimeSplices [] [] []+            evalHeistT (runNodeList template)+                       (X.TextNode "") hs++        assert $ result == spliceResult+++------------------------------------------------------------------------------+simpleApplyTest :: Property+simpleApplyTest = monadicIO $ forAllM arbitrary prop+  where+    prop :: Apply -> PropertyM IO ()+    prop apply = do+        let correct = calcCorrect apply+        result <- run $ calcResult apply+        assert $ correct == result+++------------------------------------------------------------------------------+addTest :: IO ()+addTest = do+    es <- loadEmpty [] [] [] []+    let hs = addTemplate "aoeu" [] Nothing es+    H.assertEqual "lookup test" (Just []) $+        fmap (X.docContent . dfDoc . fst) $+        lookupTemplate "aoeu" hs _templateMap+++------------------------------------------------------------------------------+hasTemplateTest :: H.Assertion+hasTemplateTest = do+    ets <- loadIO "templates" [] [] [] []+    let tm = either (error "Error loading templates") _templateMap ets+    hs <- loadEmpty [] [] [] []+    let hs's = setTemplates tm hs+    H.assertBool "hasTemplate hs's" (hasTemplate "index" hs's)+++------------------------------------------------------------------------------+getDocTest :: H.Assertion+getDocTest = do+    d <- getDoc "bkteoar"+    H.assertBool "non-existent doc" $ isLeft d+    f <- getDoc "templates/index.tpl"+    H.assertBool "index doc" $ not $ isLeft f+++------------------------------------------------------------------------------+loadTest :: H.Assertion+loadTest = do+    ets <- loadIO "templates" [] [] [] []+    either (error "Error loading templates")+           (\ts -> do let tm = _templateMap ts+                      H.assertEqual "loadTest size" 36 $ Map.size tm+           ) ets+++------------------------------------------------------------------------------+fsLoadTest :: H.Assertion+fsLoadTest = do+    ets <- loadIO "templates" [] [] [] []+    let tm = either (error "Error loading templates") _templateMap ets+    es <- loadEmpty [] [] [] []+    let hs = setTemplates tm es+    let f  = g hs++    f isNothing "abc/def/xyz"+    f isJust "a"+    f isJust "bar/a"+    f isJust "/bar/a"++  where+    g ts p n = H.assertBool ("loading template " ++ n) $ p $+               lookupTemplate (B.pack n) ts _templateMap++------------------------------------------------------------------------------+renderNoNameTest :: H.Assertion+renderNoNameTest = do+    ets <- loadT "templates" [] [] [] []+    either (error "Error loading templates")+           (\ts -> do t <- renderTemplate ts ""+                      H.assertBool "renderNoName" $ isNothing t+           ) ets+++------------------------------------------------------------------------------+doctypeTest :: H.Assertion+doctypeTest = do+    ets <- loadT "templates" [] [] [] []+    let ts = either (error "Error loading templates") id ets+    Just (indexDoc, _) <- renderTemplate ts "index"+    H.assertEqual "index doctype test" indexRes $ toByteString $ indexDoc+    Just (_, _) <- renderTemplate ts "ioc"+    H.assertEqual "ioc doctype test" indexRes $ toByteString $ indexDoc+  where+    indexRes = B.concat+        [doctype+        ,"\n&#10;<html>\n<div id='pre_{att}_post'>\n/index\n</div>\n</html>\n"+        ]++------------------------------------------------------------------------------+attrSubstTest :: H.Assertion+attrSubstTest = do+    ets <- loadT "templates" [] [] [] []+    let ts = either (error "Error loading templates") id ets+    check "attr subst 1" (bindSplices splices ts) out1+    check "attr subst 2" ts out2++  where+    splices = defaultLoadTimeSplices +++        [("foo", return [X.TextNode "meaning_of_everything"])]+        +    check str ts expected = do+        Just (resDoc, _) <- renderTemplate ts "attrs"+        H.assertEqual str expected $ toByteString $ resDoc++    out1 = B.unlines+        [doctype+        ,"<mytag flag>Empty attribute</mytag>"+        ,"<mytag flag='abc${foo}'>No ident capture</mytag>"+        ,"<div id='pre_meaning_of_everything_post'></div>"+        ]+    out2 = B.unlines+        [doctype+        ,"<mytag flag>Empty attribute</mytag>"+        ,"<mytag flag='abc${foo}'>No ident capture</mytag>"+        ,"<div id='pre__post'></div>"+        ]+++------------------------------------------------------------------------------+bindAttrTest :: H.Assertion+bindAttrTest = do+    ets <- loadT "templates" [] [] [] []+    let ts = either (error "Error loading templates") id ets+    check ts "<div id=\'zzzzz\'"++  where+    check ts str = do+        Just (resDoc, _) <- renderTemplate ts "bind-attrs"+        H.assertBool ("attr subst " ++ (show str)) $ not $ B.null $+            snd $ B.breakSubstring str $ toByteString $ resDoc+        H.assertBool ("attr subst bar") $ B.null $+            snd $ B.breakSubstring "${bar}" $ toByteString $ resDoc+++------------------------------------------------------------------------------+markdownHtmlExpected :: ByteString+markdownHtmlExpected = B.concat+    [ doctype+    , "<div class='markdown'><p>This <em>is</em> a test.</p></div>"+    ]++------------------------------------------------------------------------------+-- | Markdown test on a file+markdownTest :: H.Assertion+markdownTest = renderTest "markdown" markdownHtmlExpected+++------------------------------------------------------------------------------+jsonValueTest :: H.Assertion+jsonValueTest = do+    renderTest "json" jsonExpected1+    renderTest "json_snippet" jsonExpected2++  where+    jsonExpected1 = B.concat [ doctype+                             , "<i>&lt;b&gt;ok&lt;/b&gt;</i><i>1</i>"+                             , "<i></i><i>false</i><i>foo</i>" ]+    jsonExpected2 = B.concat+        [doctype, "<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+        [ doctype+        , "<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+            -> H.Assertion+renderTest templateName expectedResult = do+    ets <- loadT "templates" [] [] [] []+    let ts = either (error "Error loading templates") id ets++    check ts expectedResult++  where+    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+++------------------------------------------------------------------------------+-- | Expansion of a bound name inside a title-tag+titleExpansion :: H.Assertion+titleExpansion = renderTest "title_expansion" expected+  where+    expected = B.concat+        [ doctype+        , "<title>foo</title>"+        ]+++------------------------------------------------------------------------------+-- | Expansion of a bound name inside a textarea-tag+textareaExpansion :: H.Assertion+textareaExpansion = renderTest "textarea_expansion" expected+  where+    expected = B.concat+        [ "<textarea>foo</textarea>" ]+++------------------------------------------------------------------------------+-- | Expansion of a bound name inside a div-tag+divExpansion :: H.Assertion+divExpansion = renderTest "div_expansion" expected+  where+    expected = B.concat+        [ doctype+        , "<div>foo</div>"+        ]+++------------------------------------------------------------------------------+-- | Handling of <content> and bound parameters in a bound tag.+bindParam :: H.Assertion+bindParam = renderTest "bind_param" $+    B.concat [doctype, "<li>Hi there world</li>"]+++------------------------------------------------------------------------------+-- | Handling of <content> and bound parameters in a bound tag.+attrSpliceContext :: H.Assertion+attrSpliceContext = renderTest "attrsubtest2" $+    B.append doctype "<a href='asdf'>link</a><a href='before$after'>foo</a>"+++------------------------------------------------------------------------------+-- | Markdown test on supplied text+markdownTextTest :: H.Assertion+markdownTextTest = do+    hs <- loadEmpty [] [] [] []+    result <- evalHeistT markdownSplice+                         (X.TextNode "This *is* a test.")+                         hs+    H.assertEqual "Markdown text" markdownHtmlExpected +      (B.filter (/= '\n') $ toByteString $+        X.render (X.HtmlDocument X.UTF8 (Just dt) result))+  where+    dt = X.DocType "html" (X.Public "-//W3C//DTD XHTML 1.0 Strict//EN"+                                    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd")+                        X.NoInternalSubset+++------------------------------------------------------------------------------+applyTest :: H.Assertion+applyTest = do+    es <- loadEmpty [] [] [] []+    res <- evalHeistT applyImpl+        (X.Element "apply" [("template", "nonexistant")] []) es++    H.assertEqual "apply nothing" [] res+++------------------------------------------------------------------------------+ignoreTest :: H.Assertion+ignoreTest = do+    es <- loadEmpty [] [] [] []+    res <- evalHeistT ignoreImpl+        (X.Element "ignore" [("tag", "ignorable")] +          [X.TextNode "This should be ignored"]) es+    H.assertEqual "<ignore> tag" [] res+++lookupTemplateTest :: IO ()+lookupTemplateTest = do+    hs <- loadHS "templates"+    let k = do+            modifyHS (\st -> st { _curContext = ["foo"] })+            getsHS $ (\hs' -> lookupTemplate "/user/menu" hs' _templateMap)+    res <- runHeistT k (X.TextNode "") hs+    H.assertBool "lookup context test" $ isJust $ fst res+++------------------------------------------------------------------------------+identStartChar :: [Char]+identStartChar = ['a'..'z']+++------------------------------------------------------------------------------+identChar :: [Char]+identChar = '_' : identStartChar+++------------------------------------------------------------------------------+textGen :: Gen [Char]+textGen = listOf $ elements ((replicate 5 ' ') ++ identStartChar)+++------------------------------------------------------------------------------+limitedDepth :: Int -> Gen X.Node+limitedDepth 0 = liftM (X.TextNode . T.pack) textGen+limitedDepth n =+    oneof [ liftM (X.TextNode . T.pack) textGen+          , liftM3 X.Element arbitrary+                       (liftM (take 2) arbitrary)+                       (liftM (take 3) $ listOf $ limitedDepth (n - 1))+          ]+++------------------------------------------------------------------------------+-- | Returns the number of unique insertion points in the tree.+-- If h = insertAt f n g", the following property holds:+-- insSize h == (insSize f) + (insSize g) - 1+insSize :: [X.Node] -> Int+insSize ns = 1 + (sum $ map nodeSize ns)+  where nodeSize (X.Element _ _ c) = 1 + (insSize c)+        nodeSize _                 = 1+++------------------------------------------------------------------------------+insertAt :: [X.Node] -> Int -> [X.Node] -> [X.Node]+insertAt elems 0 ns = elems ++ ns+insertAt elems _ [] = elems+insertAt elems n list = maybe [] X.topNodes $+    evalState (processNode elems $ fromJust $ X.fromNodes list) n+++------------------------------------------------------------------------------+move :: Insert ()+move = modify (\x -> x - 1)+++------------------------------------------------------------------------------+processNode :: [X.Node] -> X.Cursor -> Insert (Maybe X.Cursor)+processNode elems loc =+    liftM2 mplus (move >> goDown loc) (move >> goRight loc)++  where+    goDown l =+        case X.current l of+          X.TextNode _    -> modify (+1) >> return Nothing+          X.Element _ _ _ -> doneCheck (X.insertManyFirstChild elems)+                                       X.firstChild+                                       l+          X.Comment _     -> return Nothing++    goRight = doneCheck (Just . X.insertManyRight elems) X.right++    doneCheck insertFunc next l = do+      s <- get+      if s == 0+        then return $ insertFunc l+        else maybe (return Nothing) (processNode elems) $ next l+++------------------------------------------------------------------------------+newtype Name = Name { unName :: Text } deriving (Show)++instance Arbitrary Name where+  arbitrary = do+    x     <- elements identStartChar+    n     <- choose (4,10)+    rest  <- vectorOf n $ elements identChar+    return $ Name $ T.pack (x:rest)++instance Arbitrary X.Node where+  arbitrary = limitedDepth 3+  shrink (X.Element _ [] []) = []+  shrink (X.Element n [] (_:cs)) = [X.Element n [] cs]+  shrink (X.Element n (_:as) []) = [X.Element n as []]+  shrink (X.Element n as cs) = [X.Element n as (tail cs), X.Element n (tail as) cs]+  shrink _ = []++instance Arbitrary T.Text where+  arbitrary = liftM unName arbitrary++--+-- Code for inserting nodes into any point of a tree+--+type Insert a = State Int a+++------------------------------------------------------------------------------+-- <bind> tests++-- Data type encapsulating the parameters for a bind operation+data Bind = Bind+    { _bindElemName :: Name+    , _bindChildren :: [X.Node]+    , _bindDoc :: [X.Node]+    , _bindPos :: Int+    , _bindRefPos :: Int+    } -- deriving (Show)+++instance Arbitrary Bind where+  arbitrary = do+    name <- arbitrary+    kids <- liftM (take 3) arbitrary+    doc <- liftM (take 5) arbitrary+    let s = insSize doc+    loc <- choose (0, s - 1)+    loc2 <- choose (0, s - loc - 1)+    return $ Bind name kids doc loc loc2+  shrink (Bind e [c] (_:ds) p r) = [Bind e [c] ds p r]+  shrink (Bind e (_:cs) d p r) = [Bind e cs d p r]+  shrink _ = []+++instance Show Bind where+  show b@(Bind e c d p r) = unlines+    [ "\n"+    , "Bind element name: " ++ (show e)+    , "Bind pos: " ++ (show p)+    , "Bind ref pos: " ++ (show r)+    , "Bind document:"+    , L.unpack $ L.concat $ map formatNode d+    , "Bind children:"+    , L.unpack $ L.concat $ map formatNode c+    , "Result:"+    , L.unpack $ L.concat $ map formatNode $ buildResult b+    , "Splice result:"+    , L.unpack $ L.concat $ map formatNode $ unsafePerformIO $ do+        hs <- loadEmpty [] [] [] []+        evalHeistT (runNodeList $ buildBindTemplate b)+                          (X.TextNode "") hs+    , "Template:"+    , L.unpack $ L.concat $ map formatNode $ buildBindTemplate b+    ]+    where+      formatNode n = toLazyByteString $ X.render+                                      $ X.HtmlDocument X.UTF8 Nothing [n]++------------------------------------------------------------------------------+buildNode :: Text -> Text -> Bind -> X.Node+buildNode tag attr (Bind s c _ _ _) = X.Element tag [(attr, unName s)] c+++------------------------------------------------------------------------------+buildBind :: Bind -> X.Node+buildBind = buildNode "bind" "tag"+++------------------------------------------------------------------------------+empty :: Text -> X.Node+empty n = X.Element n [] []+++------------------------------------------------------------------------------+buildBindTemplate :: Bind -> [X.Node]+buildBindTemplate s@(Bind n _ d b r) =+    insertAt [empty $ unName $ n] pos $ withBind+  where bind = [buildBind s]+        bindSize = insSize bind+        withBind = insertAt bind b d+        pos = b + bindSize - 1 + r+++------------------------------------------------------------------------------+buildResult :: Bind -> [X.Node]+buildResult (Bind _ c d b r) = insertAt c (b + r) d+++------------------------------------------------------------------------------+-- <apply> tests++data Apply = Apply+    { _applyName :: Name+    , _applyCaller :: [X.Node]+    , _applyCallee :: Template+    , _applyChildren :: [X.Node]+    , _applyPos :: Int+    } deriving (Show)+++instance Arbitrary Apply where+    arbitrary = do+      name <- arbitrary+      kids <- liftM (take 3) $ listOf $ limitedDepth 2+      caller <- liftM (take 5) arbitrary+      callee <- liftM (take 1) $ listOf $ limitedDepth 3+      let s = insSize caller+      loc <- choose (0, s - 1)+      return $ Apply name caller callee kids loc+++------------------------------------------------------------------------------+buildApplyCaller :: Apply -> [X.Node]+buildApplyCaller (Apply name caller _ kids pos) =+    insertAt [X.Element "apply" [("template", unName name)] kids] pos caller+++------------------------------------------------------------------------------+calcCorrect :: Apply -> [X.Node]+calcCorrect (Apply _ caller callee _ pos) = insertAt callee pos caller+++------------------------------------------------------------------------------+calcResult :: Apply -> IO [X.Node]+calcResult apply@(Apply name _ callee _ _) = do+    hs <- loadEmpty defaultLoadTimeSplices [] [] []+    let hs' = setTemplates (Map.singleton [T.encodeUtf8 $ unName name]+                           (DocumentFile (X.HtmlDocument X.UTF8 Nothing callee)+                                         Nothing)) hs+    evalHeistT (runNodeList $ buildApplyCaller apply)+        (X.TextNode "") hs'+++++{-+ - Convenience code for manual ghci experimentation+ -}++--html :: [Node] -> Node+--html c = X.Element "html" [] [hhead, body c]+--hhead :: Node+--hhead = X.Element "head" [] [title, X.Element "script" [] []]+--title :: Node+--title = X.Element "title" [] [X.Text "Test Page"]+--body :: [Node] -> Node+--body = X.Element "body" []+--+--para :: Int -> Node+--para n = X.Element "p" [] [X.Text $ B.pack $ "This is paragraph " ++ show n]+--para2 :: B.ByteString -> Node+--para2 c = X.Element "p" [] [X.Text c]+--para3 :: Node+--para3 = X.Element "p" [] [X.Text "AHA!"]+--+--foo :: Int -> [Node]+--foo n = insertAt [X.Element "NEW" [] []] n [html [para 1, para 2]]+--+--tdoc :: [Node]+--tdoc = [para 1, para 2, para 3, para 4]+--+--bindElem :: [Node] -> Int -> Int -> Bind+--bindElem = Bind (Name "mytag") [para2 "bound paragraph"]+--+--addBind :: Bind -> [Node] -> [Node]+--addBind b = insertAt [buildBind b] 0 . insertAt [empty $ unName $ _bindElemName b] 2+--+--prn :: Node -> IO ()+--prn = L.putStrLn . formatNode+--runTests :: IO ()+--runTests = defaultMain tests+
+ test/suite/Heist/TestCommon.hs view
@@ -0,0 +1,108 @@+module Heist.TestCommon where++------------------------------------------------------------------------------+import           Blaze.ByteString.Builder+import           Control.Error+import           Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as B+import qualified Data.HashMap.Strict as Map+import           Data.Maybe+import           Data.Text (Text)+++------------------------------------------------------------------------------+import           Heist+import qualified Heist.Compiled as C+import qualified Heist.Interpreted as I+import qualified Heist.Interpreted.Internal as I+import qualified Text.XmlHtml        as X+++------------------------------------------------------------------------------+-- | The default doctype given to templates+doctype :: ByteString+doctype = B.concat+    [ "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" "+    , "'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>" ]+++loadT :: Monad m+      => FilePath+      -> [(Text, I.Splice m)]+      -> [(Text, I.Splice IO)]+      -> [(Text, C.Splice m)]+      -> [(Text, AttrSplice m)]+      -> IO (Either [String] (HeistState m))+loadT baseDir a b c d = runEitherT $ do+    ts <- loadTemplates baseDir+    let hc = HeistConfig a (defaultLoadTimeSplices ++ b) c d ts+    initHeist hc+++------------------------------------------------------------------------------+loadIO :: FilePath+       -> [(Text, I.Splice IO)]+       -> [(Text, I.Splice IO)]+       -> [(Text, C.Splice IO)]+       -> [(Text, AttrSplice IO)]+       -> IO (Either [String] (HeistState IO))+loadIO baseDir a b c d = runEitherT $ do+    ts <- loadTemplates baseDir+    let hc = HeistConfig a (defaultLoadTimeSplices ++ b) c d ts+    initHeist hc+++------------------------------------------------------------------------------+loadHS :: FilePath -> IO (HeistState IO)+loadHS baseDir = do+    etm <- runEitherT $ do+        templates <- loadTemplates baseDir+        let hc = HeistConfig [] defaultLoadTimeSplices [] [] templates+        initHeist hc+    either (error . concat) return etm+++loadEmpty :: [(Text, I.Splice IO)]+          -> [(Text, I.Splice IO)]+          -> [(Text, C.Splice IO)]+          -> [(Text, AttrSplice IO)]+          -> IO (HeistState IO)+loadEmpty a b c d = do+    let hc = HeistConfig a (defaultLoadTimeSplices ++ b) c d Map.empty+    res <- runEitherT $ initHeist hc+    either (error . concat) return res+++testTemplate :: FilePath -> ByteString -> IO ByteString+testTemplate tdir tname = do+    ts <- loadHS tdir+    Just (resDoc, _) <- I.renderTemplate ts tname+    return $ toByteString resDoc+++testTemplateEval :: ByteString -> IO (Maybe Template)+testTemplateEval tname = do+    ts <- loadHS "templates"+    md <- evalHeistT (I.evalWithDoctypes tname) (X.TextNode "") ts+    return $ fmap X.docContent md+++------------------------------------------------------------------------------+-- | Reloads the templates from disk and renders the specified+-- template.  (Old convenience code.)+quickRender :: FilePath -> ByteString -> IO (Maybe ByteString)+quickRender baseDir name = do+    ts  <- loadHS baseDir+    res <- I.renderTemplate ts name+    return (fmap (toByteString . fst) res)++cRender :: HeistState IO -> ByteString -> IO ByteString+cRender hs name = do+    builder <- fst $ fromJust $ C.renderTemplate hs name+    return $ toByteString builder++iRender :: HeistState IO -> ByteString -> IO ByteString+iRender hs name = do+    builder <- I.renderTemplate hs name+    return $ toByteString $ fst $ fromJust builder+
+ test/suite/Heist/Tests.hs view
@@ -0,0 +1,200 @@+{-# LANGUAGE FlexibleContexts #-}+module Heist.Tests+  ( tests+  ) where+++------------------------------------------------------------------------------+import           Blaze.ByteString.Builder+import           Control.Monad.State+import qualified Data.ByteString.Char8 as B+import           Data.List+import           Data.Maybe+import           Data.Monoid+import qualified Data.Text as T+import           Test.Framework (Test)+import           Test.Framework.Providers.HUnit+import qualified Test.HUnit as H+++------------------------------------------------------------------------------+import           Heist+import qualified Heist.Compiled as C+import           Heist.Tutorial.AttributeSplices+import           Heist.Tutorial.CompiledSplices+import qualified Heist.Interpreted as I+import           Heist.Splices.Cache+import           Heist.Splices.Html+import           Heist.TemplateDirectory++import           Heist.TestCommon++tests :: [Test]+tests = [ testCase     "loadErrors"            loadErrorsTest+        , testCase     "attrsplice/autocheck"  attrSpliceTest+        , testCase     "tdirCache"             tdirCacheTest+        , testCase     "headMerge"             headMergeTest+        , testCase     "bindApplyInteraction"  bindApplyInteractionTest +        , testCase     "backslashHandling"     backslashHandlingTest +        ]+++------------------------------------------------------------------------------+-- | Tests that load fails correctly on errors.+loadErrorsTest :: H.Assertion+loadErrorsTest = do+    ets <- loadIO "templates-bad" [] [] [] []+    either (H.assertEqual "load errors test" expected . sort)+           (const $ H.assertFailure "No failure when loading templates-bad")+           ets+  where+    expected = sort+        ["templates-bad/bind-infinite-loop.tpl: template recursion exceeded max depth, you probably have infinite splice recursion!"+        ,"templates-bad/apply-template-not-found.tpl: apply tag cannot find template \"/page\""+        ,"templates-bad/bind-missing-attr.tpl: must supply \"tag\" attribute in <bind>"+        ,"templates-bad/apply-missing-attr.tpl: must supply \"template\" attribute in <apply>"+        ]+++attrSpliceTest :: IO ()+attrSpliceTest = do+    ehs <- loadT "templates" [] [] [] [("autocheck", autocheckedSplice)]+    let hs = either (error . show) id ehs+        runtime = fromJust $ C.renderTemplate hs "attr_splice"++    mres <- evalStateT (I.renderTemplate hs "attr_splice") "foo"+    H.assertEqual "interpreted foo" expected1+      (toByteString $ fst $ fromJust mres)+    mres2 <- evalStateT (I.renderTemplate hs "attr_splice") "bar"+    H.assertEqual "interpreted bar" expected2+      (toByteString $ fst $ fromJust mres2)++    builder <- evalStateT (fst runtime) "foo"+    H.assertEqual "compiled foo" expected3+      (toByteString builder)+    builder2 <- evalStateT (fst runtime) "bar"+    H.assertEqual "compiled bar" expected4+      (toByteString builder2)+  where+    expected1 = doctype `B.append` "\n<input type='checkbox' value='foo' checked />\n<input type='checkbox' value='bar' />\n"+    expected2 = doctype `B.append` "\n<input type='checkbox' value='foo' />\n<input type='checkbox' value='bar' checked />\n"+    expected3 = "<input type=\"checkbox\" value=\"foo\" checked />&#10;<input type=\"checkbox\" value=\"bar\" />&#10;"+    expected4 = "<input type=\"checkbox\" value=\"foo\" />&#10;<input type=\"checkbox\" value=\"bar\" checked />&#10;"++fooSplice :: I.Splice (StateT Int IO)+fooSplice = do+    val <- get+    put val+    I.textSplice $ T.pack $ show val++tdirCacheTest :: IO ()+tdirCacheTest = do+    let rSplices = [ ("foosplice", fooSplice) ]+        dSplices = [ ("foosplice", stateSplice) ]+        hc = HeistConfig rSplices [] dSplices [] mempty+    td <- newTemplateDirectory' "templates" hc+            +    [a,b,c,d] <- evalStateT (testInterpreted td) 5+    H.assertBool "interpreted doesn't cache" $ a == b+    H.assertBool "interpreted doesn't clear" $ b /= c+    H.assertBool "interpreted doesn't reload" $ c /= d++    td' <- newTemplateDirectory' "templates" hc+    [e,f,g,h] <- evalStateT (testCompiled td') 5+    H.assertBool "compiled doesn't cache" $ e == f+    H.assertBool "compiled doesn't clear" $ f /= g+    H.assertBool "compiled doesn't reload" $ g /= h+  where+    testInterpreted td = do+        hs <- liftIO $ getDirectoryHS td+        cts <- liftIO $ getDirectoryCTS td+        a <- I.renderTemplate hs "cache"+        modify (+1)+        b <- I.renderTemplate hs "cache"+        liftIO $ clearCacheTagState cts+        c <- I.renderTemplate hs "cache"+        modify (+1)+        _ <- liftIO $ reloadTemplateDirectory td++        -- The reload changes the HeistState, so we have to get it again+        hs' <- liftIO $ getDirectoryHS td+        d <- I.renderTemplate hs' "cache"+        return $ map (toByteString . fst . fromJust) [a,b,c,d]++    testCompiled td = do+        hs <- liftIO $ getDirectoryHS td+        cts <- liftIO $ getDirectoryCTS td+        a <- fst $ fromJust $ C.renderTemplate hs "cache"+        modify (+1)+        b <- fst $ fromJust $ C.renderTemplate hs "cache"+        liftIO $ clearCacheTagState cts+        c <- fst $ fromJust $ C.renderTemplate hs "cache"+        modify (+1)+        _ <- liftIO $ reloadTemplateDirectory td++        -- The reload changes the HeistState, so we have to get it again+        hs' <- liftIO $ getDirectoryHS td+        d <- fst $ fromJust $ C.renderTemplate hs' "cache"+        return $ map toByteString [a,b,c,d]+++headMergeTest :: IO ()+headMergeTest = do+    ehs <- loadT "templates" [] [(htmlTag, htmlImpl)] [] []+    let hs = either (error . show) id ehs+        runtime = fromJust $ C.renderTemplate hs "head_merge/index"+    mres <- fst runtime+    H.assertEqual "assertion failed" expected+      (toByteString mres)+  where+    expected = B.intercalate "\n"+      ["<html><head>\n<link href='wrapper-link' />\n"+      ,"<link href='nav-link' />\n\n<link href='index-link' />"+      ,"</head>\n\n<body>\n\n<div>nav bar</div>\n\n\n"+      ,"<div>index page</div>\n\n</body>\n</html>&#10;&#10;"+      ]++bindApplyInteractionTest :: IO ()+bindApplyInteractionTest = do+    hs <- loadHS "templates"++    cOut <- cRender hs "bind-apply-interaction/caller"+    H.assertEqual "compiled failure" cExpected cOut++    iOut <- iRender hs "bind-apply-interaction/caller"+    H.assertEqual "interpreted failure" iExpected iOut+  where+    cExpected = B.intercalate "\n"+      ["&#10;This is a test."+      ,"===bind content===&#10;Another test line."+      ,"apply content&#10;Last test line."+      ,"&#10;"+      ]+    iExpected = B.unlines+      [doctype+      ,"&#10;This is a test."+      ,"===bind content==="+      ,"Another test line."+      ,"apply content"+      ,"Last test line."+      ,""+      ]+++------------------------------------------------------------------------------+-- | Test backslash escaping in the attribute parser.+backslashHandlingTest :: IO ()+backslashHandlingTest = do+    hs <- loadHS "templates"+    cOut <- cRender hs "backslash"+    H.assertEqual "compiled failure" cExpected cOut++    iOut <- iRender hs "backslash"+    H.assertEqual "interpreted failure" iExpected iOut+  where+    cExpected = "<foo regex='d+\\d'></foo>&#10;"+    iExpected = B.unlines+      [doctype+      ,"<foo regex='d+\\d'></foo>"+      ]+
+ test/suite/Heist/Tutorial/AttributeSplices.lhs view
@@ -0,0 +1,58 @@+Attribute Splices+=================++Attribute splices are new in Heist 0.10.  They solve the problem of wanting to+be able to dynamically make empty attributes appear or disappear with a splice+without binding a splice to the whole tag.  This issue comes up most+frequently when dealing with empty attributes such as HTML's "disabled" or+"checked".++> module Heist.Tutorial.AttributeSplices where+> import           Heist.Tutorial.Imports++Consider a page with several radio buttons.  You want the correct one to be+selected based on the value of a parameter in the HTTP request.  The HTML+would look something like this:++    <input type="radio" name="color" value="red" checked>Red</input>+    <input type="radio" name="color" value="green">Green</input>+    <input type="radio" name="color" value="blue">Blue</input>++We want to automatically generate the "checked" attribute appropriately.  This+could be done with a splice bound to the input tag, but there might be a+number of other input tags on the page, so your splice would at best be+executed on more tags than necessary and at worst not have the granularity+necessary to work properly.  The ${} syntax for splices inside of attribute+values also won't work because it can only affect an attribute's value.  It+can't make the attribute disappear entirely.  This problem can be solved+nicely with attribute splices that have the following type:++< type AttrSplice m = Text -> m [(Text, Text)]++An attribute splice is a computation in the runtime monad that takes the value+of the attribute it is bound to as its argument and returns a list of+attributes to substitute back into the tag.  Here's how we might implement a+splice to solve the above problem.++> autocheckedSplice :: Text -> StateT Text IO [(Text, Text)]+> autocheckedSplice v = do+>     val <- get -- app-specific retrieval of the appropriate value here+>     let checked = if v == val+>                     then [("checked","")]+>                     else []+>     return $ ("value", v) : checked++In this toy example we are using `StateT Text IO` as our "runtime" monad where+the Text state holds the value of the radio button that should be checked.  We+assume that the current value we're checking against is passed as the bound+attribute's value, so we compare that against the value to be checked.  Then+we return a list with the appropriate value and the checked attribute if+necessary.  We bind this splice to the "autocheck" attribute by adding it to+the hcAttributeSplices list in HeistConfig.++To make everything work we use the following markup for our radio buttons:++    <input type="radio" name="color" autocheck="red">Red</input>+    <input type="radio" name="color" autocheck="green">Green</input>+    <input type="radio" name="color" autocheck="blue">Blue</input>+
+ test/suite/Heist/Tutorial/CompiledSplices.lhs view
@@ -0,0 +1,269 @@+Introduction to Compiled Heist+==============================++Before version 0.10, Heist has essentially been an interpreter.  It loads your+templates and "runs" them whenever a page is served.  This is relatively+inefficient since a lot of document transformations happen every time the+template is requested.  For Heist version 0.10 we completely rethought+everything with performance in mind.  We call it "compiled Heist".  The main+idea is to do most of your splice processing up front at load time.  There is+still a mechanism for rendering dynamic information at runtime, but it is+faster than the fully interpreted approach that Heist started with.++It should also be mentioned that the old "interpreted Heist" is not gone.  You+can still use the old approach where all the transformations happen at+render time.  This allows you to upgrade without making sweeping changes to+your code, and gradually convert your application to the more performant+compiled approach as you see fit.++Before we continue it should be mentioned that you are reading real live+literate Haskell code from our test suite.  All the code you see here is+compiled into our test suite and the results automatically checked by our+buildbot.  So first we need to get some boilerplate and imports out of the way.++> {-# LANGUAGE NoMonomorphismRestriction #-}+> module Heist.Tutorial.CompiledSplices where+> import           Heist+> import qualified Heist.Compiled as C+> import           Heist.Tutorial.Imports++As a review, normal (interpreted) Heist splices are defined like this.++< type Splice m = HeistT m m [Node]++The type parameter `m` is the runtime execution monad (in a Snap application+this will usually be `Handler` or `Snap`).  Don't worry about why the `m` is+there twice right now.  We'll get to that later.  The splice's return value is+a list of nodes that is substituted back into the document wherever the+spliced node was.  ++This kind of splice proccessing involves traversing the DOM, which is+inefficient.  Compiled Heist is designed so that all the DOM traversals happen+once at load time in the IO monad.  This is the "compile" phase.  The type+signature for compiled splices is this.++< type Splice n = HeistT n IO (DList (Chunk n))++We see that where Heist splices ran in the m monad, compiled splices run in the+IO monad.  This also explains why HeistT now has two monad type parameters.+The first parameter is a placeholder for the runtime monad and the second+parameter is the monad that we're actually running in now.++But the key point of the compiled splice type signature is the return value.+They return a DList of Chunks.  DList is a list that supports efficient+insertion to both the front and back of the list.  The Chunk type is not+exposed publicly, but there are three ways to construct a Chunk.++< yieldPure :: Builder -> DList (Chunk m)+< yieldRuntime :: RuntimeSplice m Builder -> DList (Chunk m)+< yieldRuntimeEffect :: Monad m => RuntimeSplice m () -> DList (Chunk m)++If your splice output can be calculated at load time, then you should use+`yieldPure` or one of its variants.  When you do this, Heist can concatenate+all adjacent pure chunks into a single precalculated ByteString that can be+rendered very efficiently.  If your template needs a value that has to be+calculated at runtime, then you should use the `yieldRuntime` constructor and+supply a computation in the RuntimeSplice monad transformer that is+parameterized by `m` which we saw above is the runtime monad.  Occasionally+you might want to run a runtime side effect that doesn't actually insert any+data into your template.  The `yieldRuntimeEffect` function gives you that+capability.++An Example+==========++With that background, let's get to a real example.++> stateSplice :: C.Splice (StateT Int IO)+> stateSplice = return $ C.yieldRuntimeText $ do+>     val <- lift get+>     return $ pack $ show (val+1)++Here we see that our splice's runtime monad is `StateT Int IO`.  This makes+for a simple example that can clearly demonstrate the different contexts that+we are operating in.  To make things more clear, here's a version with some+print statements that clarify the details of which monad is executed when.++> stateSplice2 :: C.Splice (StateT Int IO)+> stateSplice2 = do+>     -- :: C.Splice (StateT Int IO)+>     lift $ putStrLn "This executed at load time"+>     let res = C.yieldRuntimeText $ do+>             -- :: RuntimeSplice (StateT Int IO) a+>             lift $ lift $ putStrLn "This executed at run/render time"+>             val <- lift get+>             return $ pack $ show (val+1)+>     lift $ putStrLn "This also executed at load time"+>     return res++Note here that even though the type parameter to C.Splice is a monad, it is not+a monad transformer.  RuntimeSplice, however, is.  Now let's look at a simple+load function that sets up a default HeistState and loads templates from a+directory with compiled splices.++> load :: MonadIO n+>      => FilePath+>      -> [(Text, C.Splice n)]+>      -> IO (HeistState n)+> load baseDir splices = do+>     tmap <- runEitherT $ do+>         templates <- loadTemplates baseDir+>         let hc = HeistConfig [] defaultLoadTimeSplices splices [] templates+>         initHeist hc+>     either (error . concat) return tmap++Here's a function demonstrating all of this in action.++> runWithStateSplice :: FilePath+>                    -> IO ByteString+> runWithStateSplice baseDir = do+>     hs <- load baseDir [ ("div", stateSplice) ]+>     let runtime = fromJust $ C.renderTemplate hs "index"+>     builder <- evalStateT (fst runtime) 2+>     return $ toByteString builder++First this function loads the templates with the above compiled splice.  You+have to specify all the compiled splices in the call to loadTemplates because+loadTemplates takes care of compiling all the templates up front.  If you were+able to bind compiled splices later, then all the templates would have to be+recompiled, a potentially expensive operation.  Next, the function renders the+template called "index" using a runtime (StateT Int IO) seeded with a value of+2 and returns the resulting ByteString.++Now let's look at a more complicated example.  We want to render a data+structure with a compiled splice.++> data Person = Person+>     { pFirstName :: Text+>     , pLastName  :: Text+>     , pAge       :: Int+>     }+> +> personSplice :: (Monad n)+>              => C.Promise Person+>              -> HeistT n IO (RuntimeSplice n Builder)+> personSplice = C.promiseChildrenWithText+>     [ ("firstName", pFirstName)+>     , ("lastName", pLastName)+>     , ("age", pack . show . pAge)+>     ]+> +> peopleSplice :: (Monad n)+>              => n [Person]+>              -> C.Splice n+> peopleSplice getPeople = C.mapPromises personSplice getPeople+> +> allPeopleSplice :: C.Splice (StateT [Person] IO)+> allPeopleSplice = peopleSplice get+> +> personListTest :: FilePath+>                -> IO ByteString+> personListTest baseDir = do+>     hs <- load baseDir [ ("people", allPeopleSplice) ]+>     let runtime = fromJust $ C.renderTemplate hs "people"+>     builder <- evalStateT (fst runtime)+>                  [ Person "John" "Doe" 42+>                  , Person "Jane" "Smith" 21+>                  ]+>     return $ toByteString builder+++Disadvantages of Compiled Heist+===============================++Compiled Heist is faster than the original interpreted approach, but as with+most things in computing there is a tradeoff.  Compiled Heist is strictly less+powerful than interpreted Heist.  There are two things that compiled Heist+loses: the ability to bind new splices on the fly at runtime and splice+recursion/composability.++The first point follows immediately from the definition of compiled Heist.+When you decide to do all your splice DOM traversals once at load time you're+unavoidably limited to only those splices that you defined at load time.  But+this seems to be a good pattern to use in general because debugging your+splices will be easier if you don't have to consider the possibility that+the handler that binds them didn't run.++The loss of recursion/composability happens because of the change in the type+signature of splices.  Interpreted splices are a essentially function `[Node]+-> m [Node]`.  This means that the output of one splice can be the input of+another splice (including itself).  Compiled splices are a function `[Node] ->+IO (DList (Chunk m))`.  Therefore, once a splice processes some nodes, the+output is no longer something that can be passed into other splices.  ++This composability turns out to be a very powerful feature.  Head merging is+one feature that can't be done without it.  Head merging allows you to put+<head> tags anyhere in any template and have them all merged into a single+<head> tag at the top of your HTML document.  This is useful because it allows+you to keep concerns localized.  For instance, you can have a template+represent a small piece of functionality that uses a less common javascript or+CSS file.  Instead of having to depend on that resource being included in the+top-level <head> tag, you can include it in a <head> tag right where you're+using it.  Then it will only be included on your pages when you are using the+markup that needs it.++Our implementation of head merging uses a splice bound to the <html> tag.+This splice removes all the <head> nodes from its children, combines them, and+inserts them as its first child.  This won't work unless the <html> splice+first runs all its children to make sure all <apply> and <bind> tags have+happened first.  And that is impossible to do with compiled splices.++To get around this problem we added the concept of load time splices.  Load+time splices are just interpreted splices that are completely executed at load+time.  If interpreted splices have type `[Node] -> m [Node]` where m is the+runtime monad, then load time splices have type `[Node] -> IO [Node]`, where+IO is the monad being executed at load time.  Load time splices give you the+power and composability of interpreted splices as long as they are performing+transformations that don't require runtime data.  All of the built-in splices+that we ship with Heist work as load time splices.  So you can still have head+merging by including our html splice in the load time splice list in your+HeistConfig.+++A More Involved Example+=======================++The person example above is a very common and useful pattern for using dynamic+data in splices.  But it has the simplification that it always generates+output the same way.  Sometimes you might want a splice's output to have one+form in some cases and a different form in other cases.  A simple example is a+splice that reads some kind of a key from a request parameter then looks that+key up in some kind of map.  If the key is present the splice uses its child+nodes as a view for the retrieved value, otherwise it outputs an error message.  ++This pattern is a little tricky because you're making decisions about what to+render based on runtime data, but the actual rendering of child nodes has to+be done at load time.  To bridge the gap and allow communication between load+time and runtime processing we provide the Promise data type.  A Promise is+kind of like an IORef except that operations on them are restricted to the+appropriate Heist context.  You create a new empty promise in the HeistT n IO+(load time) monad, and you operate on it in the RuntimeSplice monad.++Here's an example of how to use a promise manually to render a splice+differently in the case of failure.++< failingSplice :: MonadSnap m => C.Splice m+< failingSplice = do+<     children <- childNodes <$> getParamNode+<     promise <- C.newEmptyPromise+<     outputChildren <- C.promiseChildrenWithNodes splices promise+<     return $ C.yieldRuntime $ do         +<         -- :: RuntimeSplice m Builder+<         mname <- lift $ getParam "username"+<         let err = return $ fromByteString "Must supply a username"+<             single name = do          +<                 euser <- liftIO $ lookupUser $ decodeUtf8 name+<                 either (return . fromByteString . encodeUtf8 . T.pack)+<                        doUser euser+<               where+<                 doUser value = do+<                   C.putPromise promise (name, value)+<                   outputChildren+<         maybe err single mname+<   +<   +< splices :: [(Text, (Text, Text) -> [Node])]+< splices = [ ("user", (:[]) . TextNode . T.pack . fst)+<           , ("value", (:[]) . TextNode . T.pack . snd)+<           ]                                                                                                                                             +
+ test/suite/Heist/Tutorial/Imports.hs view
@@ -0,0 +1,25 @@+module Heist.Tutorial.Imports+  ( module Blaze.ByteString.Builder+  , module Control.Monad+  , module Control.Monad.Trans+  , module Data.Maybe+  , module Data.Monoid+  , ST.get+  , ST.StateT(..)+  , ST.evalStateT+  , T.Text+  , T.pack+  , ByteString+  , runEitherT+  ) where++import           Blaze.ByteString.Builder+import           Control.Error (runEitherT)+import           Control.Monad+import           Control.Monad.Trans+import qualified Control.Monad.Trans.State as ST+import           Data.ByteString.Char8 (ByteString)+import           Data.Maybe+import           Data.Monoid+import qualified Data.Text as T+
test/suite/TestSuite.hs view
@@ -2,10 +2,16 @@  import Test.Framework (defaultMain, testGroup) -import qualified Text.Templating.Heist.Tests+import qualified Heist.Interpreted.Tests+import qualified Heist.Compiled.Tests+import qualified Heist.Tests+ main :: IO () main = defaultMain tests-  where tests = [-                  testGroup "Text.Templating.Heist.Tests"-                            Text.Templating.Heist.Tests.tests+  where tests = [ testGroup "Heist.Interpreted.Tests"+                            Heist.Interpreted.Tests.tests+                , testGroup "Heist.Compiled.Tests"+                            Heist.Compiled.Tests.tests+                , testGroup "Heist.Tests"+                            Heist.Tests.tests                 ]
− test/suite/Text/Templating/Heist/Tests.hs
@@ -1,692 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE NoMonomorphismRestriction  #-}-{-# LANGUAGE OverloadedStrings          #-}-{-# LANGUAGE TypeSynonymInstances       #-}-{-# OPTIONS_GHC -fno-warn-orphans       #-}--module Text.Templating.Heist.Tests-  ( tests-  , quickRender-  ) where---------------------------------------------------------------------------------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-import qualified Data.HashMap.Strict as Map-import           Data.Maybe-import           Data.Monoid-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import           Data.Text (Text)-import           System.IO.Unsafe-import           Test.Framework (Test)-import           Test.Framework.Providers.HUnit-import           Test.Framework.Providers.QuickCheck2-import qualified Test.HUnit as H-import           Test.QuickCheck-import           Test.QuickCheck.Monadic----------------------------------------------------------------------------------import           Text.Templating.Heist-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----------------------------------------------------------------------------------tests :: [Test]-tests = [ testProperty "heist/simpleBind"            simpleBindTest-        , testProperty "heist/simpleApply"           simpleApplyTest-        , testCase     "heist/stateMonoid"           monoidTest-        , testCase     "heist/templateAdd"           addTest-        , testCase     "heist/hasTemplate"           hasTemplateTest-        , testCase     "heist/getDoc"                getDocTest-        , testCase     "heist/load"                  loadTest-        , testCase     "heist/fsLoad"                fsLoadTest-        , testCase     "heist/renderNoName"          renderNoNameTest-        , testCase     "heist/doctype"               doctypeTest-        , testCase     "heist/attributeSubstitution" attrSubstTest-        , testCase     "heist/bindAttribute"         bindAttrTest-        , testCase     "heist/markdown"              markdownTest-        , testCase     "heist/title_expansion"       titleExpansion-        , testCase     "heist/textarea_expansion"    textareaExpansion-        , testCase     "heist/div_expansion"         divExpansion-        , testCase     "heist/bind_param"            bindParam-        , testCase     "heist/markdownText"          markdownTextTest-        , testCase     "heist/apply"                 applyTest-        , testCase     "heist/ignore"                ignoreTest-        , testCase     "heist/lookupTemplateContext" lookupTemplateTest-        , testCase     "heist/attrSpliceContext"     attrSpliceContext-        , testCase     "heist/json/values"           jsonValueTest-        , testCase     "heist/json/object"           jsonObjectTest-        ]----------------------------------------------------------------------------------simpleBindTest :: Property-simpleBindTest = monadicIO $ forAllM arbitrary prop-  where-    prop :: Bind -> PropertyM IO ()-    prop bind = do-        let template = buildBindTemplate bind-        let result   = buildResult bind--        spliceResult <- run $ evalHeistT (runNodeList template)-                                         (X.TextNode "")-                                         defaultHeistState-        assert $ result == spliceResult----------------------------------------------------------------------------------simpleApplyTest :: Property-simpleApplyTest = monadicIO $ forAllM arbitrary prop-  where-    prop :: Apply -> PropertyM IO ()-    prop apply = do-        let correct = calcCorrect apply-        result <- run $ calcResult apply-        assert $ correct == result----------------------------------------------------------------------------------monoidTest :: IO ()-monoidTest = do-    H.assertBool "left monoid identity" $ mempty `mappend` es == es-    H.assertBool "right monoid identity" $ es `mappend` mempty == es-  where es = defaultHeistState :: HeistState IO----------------------------------------------------------------------------------addTest :: IO ()-addTest = do-    H.assertEqual "lookup test" (Just []) $-        fmap (X.docContent . dfDoc . fst) $ lookupTemplate "aoeu" ts--    H.assertEqual "splice touched" 0 $ Map.size (_spliceMap ts)--  where-    ts = addTemplate "aoeu" [] Nothing (mempty::HeistState IO)----------------------------------------------------------------------------------hasTemplateTest :: H.Assertion-hasTemplateTest = do-    ets <- loadT "templates"-    let tm = either (error "Error loading templates") _templateMap ets-    let ts = setTemplates tm defaultHeistState :: HeistState IO-    H.assertBool "hasTemplate ts" (hasTemplate "index" ts)----------------------------------------------------------------------------------getDocTest :: H.Assertion-getDocTest = do-    d <- getDoc "bkteoar"-    H.assertBool "non-existent doc" $ isLeft d-    f <- getDoc "templates/index.tpl"-    H.assertBool "index doc" $ not $ isLeft f----------------------------------------------------------------------------------loadTest :: H.Assertion-loadTest = do-    ets <- loadT "templates"-    either (error "Error loading templates")-           (\ts -> do let tm = _templateMap ts-                      H.assertBool "loadTest size" $ Map.size tm == 26-           ) ets----------------------------------------------------------------------------------fsLoadTest :: H.Assertion-fsLoadTest = do-    ets <- loadT "templates"-    let tm = either (error "Error loading templates") _templateMap ets-    let ts = setTemplates tm defaultHeistState :: HeistState IO-    let f  = g ts--    f isNothing "abc/def/xyz"-    f isJust "a"-    f isJust "bar/a"-    f isJust "/bar/a"--  where-    g ts p n = H.assertBool ("loading template " ++ n) $ p $-               lookupTemplate (B.pack n) ts---------------------------------------------------------------------------------renderNoNameTest :: H.Assertion-renderNoNameTest = do-    ets <- loadT "templates"-    either (error "Error loading templates")-           (\ts -> do t <- renderTemplate ts ""-                      H.assertBool "renderNoName" $ isNothing t-           ) ets----------------------------------------------------------------------------------doctypeTest :: H.Assertion-doctypeTest = do-    ets <- loadT "templates"-    let ts = either (error "Error loading templates") id ets-    Just (indexDoc, indexMIME) <- renderTemplate ts "index"-    H.assertBool "doctype test index" $ isJust $ X.docType $-        fromRight $ (X.parseHTML "index") $ toByteString $ indexDoc-    Just (iocDoc, iocMIME) <- renderTemplate ts "ioc"-    H.assertBool "doctype test ioc" $ isJust $ X.docType $-        fromRight $ (X.parseHTML "index") $ toByteString $ iocDoc-  where fromRight (Right x) = x-        fromRight (Left  s) = error s---------------------------------------------------------------------------------attrSubstTest :: H.Assertion-attrSubstTest = do-    ets <- loadT "templates"-    let ts = either (error "Error loading templates") id ets-    check (setTs "meaning_of_everything" ts) "pre_meaning_of_everything_post"-    check ts "pre__post"--  where-    setTs val = bindSplice "foo" (return [X.TextNode val])-    check ts str = do-        Just (resDoc, resMIME) <- renderTemplate ts "attrs"-        H.assertBool ("attr subst " ++ (show str)) $ not $ B.null $-            snd $ B.breakSubstring str $ toByteString $ resDoc-        H.assertBool ("attr subst foo") $ not $ B.null $-            snd $ B.breakSubstring "${foo}" $ toByteString $ resDoc----------------------------------------------------------------------------------bindAttrTest :: H.Assertion-bindAttrTest = do-    ets <- loadT "templates"-    let ts = either (error "Error loading templates") id ets-    check ts "<div id=\'zzzzz\'"--  where-    check ts str = do-        Just (resDoc, resMIME) <- renderTemplate ts "bind-attrs"-        H.assertBool ("attr subst " ++ (show str)) $ not $ B.null $-            snd $ B.breakSubstring str $ toByteString $ resDoc-        H.assertBool ("attr subst bar") $ B.null $-            snd $ B.breakSubstring "${bar}" $ toByteString $ resDoc----------------------------------------------------------------------------------htmlExpected :: ByteString-htmlExpected = "<div class=\'markdown\'><p>This <em>is</em> a test.</p></div>"------------------------------------------------------------------------------------ | Markdown test on a file-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-            -> H.Assertion-renderTest templateName expectedResult = do-    ets <- loadT "templates"-    let ts = either (error "Error loading templates") id ets--    check ts expectedResult--  where-    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------------------------------------------------------------------------------------ | Expansion of a bound name inside a title-tag-titleExpansion :: H.Assertion-titleExpansion = renderTest "title_expansion" "<title>foo</title>"------------------------------------------------------------------------------------ | Expansion of a bound name inside a textarea-tag-textareaExpansion :: H.Assertion-textareaExpansion = renderTest "textarea_expansion" "<textarea>foo</textarea>"------------------------------------------------------------------------------------ | Expansion of a bound name inside a div-tag-divExpansion :: H.Assertion-divExpansion = renderTest "div_expansion" "<div>foo</div>"------------------------------------------------------------------------------------ | 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 bound tag.-attrSpliceContext :: H.Assertion-attrSpliceContext = renderTest "attrsubtest2" "<a href='asdf'>link</a><a href='before$after'>foo</a>"------------------------------------------------------------------------------------ | Markdown test on supplied text-markdownTextTest :: H.Assertion-markdownTextTest = do-    result <- evalHeistT markdownSplice-                         (X.TextNode "This *is* a test.")-                         defaultHeistState-    H.assertEqual "Markdown text" htmlExpected -      (B.filter (/= '\n') $ toByteString $-        X.render (X.HtmlDocument X.UTF8 Nothing result))----------------------------------------------------------------------------------applyTest :: H.Assertion-applyTest = do-    let es = defaultHeistState :: HeistState IO-    res <- evalHeistT applyImpl-        (X.Element "apply" [("template", "nonexistant")] []) es--    H.assertEqual "apply nothing" [] res----------------------------------------------------------------------------------ignoreTest :: H.Assertion-ignoreTest = do-    let es = defaultHeistState :: HeistState IO-    res <- evalHeistT ignoreImpl-        (X.Element "ignore" [("tag", "ignorable")] -          [X.TextNode "This should be ignored"]) es-    H.assertEqual "<ignore> tag" [] res-----localTSTest :: H.Assertion---localTSTest = do---    let es = defaultHeistState :: HeistState IO--lookupTemplateTest = do-    ts <- loadTS "templates"-    let k = do-            setContext ["foo"]-            getsTS $ lookupTemplate "/user/menu"-    res <- runHeistT k (X.TextNode "") ts-    H.assertBool "lookup context test" $ isJust $ fst res------------------------------------------------------------------------------------ Utility functions--isLeft :: Either a b -> Bool-isLeft (Left _) = True-isLeft (Right _) = False----------------------------------------------------------------------------------loadT :: String -> IO (Either String (HeistState IO))-loadT s = loadTemplates s defaultHeistState----------------------------------------------------------------------------------loadTS :: FilePath -> IO (HeistState IO)-loadTS baseDir = do-    etm <- loadTemplates baseDir defaultHeistState-    return $ either error id etm---testTemplate tname = do-    ts <- loadTS "templates"-    Just (resDoc, _) <- renderTemplate ts tname-    return $ toByteString resDoc---testTemplateEval tname = do-    ts <- loadTS "templates"-    evalHeistT (evalWithHooks tname) (X.TextNode "") ts----------------------------------------------------------------------------------identStartChar :: [Char]-identStartChar = ['a'..'z']----------------------------------------------------------------------------------identChar :: [Char]-identChar = '_' : identStartChar----------------------------------------------------------------------------------textGen :: Gen [Char]-textGen = listOf $ elements ((replicate 5 ' ') ++ identStartChar)----------------------------------------------------------------------------------limitedDepth :: Int -> Gen X.Node-limitedDepth 0 = liftM (X.TextNode . T.pack) textGen-limitedDepth n =-    oneof [ liftM (X.TextNode . T.pack) textGen-          , liftM3 X.Element arbitrary-                       (liftM (take 2) arbitrary)-                       (liftM (take 3) $ listOf $ limitedDepth (n - 1))-          ]------------------------------------------------------------------------------------ | Returns the number of unique insertion points in the tree.--- If h = insertAt f n g", the following property holds:--- insSize h == (insSize f) + (insSize g) - 1-insSize :: [X.Node] -> Int-insSize ns = 1 + (sum $ map nodeSize ns)-  where nodeSize (X.TextNode _)    = 1-        nodeSize (X.Element _ _ c) = 1 + (insSize c)----------------------------------------------------------------------------------insertAt :: [X.Node] -> Int -> [X.Node] -> [X.Node]-insertAt elems 0 ns = elems ++ ns-insertAt elems _ [] = elems-insertAt elems n list = maybe [] X.topNodes $-    evalState (processNode elems $ fromJust $ X.fromNodes list) n----------------------------------------------------------------------------------move :: Insert ()-move = modify (\x -> x - 1)----------------------------------------------------------------------------------processNode :: [X.Node] -> X.Cursor -> Insert (Maybe X.Cursor)-processNode elems loc =-    liftM2 mplus (move >> goDown loc) (move >> goRight loc)--  where-    goDown l =-        case X.current l of-          X.TextNode _    -> modify (+1) >> return Nothing-          X.Element _ _ _ -> doneCheck (X.insertManyFirstChild elems)-                                       X.firstChild-                                       l--    goRight = doneCheck (Just . X.insertManyRight elems) X.right--    doneCheck insertFunc next l = do-      s <- get-      if s == 0-        then return $ insertFunc l-        else maybe (return Nothing) (processNode elems) $ next l------------------------------------------------------------------------------------ | Reloads the templates from disk and renders the specified--- template.  (Old convenience code.)-quickRender :: FilePath -> ByteString -> IO (Maybe ByteString)-quickRender baseDir name = do-    ts  <- loadTS baseDir-    res <- renderTemplate ts name-    return (fmap (toByteString . fst) res)----------------------------------------------------------------------------------newtype Name = Name { unName :: Text } deriving (Show)--instance Arbitrary Name where-  arbitrary = do-    x     <- elements identStartChar-    n     <- choose (4,10)-    rest  <- vectorOf n $ elements identChar-    return $ Name $ T.pack (x:rest)--instance Arbitrary X.Node where-  arbitrary = limitedDepth 3-  shrink (X.TextNode _) = []-  shrink (X.Element _ [] []) = []-  shrink (X.Element n [] (_:cs)) = [X.Element n [] cs]-  shrink (X.Element n (_:as) []) = [X.Element n as []]-  shrink (X.Element n as cs) = [X.Element n as (tail cs), X.Element n (tail as) cs]--instance Arbitrary T.Text where-  arbitrary = liftM unName arbitrary------- Code for inserting nodes into any point of a tree----type Insert a = State Int a------------------------------------------------------------------------------------ <bind> tests---- Data type encapsulating the parameters for a bind operation-data Bind = Bind-    { _bindElemName :: Name-    , _bindChildren :: [X.Node]-    , _bindDoc :: [X.Node]-    , _bindPos :: Int-    , _bindRefPos :: Int-    } -- deriving (Show)---instance Arbitrary Bind where-  arbitrary = do-    name <- arbitrary-    kids <- liftM (take 3) arbitrary-    doc <- liftM (take 5) arbitrary-    let s = insSize doc-    loc <- choose (0, s - 1)-    loc2 <- choose (0, s - loc - 1)-    return $ Bind name kids doc loc loc2-  shrink (Bind e [c] (_:ds) p r) = [Bind e [c] ds p r]-  shrink (Bind e (_:cs) d p r) = [Bind e cs d p r]-  shrink _ = []---instance Show Bind where-  show b@(Bind e c d p r) = unlines-    [ "\n"-    , "Bind element name: " ++ (show e)-    , "Bind pos: " ++ (show p)-    , "Bind ref pos: " ++ (show r)-    , "Bind document:"-    , L.unpack $ L.concat $ map formatNode d-    , "Bind children:"-    , L.unpack $ L.concat $ map formatNode c-    , "Result:"-    , L.unpack $ L.concat $ map formatNode $ buildResult b-    , "Splice result:"-    , L.unpack $ L.concat $ map formatNode $ unsafePerformIO $-        evalHeistT (runNodeList $ buildBindTemplate b)-                          (X.TextNode "") defaultHeistState-    , "Template:"-    , L.unpack $ L.concat $ map formatNode $ buildBindTemplate b-    ]-    where-      formatNode n = toLazyByteString $ X.render-                                      $ X.HtmlDocument X.UTF8 Nothing [n]---------------------------------------------------------------------------------buildNode :: Text -> Text -> Bind -> X.Node-buildNode tag attr (Bind s c _ _ _) = X.Element tag [(attr, unName s)] c----------------------------------------------------------------------------------buildBind :: Bind -> X.Node-buildBind = buildNode "bind" "tag"----------------------------------------------------------------------------------empty :: Text -> X.Node-empty n = X.Element n [] []----------------------------------------------------------------------------------buildBindTemplate :: Bind -> [X.Node]-buildBindTemplate s@(Bind n _ d b r) =-    insertAt [empty $ unName $ n] pos $ withBind-  where bind = [buildBind s]-        bindSize = insSize bind-        withBind = insertAt bind b d-        pos = b + bindSize - 1 + r----------------------------------------------------------------------------------buildResult :: Bind -> [X.Node]-buildResult (Bind _ c d b r) = insertAt c (b + r) d------------------------------------------------------------------------------------ <apply> tests--data Apply = Apply-    { _applyName :: Name-    , _applyCaller :: [X.Node]-    , _applyCallee :: Template-    , _applyChildren :: [X.Node]-    , _applyPos :: Int-    } deriving (Show)---instance Arbitrary Apply where-    arbitrary = do-      name <- arbitrary-      kids <- liftM (take 3) $ listOf $ limitedDepth 2-      caller <- liftM (take 5) arbitrary-      callee <- liftM (take 1) $ listOf $ limitedDepth 3-      let s = insSize caller-      loc <- choose (0, s - 1)-      return $ Apply name caller callee kids loc----------------------------------------------------------------------------------buildApplyCaller :: Apply -> [X.Node]-buildApplyCaller (Apply name caller _ kids pos) =-    insertAt [X.Element "apply" [("template", unName name)] kids] pos caller----------------------------------------------------------------------------------calcCorrect :: Apply -> [X.Node]-calcCorrect (Apply _ caller callee _ pos) = insertAt callee pos caller----------------------------------------------------------------------------------calcResult :: (MonadIO m) => Apply -> m [X.Node]-calcResult apply@(Apply name _ callee _ _) =-    evalHeistT (runNodeList $ buildApplyCaller apply)-        (X.TextNode "") ts--  where ts = setTemplates (Map.singleton [T.encodeUtf8 $ unName name]-                          (DocumentFile (X.HtmlDocument X.UTF8 Nothing callee)-                                        Nothing))-                          defaultHeistState----{---- The beginning of some future tests for hook functions.--p :: ByteString -> Node-p t = X.Element "p" [] [X.Text t]--hookG :: Monad m => ByteString -> Template -> m Template-hookG str t = return $ (p str) : t--onLoad = hookG "Inserted on load"-preRun = hookG "Inserted on preRun"-postRun = hookG "Inserted on postRun"--ts :: IO (Either String (HeistState IO))-ts = loadTemplates "test/templates" $-    foldr ($) (defaultHeistState ".")-    [setOnLoadHook onLoad-    ,setPreRunHook preRun-    ,setPostRunHook postRun-    ]--r name etm = do-    let ts = either (error "Danger Will Robinson!") id etm-    ns <- runNodeList ts name-    return $ (Just . formatList') =<< ns--}---{-- - Convenience code for manual ghci experimentation- -}----html :: [Node] -> Node---html c = X.Element "html" [] [hhead, body c]---hhead :: Node---hhead = X.Element "head" [] [title, X.Element "script" [] []]---title :: Node---title = X.Element "title" [] [X.Text "Test Page"]---body :: [Node] -> Node---body = X.Element "body" []------para :: Int -> Node---para n = X.Element "p" [] [X.Text $ B.pack $ "This is paragraph " ++ show n]---para2 :: B.ByteString -> Node---para2 c = X.Element "p" [] [X.Text c]---para3 :: Node---para3 = X.Element "p" [] [X.Text "AHA!"]------foo :: Int -> [Node]---foo n = insertAt [X.Element "NEW" [] []] n [html [para 1, para 2]]------tdoc :: [Node]---tdoc = [para 1, para 2, para 3, para 4]------bindElem :: [Node] -> Int -> Int -> Bind---bindElem = Bind (Name "mytag") [para2 "bound paragraph"]------addBind :: Bind -> [Node] -> [Node]---addBind b = insertAt [buildBind b] 0 . insertAt [empty $ unName $ _bindElemName b] 2------prn :: Node -> IO ()---prn = L.putStrLn . formatNode---runTests :: IO ()---runTests = defaultMain tests-
+ test/templates-bad/apply-missing-attr.tpl view
@@ -0,0 +1,3 @@+<apply>+noroot+</apply>
+ test/templates-bad/apply-template-not-found.tpl view
@@ -0,0 +1,3 @@+<apply template="/page">+This template is missing+</apply>
+ test/templates-bad/bind-infinite-loop.tpl view
@@ -0,0 +1,2 @@+<bind tag="foo"><p>line</p><foo/></bind>+<foo/>
+ test/templates-bad/bind-missing-attr.tpl view
@@ -0,0 +1,2 @@+<bind><p>line</p></bind>+<foo/>
+ test/templates/attr_splice.tpl view
@@ -0,0 +1,2 @@+<input type="checkbox" autocheck="foo"/>+<input type="checkbox" autocheck="bar"/>
+ test/templates/attrsubtest1.tpl view
@@ -0,0 +1,1 @@+<apply-content />
+ test/templates/attrsubtest2.tpl view
@@ -0,0 +1,1 @@+<bind tag="randName284">asdf</bind><apply template="attrsubtest1"><a href="${randName284}">link</a></apply><a href="before$after">foo</a>
+ test/templates/bind-apply-interaction/_outer.tpl view
@@ -0,0 +1,6 @@+<bind tag="foo">===<bind-content/>===</bind>+This is a test.+<foo>bind content</foo>+Another test line.+<apply-content/>+Last test line.
+ test/templates/bind-apply-interaction/caller.tpl view
@@ -0,0 +1,1 @@+<apply template="_outer">apply content</apply>
+ test/templates/bind_param.tpl view
@@ -0,0 +1,1 @@+<bind tag="item"><li><bind-content/> <foo/></li></bind><item><bind tag="foo">world</bind>Hi there<bind tag="bar">hello</bind></item>
+ test/templates/cache.tpl view
@@ -0,0 +1,3 @@+<cache ttl="0">+<foosplice/>+</cache>
+ test/templates/div_expansion.tpl view
@@ -0,0 +1,1 @@+<bind tag="mytext">foo</bind><div><mytext/></div>
− test/templates/etc.tpl
@@ -1,3 +0,0 @@-/etc-<bind tag="foo"><p>line</p><foo/></bind>-<foo/>
+ test/templates/foo/markdown-chdir.tpl view
@@ -0,0 +1,5 @@+Different directory markdown+<apply template="markdown"/>+This file doesn't have any actual test code referring to it because compiled+Heist will automatically fail the whole test suite if the case tested by this+template isn't tested correctly.
+ test/templates/foo/markdown-origdir.tpl view
@@ -0,0 +1,3 @@+<apply template="page">+<markdown file="test2.md"/>+</apply>
+ test/templates/foo/test2.md view
@@ -0,0 +1,1 @@+This *is* another test.
+ test/templates/head_merge/index.tpl view
@@ -0,0 +1,6 @@+<apply template="wrap">+<head>+<link href="index-link">+</head>+<div>index page</div>+</apply>
+ test/templates/head_merge/nav.tpl view
@@ -0,0 +1,4 @@+<head>+<link href="nav-link">+</head>+<div>nav bar</div>
+ test/templates/head_merge/wrap.tpl view
@@ -0,0 +1,9 @@+<html>+<head>+<link href="wrapper-link">+</head>+<body>+<apply template="nav"/>+<apply-content/>+</body>+</html>
test/templates/ioc.tpl view
@@ -1,6 +1,3 @@-<apply>-Apply with no parameter.-</apply> <apply template="/page"> Inversion of control content </apply>
− test/templates/noroot.tpl
@@ -1,3 +0,0 @@-<apply>-noroot-</apply>
test/templates/page.tpl view
@@ -1,5 +1,5 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <wrapper>-<content/>+<apply-content/> </wrapper>
+ test/templates/people.tpl view
@@ -0,0 +1,3 @@+<people>+<p><lastName/>, <firstName/>: <age/> years old</p>+</people>
+ test/templates/post.tpl view
@@ -0,0 +1,4 @@+<post>+<h1><postTitle/></h1>+<p><postBody/></p>+</post>
+ test/templates/textarea_expansion.tpl view
@@ -0,0 +1,1 @@+<bind tag="mytext">foo</bind><textarea><mytext/></textarea>
+ test/templates/title_expansion.tpl view
@@ -0,0 +1,1 @@+<bind tag="mytext">foo</bind><title><mytext/></title>
test/templates/user/admin/main.tpl view
@@ -1,6 +1,4 @@ <html> Admin Page <apply template="menu"/>-<apply template="../menu"/>-<apply template="../../a"/> </html>