heist 0.8.2 → 1.1.1.2
raw patch · 95 files changed
Files
- .ghci +2/−0
- CHANGELOG.md +63/−0
- README.md +3/−1
- heist.cabal +245/−57
- src/Data/HeterogeneousEnvironment.hs +84/−0
- src/Heist.hs +356/−0
- src/Heist/Common.hs +370/−0
- src/Heist/Compiled.hs +66/−0
- src/Heist/Compiled/Internal.hs +805/−0
- src/Heist/Compiled/LowLevel.hs +20/−0
- src/Heist/Internal/Types.hs +279/−0
- src/Heist/Internal/Types/HeistState.hs +680/−0
- src/Heist/Interpreted.hs +83/−0
- src/Heist/Interpreted/Internal.hs +423/−0
- src/Heist/Splices.hs +77/−0
- src/Heist/Splices/Apply.hs +107/−0
- src/Heist/Splices/Bind.hs +42/−0
- src/Heist/Splices/BindStrict.hs +36/−0
- src/Heist/Splices/Cache.hs +205/−0
- src/Heist/Splices/Html.hs +47/−0
- src/Heist/Splices/Ignore.hs +22/−0
- src/Heist/Splices/Json.hs +250/−0
- src/Heist/Splices/Markdown.hs +324/−0
- src/Heist/TemplateDirectory.hs +105/−0
- src/Text/Templating/Heist.hs +0/−176
- src/Text/Templating/Heist/Internal.hs +0/−743
- src/Text/Templating/Heist/Splices.hs +0/−16
- src/Text/Templating/Heist/Splices/Apply.hs +0/−65
- src/Text/Templating/Heist/Splices/Bind.hs +0/−39
- src/Text/Templating/Heist/Splices/BindStrict.hs +0/−32
- src/Text/Templating/Heist/Splices/Cache.hs +0/−152
- src/Text/Templating/Heist/Splices/Html.hs +0/−47
- src/Text/Templating/Heist/Splices/Ignore.hs +0/−22
- src/Text/Templating/Heist/Splices/Json.hs +0/−222
- src/Text/Templating/Heist/Splices/Markdown.hs +0/−183
- src/Text/Templating/Heist/TemplateDirectory.hs +0/−85
- src/Text/Templating/Heist/Types.hs +0/−406
- test/.ghci +0/−5
- test/README +0/−1
- test/heist-testsuite.cabal +0/−40
- test/runTestsAndCoverage.sh +0/−48
- test/suite/Benchmark.hs +118/−0
- test/suite/Heist/Compiled/Tests.hs +409/−0
- test/suite/Heist/Interpreted/Tests.hs +670/−0
- test/suite/Heist/TestCommon.hs +134/−0
- test/suite/Heist/Tests.hs +209/−0
- test/suite/Heist/Tutorial/AttributeSplices.lhs +58/−0
- test/suite/Heist/Tutorial/CompiledSplices.lhs +274/−0
- test/suite/Heist/Tutorial/Imports.hs +26/−0
- test/suite/TestSuite.hs +15/−5
- test/suite/Text/Templating/Heist/Tests.hs +0/−692
- test/templates-bad/apply-missing-attr.tpl +3/−0
- test/templates-bad/apply-template-not-found.tpl +3/−0
- test/templates-bad/bind-infinite-loop.tpl +2/−0
- test/templates-bad/bind-missing-attr.tpl +2/−0
- test/templates-defer/test.tpl +5/−0
- test/templates-loaderror/_error.tpl +1/−0
- test/templates-loaderror/_ok.tpl +1/−0
- test/templates-loaderror/test.tpl +3/−0
- test/templates-ns-nested/test.tpl +1/−0
- test/templates-nsbind/nsbind.tpl +7/−0
- test/templates-nsbind/nsbinderror.tpl +10/−0
- test/templates-nscall/_call.tpl +2/−0
- test/templates-nscall/_invalid.tpl +1/−0
- test/templates-nscall/nscall.tpl +6/−0
- test/templates/attr_splice.tpl +2/−0
- test/templates/attrs.tpl +1/−1
- test/templates/attrsubtest1.tpl +1/−0
- test/templates/attrsubtest2.tpl +1/−0
- test/templates/backslash.tpl +1/−0
- test/templates/bind-apply-interaction/_outer.tpl +6/−0
- test/templates/bind-apply-interaction/caller.tpl +1/−0
- test/templates/bind_param.tpl +1/−0
- test/templates/cache.tpl +3/−0
- test/templates/div_expansion.tpl +1/−0
- test/templates/etc.tpl +0/−3
- test/templates/foo/markdown-chdir.tpl +5/−0
- test/templates/foo/markdown-origdir.tpl +3/−0
- test/templates/foo/test2.md +1/−0
- test/templates/head_merge/index.tpl +6/−0
- test/templates/head_merge/nav.tpl +4/−0
- test/templates/head_merge/wrap.tpl +9/−0
- test/templates/ioc.tpl +0/−3
- test/templates/json_array.tpl +1/−0
- test/templates/namespaces.tpl +5/−0
- test/templates/noroot.tpl +0/−3
- test/templates/page.tpl +1/−1
- test/templates/pandoc.tpl +1/−0
- test/templates/pandocdiv.tpl +1/−0
- test/templates/people.tpl +3/−0
- test/templates/post.tpl +4/−0
- test/templates/rss.xtpl +1/−0
- test/templates/textarea_expansion.tpl +1/−0
- test/templates/title_expansion.tpl +1/−0
- test/templates/user/admin/main.tpl +0/−2
.ghci view
@@ -1,5 +1,7 @@ :set -XOverloadedStrings+:set -XCPP :set -Wall :set -isrc :set -itest/suite :set -hide-package MonadCatchIO-mtl+:set -hide-package monads-tf
+ CHANGELOG.md view
@@ -0,0 +1,63 @@+# 1.1.1.2++* Support GHC 9.8+* Fix broken test++# 1.1.1.1++* Support GHC 9.6++# 1.1.1.0++* Expose `lookupTemplate` and `splitTemplatePath`++* Bump dependency bounds for 9.4++# 1.1++* Remove pandoc and pandocBS++* Stop exporting readProcessWithExitCode'++* Remove -S and --no-wrap arguments to pandoc for compatibility with both 1.x+ and 2.x versions of the pandoc command line tool++* Bump map-syntax lower bound to fix 8.4 build problem++# 1.0.1.3++* Add Semigroup instances to support GHC 8.4++# 1.0.1.0++* Change benchmark from an executable section to a benchmark section in the+ cabal file. This eliminates the criterion dependency when doing "cabal+ install heist".+* Export manyWith++# 1.0.0.1++* Drop the dependency on `errors` packages from heist testsuite and benchmark+* Fix nested splice namespace warning bug (issue #85)++# 1.0.0.0++* Switch from MonadCatchIO-transformers to monad-control for Snap 1.0++# 0.14.0++See http://snapframework.com/blog/2014/09/24/heist-0.14-released++* Add namespace support (hcNamespace and hcErrorNotBound)+* Add tellSpliceError for generalized error reporting+* Restructured HeistConfig, export lenses instead of field accessors+* Moved old HeistConfig into SpliceConfig+* Factored SpliceAPI module out into separate map-syntax package++# 0.13.0++See http://snapframework.com/blog/2013/09/09/snap-0.13-released++* Simpler compiled splice API+* New splice syntax+
README.md view
@@ -1,5 +1,7 @@ # Heist +[](https://github.com/snapframework/heist/actions)+ Heist, part of the [Snap Framework](http://www.snapframework.com/), is a Haskell library for xml/html templating. It uses simple XML tags to bind values to your templates in a straightforward way. For example, if you were to@@ -15,7 +17,7 @@ Likewise, if you need to add text to an attribute, <bind tag="special">special-id</bind>- <div id="$(special)">very special</div>+ <div id="${special}">very special</div> gives you
heist.cabal view
@@ -1,16 +1,48 @@+cabal-version: 2.2 name: heist-version: 0.8.2-synopsis: An (x)html templating system-description: An (x)html templating system-license: BSD3+version: 1.1.1.2+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: BSD-3-Clause license-file: LICENSE author: Doug Beardsley, Gregory Collins maintainer: snap@snapframework.com build-type: Simple-cabal-version: >= 1.6 homepage: http://snapframework.com/ category: Web, Snap +tested-with:+ GHC == 8.8.4+ GHC == 8.10.7+ GHC == 9.0.2+ GHC == 9.2.8+ GHC == 9.4.7+ GHC == 9.6.3+ GHC == 9.8.1++extra-doc-files:+ CHANGELOG.md+ extra-source-files: .ghci, CONTRIBUTORS,@@ -32,98 +64,254 @@ LICENSE, README.md, README.SNAP.md,- test/.ghci,- 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/backslash.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_array.tpl test/templates/json_object.tpl, test/templates/json_snippet.tpl, test/templates/markdown.tpl,- test/templates/noroot.tpl,+ test/templates/namespaces.tpl test/templates/page.tpl,+ test/templates/pandoc.tpl+ test/templates/pandocdiv.tpl+ test/templates/people.tpl,+ test/templates/post.tpl, test/templates/readme.txt,+ test/templates/rss.xtpl 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,+ test/templates-defer/test.tpl,+ test/templates-loaderror/_error.tpl,+ test/templates-loaderror/_ok.tpl,+ test/templates-loaderror/test.tpl,+ test/templates-nsbind/nsbind.tpl,+ test/templates-nsbind/nsbinderror.tpl,+ test/templates-ns-nested/test.tpl,+ test/templates-nscall/_call.tpl,+ test/templates-nscall/_invalid.tpl,+ test/templates-nscall/nscall.tpl, TODO +common universal+ default-language: Haskell2010 + default-extensions:+ DeriveDataTypeable+ FlexibleInstances+ GeneralizedNewtypeDeriving+ MultiParamTypeClasses+ OverloadedStrings+ PackageImports+ ScopedTypeVariables+ TypeSynonymInstances++ build-depends:+ , base >= 4.5 && < 5++ if !impl(ghc >= 8.0)+ build-depends:+ semigroups >= 0.16 && < 0.19,+ Library+ import: universal 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.Compiled.LowLevel,+ Heist.Internal.Types,+ 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.Internal.Types.HeistState,+ Heist.Interpreted.Internal 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,+ aeson >= 0.6 && < 2.3,+ attoparsec >= 0.10 && < 0.15,+ blaze-builder >= 0.2 && < 0.5,+ blaze-html >= 0.4 && < 0.10,+ bytestring >= 0.9 && < 0.13,+ containers >= 0.2 && < 1.0,+ directory >= 1.1 && < 1.4,+ directory-tree >= 0.10 && < 0.13,+ dlist >= 0.5 && < 1.1,+ filepath >= 1.3 && < 1.5,+ hashable >= 1.1 && < 1.5,+ lifted-base >= 0.2 && < 0.3,+ map-syntax >= 0.3 && < 0.4,+ monad-control >= 0.3 && < 1.1,+ mtl >= 2.0 && < 2.4,+ process >= 1.1 && < 1.7,+ random >= 1.0.1.0 && < 1.3,+ text >= 0.10 && < 2.2,+ time >= 1.1 && < 1.13,+ transformers >= 0.3 && < 0.7,+ transformers-base >= 0.4 && < 0.5,+ unordered-containers >= 0.1.4 && < 0.3,+ vector >= 0.9 && < 0.14,+ xmlhtml >= 0.2.3.5 && < 0.4,+ indexed-traversable >= 0.1.1 && < 0.2++ ghc-options: -Wall -fwarn-tabs -funbox-strict-fields++ default-extensions:+ UndecidableInstances,+ NoMonomorphismRestriction+++source-repository head+ type: git+ location: https://github.com/snapframework/heist.git++Test-suite testsuite+ import: universal+ hs-source-dirs: src test/suite+ type: exitcode-stdio-1.0+ main-is: TestSuite.hs++ build-depends:+ HUnit >= 1.2 && < 2,+ QuickCheck >= 2 && < 2.15,+ lens >= 4.3 && < 5.3,+ test-framework >= 0.4 && < 0.9,+ test-framework-hunit >= 0.2.7 && < 0.4,+ test-framework-quickcheck2 >= 0.2.12.1 && < 0.4,+ aeson,+ attoparsec,+ bifunctors >= 5.3 && < 5.7,+ blaze-builder,+ blaze-html, bytestring,+ containers, directory, directory-tree,+ dlist, filepath,- MonadCatchIO-transformers >= 0.2.1 && < 0.4,- mtl >= 2.0 && < 2.2,+ hashable,+ lifted-base,+ map-syntax,+ monad-control,+ mtl, 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+ text,+ time,+ transformers,+ transformers-base,+ unordered-containers,+ vector,+ xmlhtml,+ indexed-traversable - if impl(ghc >= 6.12.0)- ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2- -fno-warn-unused-do-bind- else- ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2+ if impl(ghc >= 7.8) && impl(ghc < 7.10)+ build-depends: transformers-compat >= 0.3 && < 0.7 - ghc-prof-options: -prof -auto-all+ ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -threaded - Extensions:- GeneralizedNewtypeDeriving,- PackageImports,- ScopedTypeVariables,- DeriveDataTypeable,- FlexibleInstances,- MultiParamTypeClasses,- UndecidableInstances,- OverloadedStrings,- TypeSynonymInstances,- NoMonomorphismRestriction+Benchmark benchmark+ import: universal+ hs-source-dirs: src test/suite+ type: exitcode-stdio-1.0+ main-is: Benchmark.hs + build-depends:+ HUnit,+ criterion >= 1.0,+ criterion-measurement >= 0.1,+ test-framework,+ test-framework-hunit, -source-repository head- type: git- location: http://git.snapframework.com/heist.git+ -- Copied from regular dependencies:++ aeson,+ attoparsec,+ blaze-builder,+ blaze-html,+ bytestring,+ containers,+ directory,+ directory-tree,+ dlist,+ filepath,+ hashable,+ lifted-base,+ map-syntax,+ monad-control,+ mtl,+ process,+ random,+ statistics >= 0.11,+ text,+ time,+ transformers,+ transformers-base,+ unordered-containers,+ vector,+ xmlhtml,+ indexed-traversable++ if impl(ghc >= 7.8) && impl(ghc < 7.10)+ build-depends: transformers-compat++ ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -threaded+ -fno-warn-unused-do-bind -rtsopts++ default-extensions:+ UndecidableInstances,+ NoMonomorphismRestriction
+ 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,356 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++{-|++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+ , reloadTemplates+ , addTemplatePathPrefix+ , initHeist+ , initHeistWithCacheTag+ , defaultInterpretedSplices+ , defaultLoadTimeSplices+ , emptyHeistConfig++ -- * Core Heist data types+ , SpliceConfig+ , HeistConfig+ , TemplateRepo+ , TemplateLocation+ , Template+ , TPath+ , MIMEType+ , DocumentFile(..)+ , AttrSplice+ , RuntimeSplice+ , Chunk+ , HeistState+ , SpliceError(..)+ , CompileException(..)+ , HeistT++ -- * Lenses (can be used with lens or lens-family)+ , scInterpretedSplices+ , scLoadTimeSplices+ , scCompiledSplices+ , scAttributeSplices+ , scTemplateLocations+ , scCompiledTemplateFilter+ , hcSpliceConfig+ , hcNamespace+ , hcErrorNotBound+ , hcInterpretedSplices+ , hcLoadTimeSplices+ , hcCompiledSplices+ , hcAttributeSplices+ , hcTemplateLocations+ , hcCompiledTemplateFilter++ -- * HeistT functions+ , templateNames+ , compiledTemplateNames+ , hasTemplate+ , spliceNames+ , compiledSpliceNames+ , evalHeistT+ , getParamNode+ , getContext+ , getTemplateFilePath+ , localParamNode+ , getsHS+ , getHS+ , putHS+ , modifyHS+ , restoreHS+ , localHS+ , getDoc+ , getXMLDoc+ , tellSpliceError+ , spliceErrorText+ , orError+ , Splices++ -- * TPath functions+ , lookupTemplate+ , splitTemplatePath+ ) where+++------------------------------------------------------------------------------+import Control.Exception.Lifted+import Control.Monad.State+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BC+import qualified Data.ByteString as B+import Data.Either+import qualified Data.Foldable as F+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as Map+import qualified Data.HeterogeneousEnvironment as HE+import Data.Map.Syntax+#if !MIN_VERSION_base(4,11,0)+import Data.Monoid+#endif+import Data.Text (Text)+import qualified Data.Text as T+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.Internal.Types+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | The built-in set of splices that you should use in compiled splice mode.+-- This list includes everything from 'defaultInterpretedSplices' plus a+-- splice for the content tag that errors out when it sees any instance of the+-- old content tag, which has now been moved to two separate tags called+-- apply-content and bind-content.+defaultLoadTimeSplices :: MonadIO m => Splices (I.Splice m)+defaultLoadTimeSplices = do+ -- To be removed in later versions+ defaultInterpretedSplices+ "content" #! deprecatedContentCheck++++------------------------------------------------------------------------------+-- | 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 scLoadTimeSplices list in your SpliceConfig.+-- If you are using interpreted splice mode, then you might also want to bind+-- the 'deprecatedContentCheck' splice to the content tag as a load time+-- splice.+defaultInterpretedSplices :: MonadIO m => Splices (I.Splice m)+defaultInterpretedSplices = do+ applyTag ## applyImpl+ bindTag ## bindImpl+ ignoreTag ## ignoreImpl+ markdownTag ## markdownSplice++++------------------------------------------------------------------------------+-- | An empty HeistConfig that uses the \"h\" namespace with error checking+-- turned on.+emptyHeistConfig :: HeistConfig m+emptyHeistConfig = HeistConfig mempty "h" True+++allErrors :: [Either String (TPath, v)]+ -> Either [String] (HashMap TPath v)+allErrors tlist =+ case errs of+ [] -> Right $ Map.fromList $ rights tlist+ _ -> Left errs+ where+ errs = lefts tlist+++------------------------------------------------------------------------------+-- | 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 -> IO (Either [String] TemplateRepo)+loadTemplates dir = do+ d <- readDirectoryWith (loadTemplate dir) dir+#if MIN_VERSION_directory_tree(0,11,0)+ return $ allErrors $ F.fold (dirTree d)+#else+ return $ allErrors $ F.fold (free d)+#endif+++------------------------------------------------------------------------------+-- | Reloads all the templates an an existing TemplateRepo.+reloadTemplates :: TemplateRepo -> IO (Either [String] TemplateRepo)+reloadTemplates repo = do+ tlist <- mapM loadOrKeep $ Map.toList repo+ return $ allErrors tlist+ where+ loadOrKeep (p,df) =+ case dfFile df of+ Nothing -> return $ Right (p, df)+ Just fp -> do+ df' <- loadTemplate' fp+ return $ fmap (p,) $ case df' of+ [t] -> t+ _ -> Left "Template repo has non-templates"+++------------------------------------------------------------------------------+-- | 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 -> TemplateRepo -> TemplateRepo+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+++------------------------------------------------------------------------------+-- | Creates an empty HeistState.+emptyHS :: HE.KeyGen -> HeistState m+emptyHS kg = HeistState Map.empty Map.empty Map.empty Map.empty Map.empty+ True [] [] 0 [] Nothing kg False Html "" [] False 0+++------------------------------------------------------------------------------+-- | 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+ -> IO (Either [String] (HeistState n))+initHeist hc = do+ keyGen <- HE.newKeyGen+ repos <- sequence $ _scTemplateLocations $ _hcSpliceConfig hc+ case sequence repos of+ Left es -> return $ Left es+ Right rs -> initHeist' keyGen hc (Map.unions rs)+++------------------------------------------------------------------------------+mkSplicePrefix :: Text -> Text+mkSplicePrefix ns+ | T.null ns = ""+ | otherwise = ns `mappend` ":"+++------------------------------------------------------------------------------+initHeist' :: Monad n+ => HE.KeyGen+ -> HeistConfig n+ -> TemplateRepo+ -> IO (Either [String] (HeistState n))+initHeist' keyGen (HeistConfig sc ns enn) repo = do+ let empty = emptyHS keyGen+ let (SpliceConfig i lt c a _ f) = sc+ etmap <- preproc keyGen lt repo ns+ let prefix = mkSplicePrefix ns+ let eis = runHashMap $ mapK (prefix<>) i+ ecs = runHashMap $ mapK (prefix<>) c+ eas = runHashMap $ mapK (prefix<>) a+ let hs1 = do+ tmap <- etmap+ is <- eis+ cs <- ecs+ as <- eas+ return $ empty { _spliceMap = is+ , _templateMap = tmap+ , _compiledSpliceMap = cs+ , _attrSpliceMap = as+ , _splicePrefix = prefix+ , _errorNotBound = enn+ }+ either (return . Left) (C.compileTemplates f) hs1+++------------------------------------------------------------------------------+-- | Runs preprocess on a TemplateRepo and returns the modified templates.+preproc :: HE.KeyGen+ -> Splices (I.Splice IO)+ -> TemplateRepo+ -> Text+ -> IO (Either [String] TemplateRepo)+preproc keyGen splices templates ns = do+ let esm = runHashMap splices+ case esm of+ Left errs -> return $ Left errs+ Right sm -> do+ let hs = (emptyHS keyGen) { _spliceMap = sm+ , _templateMap = templates+ , _preprocessingMode = True+ , _splicePrefix = mkSplicePrefix ns }+ let eval a = evalHeistT a (X.TextNode "") hs+ tPairs <- mapM (eval . preprocess) $ Map.toList templates+ let bad = lefts tPairs+ return $ if not (null bad)+ then Left bad+ else Right $ Map.fromList $ rights tPairs+++------------------------------------------------------------------------------+-- | Processes a single template, running load time splices.+preprocess :: (TPath, DocumentFile)+ -> HeistT IO IO (Either String (TPath, DocumentFile))+preprocess (tpath, docFile) = do+ let tname = tpathName tpath+ die = error $ "Preprocess failed because the template `"+ ++ BC.unpack tname+ ++ "` was not found in the template repository."+ !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+++------------------------------------------------------------------------------+-- | 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+-- 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+ -> IO (Either [String] (HeistState n, CacheTagState))+initHeistWithCacheTag (HeistConfig sc ns enn) = do+ (ss, cts) <- liftIO mkCacheTag+ let tag = "cache"+ keyGen <- HE.newKeyGen++ erepos <- sequence $ _scTemplateLocations sc+ case sequence erepos of+ Left es -> return $ Left es+ Right repos -> do+ -- We have to do one preprocessing pass with the cache setup splice. This+ -- has to happen for both interpreted and compiled templates, so we do it+ -- here by itself because interpreted templates don't get the same load+ -- time splices as compiled templates.+ eRawWithCache <- preproc keyGen (tag ## ss) (Map.unions repos) ns+ case eRawWithCache of+ Left es -> return $ Left es+ Right rawWithCache -> do+ let sc' = SpliceConfig (tag #! cacheImpl cts) mempty+ (tag #! cacheImplCompiled cts)+ mempty mempty (const True)+ let hc = HeistConfig (mappend sc sc') ns enn+ hs <- initHeist' keyGen hc rawWithCache+ return $ fmap (,cts) hs
+ src/Heist/Common.hs view
@@ -0,0 +1,370 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Heist.Common where++------------------------------------------------------------------------------+import Control.Applicative (Alternative (..))+import Control.Exception (SomeException)+import qualified Control.Exception.Lifted as C+import Control.Monad (liftM, mplus)+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.Hashable (Hashable)+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as Map+import Data.List (isSuffixOf, sort)+import Data.Map.Syntax+import Data.Maybe (isJust)+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import Heist.Internal.Types.HeistState+import System.FilePath (pathSeparator)+import qualified Text.XmlHtml as X+#if !MIN_VERSION_base(4,8,0)+import Control.Applicative (Applicative (..), (<$>))+import Data.Monoid (Monoid (..))+#endif+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+runHashMap+ :: Splices s+ -> Either [String] (HashMap T.Text s)+runHashMap ms =+ case runMapSyntax Map.lookup Map.insert ms of+ Left keys -> Left $ map (T.unpack . mkMsg) keys+ Right hm -> Right hm+ where+ mkMsg k = "You tried to bind "<>k<>" more than once!"+++------------------------------------------------------------------------------+runMapNoErrors :: (Eq k, Hashable k) => MapSyntaxM k v a -> HashMap k v+runMapNoErrors = either (const mempty) id .+ runMapSyntax' (\_ new _ -> Just new) Map.lookup Map.insert++applySpliceMap :: HeistState n+ -> (HeistState n -> HashMap Text v)+ -> MapSyntaxM Text v a+ -> HashMap Text v+applySpliceMap hs f = (flip Map.union (f hs)) .+ runMapNoErrors .+ mapK (mappend pre)+ where+ pre = _splicePrefix hs++------------------------------------------------------------------------------+-- | 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 do fullMsg <- heistErrMsg (T.pack msg)+ error $ T.unpack fullMsg+ else silent+++------------------------------------------------------------------------------+-- | Prepends the location of the template currently being processed to an+-- error message.+heistErrMsg :: Monad m => Text -> HeistT n m Text+heistErrMsg msg = do+ tf <- getsHS _curTemplateFile+ return $ (maybe "" ((`mappend` ": ") . T.pack) tf) `mappend` msg+++------------------------------------------------------------------------------+-- | Adds an error message to the list of splice processing errors.+tellSpliceError :: Monad m => Text -> HeistT n m ()+tellSpliceError msg = do+ hs <- getHS+ node <- getParamNode+ let spliceError = SpliceError+ { spliceHistory = _splicePath hs+ , spliceTemplateFile = _curTemplateFile hs+ , visibleSplices = sort $ Map.keys $ _compiledSpliceMap hs+ , contextNode = node+ , spliceMsg = msg+ }+ modifyHS (\hs' -> hs { _spliceErrors = spliceError : _spliceErrors hs' })+++------------------------------------------------------------------------------+-- | Function for showing a TPath.+showTPath :: TPath -> String+showTPath = BC.unpack . (`BC.append` ".tpl") . tpathName+++------------------------------------------------------------------------------+-- | Convert a TPath into a ByteString path.+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+ idp <- identParser+ dl' <- finish subDL+ loop $! append dl' idp)++ gobbleText = AP.takeWhile1 (AP.notInClass "$")++ 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+ [] -> ("", [])+ x:xs -> (x, xs)+ 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 = do+ c <- loadTemplate' fname+ return $ map (fmap (\t -> (splitLocalPath $ BC.pack tName, t))) c+ where -- tName is path relative to the template root directory+ isHTMLTemplate = ".tpl" `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+++------------------------------------------------------------------------------+-- | Loads a template at the specified path, choosing the appropriate parser+-- based on the file extension. The template is only loaded if it has a+-- \".tpl\" or \".xtpl\" extension. Returns an empty list if the extension+-- doesn't match.+loadTemplate' :: String -> IO [Either String DocumentFile]+loadTemplate' fullDiskPath+ | isHTMLTemplate = liftM (:[]) $ getDoc fullDiskPath+ | isXMLTemplate = liftM (:[]) $ getXMLDoc fullDiskPath+ | otherwise = return []+ where+ isHTMLTemplate = ".tpl" `isSuffixOf` fullDiskPath+ isXMLTemplate = ".xtpl" `isSuffixOf` fullDiskPath+++------------------------------------------------------------------------------+-- | 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 <- C.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"+ enc X.ISO_8859_1 = "iso-8859-1"+++------------------------------------------------------------------------------+-- | Binds a set of new splice declarations within a 'HeistState'.+bindAttributeSplices :: Splices (AttrSplice n) -- ^ splices to bind+ -> HeistState n -- ^ start state+ -> HeistState n+bindAttributeSplices ss hs =+ hs { _attrSpliceMap = applySpliceMap hs _attrSpliceMap ss }++------------------------------------------------------------------------------+-- | 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 })++
+ src/Heist/Compiled.hs view
@@ -0,0 +1,66 @@+{-|++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+ , codeGen+ , runChildren++ -- * Functions for manipulating lists of compiled splices+ , textSplice+ , nodeSplice+ , xmlNodeSplice+ , htmlNodeSplice+ , pureSplice++ , deferMany+ , defer+ , deferMap+ , mayDeferMap+ , bindLater+ , withSplices+ , manyWithSplices+ , manyWith+ , withLocalSplices++ -- * Constructing Chunks+ -- $yieldOverview+ , yieldPure+ , yieldRuntime+ , yieldRuntimeEffect+ , yieldPureText+ , yieldRuntimeText++ -- * Running nodes and splices+ , runNodeList+ , runNode+ , runAttributes+ , runAttributesRaw+ , callTemplate++ ) 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 level construction of DLists of Chunks.+--+-- Most of the time you will use these functions composed with return to+-- generate a Splice. But we decided not to include the return in these+-- functions to allow you to work with the DLists purely.
+ src/Heist/Compiled/Internal.hs view
@@ -0,0 +1,805 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeSynonymInstances #-}++module Heist.Compiled.Internal where+++------------------------------------------------------------------------------+import Blaze.ByteString.Builder+import Blaze.ByteString.Builder.Char.Utf8+import Control.Arrow+import Control.Exception+import Control.Monad+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.HashSet as S+import qualified Data.HeterogeneousEnvironment as HE+import Data.Map.Syntax+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.Printf+import qualified Text.XmlHtml as X+import qualified Text.XmlHtml.HTML.Meta as X+------------------------------------------------------------------------------+#if !MIN_VERSION_base(4,8,0)+import Data.Foldable (Foldable)+#endif+import qualified Data.Foldable as Foldable+------------------------------------------------------------------------------+import Heist.Common+import Heist.Internal.Types.HeistState+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | 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 us 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))+++------------------------------------------------------------------------------+-- | Runs the parameter node's children and returns the resulting compiled+-- chunks. 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+{-# INLINE runChildren #-}+++renderFragment :: Markup -> [X.Node] -> Builder+renderFragment markup ns =+ case markup of+ Html -> X.renderHtmlFragment X.UTF8 ns+ Xml -> X.renderXmlFragment X.UTF8 ns+++------------------------------------------------------------------------------+-- | 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 n)+yieldPure = DL.singleton . Pure . toByteString+{-# INLINE yieldPure #-}+++------------------------------------------------------------------------------+-- | Yields a runtime action that returns a builder.+yieldRuntime :: RuntimeSplice n Builder -> DList (Chunk n)+yieldRuntime = DL.singleton . RuntimeHtml+{-# INLINE yieldRuntime #-}+++------------------------------------------------------------------------------+-- | Yields a runtime action that returns no value and is only needed for its+-- side effect.+yieldRuntimeEffect :: Monad n => RuntimeSplice n () -> DList (Chunk n)+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 n)+yieldPureText = DL.singleton . pureTextChunk+{-# INLINE yieldPureText #-}+++------------------------------------------------------------------------------+-- | Convenience wrapper around yieldRuntime allowing you to work with Text.+yieldRuntimeText :: Monad n => RuntimeSplice n Text -> DList (Chunk n)+yieldRuntimeText = yieldRuntime . liftM fromText+{-# INLINE yieldRuntimeText #-}+++------------------------------------------------------------------------------+-- | 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 DocumentFile with the appropriate template context set.+runDocumentFile :: Monad n+ => TPath+ -> DocumentFile+ -> Splice n+runDocumentFile tpath df = do+ let markup = case dfDoc df of+ X.XmlDocument _ _ _ -> Xml+ X.HtmlDocument _ _ _ -> Html+ modifyHS (\hs -> hs { _curMarkup = markup })+ let inDoctype = X.docType $ dfDoc df+ addDoctype $ maybeToList inDoctype+ modifyHS (setCurTemplateFile curPath . setCurContext tpath)+ res <- runNodeList nodes+ dt <- getsHS (listToMaybe . _doctypes)+ let enc = X.docEncoding $ dfDoc df+ return $! (yieldPure (X.renderDocType enc dt) `mappend` res)+ where+ curPath = dfFile df+ nodes = X.docContent $! dfDoc df+++------------------------------------------------------------------------------+compileTemplate+ :: Monad n+ => TPath+ -> DocumentFile+ -> HeistT n IO [Chunk n]+compileTemplate tpath df = do+ !chunks <- runDocumentFile tpath df+ return $! consolidate chunks+++------------------------------------------------------------------------------+compileTemplates+ :: Monad n+ => (TPath -> Bool)+ -> HeistState n+ -> IO (Either [String] (HeistState n))+compileTemplates f hs = do+ (tmap, hs') <- runHeistT (compileTemplates' f) (X.TextNode "") hs+ let pre = _splicePrefix hs'+ let canError = _errorNotBound hs'+ let errs = _spliceErrors hs'+ let nsErr = if not (T.null pre) && (_numNamespacedTags hs' == 0)+ then Left [noNamespaceSplicesMsg $ T.unpack pre]+ else Right ()+ return $ if canError+ then case errs of+ [] -> nsErr >>+ (Right $! hs { _compiledTemplateMap = tmap })+ es -> Left $ either (++) (const id) nsErr $+ map (T.unpack . spliceErrorText) es+ else nsErr >> (Right $! hs { _compiledTemplateMap = tmap+ , _spliceErrors = errs+ })+++------------------------------------------------------------------------------+noNamespaceSplicesMsg :: String -> String+noNamespaceSplicesMsg pre = unwords+ [ printf "You are using a namespace of '%s', but you don't have any" ns+ , printf "tags starting with '%s'. If you have not defined any" pre+ , "splices, then change your namespace to the empty string to get rid"+ , "of this message."+ ]+ where+ ns = reverse $ drop 1 $ reverse pre+++------------------------------------------------------------------------------+compileTemplates'+ :: Monad n+ => (TPath -> Bool)+ -> HeistT n IO (H.HashMap TPath ([Chunk n], MIMEType))+compileTemplates' f = do+ hs <- getHS+ let tpathDocfiles :: [(TPath, DocumentFile)]+ tpathDocfiles = filter (f . fst)+ (H.toList $ _templateMap hs)+ foldM runOne H.empty tpathDocfiles+ where+ runOne tmap (tpath, df) = do+ modifyHS (\hs -> hs { _doctypes = []})+ !mHtml <- compileTemplate tpath df+ return $! H.insert tpath (mHtml, mimeType $! dfDoc df) tmap+++------------------------------------------------------------------------------+-- | Consolidate consecutive Pure Chunks.+consolidate :: (Monad n) => DList (Chunk n) -> [Chunk n]+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, codeGen turns consecutive runs of+-- @Pure Html@ values into maximally-efficient pre-rendered strict+-- 'ByteString' chunks.+codeGen :: Monad n => DList (Chunk n) -> RuntimeSplice n Builder+codeGen l = V.foldr mappend mempty $!+ V.map toAct $! V.fromList $! consolidate 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 = do+ pre <- getsHS _splicePrefix+ res <- getsHS (H.lookup nm . _compiledSpliceMap)+ if isNothing res && T.isPrefixOf pre nm && not (T.null pre)+ then do+ tellSpliceError $ "No splice bound for " `mappend` nm+ return Nothing+ else return res+++------------------------------------------------------------------------------+-- | Runs a single node. If there is no splice referenced anywhere in the+-- subtree, then it is rendered as a pure chunk, otherwise it calls+-- compileNode to generate the appropriate runtime computation.+runNode :: Monad n => X.Node -> Splice n+runNode node = localParamNode (const node) $ do+ hs <- getHS+ let pre = _splicePrefix hs+ let hasPrefix = (T.isPrefixOf pre `fmap` X.tagName node) == Just True+ when (not (T.null pre) && hasPrefix) incNamespacedTags+ hs' <- getHS+ -- Plain rethrows for CompileException to avoid multiple annotations.+ (res, hs'') <- liftIO $ catches (compileIO hs')+ [ Handler (\(ex :: CompileException) -> throwIO ex)+ , Handler (\(ex :: SomeException) -> handleError ex hs')]+ putHS hs''+ return res+ where+ localSplicePath =+ localHS (\hs -> hs {_splicePath = (_curContext hs,+ _curTemplateFile hs,+ X.elementTag node):+ (_splicePath hs)})+ compileIO hs = runHeistT compile node hs+ compile = do+ isStatic <- subtreeIsStatic node+ dl <- compile' isStatic+ liftIO $ evaluate $ DL.fromList $! consolidate dl+ compile' True = do+ markup <- getsHS _curMarkup+ return $! yieldPure $! renderFragment markup [parseAttrs node]+ compile' False = localSplicePath $ compileNode node+ handleError ex hs = do+ errs <- evalHeistT (do localSplicePath $ tellSpliceError $ T.pack $+ "Exception in splice compile: " ++ show ex+ getsHS _spliceErrors) node hs+ throwIO $ CompileException ex errs+++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 (Ident i) = T.concat ["${", i, "}"]++------------------------------------------------------------------------------+-- | 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 _ ) -> []+++------------------------------------------------------------------------------+-- | Given a 'X.Node' in the DOM tree, produces a \"runtime splice\" that will+-- generate html at runtime.+compileNode :: Monad n => X.Node -> Splice n+compileNode (X.Element nm attrs ch) = do+ msplice <- lookupSplice nm+ fromMaybe compileStaticElement msplice+ 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 <- runAttributes attrs++ childHtml <- runNodeList ch++ return $! if null (DL.toList childHtml) && nm `S.member` X.voidTags+ 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"+++------------------------------------------------------------------------------+-- |+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 (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 $ do+ res <- splice v+ return $ mconcat $ map attrToBuilder res+++------------------------------------------------------------------------------+-- |+parseAtt2 :: Monad n+ => (Text, Text)+ -> HeistT n IO (RuntimeSplice n [(Text, Text)])+parseAtt2 (k,v) = do+ mas <- getsHS (H.lookup k . _attrSpliceMap)+ maybe doInline (return . doAttrSplice) mas++ where+ cvt (Literal x) = return $ return x+ cvt (Ident x) =+ localParamNode (const $ X.Element x [] []) $ getAttributeSplice2 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+ return $ do+ list <- sequence chunks+ return [(k, T.concat list)]++ -- Handles attribute splices+ doAttrSplice splice = splice v+++------------------------------------------------------------------------------+-- | Performs splice processing on a list of attributes. This is useful in+-- situations where you need to stop recursion, but still run splice+-- processing on the node's attributes.+runAttributes :: Monad n+ => [(Text, Text)] -- ^ List of attributes+ -> HeistT n IO [DList (Chunk n)]+runAttributes = mapM parseAtt+++------------------------------------------------------------------------------+-- | Performs splice processing on a list of attributes. This is useful in+-- situations where you need to stop recursion, but still run splice+-- processing on the node's attributes.+runAttributesRaw :: Monad n+ -- Note that this parameter should not be changed to Splices+ => [(Text, Text)] -- ^ List of attributes+ -> HeistT n IO (RuntimeSplice n [(Text, Text)])+runAttributesRaw attrs = do+ arrs <- mapM parseAtt2 attrs+ return $ liftM concat $ sequence arrs+++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+ [ fromText " "+ , fromText k+ ]+ | otherwise = mconcat+ [ fromText " "+ , fromText k+ , fromText "=\""+ , fromText v+ , fromText "\""+ ]+++------------------------------------------------------------------------------+getAttributeSplice :: Text -> HeistT n IO (DList (Chunk n))+getAttributeSplice name =+ lookupSplice name >>= fromMaybe+ (return $ DL.singleton $ Pure $ T.encodeUtf8 $+ T.concat ["${", name, "}"])+{-# INLINE getAttributeSplice #-}+++getAttributeSplice2 :: Monad n => Text -> HeistT n IO (RuntimeSplice n Text)+getAttributeSplice2 name = do+ mSplice <- lookupSplice name+ case mSplice of+ Nothing -> return $ return $ T.concat ["${", name, "}"]+ Just splice -> do+ res <- splice+ return $ liftM (T.decodeUtf8 . toByteString) $ codeGen res+{-# INLINE getAttributeSplice2 #-}+++------------------------------------------------------------------------------+-- | 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 n) => Promise a -> RuntimeSplice n 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 n) => Promise a -> a -> RuntimeSplice n ()+putPromise (Promise k) x = modify (HE.insert k x)+{-# INLINE putPromise #-}+++------------------------------------------------------------------------------+-- | Modifies a promise.+adjustPromise :: Monad n => Promise a -> (a -> a) -> RuntimeSplice n ()+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 #-}+++------------------------------------------------------------------------------+-- | 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) }+ where+ n' = _splicePrefix ts `mappend` n++------------------------------------------------------------------------------+-- | Binds a list of compiled splices. This function should not be exported.+bindSplices :: Splices (Splice n) -- ^ splices to bind+ -> HeistState n -- ^ source state+ -> HeistState n+bindSplices ss hs =+ hs { _compiledSpliceMap = applySpliceMap hs _compiledSpliceMap 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.+withLocalSplices :: Splices (Splice n)+ -> Splices (AttrSplice n)+ -> HeistT n IO a+ -> HeistT n IO a+withLocalSplices ss as = localHS (bindSplices ss . bindAttributeSplices as)+++------------------------------------------------------------------------------+-- | Looks up a compiled template and returns a runtime monad computation that+-- constructs a builder.+--+-- Note that template names should not include the .tpl extension:+--+-- @renderTemplate hs "index"@+renderTemplate :: Monad n+ => HeistState n+ -> ByteString+ -> Maybe (n Builder, MIMEType)+renderTemplate hs nm =+ fmap (first (interpret . DL.fromList) . fst) $!+ lookupTemplate nm hs _compiledTemplateMap+++------------------------------------------------------------------------------+-- | Looks up a compiled template and returns a compiled splice.+callTemplate :: Monad n+ => ByteString+ -> Splice n+callTemplate nm = do+ hs <- getHS+ maybe (error err) call $ lookupTemplate nm hs _templateMap+ where+ err = "callTemplate: "++(T.unpack $ T.decodeUtf8 nm)++(" does not exist")+ call (df,_) = localHS (\hs' -> hs' {_curTemplateFile = dfFile df}) $+ runNodeList $ X.docContent $ dfDoc df+++interpret :: Monad n => DList (Chunk n) -> n Builder+interpret = flip evalStateT HE.empty . unRT . codeGen+++------------------------------------------------------------------------------+-- | Converts a pure text splice function to a pure Builder splice function.+textSplice :: (a -> Text) -> a -> Builder+textSplice f = fromText . f+++ ---------------+ -- New Stuff --+ ---------------+++------------------------------------------------------------------------------+-- | This is the same as htmlNodeSplice. +nodeSplice :: (a -> [X.Node]) -> a -> Builder+nodeSplice f = X.renderHtmlFragment X.UTF8 . f+{-# DEPRECATED nodeSplice+ "Use xmlNodeSplice or htmlNodeSplice, will be removed in Heist 1.1" #-}+++------------------------------------------------------------------------------+-- | Converts a pure XML Node splice function to a pure Builder splice+-- function.+xmlNodeSplice :: (a -> [X.Node]) -> a -> Builder+xmlNodeSplice f = X.renderXmlFragment X.UTF8 . f+++------------------------------------------------------------------------------+-- | Converts a pure HTML Node splice function to a pure Builder splice+-- function.+htmlNodeSplice :: (a -> [X.Node]) -> a -> Builder+htmlNodeSplice f = X.renderHtmlFragment X.UTF8 . f+++------------------------------------------------------------------------------+-- | Converts a pure Builder splice function into a monadic splice function+-- of a RuntimeSplice.+pureSplice :: Monad n => (a -> Builder) -> RuntimeSplice n a -> Splice n+pureSplice f n = return $ yieldRuntime (return . f =<< n)+++------------------------------------------------------------------------------+-- | Runs a splice, but first binds splices given by splice functions that+-- need some runtime data.+withSplices :: Monad n+ => Splice n+ -- ^ Splice to be run+ -> Splices (RuntimeSplice n a -> Splice n)+ -- ^ Splices to be bound first+ -> RuntimeSplice n a+ -- ^ Runtime data needed by the above splices+ -> Splice n+withSplices splice splices runtimeAction =+ withLocalSplices splices' mempty splice+ where+ splices' = mapV ($ runtimeAction) splices+++------------------------------------------------------------------------------+{-# INLINE foldMapM #-}+foldMapM :: (Monad f, Monoid m, Foldable list)+ => (a -> f m)+ -> list a+ -> f m+foldMapM f =+ Foldable.foldlM (\xs x -> xs `seq` liftM (xs <>) (f x)) mempty++------------------------------------------------------------------------------+-- | Like withSplices, but evaluates the splice repeatedly for each element in+-- a list generated at runtime.+manyWithSplices :: (Foldable f, Monad n)+ => Splice n+ -> Splices (RuntimeSplice n a -> Splice n)+ -> RuntimeSplice n (f a)+ -> Splice n+manyWithSplices splice splices runtimeAction =+ manyWith splice splices mempty runtimeAction+++------------------------------------------------------------------------------+-- | More powerful version of manyWithSplices that lets you also define+-- attribute splices.+manyWith :: (Foldable f, Monad n)+ => Splice n+ -> Splices (RuntimeSplice n a -> Splice n)+ -> Splices (RuntimeSplice n a -> AttrSplice n)+ -> RuntimeSplice n (f a)+ -> Splice n+manyWith splice splices attrSplices runtimeAction = do+ p <- newEmptyPromise+ let splices' = mapV ($ getPromise p) splices+ let attrSplices' = mapV ($ getPromise p) attrSplices+ chunks <- withLocalSplices splices' attrSplices' splice+ return $ yieldRuntime $ do+ items <- runtimeAction+ foldMapM (\item -> putPromise p item >> codeGen chunks) items+++------------------------------------------------------------------------------+-- | Similar to 'mapSplices' in interpreted mode. Gets a runtime list of+-- items and applies a compiled runtime splice function to each element of the+-- list.+deferMany :: (Foldable f, Monad n)+ => (RuntimeSplice n a -> Splice n)+ -> RuntimeSplice n (f a)+ -> Splice n+deferMany f getItems = do+ promise <- newEmptyPromise+ chunks <- f $ getPromise promise+ return $ yieldRuntime $ do+ items <- getItems+ foldMapM (\item -> putPromise promise item >> codeGen chunks) items+++------------------------------------------------------------------------------+-- | Saves the results of a runtime computation in a 'Promise' so they don't+-- get recalculated if used more than once.+--+-- Note that this is just a specialized version of function application ($)+-- done for the side effect in runtime splice.+defer :: Monad n+ => (RuntimeSplice n a -> Splice n)+ -> RuntimeSplice n a -> Splice n+defer pf n = do+ p2 <- newEmptyPromise+ let action = yieldRuntimeEffect $ putPromise p2 =<< n+ res <- pf $ getPromise p2+ return $ action `mappend` res+++------------------------------------------------------------------------------+-- | A version of defer which applies a function on the runtime value.+deferMap :: Monad n+ => (a -> RuntimeSplice n b)+ -> (RuntimeSplice n b -> Splice n)+ -> RuntimeSplice n a -> Splice n+deferMap f pf n = defer pf $ f =<< n+++------------------------------------------------------------------------------+-- | Like deferMap, but only runs the result if a Maybe function of the+-- runtime value returns Just. If it returns Nothing, then no output is+-- generated.+mayDeferMap :: Monad n+ => (a -> RuntimeSplice n (Maybe b))+ -> (RuntimeSplice n b -> Splice n)+ -> RuntimeSplice n a -> Splice n+mayDeferMap f pf n = deferMany pf $ f =<< n+++------------------------------------------------------------------------------+-- | Converts an RuntimeSplice into a Splice, given a helper function that+-- generates a Builder.+bindLater :: (Monad n)+ => (a -> RuntimeSplice n Builder)+ -> RuntimeSplice n a+ -> Splice n+bindLater f p = return $ yieldRuntime $ f =<< p++
+ src/Heist/Compiled/LowLevel.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeSynonymInstances #-}++module Heist.Compiled.LowLevel+ (+ -- * Lower level promise functions+ Promise+ , newEmptyPromise+ , getPromise+ , putPromise+ , adjustPromise++ ) where++import Heist.Compiled.Internal
+ src/Heist/Internal/Types.hs view
@@ -0,0 +1,279 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UndecidableInstances #-}++{-|++Internal types and accessors. There are no guarantees that heist will+preserve backwards compatibility for symbols in this module. If you use them,+no complaining when your code breaks.++-}++module Heist.Internal.Types+ ( module Heist.Internal.Types.HeistState+ , module Heist.Internal.Types+ ) where++------------------------------------------------------------------------------+import Data.HashMap.Strict (HashMap)+import Data.Text (Text)++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative+#endif+#if !MIN_VERSION_base(4,11,0)+import Data.Semigroup+#endif++------------------------------------------------------------------------------+import qualified Heist.Compiled.Internal as C+import qualified Heist.Interpreted.Internal as I+import Heist.Internal.Types.HeistState+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+type TemplateRepo = HashMap TPath DocumentFile+++------------------------------------------------------------------------------+-- | An IO action for getting a template repo from this location. By not just+-- using a directory path here, we support templates loaded from a database,+-- retrieved from the network, or anything else you can think of.+type TemplateLocation = IO (Either [String] TemplateRepo)+++------------------------------------------------------------------------------+-- | My lens creation function to avoid a dependency on lens.+lens :: Functor f => (t1 -> t) -> (t1 -> a -> b) -> (t -> f a) -> t1 -> f b+lens sa sbt afb s = sbt s <$> afb (sa s)+++------------------------------------------------------------------------------+-- | The splices and templates Heist will use. To bind a splice simply+-- include it in the appropriate place here.+data SpliceConfig m = SpliceConfig+ { _scInterpretedSplices :: Splices (I.Splice m)+ -- ^ Interpreted splices are the splices that Heist has always had.+ -- They return a list of nodes and are processed at runtime.+ , _scLoadTimeSplices :: Splices (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.+ , _scCompiledSplices :: Splices (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.+ , _scAttributeSplices :: Splices (AttrSplice m)+ -- ^ Attribute splices are bound to attribute names and return a list+ -- of attributes.+ , _scTemplateLocations :: [TemplateLocation]+ -- ^ A list of all the locations that Heist should get its templates+ -- from.+ , _scCompiledTemplateFilter :: TPath -> Bool+ -- ^ Predicate function to control which templates to compile. Using+ -- templates filtered out with this is still possible via+ -- callTemplate.+ }+++------------------------------------------------------------------------------+-- | Lens for interpreted splices+-- :: Simple Lens (SpliceConfig m) (Splices (I.Splice m))+scInterpretedSplices+ :: Functor f+ => (Splices (I.Splice m) -> f (Splices (I.Splice m)))+ -> SpliceConfig m -> f (SpliceConfig m)+scInterpretedSplices = lens _scInterpretedSplices setter+ where+ setter sc v = sc { _scInterpretedSplices = v }+++------------------------------------------------------------------------------+-- | Lens for load time splices+-- :: Simple Lens (SpliceConfig m) (Splices (I.Splice IO))+scLoadTimeSplices+ :: Functor f+ => (Splices (I.Splice IO) -> f (Splices (I.Splice IO)))+ -> SpliceConfig m -> f (SpliceConfig m)+scLoadTimeSplices = lens _scLoadTimeSplices setter+ where+ setter sc v = sc { _scLoadTimeSplices = v }+++------------------------------------------------------------------------------+-- | Lens for complied splices+-- :: Simple Lens (SpliceConfig m) (Splices (C.Splice m))+scCompiledSplices+ :: Functor f+ => (Splices (C.Splice m) -> f (Splices (C.Splice m)))+ -> SpliceConfig m -> f (SpliceConfig m)+scCompiledSplices = lens _scCompiledSplices setter+ where+ setter sc v = sc { _scCompiledSplices = v }+++------------------------------------------------------------------------------+-- | Lens for attribute splices+-- :: Simple Lens (SpliceConfig m) (Splices (AttrSplice m))+scAttributeSplices+ :: Functor f+ => (Splices (AttrSplice m) -> f (Splices (AttrSplice m)))+ -> SpliceConfig m -> f (SpliceConfig m)+scAttributeSplices = lens _scAttributeSplices setter+ where+ setter sc v = sc { _scAttributeSplices = v }+++------------------------------------------------------------------------------+-- | Lens for template locations+-- :: Simple Lens (SpliceConfig m) [TemplateLocation]+scTemplateLocations+ :: Functor f+ => ([TemplateLocation] -> f [TemplateLocation])+ -> SpliceConfig m -> f (SpliceConfig m)+scTemplateLocations = lens _scTemplateLocations setter+ where+ setter sc v = sc { _scTemplateLocations = v }+++------------------------------------------------------------------------------+-- | Lens for compiled template filter+-- :: Simple Lens (SpliceConfig m) (TBool -> Bool)+scCompiledTemplateFilter+ :: Functor f+ => ((TPath -> Bool) -> f (TPath -> Bool))+ -> SpliceConfig m -> f (SpliceConfig m)+scCompiledTemplateFilter = lens _scCompiledTemplateFilter setter+ where+ setter sc v = sc { _scCompiledTemplateFilter = v }+++instance Semigroup (SpliceConfig m) where+ SpliceConfig a1 b1 c1 d1 e1 f1 <> SpliceConfig a2 b2 c2 d2 e2 f2 =+ SpliceConfig (a1 <> a2) (b1 <> b2) (c1 <> c2)+ (d1 <> d2) (e1 <> e2) (\x -> f1 x && f2 x)++instance Monoid (SpliceConfig m) where+ mempty = SpliceConfig mempty mempty mempty mempty mempty (const True)+#if !MIN_VERSION_base(4,11,0)+ mappend = (<>)+#endif+++data HeistConfig m = HeistConfig+ { _hcSpliceConfig :: SpliceConfig m+ -- ^ Splices and templates+ , _hcNamespace :: Text+ -- ^ A namespace to use for all tags that are bound to splices. Use+ -- empty string for no namespace.+ , _hcErrorNotBound :: Bool+ -- ^ Whether to throw an error when a tag wih the heist namespace does+ -- not correspond to a bound splice. When not using a namespace, this+ -- flag is ignored.+ }+++------------------------------------------------------------------------------+-- | Lens for the SpliceConfig+-- :: Simple Lens (HeistConfig m) (SpliceConfig m)+hcSpliceConfig+ :: Functor f+ => ((SpliceConfig m) -> f (SpliceConfig m))+ -> HeistConfig m -> f (HeistConfig m)+hcSpliceConfig = lens _hcSpliceConfig setter+ where+ setter hc v = hc { _hcSpliceConfig = v }+++------------------------------------------------------------------------------+-- | Lens for the namespace+-- :: Simple Lens (HeistConfig m) Text+hcNamespace+ :: Functor f+ => (Text -> f Text)+ -> HeistConfig m -> f (HeistConfig m)+hcNamespace = lens _hcNamespace setter+ where+ setter hc v = hc { _hcNamespace = v }+++------------------------------------------------------------------------------+-- | Lens for the namespace error flag+-- :: Simple Lens (HeistConfig m) Bool+hcErrorNotBound+ :: Functor f+ => (Bool -> f Bool)+ -> HeistConfig m -> f (HeistConfig m)+hcErrorNotBound = lens _hcErrorNotBound setter+ where+ setter hc v = hc { _hcErrorNotBound = v }+++------------------------------------------------------------------------------+-- | Lens for interpreted splices+-- :: Simple Lens (HeistConfig m) (Splices (I.Splice m))+hcInterpretedSplices+ :: Functor f+ => (Splices (I.Splice m) -> f (Splices (I.Splice m)))+ -> HeistConfig m -> f (HeistConfig m)+hcInterpretedSplices = hcSpliceConfig . scInterpretedSplices+++------------------------------------------------------------------------------+-- | Lens for load time splices+-- :: Simple Lens (HeistConfig m) (Splices (I.Splice IO))+hcLoadTimeSplices+ :: Functor f+ => (Splices (I.Splice IO) -> f (Splices (I.Splice IO)))+ -> HeistConfig m -> f (HeistConfig m)+hcLoadTimeSplices = hcSpliceConfig . scLoadTimeSplices +++------------------------------------------------------------------------------+-- | Lens for compiled splices+-- :: Simple Lens (HeistConfig m) (Splices (C.Splice m))+hcCompiledSplices+ :: Functor f+ => (Splices (C.Splice m) -> f (Splices (C.Splice m)))+ -> HeistConfig m -> f (HeistConfig m)+hcCompiledSplices = hcSpliceConfig . scCompiledSplices +++------------------------------------------------------------------------------+-- | Lens for attribute splices+-- :: Simple Lens (HeistConfig m) (Splices (AttrSplice m))+hcAttributeSplices+ :: Functor f+ => (Splices (AttrSplice m) -> f (Splices (AttrSplice m)))+ -> HeistConfig m -> f (HeistConfig m)+hcAttributeSplices = hcSpliceConfig . scAttributeSplices +++------------------------------------------------------------------------------+-- | Lens for template locations+-- :: Simple Lens (HeistConfig m) [TemplateLocation]+hcTemplateLocations+ :: Functor f+ => ([TemplateLocation] -> f [TemplateLocation])+ -> HeistConfig m -> f (HeistConfig m)+hcTemplateLocations = hcSpliceConfig . scTemplateLocations +++------------------------------------------------------------------------------+-- | Lens for compiled template filter+-- :: Simple Lens (SpliceConfig m) (TBool -> Bool)+hcCompiledTemplateFilter+ :: Functor f+ => ((TPath -> Bool) -> f (TPath -> Bool))+ -> HeistConfig m -> f (HeistConfig m)+hcCompiledTemplateFilter = hcSpliceConfig . scCompiledTemplateFilter++
+ src/Heist/Internal/Types/HeistState.hs view
@@ -0,0 +1,680 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# 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.Internal.Types.HeistState where++------------------------------------------------------------------------------+import Blaze.ByteString.Builder (Builder)+import Control.Applicative (Alternative (..))+import Control.Arrow (first)+import Control.Exception (Exception)+import Control.Monad (MonadPlus (..), ap)+import Control.Monad.Base+import Control.Monad.Cont (MonadCont (..))+#if MIN_VERSION_mtl(2,2,1)+import Control.Monad.Except (MonadError (..))+#else+import Control.Monad.Error (MonadError (..))+#endif+#if MIN_VERSION_base(4,9,0)+import qualified Control.Monad.Fail as Fail+#endif+import Control.Monad.Fix (MonadFix (..))+import Control.Monad.Reader (MonadReader (..))+import Control.Monad.State.Strict (MonadState (..), StateT)+import Control.Monad.Trans (MonadIO (..), MonadTrans (..))+import Control.Monad.Trans.Control+import Data.ByteString.Char8 (ByteString)+import Data.DList (DList)+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as H+import Data.HeterogeneousEnvironment (HeterogeneousEnvironment)+import qualified Data.HeterogeneousEnvironment as HE+import Data.Map.Syntax+#if !MIN_VERSION_base(4,11,0)+import Data.Semigroup+#endif+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Encoding (decodeUtf8)+#if MIN_VERSION_base (4,7,0)+import Data.Typeable (Typeable)+#else+import Data.Typeable (TyCon, Typeable(..),+ Typeable1(..), mkTyCon,+ mkTyConApp)+#endif++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative (Applicative (..), (<$>))+import Data.Monoid (Monoid(..))+#endif++import qualified Text.XmlHtml as X+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | Convenient type alies for splices.+type Splices s = MapSyntax Text s+++------------------------------------------------------------------------------+-- | 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, Show+#if MIN_VERSION_base(4,7,0)+ , Typeable+#endif+ )+++------------------------------------------------------------------------------+-- | Designates whether a document should be treated as XML or HTML.+data Markup = Xml | Html+++------------------------------------------------------------------------------+-- | Monad used for runtime splice execution.+newtype RuntimeSplice m a = RuntimeSplice {+ unRT :: StateT HeterogeneousEnvironment m a+ } deriving ( Applicative+ , Functor+ , Monad+ , MonadIO+ , MonadState HeterogeneousEnvironment+ , MonadTrans+#if MIN_VERSION_base(4,7,0)+ , Typeable+#endif+ )+++------------------------------------------------------------------------------+instance (Monad m, Semigroup a) => Semigroup (RuntimeSplice m a) where+ a <> b = do+ !x <- a+ !y <- b+ return $! x <> y+++#if !MIN_VERSION_base(4,11,0)+instance (Monad m, Semigroup a, Monoid a) => Monoid (RuntimeSplice m a) where+#else+instance (Monad m, Monoid a) => Monoid (RuntimeSplice m a) where+#endif+ mempty = return mempty+#if !MIN_VERSION_base(4,11,0)+ mappend = (<>)+#endif+++------------------------------------------------------------------------------+-- | 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+#if MIN_VERSION_base(4,7,0)+ deriving Typeable+#endif++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 -> RuntimeSplice m [(Text, Text)]+++------------------------------------------------------------------------------+-- | Detailed information about a splice error.+data SpliceError = SpliceError+ { spliceHistory :: [(TPath, Maybe FilePath, Text)]+ , spliceTemplateFile :: Maybe FilePath+ , visibleSplices :: [Text]+ , contextNode :: X.Node+ , spliceMsg :: Text+ } deriving ( Show, Eq )+++------------------------------------------------------------------------------+-- | Transform a SpliceError record to a Text message.+spliceErrorText :: SpliceError -> Text+spliceErrorText (SpliceError hist tf splices node msg) =+ (maybe "" ((`mappend` ": ") . T.pack) tf) `T.append` msg `T.append`+ foldr (\(_, tf', tag) -> (("\n ... via " `T.append`+ (maybe "" ((`mappend` ": ") . T.pack) tf')+ `T.append` tag) `T.append`)) T.empty hist+ `T.append`+ if null splices+ then T.empty+ else "\nBound splices:" `T.append`+ foldl (\x y -> x `T.append` " " `T.append` y) T.empty splices+ `T.append`+ (T.pack $ "\nNode: " ++ (show node))+++------------------------------------------------------------------------------+-- | Exception type for splice compile errors. Wraps the original+-- exception and provides context.+--data (Exception e) => CompileException e = CompileException+data CompileException = forall e . Exception e => CompileException+ { originalException :: e+ -- The list of splice errors. The head of it has the context+ -- related to the exception.+ , exceptionContext :: [SpliceError]+ } deriving ( Typeable )+++instance Show CompileException where+ show (CompileException e []) =+ "Heist load exception (unknown context): " ++ (show e)+ show (CompileException _ (c:_)) = (T.unpack $ spliceErrorText c)+++instance Exception CompileException+++------------------------------------------------------------------------------+-- | 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+ -- | Stack of the splices used.+ , _splicePath :: [(TPath, Maybe FilePath, Text)]+ -- | 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++ -- | This is needed because compiled templates are generated with a bunch+ -- of calls to renderFragment rather than a single call to render.+ , _curMarkup :: Markup++ -- | A prefix for all splices (namespace ++ ":").+ , _splicePrefix :: Text++ -- | List of errors encountered during splice processing.+ , _spliceErrors :: [SpliceError]++ -- | Whether to throw an error when a tag wih the heist namespace does not+ -- correspond to a bound splice. When not using a namespace, this flag is+ -- ignored.+ , _errorNotBound :: Bool+ , _numNamespacedTags :: Int+#if MIN_VERSION_base(4,7,0)+} deriving (Typeable)+#else+}+#endif++#if !MIN_VERSION_base(4,7,0)+-- 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 ())]++#endif++------------------------------------------------------------------------------+-- | 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)+#if MIN_VERSION_base(4,7,0)+} deriving Typeable+#else+}+#endif+++------------------------------------------------------------------------------+-- | 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 interpreted splices defined in a HeistState.+spliceNames :: HeistState m -> [Text]+spliceNames ts = H.keys $ _spliceMap ts+++------------------------------------------------------------------------------+-- | Gets the names of all the compiled splices defined in a HeistState.+compiledSpliceNames :: HeistState m -> [Text]+compiledSpliceNames ts = H.keys $ _compiledSpliceMap ts+++#if !MIN_VERSION_base(4,7,0)+------------------------------------------------------------------------------+-- | The Typeable instance is here so Heist can be dynamically executed with+-- Hint.+templateStateTyCon :: TyCon+templateStateTyCon = mkTyCon "Heist.HeistState"+{-# NOINLINE templateStateTyCon #-}+#endif+++------------------------------------------------------------------------------+-- | 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 (>>=) #-}+++#if MIN_VERSION_base(4,9,0)+------------------------------------------------------------------------------+-- | MonadFail instance+instance Fail.MonadFail m => Fail.MonadFail (HeistT n m) where+ fail = lift . Fail.fail+#endif+++------------------------------------------------------------------------------+-- | 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)+++instance MonadBase b m => MonadBase b (HeistT n m) where+ liftBase = lift . liftBase++#if MIN_VERSION_monad_control(1,0,0)+instance MonadTransControl (HeistT n) where+ type StT (HeistT n) a = (a, HeistState n)+ liftWith f = HeistT $ \n s -> do+ res <- f $ \(HeistT g) -> g n s+ return (res, s)+ restoreT k = HeistT $ \_ _ -> k+ {-# INLINE liftWith #-}+ {-# INLINE restoreT #-}+++instance MonadBaseControl b m => MonadBaseControl b (HeistT n m) where+ type StM (HeistT n m) a = ComposeSt (HeistT n) m a+ liftBaseWith = defaultLiftBaseWith+ restoreM = defaultRestoreM+ {-# INLINE liftBaseWith #-}+ {-# INLINE restoreM #-}+#else+instance MonadTransControl (HeistT n) where+ newtype StT (HeistT n) a = StHeistT {unStHeistT :: (a, HeistState n)}+ liftWith f = HeistT $ \n s -> do+ res <- f $ \(HeistT g) -> liftM StHeistT $ g n s+ return (res, s)+ restoreT k = HeistT $ \_ _ -> liftM unStHeistT k+ {-# INLINE liftWith #-}+ {-# INLINE restoreT #-}+++instance MonadBaseControl b m => MonadBaseControl b (HeistT n m) where+ newtype StM (HeistT n m) a = StMHeist {unStMHeist :: ComposeSt (HeistT n) m a}+ liftBaseWith = defaultLiftBaseWith StMHeist+ restoreM = defaultRestoreM unStMHeist+ {-# INLINE liftBaseWith #-}+ {-# INLINE restoreM #-}+#endif++------------------------------------------------------------------------------+-- | 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+++#if !MIN_VERSION_base(4,7,0)+------------------------------------------------------------------------------+-- | 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 ())]+#endif+++------------------------------------------------------------------------------+-- 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 $ curry return+{-# 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 and splice errors. 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+ , _numNamespacedTags =+ _numNamespacedTags cur+ , _spliceErrors = _spliceErrors 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) })+++------------------------------------------------------------------------------+-- | Increments the namespaced tag count+incNamespacedTags :: Monad m => HeistT n m ()+incNamespacedTags =+ modifyHS (\st -> st { _numNamespacedTags = _numNamespacedTags st + 1 })+++------------------------------------------------------------------------------+-- | 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)+++------------------------------------------------------------------------------+isIdent :: AttAST -> Bool+isIdent (Ident _) = True+isIdent _ = False++
+ src/Heist/Interpreted.hs view
@@ -0,0 +1,83 @@+{-|++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+ , bindAttributeSplices++ -- * Functions for creating splices+ , textSplice+ , runChildren+ , runChildrenWith+ , runChildrenWithTrans+ , runChildrenWithTemplates+ , runChildrenWithText+ , mapSplices++ -- * HeistT functions+ , stopRecursion+ , runNode+ , runAttributes+ , runNodeList+ , evalTemplate+ , bindStrings+ , bindString+ , callTemplate+ , callTemplateWithText+ , renderTemplate+ , renderTemplateToDoc+ , renderWithArgs+ ) where++import Heist.Interpreted.Internal+import Heist.Common (mapSplices, bindAttributeSplices)+
+ src/Heist/Interpreted/Internal.hs view
@@ -0,0 +1,423 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Heist.Interpreted.Internal where++------------------------------------------------------------------------------+import Blaze.ByteString.Builder+import Control.Monad+import Control.Monad.State.Strict+import qualified Data.Attoparsec.Text as AP+import Data.ByteString (ByteString)+import qualified Data.HashMap.Strict as Map+import qualified Data.HeterogeneousEnvironment as HE+import Data.Map.Syntax+import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as T+import qualified Text.XmlHtml as X+------------------------------------------------------------------------------+import Heist.Common+import Heist.Internal.Types.HeistState+------------------------------------------------------------------------------+++type Splice n = HeistT n n Template+++------------------------------------------------------------------------------+-- 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 hs = hs {_spliceMap = Map.insert n v (_spliceMap hs)}+++------------------------------------------------------------------------------+-- | Binds a set of new splice declarations within a 'HeistState'.+bindSplices :: Splices (Splice n) -- ^ splices to bind+ -> HeistState n -- ^ start state+ -> HeistState n+bindSplices ss hs =+ hs { _spliceMap = applySpliceMap hs _spliceMap ss }+++------------------------------------------------------------------------------+-- | Converts 'Text' to a splice returning a single 'TextNode'.+textSplice :: Monad m => Text -> HeistT n m Template+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)+ => Splices (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+ -> Splices b+ -- ^ List of tuples to be bound+ -> Splice n+runChildrenWithTrans f = runChildrenWith . mapV f+++------------------------------------------------------------------------------+-- | Like runChildrenWith but using constant templates rather than dynamic+-- splices.+runChildrenWithTemplates :: (Monad n) => Splices Template -> Splice n+runChildrenWithTemplates = runChildrenWithTrans return+++------------------------------------------------------------------------------+-- | Like runChildrenWith but using literal text rather than dynamic splices.+runChildrenWithText :: (Monad n) => Splices Text -> Splice n+runChildrenWithText = runChildrenWithTrans textSplice+++------------------------------------------------------------------------------+-- | Convenience function for looking up a splice.+lookupSplice :: Text+ -> HeistState n+ -> Maybe (Splice n)+lookupSplice nm hs = Map.lookup nm $ _spliceMap hs+{-# 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 <- runAttributes 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]+++------------------------------------------------------------------------------+-- | Performs splice processing on a list of attributes. This is useful in+-- situations where you need to stop recursion, but still run splice+-- processing on the node's attributes.+runAttributes :: Monad n => [(Text, Text)] -> HeistT n n [(Text, Text)]+runAttributes attrs = (return . concat) =<< mapM runAttrSplice attrs+++------------------------------------------------------------------------------+-- | 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 . flip evalStateT HE.empty . unRT . ($ 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 (Ident x) =+ localParamNode (const $ X.Element x [] []) $ getAttributeSplice x+++------------------------------------------------------------------------------+-- | 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 = return $ T.concat ["${", name, "}"]+ 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 do modifyHS (\st -> st { _recurse = True })+ 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+ hs <- getHS+ let mt = lookupTemplate name hs _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 (\hs -> hs {_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+ hs <- getHS+ let nodes = X.docContent $ dfDoc t+ putHS (hs {_curContext = ctx})+ newNodes <- runNodeList nodes+ restoreHS hs+ newDoc <- fixDocType $ (dfDoc t) { X.docContent = newNodes }+ return (Just newDoc)+++------------------------------------------------------------------------------+-- | Binds a list of constant string splices.+bindStrings :: Monad n+ => Splices Text+ -> HeistState n+ -> HeistState n+bindStrings splices = bindSplices (mapV textSplice splices)+++------------------------------------------------------------------------------+-- | 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+ -> Splices (Splice n) -- ^ Splices to call the template with+ -> HeistT n n Template+callTemplate name splices = do+ modifyHS $ bindSplices splices+ 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+ -> Splices Text -- ^ Splices to call the template with+ -> HeistT n n Template+callTemplateWithText name splices = callTemplate name $ mapV textSplice splices+++------------------------------------------------------------------------------+-- | 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.+--+-- Note that template names should not include the .tpl extension:+--+-- @renderTemplate hs "index"@+renderTemplate :: Monad n+ => HeistState n+ -> ByteString+ -> n (Maybe (Builder, MIMEType))+renderTemplate hs name = evalHeistT tpl (X.TextNode "") hs+ 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+ => Splices Text+ -> HeistState n+ -> ByteString+ -> n (Maybe (Builder, MIMEType))+renderWithArgs args hs = renderTemplate (bindStrings args hs)+++renderTemplateToDoc :: Monad n+ => HeistState n+ -> ByteString+ -> n (Maybe X.Document)+renderTemplateToDoc hs name =+ evalHeistT (evalWithDoctypes name) (X.TextNode "") hs
+ src/Heist/Splices.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE CPP #-}++module Heist.Splices+ ( ifISplice+ , ifCSplice+ , ifElseISplice+ , ifElseCSplice+ , 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++#if !MIN_VERSION_base(4,8,0)+import Data.Monoid (Monoid(..))+#endif++import qualified Heist.Compiled as C+import qualified Heist.Interpreted as I+import Heist.Splices.Apply+import Heist.Splices.Bind+import Heist.Splices.Cache+import Heist.Splices.Html+import Heist.Splices.Ignore+import Heist.Splices.Markdown+import Heist.Internal.Types.HeistState+import qualified Text.XmlHtml as X++------------------------------------------------------------------------------+-- | Run the splice contents if given condition is True, make splice disappear+-- if not.+ifISplice :: Monad m => Bool -> I.Splice m+ifISplice cond =+ case cond of+ False -> return []+ True -> I.runChildren+++------------------------------------------------------------------------------+-- | Function for constructing if splices that use a runtime predicate+-- function to determine whether the node's children should be rendered.+ifCSplice :: Monad m+ => (t -> Bool)+ -> RuntimeSplice m t+ -> C.Splice m+ifCSplice predicate runtime = do+ chunks <- C.runChildren+ return $ C.yieldRuntime $ do+ a <- runtime+ if predicate a+ then+ C.codeGen chunks+ else+ return mempty+++------------------------------------------------------------------------------+-- | Implements an if\/then\/else conditional splice. It splits its children+-- around the \<else\/\> element to get the markup to be used for the two cases.+ifElseISplice :: Monad m => Bool -> I.Splice m+ifElseISplice cond = getParamNode >>= (rewrite . X.childNodes)+ where+ rewrite nodes = + let (ns, ns') = break (\n -> X.tagName n==Just "else") nodes+ in I.runNodeList $ if cond then ns else (drop 1 ns') +++------------------------------------------------------------------------------+-- | Implements an if\/then\/else conditional splice. It splits its children+-- around the \<else\/\> element to get the markup to be used for the two cases.+ifElseCSplice :: Monad m => Bool -> C.Splice m+ifElseCSplice cond = getParamNode >>= (rewrite . X.childNodes)+ where rewrite nodes = + let (ns, ns') = break (\n -> X.tagName n==Just "else") nodes+ in C.runNodeList $ if cond then ns else (drop 1 ns')
+ src/Heist/Splices/Apply.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+module Heist.Splices.Apply where++------------------------------------------------------------------------------+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.Internal.Types.HeistState+++------------------------------------------------------------------------------+-- | 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!" :: String+ 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 :: Monad 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 :: Monad 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+++------------------------------------------------------------------------------+-- | This splice crashes with an error message. Its purpose is to provide a+-- load-time warning to anyone still using the old content tag in their+-- templates. In Heist 0.10, tho content tag was replaced by two separate+-- apply-content and bind-content tags used by the apply and bind splices+-- respectively.+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,42 @@+module Heist.Splices.Bind where++------------------------------------------------------------------------------+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.Internal.Types.HeistState++-- | 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 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,36 @@+module Heist.Splices.BindStrict where++------------------------------------------------------------------------------+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.Internal.Types.HeistState++-- | Default name for the bind splice.+bindStrictTag :: Text+bindStrictTag = "bindStrict"+++------------------------------------------------------------------------------+-- | Implementation of the bind splice.+bindStrictImpl :: Monad 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,205 @@+{-# LANGUAGE CPP #-}+{-# 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 System.Random+import Text.XmlHtml++#if !MIN_VERSION_base(4,8,0)+import Data.Word (Word)+#endif++------------------------------------------------------------------------------+import qualified Heist.Compiled.Internal as C+import Heist.Interpreted.Internal+import Heist.Internal.Types.HeistState+++------------------------------------------------------------------------------+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 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.Internal.Types.HeistState+++------------------------------------------------------------------------------+-- | 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,250 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE FlexibleContexts #-}++module Heist.Splices.Json (+ bindJson+) where++------------------------------------------------------------------------------+import Control.Monad.Reader+import Data.Aeson+import qualified Data.ByteString.Char8 as S+import qualified Data.ByteString.Lazy.Char8 as L+#if MIN_VERSION_aeson(2,0,0)+import qualified Data.Aeson.KeyMap as KM+import qualified Data.Aeson.Key as K+import qualified Data.Foldable.WithIndex as FI+#else+import qualified Data.HashMap.Strict as Map+#endif+import Data.Map.Syntax+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.Blaze.Html5 ((!))+import qualified Text.Blaze.Html5 as B+import Text.Blaze.Renderer.XmlHtml+import Text.XmlHtml+------------------------------------------------------------------------------+import Heist.Interpreted.Internal+import Heist.Internal.Types.HeistState+------------------------------------------------------------------------------++ ------------+ -- 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 :: ToJSON a => a -> 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+#if MIN_VERSION_aeson(2,0,0)+ findIn (Object obj) = KM.lookup (K.fromText x) obj+#else+ findIn (Object obj) = Map.lookup x obj+#endif+ findIn (Array arr) = tryReadIndex >>= \i -> arr V.!? i+ findIn _ = Nothing++ tryReadIndex = fmap fst . listToMaybe . reads . T.unpack $ x+++------------------------------------------------------------------------------+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 :: forall n. (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 $ do+ "value" ## return [TextNode t]+ "snippet" ## asHtml t++ --------------------------------------------------------------------------+ goArray :: 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 :: Value -> (JsonMonad n n [Node]) -> Splice n+ 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 :: JsonMonad n n (Splices (Splice n))+ genericBindings = ask >>= \v -> return $ do+ "with" ## varAttrTag v explodeTag+ "snippet" ## varAttrTag v snippetTag+ "value" ## varAttrTag v valueTag+++ --------------------------------------------------------------------------+ goObject obj = do+ start <- genericBindings+#if MIN_VERSION_aeson(2,0,0)+ let bindings = FI.ifoldl' (flip bindKvp) start obj+#else+ let bindings = Map.foldlWithKey' bindKvp start obj+#endif+ lift $ runChildrenWith bindings++ --------------------------------------------------------------------------+ + bindKvp bindings k v =+#if MIN_VERSION_aeson(2,0,0)+ let k' = K.toText k+#else+ let k' = k+#endif+ newBindings = do+ T.append "with:" k' ## withValue v explodeTag+ T.append "snippet:" k' ## withValue v snippetTag+ T.append "value:" k' ## withValue v valueTag+ in bindings >> newBindings
+ src/Heist/Splices/Markdown.hs view
@@ -0,0 +1,324 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE RecordWildCards #-}+{-| 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.++You can add custom pandoc splice with 'pandocSplice'. It is not limited to+markdown input, and can process anything pandoc can.++For example you can create a page with generated table of contents, using+heist template as pandoc template.++> <!-- _wrap.tpl -->+> <html>+> <head> <title> <pageTitle/> </title> </head>+>+> <div class="nav"> <pageToc/> </div>+> <apply-content/>+> </html>++And pandoc template, which would bind @pageTitle@ and @pageToc@ splices and+applies "_wrap" template.++> <!-- _pandoc.tpl -->+> <apply template="_wrap.tpl">+> <bind tag="pageTitle"> $title$</bind>+> <bind tag="pageToc"> $toc$</bind>+> $body$+> </apply>++Bind splice pandoc splice. Set it to not wrap in div, or it will break html+from _wrap.tpl++> splices = "docmarkdown" ## pandocSplice opts+> where+> opts = setPandocArgs ["-S", "--no-wrap", "--toc"+> , "--standalone"+> , "--template", "_pandoc.tpl"+> , "--html5"]+> $ setPandocWrapDiv Nothing+> $ defaultPandocOptions+>++And then use it to render your markdown file+++> <!-- apidocs.tpl -->+> <DOCTYPE html>+> <html lang="en">+> <head>+> <link href="/static/css/site.css rel="stylesheet">+> </head>+> <body>+> <apply template="_navbar.tpl" />+> <docmarkdown file="apidocs.md"/>+> </body>++-}+module Heist.Splices.Markdown+ (+ -- * Exceptions+ PandocMissingException+ , MarkdownException+ , NoMarkdownFileException+ -- * Markdown Splice+ , markdownTag+ , markdownSplice+ -- * Generic pandoc splice+ , pandocSplice+ -- ** Pandoc Options+ , PandocOptions+ , defaultPandocOptions+ , setPandocExecutable+ , setPandocArgs+ , setPandocBaseDir+ , setPandocWrapDiv+ -- ** Lens for 'PandocOptions'+ , pandocExecutable+ , pandocArgs+ , pandocBaseDir+ , pandocWrapDiv+ ) where++------------------------------------------------------------------------------+import Control.Concurrent+import Control.Exception.Lifted+import Control.Monad+import Control.Monad.Trans+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Typeable+import System.Directory+import System.Exit+import System.FilePath.Posix+import System.IO+import System.Process+import Text.XmlHtml++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative ((<$>))+#endif++------------------------------------------------------------------------------+import Heist.Common+import Heist.Internal.Types.HeistState+import Heist.Interpreted.Internal++data PandocMissingException = PandocMissingException+ deriving (Typeable)++instance Show PandocMissingException where+ show PandocMissingException =+ "Cannot find the \"pandoc\" executable. If you have Haskell, then install it with \"cabal install\". Otherwise you can download it from http://johnmacfarlane.net/pandoc/installing.html. Then make sure it is in 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++--------------------------------------------------------------------------------++data PandocOptions = PandocOptions+ { _pandocExecutable :: FilePath+ , _pandocArgs :: [String] -- ^ Arguments to pandoc+ , _pandocBaseDir :: Maybe FilePath -- ^ Base directory for input files+ -- defaults to template path+ , _pandocWrapDiv :: Maybe Text -- ^ Wrap content in div with class+ } deriving (Eq, Ord, Show)++-- | Default options+defaultPandocOptions :: PandocOptions+defaultPandocOptions = PandocOptions "pandoc"+ []+ Nothing+ (Just "markdown")++-- | Name of pandoc executable+setPandocExecutable :: FilePath -> PandocOptions -> PandocOptions+setPandocExecutable e opt = opt { _pandocExecutable = e }++-- | Arguments passed to pandoc+setPandocArgs :: [String] -> PandocOptions -> PandocOptions+setPandocArgs args opt = opt { _pandocArgs = args }++-- | Base directory for input files, defaults to current template dir+setPandocBaseDir :: Maybe FilePath -> PandocOptions -> PandocOptions+setPandocBaseDir bd opt = opt { _pandocBaseDir = bd }++-- | Wrap pandoc output in div with class. Appends node attributes to+-- div and appends class to ones specified on node.+setPandocWrapDiv :: Maybe Text -> PandocOptions -> PandocOptions+setPandocWrapDiv wd opt = opt { _pandocWrapDiv = wd }++pandocExecutable :: Functor f =>+ (FilePath -> f FilePath) -> PandocOptions -> f PandocOptions+pandocExecutable f po = (\e -> po { _pandocExecutable = e})+ <$> f (_pandocExecutable po)++pandocArgs :: Functor f =>+ ([String] -> f [String]) -> PandocOptions -> f PandocOptions+pandocArgs f po = (\a -> po { _pandocArgs = a}) <$> f (_pandocArgs po)++pandocBaseDir :: Functor f =>+ (Maybe FilePath -> f (Maybe FilePath)) -> PandocOptions -> f PandocOptions+pandocBaseDir f po = (\b -> po {_pandocBaseDir = b }) <$> f (_pandocBaseDir po)++pandocWrapDiv :: Functor f =>+ (Maybe Text -> f (Maybe Text)) -> PandocOptions -> f PandocOptions+pandocWrapDiv f po = (\w -> po {_pandocWrapDiv = w}) <$> f (_pandocWrapDiv po)++------------------------------------------------------------------------------+-- | Default name for the markdown splice.+markdownTag :: Text+markdownTag = "markdown"++------------------------------------------------------------------------------+-- | Default markdown splice with executable "pandoc"+markdownSplice :: MonadIO m => Splice m+markdownSplice= pandocSplice defaultPandocOptions++-- | Implementation of the markdown splice.+pandocSplice :: MonadIO m => PandocOptions -> Splice m+pandocSplice PandocOptions{..} = do+ templateDir <- liftM (fmap takeDirectory) getTemplateFilePath+ pdMD <- liftIO $ findExecutable _pandocExecutable++ pandocExe <- case pdMD of+ Nothing -> liftIO $ throwIO PandocMissingException+ Just pd -> return pd+ let withDir tp = fromMaybe tp _pandocBaseDir+ pandocFile f tp = pandocWith pandocExe _pandocArgs (withDir tp) f+ tree <- getParamNode+ (source,markup) <- liftIO $+ case getAttribute "file" tree of+ Just f -> do+ m <- maybe (liftIO $ throwIO NoMarkdownFileException )+ (pandocFile (T.unpack f))+ templateDir+ return (T.unpack f,m)+ Nothing -> do+ m <- pandocWithBS pandocExe _pandocArgs $ T.encodeUtf8 $ nodeText tree+ return ("inline_splice",m)++ let ee = parseHTML source markup+ nodeAttrs = case tree of+ Element _ a _ -> a+ _ -> []+ nodeClass = lookup "class" nodeAttrs+ attrs = filter (\(name, _) -> name /= "class" && name /= "file") nodeAttrs+ case ee of+ Left e -> throw $ MarkdownException+ $ BC.pack ("Error parsing markdown output: " ++ e)+ Right d -> return $ wrapResult nodeClass attrs (docContent d)++ where+ wrapResult nodeClass attrs body = case _pandocWrapDiv of+ Nothing -> body+ Just cls -> let finalAttrs = ("class", appendClass nodeClass cls):attrs+ in [Element "div" finalAttrs body]+ appendClass Nothing cls = cls+ appendClass (Just orig) cls = T.concat [orig, " ", cls]+++pandocWith :: FilePath -> [String] -> FilePath -> FilePath -> IO ByteString+pandocWith path args templateDir inputFile = do+ (ex, sout, serr) <- readProcessWithExitCode' path args' ""++ when (isFail ex) $ throw $ MarkdownException serr+ return sout++ where+ isFail ExitSuccess = False+ isFail _ = True++ args' = args ++ [templateDir </> inputFile ]++pandocWithBS :: FilePath -> [String] -> ByteString -> IO ByteString+pandocWithBS pandocPath args s = do+ -- using the crummy string functions for convenience here+ (ex, sout, serr) <- readProcessWithExitCode' pandocPath args s++ when (isFail ex) $ throw $ MarkdownException serr+ return sout++ where+ isFail ExitSuccess = False+ isFail _ = True+++-- 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,105 @@+{-|++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.Monad+import Control.Monad.Trans+import Heist+import Heist.Internal.Types+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+ -- namespaced tag.+ -> IO (Either [String] (TemplateDirectory n))+newTemplateDirectory dir hc = do+ let sc = (_hcSpliceConfig hc) { _scTemplateLocations = [loadTemplates dir] }+ let hc' = hc { _hcSpliceConfig = sc }+ epair <- initHeistWithCacheTag hc'+ case epair of+ Left es -> return $ Left es+ Right (hs,cts) -> do+ tsMVar <- liftIO $ newMVar hs+ ctsMVar <- liftIO $ newMVar cts+ return $ Right $ 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 <- 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+ let sc = (_hcSpliceConfig hc) { _scTemplateLocations = [loadTemplates p] }+ ehs <- initHeistWithCacheTag (hc { _hcSpliceConfig = sc })+ 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: " :: String)
− 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/.ghci
@@ -1,5 +0,0 @@-:set -XOverloadedStrings-:set -Wall-:set -i../src-:set -isuite-:set -hide-package MonadCatchIO-mtl
− test/README
@@ -1,1 +0,0 @@-Various test applications and such for the Snap Framework
− test/heist-testsuite.cabal
@@ -1,40 +0,0 @@-name: heist-testsuite-version: 0.1.1-build-type: Simple-cabal-version: >= 1.6--Executable testsuite- hs-source-dirs: ../src suite- 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,- 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--- ghc-options: -O2 -Wall -fhpc -fwarn-tabs -funbox-strict-fields -threaded- Extensions: OverloadedStrings-
− test/runTestsAndCoverage.sh
@@ -1,48 +0,0 @@-#!/bin/sh--set -e--SUITE=./dist/build/testsuite/testsuite--rm -f testsuite.tix--if [ ! -f $SUITE ]; then- cat <<EOF-Testsuite executable not found, please run:- cabal configure -ftest-then- cabal build-EOF- exit;-fi--./dist/build/testsuite/testsuite -j4 -a1000 $*--DIR=dist/hpc--rm -Rf $DIR-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'--EXCL=""--for m in $EXCLUDES; do- EXCL="$EXCL --exclude=$m"-done--hpc markup $EXCL --destdir=$DIR testsuite >/dev/null 2>&1--rm -f testsuite.tix--cat <<EOF--Test coverage report written to $DIR.-EOF
+ test/suite/Benchmark.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}++module Main where++------------------------------------------------------------------------------+import Blaze.ByteString.Builder+import Control.Concurrent+import Control.Exception (evaluate)+import Control.Monad+import Control.Monad.Trans.Except (ExceptT(ExceptT), runExceptT)+import Criterion+import Criterion.Main+import Criterion.Measurement hiding (getTime)+import qualified Data.ByteString as B+import qualified Data.DList as DL+import Data.Maybe+import Data.Monoid+import qualified Data.Text as T+import Data.Text.Encoding+import Data.Time.Clock+import System.Environment+import qualified Text.XmlHtml as X+------------------------------------------------------------------------------+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.Internal.Types+------------------------------------------------------------------------------++loadWithCache baseDir = do+ etm <- runExceptT $ do+ let sc = SpliceConfig mempty defaultLoadTimeSplices mempty mempty+ [loadTemplates baseDir] (const True)+ ExceptT $ initHeistWithCacheTag $ HeistConfig sc "" False+ 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") (whnfIO 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,409 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Heist.Compiled.Tests where++import Blaze.ByteString.Builder+import Control.Applicative+import Control.Exception+import Control.Monad.Trans.Except+import Control.Lens+import Control.Monad.Trans+import Data.Bifunctor (first)+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import Data.Char+import Data.IORef+import Data.Maybe+import Data.Map.Syntax+import Data.Monoid+import qualified Data.Set as Set+import qualified Data.Text as T+import Data.Text.Encoding+import Test.Framework (Test)+import Test.Framework.Providers.HUnit+import qualified Test.HUnit as H+import qualified Text.XmlHtml as X+++------------------------------------------------------------------------------+import Heist+import Heist.Compiled+import Heist.Compiled.Internal+import Heist.Internal.Types+import Heist.Tutorial.CompiledSplices+import Heist.TestCommon++-- 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+ , testCase "compiled/namespace1" namespaceTest1+ , testCase "compiled/namespace2" namespaceTest2+ , testCase "compiled/namespace3" namespaceTest3+ , testCase "compiled/namespace4" namespaceTest4+ , testCase "compiled/namespace5" namespaceTest5+ , testCase "compiled/no-ns-splices" noNsSplices+ , testCase "compiled/ns-nested" nsNestedUnused+ , testCase "compiled/nsbind" nsBindTest+ , testCase "compiled/nsbinderr" nsBindErrorTest+ , testCase "compiled/nscall" nsCallTest+ , testCase "compiled/nscallerr" nsCallErrTest+ , testCase "compiled/nsbindstack" nsBindStackTest+ , testCase "compiled/doctype" doctypeTest+ , testCase "compiled/exceptions" exceptionsTest+ , testCase "compiled/defer" deferTest+ ]++simpleCompiledTest :: IO ()+simpleCompiledTest = do+ res <- runWithStateSplice "templates"+ H.assertEqual "compiled state splice" expected res+ where+ expected =+ mappend doctype "\n\n<html>\n3\n</html>\n"++peopleTest :: IO ()+peopleTest = do+ res <- personListTest "templates"+ H.assertEqual "people splice" expected res+ where+ expected =+ "\n<p>Doe, John: 42 years old</p>\n\n<p>Smith, Jane: 21 years old</p>\n\n"++templateHC :: HeistConfig IO+templateHC = HeistConfig sc "" False+ where+ sc = mempty & scLoadTimeSplices .~ defaultLoadTimeSplices+ & scCompiledSplices .~ ("foo" ## return (yieldPureText "aoeu"))+ & scTemplateLocations .~ [loadTemplates "templates"]++genericTest :: String -> ByteString -> ByteString -> IO ()+genericTest nm template expected = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist templateHC+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs template+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual nm (Right expected) res++doctypeTest :: IO ()+doctypeTest = genericTest "doctype test" "rss" expected+ where+ expected = encodeUtf8+ "<rss><channel><link>http://www.devalot.com/</link></channel></rss>\n"++namespaceTest1 :: IO ()+namespaceTest1 = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist templateHC+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "namespaces"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "namespace test 1" (Right expected) res+ where+ expected = "Alpha\naoeu\nBeta\n<h:foo aoeu='htns'>Inside h:foo</h:foo>\nEnd\n"+++namespaceTest2 :: IO ()+namespaceTest2 = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist $ templateHC & hcErrorNotBound .~ True+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "namespaces"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "namespace test 2" (Right expected) res+ where+ expected = "Alpha\naoeu\nBeta\n<h:foo aoeu='htns'>Inside h:foo</h:foo>\nEnd\n"+++namespaceTest3 :: IO ()+namespaceTest3 = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist $ templateHC & hcNamespace .~ "h"+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "namespaces"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "namespace test 3" (Right expected) res+ where+ expected = "Alpha\n<foo aoeu='htns'>Inside foo</foo>\nBeta\naoeu\nEnd\n"+++namespaceTest4 :: IO ()+namespaceTest4 = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist $ templateHC & hcNamespace .~ "h"+ & hcErrorNotBound .~ True+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "namespaces"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "namespace test 4" (Right expected) res+ where+ expected = "Alpha\n<foo aoeu='htns'>Inside foo</foo>\nBeta\naoeu\nEnd\n"+++namespaceTest5 :: IO ()+namespaceTest5 = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist $ templateHC & hcNamespace .~ "h"+ & hcCompiledSplices .~ mempty+ & hcErrorNotBound .~ True+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "namespaces"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "namespace test 5" (Left ["templates/namespaces.tpl: No splice bound for h:foo"]) res++------------------------------------------------------------------------------+-- | The templates-no-ns directory should have no tags beginning with h: so+-- this test will throw an error.+noNsSplices :: IO ()+noNsSplices = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist hc+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "test"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "noNsSplices" (Left [noNamespaceSplicesMsg "h:"]) res+ where+ hc = HeistConfig sc "h" True+ sc = mempty & scLoadTimeSplices .~ defaultLoadTimeSplices+ & scCompiledSplices .~ ("foo" ## return (yieldPureText "aoeu"))+ & scTemplateLocations .~ [loadTemplates "templates-no-ns"]+++------------------------------------------------------------------------------+-- | Test that no namespace splice message works correctly when there are no+-- top level splices used+nsNestedUnused :: IO ()+nsNestedUnused = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist hc+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "test"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "ns nested unused warn test" (Right "<div>aeou</div>\n") res+ where+ hc = HeistConfig sc "h" False+ sc = mempty & scCompiledSplices .~ ("foo" ## return $ yieldPureText "aeou")+ & scTemplateLocations .~ [loadTemplates "templates-ns-nested"]+++nsBindTemplateHC :: String -> HeistConfig IO+nsBindTemplateHC dir = HeistConfig sc "h" False+ where+ sc = mempty & scLoadTimeSplices .~ defaultLoadTimeSplices+ & scCompiledSplices .~ nsBindTestSplices+ & scTemplateLocations .~ [loadTemplates dir]+++nsBindTestSplices :: Splices (Splice IO)+nsBindTestSplices = do+ "call" ## do+ tpl <- withSplices (callTemplate "_call")+ nsBindSubSplices (return ())+ return $ yieldRuntime $ codeGen tpl+ "main" ## nsBindSubImpl (return ())+ "main2" ## nsBindSubImpl (return ())+++nsBindSubImpl :: RuntimeSplice IO b -> Splice IO+nsBindSubImpl _ = do+ tpl <- withSplices runChildren nsBindSubSplices (return ())+ return $ yieldRuntime $ codeGen tpl+++nsBindSubSplices :: Splices (RuntimeSplice IO () -> Splice IO)+nsBindSubSplices = do+ "sub" ## pureSplice . textSplice $ const "asdf"+ "recurse" ## nsBindSubImpl+++nsBindTest :: IO ()+nsBindTest = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist $ (nsBindTemplateHC "templates-nsbind")+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "nsbind"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "namespace bind test" (Right expected) res+ where+ expected = "Alpha\n\nBeta\nasdf\nGamma\n<sub></sub>\n\n"+++------------------------------------------------------------------------------+-- | Test splice error reporting.+nsBindErrorTest :: IO ()+nsBindErrorTest = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist $ (nsBindTemplateHC "templates-nsbind")+ & hcErrorNotBound .~ True+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "nsbinderror"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "namespace bind error test" (Left [ err1, err2, err3 ]) res+ where+ err1 = "templates-nsbind/nsbinderror.tpl: No splice bound for h:invalid3\n ... via templates-nsbind/nsbinderror.tpl: h:main2\nBound splices: h:call h:main h:main2 h:recurse h:sub\nNode: Element {elementTag = \"h:invalid3\", elementAttrs = [], elementChildren = []}"+ err2 = "templates-nsbind/nsbinderror.tpl: No splice bound for h:invalid2\n ... via templates-nsbind/nsbinderror.tpl: h:recurse\n ... via templates-nsbind/nsbinderror.tpl: h:main\nBound splices: h:call h:main h:main2 h:recurse h:sub\nNode: Element {elementTag = \"h:invalid2\", elementAttrs = [], elementChildren = []}"+ err3 = "templates-nsbind/nsbinderror.tpl: No splice bound for h:invalid1\nBound splices: h:call h:main h:main2\nNode: Element {elementTag = \"h:invalid1\", elementAttrs = [], elementChildren = []}"+++------------------------------------------------------------------------------+-- | Test splice error data structure.+nsBindStackTest :: IO ()+nsBindStackTest = do+ res <- initHeist (nsBindTemplateHC "templates-nsbind") >>=+ return . (either Left (Right . _spliceErrors))++ H.assertEqual "namespace bind stack test" (Right [ err1, err2, err3 ]) res+ where+ err1 = SpliceError [ ( ["nsbinderror"]+ , Just "templates-nsbind/nsbinderror.tpl"+ , "h:main2") ]+ (Just "templates-nsbind/nsbinderror.tpl")+ ["h:call","h:main","h:main2","h:recurse","h:sub"]+ (X.Element "h:invalid3" [] [])+ "No splice bound for h:invalid3"+ err2 = SpliceError [ ( ["nsbinderror"]+ , Just "templates-nsbind/nsbinderror.tpl"+ , "h:recurse")+ , ( ["nsbinderror"]+ , Just "templates-nsbind/nsbinderror.tpl"+ ,"h:main") ]+ (Just "templates-nsbind/nsbinderror.tpl")+ ["h:call","h:main","h:main2","h:recurse","h:sub"]+ (X.Element "h:invalid2" [] [])+ "No splice bound for h:invalid2"+ err3 = SpliceError []+ (Just "templates-nsbind/nsbinderror.tpl")+ ["h:call","h:main","h:main2"]+ (X.Element "h:invalid1" [] [])+ "No splice bound for h:invalid1"+++nsCallTest :: IO ()+nsCallTest = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist $ (nsBindTemplateHC "templates-nscall")+ & hcErrorNotBound .~ True+ & hcCompiledTemplateFilter .~ nsFilter+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "nscall"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "namespace call test" (Right "Top\n\nInside 1\nCalled\nasdf\n\nInside 2\n\n") res+ where+ nsFilter = (/=) (fromIntegral $ ord '_') . B.head . head+++nsCallErrTest :: IO ()+nsCallErrTest = do+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist $ (nsBindTemplateHC "templates-nscall")+ & hcErrorNotBound .~ True+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "nscall"+ b <- lift $ fst runner+ return $ toByteString b++ H.assertEqual "namespace call error test"+ (Left $ Set.fromList [ err1, err2 ])+ (first Set.fromList res)+ where+ err1 = "templates-nscall/_call.tpl: No splice bound for h:sub\nBound splices: h:call h:main h:main2\nNode: Element {elementTag = \"h:sub\", elementAttrs = [], elementChildren = []}"+ err2 = "templates-nscall/_invalid.tpl: No splice bound for h:invalid\nBound splices: h:call h:main h:main2\nNode: Element {elementTag = \"h:invalid\", elementAttrs = [], elementChildren = []}"+++------------------------------------------------------------------------------+-- | Test exception handling in template load.+exceptionsTest :: IO ()+exceptionsTest = do+ res <- Control.Exception.catch+ (runExceptT $ do+ hs <- ExceptT $ initHeist hc+ -- The rest needed only for type inference.+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs ""+ _ <- lift $ fst runner+ throwE ["Unexpected success"])+ (\(e :: CompileException) -> return $ case lines (show e) of+ l:ls -> Right l+ _ -> Left [show e])+ H.assertEqual "exceptions" (Right firstLine) res++ where+ firstLine = "templates-loaderror/_error.tpl: Exception in splice compile: Prelude.read: no parse"++ hc = HeistConfig sc "h" True+ sc = mempty & scLoadTimeSplices .~ defaultLoadTimeSplices+ & scCompiledSplices .~ splices+ & scTemplateLocations .~ [loadTemplates "templates-loaderror"]+ splices = do+ "call1" ## callTemplate "_ok"+ "call2" ## callTemplate "_error"+ "adder" ## do+ value :: Int <- read . T.unpack . fromJust .+ X.getAttribute "value" <$> getParamNode+ return $ yieldPureText $ T.pack $ show $ 1 + value+++------------------------------------------------------------------------------+-- | Test for defer functions to see that they correctly save the result of+-- a runtime computation.+deferTest :: IO ()+deferTest = do+ rs <- mapM newIORef $ replicate 5 (0 :: Int)+ res <- runExceptT $ do+ hs <- ExceptT $ initHeist $ hc rs+ runner <- noteT ["Error rendering"] $ hoistMaybe $+ renderTemplate hs "test"+ b <- lift $ fst runner+ return $ toByteString b++ vs <- mapM readIORef rs+ H.assertEqual "defer test" ([2, 1, 1, 1, 1], Right msg) (vs, res)+ where+ msg = "1 2\n1 1\n1 1\n\n1 1\n"+ hc rs = HeistConfig (sc rs) "h" True+ sc rs = mempty & scLoadTimeSplices .~ defaultLoadTimeSplices+ & scCompiledSplices .~ (splices rs)+ & scTemplateLocations .~ [loadTemplates "templates-defer"]+ splices [r1, r2, r3, r4, r5] = do+ "plain" ## subSplice $ addAndReturn r1+ "defer" ## deferMap return subSplice $ addAndReturn r2+ "maydefer" ## mayDeferMap (return . Just) subSplice $ addAndReturn r3+ "maydefer2" ## mayDeferMap (const $ return Nothing) subSplice $+ addAndReturn r4+ "defermany" ## deferMany subSplice $ addAndReturn' r5+ subSplice =+ withSplices runChildren+ ("use" ## \n -> return $ yieldRuntimeText $ return . T.pack . show =<< n)+ addAndReturn r = liftIO $ modifyIORef r (+1) >> readIORef r+ addAndReturn' r = liftIO $ do+ modifyIORef r (+1)+ val <- readIORef r+ return [val]
+ test/suite/Heist/Interpreted/Tests.hs view
@@ -0,0 +1,670 @@+{-# 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.Monad+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.Map.Syntax+import Data.Maybe+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+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.Internal.Types+import Heist.Interpreted.Internal+import Heist.Splices.Apply+import Heist.Splices.Ignore+import Heist.Splices.Json+import Heist.Splices.Markdown+import Heist.TestCommon+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/pandoc" pandocTest+ , testCase "heist/pandoc_div" pandocDivTest+ , 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+ , testCase "heist/renderXML" xmlNotHtmlTest+ ]+++------------------------------------------------------------------------------+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 mempty mempty mempty+ 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 mempty mempty mempty mempty+ 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" mempty mempty mempty mempty+ let tm = either (error . unlines) _templateMap ets+ hs <- loadEmpty mempty mempty mempty mempty+ 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" mempty mempty mempty mempty+ either (error "Error loading templates")+ (\ts -> do let tm = _templateMap ts+ H.assertEqual "loadTest size" 41 $ Map.size tm+ ) ets+++------------------------------------------------------------------------------+fsLoadTest :: H.Assertion+fsLoadTest = do+ ets <- loadIO "templates" mempty mempty mempty mempty+ let tm = either (error "Error loading templates") _templateMap ets+ es <- loadEmpty mempty mempty mempty mempty+ 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" mempty mempty mempty mempty+ either (error "Error loading templates")+ (\ts -> do t <- renderTemplate ts ""+ H.assertBool "renderNoName" $ isNothing t+ ) ets+++------------------------------------------------------------------------------+doctypeTest :: H.Assertion+doctypeTest = do+ ets <- loadT "templates" mempty mempty mempty mempty+ 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\n<html>\n<div id='pre_{att}_post'>\n/index\n</div>\n</html>\n"+ ]++------------------------------------------------------------------------------+attrSubstTest :: H.Assertion+attrSubstTest = do+ ets <- loadT "templates" mempty mempty mempty mempty+ 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 `mappend`+ ("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+ ["<mytag flag>Empty attribute</mytag>"+ ,"<mytag flag='abc${bar}'>No ident capture</mytag>"+ ,"<div id='pre_meaning_of_everything_post'></div>"+ ]+ out2 = B.unlines+ ["<mytag flag>Empty attribute</mytag>"+ ,"<mytag flag='abc${bar}'>No ident capture</mytag>"+ ,"<div id='pre_${foo}_post'></div>"+ ]+++------------------------------------------------------------------------------+bindAttrTest :: H.Assertion+bindAttrTest = do+ ets <- loadT "templates" mempty mempty mempty mempty+ 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 =+ "<div class='markdown'><p>This <em>is</em> a test.</p></div>"++------------------------------------------------------------------------------+-- | Markdown test on a file+markdownTest :: H.Assertion+markdownTest = renderTest "markdown" markdownHtmlExpected++-----------------------------------------------------------------------------++-- | Pandoc test on a file+pandocTest :: H.Assertion+pandocTest = renderTest "pandoc" pandocNoDivHtmlExpected++pandocDivTest :: H.Assertion+pandocDivTest = renderTest "pandocdiv" pandocDivHtmlExpected++pandocNoDivHtmlExpected :: ByteString+pandocNoDivHtmlExpected = "<p>This <em>is</em> a test.</p>"++pandocDivHtmlExpected :: ByteString+pandocDivHtmlExpected =+ "<div class='foo test' id='pandoc'><p>This <em>is</em> a test.</p></div>"+ -- Implementaton dependent. Class is prepended in current implementation,+ -- it will be first attribute++pandocTestSplices :: Splices (Splice IO)+pandocTestSplices = do+ "pandocnodiv" ## pandocSplice optsNoDiv+ "pandocdiv" ## pandocSplice optsDiv+ where+ optsNoDiv = setPandocWrapDiv Nothing defaultPandocOptions+ optsDiv = setPandocWrapDiv (Just "test") defaultPandocOptions++------------------------------------------------------------------------------+jsonValueTest :: H.Assertion+jsonValueTest = do+ renderTest "json" jsonExpected1+ renderTest "json_snippet" jsonExpected2++ where+ jsonExpected1 = B.concat [ "<i><b>ok</b></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 = "<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" mempty pandocTestSplices mempty mempty+ 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 splices = do+ "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\" }"+ ])+ let ts = bindSplices splices 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 = "<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 = "<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+ hs <- loadEmpty mempty mempty mempty mempty+ 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 Nothing result))+++------------------------------------------------------------------------------+applyTest :: H.Assertion+applyTest = do+ es <- loadEmpty mempty mempty mempty mempty+ res <- evalHeistT applyImpl+ (X.Element "apply" [("template", "nonexistant")] []) es++ H.assertEqual "apply nothing" [] res+++------------------------------------------------------------------------------+ignoreTest :: H.Assertion+ignoreTest = do+ es <- loadEmpty mempty mempty mempty mempty+ 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++------------------------------------------------------------------------------+xmlNotHtmlTest :: H.Assertion+xmlNotHtmlTest = renderTest "rss" expected where+ expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss><channel><link>http://www.devalot.com/</link></channel></rss>"++------------------------------------------------------------------------------+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 mempty mempty mempty mempty+ 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 mempty mempty mempty+ 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,134 @@+module Heist.TestCommon where++------------------------------------------------------------------------------+import Blaze.ByteString.Builder+import Control.Monad.Trans+import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)+import Control.Monad.Trans.Except (ExceptT(ExceptT), runExceptT)+import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as B+import Data.Maybe+import Data.Monoid+++------------------------------------------------------------------------------+import Heist+import qualified Heist.Compiled as C+import Heist.Internal.Types+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 :: MonadIO m+ => FilePath+ -> Splices (I.Splice m)+ -> Splices (I.Splice IO)+ -> Splices (C.Splice m)+ -> Splices (AttrSplice m)+ -> IO (Either [String] (HeistState m))+loadT baseDir a b c d = runExceptT $ do+ let sc = SpliceConfig (defaultInterpretedSplices `mappend` a)+ (defaultLoadTimeSplices `mappend` b) c d+ [loadTemplates baseDir] (const True)+ ExceptT $ initHeist $ HeistConfig sc "" False+++------------------------------------------------------------------------------+loadIO :: FilePath+ -> Splices (I.Splice IO)+ -> Splices (I.Splice IO)+ -> Splices (C.Splice IO)+ -> Splices (AttrSplice IO)+ -> IO (Either [String] (HeistState IO))+loadIO baseDir a b c d = runExceptT $ do+ let sc = SpliceConfig (defaultInterpretedSplices >> a)+ (defaultLoadTimeSplices >> b) c d+ [loadTemplates baseDir] (const True)+ ExceptT $ initHeist $ HeistConfig sc "" False+++------------------------------------------------------------------------------+loadHS :: FilePath -> IO (HeistState IO)+loadHS baseDir = do+ etm <- runExceptT $ do+ let sc = SpliceConfig defaultInterpretedSplices+ defaultLoadTimeSplices mempty mempty+ [loadTemplates baseDir] (const True)+ ExceptT $ initHeist $ HeistConfig sc "" False+ either (error . concat) return etm+++loadEmpty :: Splices (I.Splice IO)+ -> Splices (I.Splice IO)+ -> Splices (C.Splice IO)+ -> Splices (AttrSplice IO)+ -> IO (HeistState IO)+loadEmpty a b c d = do+ let sc = SpliceConfig (defaultInterpretedSplices `mappend` a)+ (defaultLoadTimeSplices `mappend` b) c d mempty+ (const True)+ res <- initHeist $ HeistConfig sc "" False+ 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+++------------------------------------------------------------------------------+isLeft :: Either e a -> Bool+isLeft (Left _) = True+isLeft _ = False+++------------------------------------------------------------------------------+noteT :: Monad m => e -> MaybeT m a -> ExceptT e m a+noteT e ma = do+ x <- lift $ runMaybeT ma+ case x of+ Nothing -> ExceptT $ return (Left e)+ Just a -> ExceptT $ return (Right a)+++------------------------------------------------------------------------------+hoistMaybe :: Monad m => Maybe a -> MaybeT m a+hoistMaybe = MaybeT . return
+ test/suite/Heist/Tests.hs view
@@ -0,0 +1,209 @@+{-# LANGUAGE CPP #-}+{-# 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.Map.Syntax+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.Internal.Types+import qualified Heist.Interpreted as I+import Heist.Splices.Cache+import Heist.Splices.Html+import Heist.TemplateDirectory+import Heist.Tutorial.AttributeSplices+import Heist.Tutorial.CompiledSplices++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" mempty mempty mempty mempty+ either (H.assertEqual "load errors test" expected . sort)+ (const $ H.assertFailure "No failure when loading templates-bad")+ ets+ where+ expected = sort+#if MIN_VERSION_base(4,9,0)+ ["templates-bad/apply-missing-attr.tpl: must supply \"template\" attribute in <apply>\nCallStack (from HasCallStack):\n error, called at src/Heist/Common.hs:76:15 in main:Heist.Common"+ ,"templates-bad/apply-template-not-found.tpl: apply tag cannot find template \"/page\"\nCallStack (from HasCallStack):\n error, called at src/Heist/Common.hs:76:15 in main:Heist.Common"+ ,"templates-bad/bind-infinite-loop.tpl: template recursion exceeded max depth, you probably have infinite splice recursion!\nCallStack (from HasCallStack):\n error, called at src/Heist/Common.hs:76:15 in main:Heist.Common"+ ,"templates-bad/bind-missing-attr.tpl: must supply \"tag\" attribute in <bind>\nCallStack (from HasCallStack):\n error, called at src/Heist/Common.hs:76:15 in main:Heist.Common"+ ]+#else+ ["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>"+ ]+#endif+++attrSpliceTest :: IO ()+attrSpliceTest = do+ ehs <- loadT "templates" mempty mempty mempty+ ("autocheck" ## lift . 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 = "<input type='checkbox' value='foo' checked />\n<input type='checkbox' value='bar' />\n"+ expected2 = "<input type='checkbox' value='foo' />\n<input type='checkbox' value='bar' checked />\n"+ expected3 = "<input type=\"checkbox\" value=\"foo\" checked />\n<input type=\"checkbox\" value=\"bar\" />\n"+ expected4 = "<input type=\"checkbox\" value=\"foo\" />\n<input type=\"checkbox\" value=\"bar\" checked />\n"++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)+ sc = SpliceConfig rSplices mempty dSplices mempty mempty (const True)+ hc = HeistConfig sc "" False+ 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" mempty (htmlTag ## htmlImpl) mempty mempty+ 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>\n\n"+ ]++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"+ ["\nThis is a test."+ ,"===bind content===\nAnother test line."+ ,"apply content\nLast test line."+ ,"\n"+ ]+ iExpected = B.unlines+ ["\nThis 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" expected cOut++ iOut <- iRender hs "backslash"+ H.assertEqual "interpreted failure" expected iOut+ where+ expected = "<foo regex='abc\\d+\\\\e'></foo>\n"+
+ 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,274 @@+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++> import Control.Applicative+> import Control.Lens+> import Data.Map.Syntax++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+> -> Splices (C.Splice n)+> -> IO (HeistState n)+> load baseDir splices = do+> tmap <- runExceptT $ do+> let sc = mempty & scLoadTimeSplices .~ defaultLoadTimeSplices+> & scCompiledSplices .~ splices+> & scTemplateLocations .~ [loadTemplates baseDir]+> ExceptT $ initHeist $ emptyHeistConfig & hcNamespace .~ ""+> & hcErrorNotBound .~ False+> & hcSpliceConfig .~ sc+> 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+> }+> +> personSplices :: Monad n+> => Splices (RuntimeSplice n Person -> C.Splice n)+> personSplices = mapV (C.pureSplice . C.textSplice) $ do+> "firstName" ## pFirstName+> "lastName" ## pLastName+> "age" ## pack . show . pAge+> +> peopleSplice :: (Monad n)+> => RuntimeSplice n [Person]+> -> C.Splice n+> peopleSplice = C.manyWithSplices C.runChildren personSplices+> +> allPeopleSplice :: C.Splice (StateT [Person] IO)+> allPeopleSplice = peopleSplice (lift 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+< promise <- C.newEmptyPromise+< outputChildren <- C.withSplices C.runChildren splices (C.getPromise promise)+< return $ C.yieldRuntime $ do +< -- :: RuntimeSplice m Builder+< mname <- lift $ getParam "username"+< let err = return $ fromByteString "Must supply a username"+< single name = do +< euser <- lift $ lookupUser name+< either (return . fromByteString . encodeUtf8 . T.pack)+< doUser euser+< where+< doUser value = do+< C.putPromise promise (name, value)+< C.codeGen outputChildren+< maybe err single mname+< +< +< splices :: Monad n+< => Splices (RuntimeSplice n (Text, Text) -> C.Splice n)+< splices = mapS (C.pureSplice . C.htmlNodeSplice) $ do+< "user" ## (:[]) . TextNode . fst+< "value" ## (:[]) . TextNode . snd+
+ test/suite/Heist/Tutorial/Imports.hs view
@@ -0,0 +1,26 @@+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+ , runExceptT+ , ExceptT(..)+ ) where++import Blaze.ByteString.Builder+import Control.Monad+import Control.Monad.Trans+import Control.Monad.Trans.Except (ExceptT(..), runExceptT)+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
@@ -1,11 +1,21 @@ module Main where +import System.Directory 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+main = do+ -- Need to change directory after we switched to cabal test infra+ setCurrentDirectory "test"+ defaultMain 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><b>ok</b></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-defer/test.tpl view
@@ -0,0 +1,5 @@+<h:plain><h:use/> <h:use/></h:plain>+<h:defer><h:use/> <h:use/></h:defer>+<h:maydefer><h:use/> <h:use/></h:maydefer>+<h:maydefer2><h:use/> <h:use/></h:maydefer2>+<h:defermany><h:use/> <h:use/></h:defermany>
+ test/templates-loaderror/_error.tpl view
@@ -0,0 +1,1 @@+<h:adder value="noparse"/>
+ test/templates-loaderror/_ok.tpl view
@@ -0,0 +1,1 @@+<h:adder value="1"/>
+ test/templates-loaderror/test.tpl view
@@ -0,0 +1,3 @@+<h:call1/>+<h:call2/>+<h:call1/>
+ test/templates-ns-nested/test.tpl view
@@ -0,0 +1,1 @@+<div><h:foo/></div>
+ test/templates-nsbind/nsbind.tpl view
@@ -0,0 +1,7 @@+Alpha+<h:main>+Beta+<h:sub/>+Gamma+<sub/>+</h:main>
+ test/templates-nsbind/nsbinderror.tpl view
@@ -0,0 +1,10 @@+Alpha+<h:invalid1/>+<h:main>+<h:recurse>+<h:invalid2/>+</h:recurse>+</h:main>+<h:main2>+<h:invalid3/>+</h:main2>
+ test/templates-nscall/_call.tpl view
@@ -0,0 +1,2 @@+Called+<h:sub/>
+ test/templates-nscall/_invalid.tpl view
@@ -0,0 +1,1 @@+<h:invalid/>
+ test/templates-nscall/nscall.tpl view
@@ -0,0 +1,6 @@+Top+<h:main>+Inside 1+<h:call/>+Inside 2+</h:main>
+ test/templates/attr_splice.tpl view
@@ -0,0 +1,2 @@+<input type="checkbox" autocheck="foo"/>+<input type="checkbox" autocheck="bar"/>
test/templates/attrs.tpl view
@@ -1,3 +1,3 @@ <mytag flag="">Empty attribute</mytag>-<mytag flag="abc\${foo}">No ident capture</mytag>+<mytag flag="abc${bar}">No ident capture</mytag> <div id="pre_${foo}_post"/>
+ 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/backslash.tpl view
@@ -0,0 +1,1 @@+<foo regex='abc\d+\\e'/>
+ 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>
@@ -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/json_array.tpl view
@@ -0,0 +1,1 @@+<jsonArray><value var="ctx.0"/>, <value var="ctx.1"/> and <with var="ctx.2"><value:deep/> <value:inside/></with></jsonArray>
+ test/templates/namespaces.tpl view
@@ -0,0 +1,5 @@+Alpha+<foo aoeu="htns">Inside foo</foo>+Beta+<h:foo aoeu="htns">Inside h:foo</h:foo>+End
− 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/pandoc.tpl view
@@ -0,0 +1,1 @@+<pandocnodiv file="test.md"/>
+ test/templates/pandocdiv.tpl view
@@ -0,0 +1,1 @@+<pandocdiv file="test.md" id="pandoc" class="foo"/>
+ 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/rss.xtpl view
@@ -0,0 +1,1 @@+<rss><channel><link>http://www.devalot.com/</link></channel></rss>
+ 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>