fork mustache

This commit is contained in:
Mirek Kratochvil 2023-05-27 20:20:43 +02:00
parent 35837f5607
commit 73498534cf
19 changed files with 2309 additions and 0 deletions

1
cabal.project Normal file
View file

@ -0,0 +1 @@
packages: reploy.cabal, */*.cabal

104
mustache/CHANGELOG.md Normal file
View file

@ -0,0 +1,104 @@
# Mustache library changelog
## v2.4.2
- Also treat Null as falsey in inverted sections
## v2.4.1
- Compatibility with `containers >= 0.2.17`
## v2.4.0
- Support for aeson 2
## v2.3.2
- Added support for GHC 9.0.1
## v2.3.0
- Changed `EitherT` to `ExceptT` (deprecation)
- removed `getFile` from public API
## v2.2.3
- Quick fix to prevent catchSubstitute from reporting substitutions to the renderer.
## v2.2.2
- Added a function to catch a substitution result
## v2.2.1
- Quickfix for an issue with resolving in context
## v2.2
- changed substitution into a new monad
+ easier usage in lambdas and lambdas can now do nested substitution
## v2.1.4
- Treat Null as falsy in sections
## v2.1.3
- Added excaping for the apostrophe "'" as per xml spec, courtesy to @tfausak
## v2.1.2
- Fixed template cache again, as the spec requires access to the previous cache in partials as well
## v2.1.1
- Fixed an error where the substitution of partials would not use the template cache of the new partial
## v2.1
- Added API preserving checked substitution with 'checkedSubstitute' and 'checkedSubstituteValue'
- Better and more ToMustache instances. No longer are all sequences of characters serialised as strings
## v2.0
- Added QuasiQuotes and template Haskell functions for compile time template embedding.
## v1.0
- Stabilised API's
## v0.5.1.0rc-7
- Removed dependency tagsoup
- Added ToMustache instances for some numbers
## v0.5.0.0rc-6
- Removed any dependency on ghc 7.10-type OverlappingInstances
- Resolved String/List overlapping instances
## v0.4.0.1rc-5
- Added a necessary OVERLAPPABLE pragma
## v0.4.0.0rc-4 (current stable version)
- Removed `conversion` and `conversion-text` dependency.
- Subsequently removed any dependency on overlapping instances
- Readded support for ghc version 7.8
- Removed `Char -> Value` instance of `ToMustache` (because of overlap)
- Renamed `AST`
## v0.3.1.0rc-3
- Added infix precedence to conversion operators
- Added `INLINEABLE` pragma to conversion functions
## v0.3.0.1rc-2
Dropped GHC 7.8 support in favor of efficient and easy data conversion.
## v0.3.0.0rc-1
- improved documentation
- fixed a bug with scope
- small interface changes

30
mustache/LICENSE Normal file
View file

@ -0,0 +1,30 @@
Copyright (c) 2015, 2016 Justus Adam
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Justus Adam nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

54
mustache/README.md Normal file
View file

@ -0,0 +1,54 @@
# mustache [![Travis Status](https://travis-ci.org/JustusAdam/mustache.svg?branch=master)](https://travis-ci.org/JustusAdam/mustache) [![Hackage](https://img.shields.io/hackage/v/mustache.svg)](https://hackage.haskell.org/package/mustache) [![Join the chat at https://gitter.im/JustusAdam/mustache](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/JustusAdam/mustache?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Haskell implementation of [mustache templates][mustache-homepage].
[mustache-homepage]: https://mustache.github.io
Implements the official [specs version 1.1.3](https://github.com/mustache/spec/releases/tag/v1.1.3)
## Motivation
The old Haskell implementation of mustache templates [hastache][] seemed pretty abandoned to me. This implementation aims to be much easier to use and (fingers crossed) better maintained.
[hastache]: https://hackage.haskell.org/package/hastache
Since it is so easy to use and requires but a few files of code, I've also written a small executable that compiles and renders mustache templates with data input from json or yaml files.
## Usage
### Library
Please refer to the [documentation][] on hackage.
[documentation]: https://hackage.haskell.org/package/mustache
### Executable `haskell-mustache`
$ haskell-mustache --help
Simple mustache template substitution
arguments [OPTIONS] TEMPLATE [DATA-FILES]
Common flags:
-t --templatedirs[=DIRECTORY] The directory in which to search for the
templates
-? --help Display help message
-V --version Print version information
Current implementation substitutes the `TEMPLATE` once with each `DATA-FILE`
#### Example
$ haskell-mustache my-template-file data-file-1.json data-file-2.json data-file-3.json
## Roadmap
- [x] String parser for mustache templates
- [x] Template substitution
- [x] Standalone executable
- [x] Support for 'set delimiter'
- [x] More efficiency using `Text` rather than `String`
- [x] More efficient Text parsing
- [x] Test coverage provided via the official [specs](https://github.com/mustache/spec)
- [x] Haddock documentation
- [ ] More instances for `ToMustache` typeclass

2
mustache/Setup.hs Normal file
View file

@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

72
mustache/app/Main.hs Normal file
View file

@ -0,0 +1,72 @@
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE NamedFieldPuns #-}
module Main (main) where
import Data.Aeson (Value, eitherDecode)
import Data.Bifunctor (first)
import qualified Data.ByteString as B (readFile)
import qualified Data.ByteString.Lazy as BS (readFile)
import Data.Foldable (for_)
import qualified Data.Text.IO as TIO (putStrLn)
import Data.Yaml (decodeEither')
import System.Console.CmdArgs.Implicit (Data, Typeable, argPos, args,
cmdArgs, def, help, summary,
typ, (&=))
import System.FilePath (takeExtension)
import Text.Mustache (automaticCompile, substitute,
toMustache)
data Arguments = Arguments
{ template :: FilePath
, templateDirs :: [FilePath]
, dataFiles :: [FilePath]
} deriving (Show, Data, Typeable)
commandArgs :: Arguments
commandArgs = Arguments
{ template = def
&= argPos 0
&= typ "TEMPLATE"
, dataFiles = def
&= args
&= typ "DATA-FILES"
, templateDirs = ["."]
&= help "The directories in which to search for the templates"
&= typ "DIRECTORIES"
} &= summary "Simple mustache template subtitution"
readJSON :: FilePath -> IO (Either String Value)
readJSON = fmap eitherDecode . BS.readFile
readYAML :: FilePath -> IO (Either String Value)
readYAML = fmap (first show . decodeEither') . B.readFile
main :: IO ()
main = do
(Arguments { template, templateDirs, dataFiles }) <- cmdArgs commandArgs
eitherTemplate <- automaticCompile templateDirs template
case eitherTemplate of
Left err -> print err
Right compiledTemplate ->
for_ dataFiles $ \file -> do
let decoder =
case takeExtension file of
".yml" -> readYAML
".yaml" -> readYAML
_ -> readJSON
decoded <- decoder file
either
putStrLn
(TIO.putStrLn . substitute compiledTemplate . toMustache)
decoded

141
mustache/mustache.cabal Normal file
View file

@ -0,0 +1,141 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
name: mustache
version: 2.4.2
synopsis: A mustache template parser library.
description: Allows parsing and rendering template files with mustache markup. See the
mustache <http://mustache.github.io/mustache.5.html language reference>.
.
Implements the mustache spec version 1.1.3.
.
/Note/: Versions including and beyond 0.4 are compatible with ghc 7.8 again.
category: Development
homepage: https://github.com/JustusAdam/mustache
bug-reports: https://github.com/JustusAdam/mustache/issues
author: Justus Adam
maintainer: dev@justus.science
copyright: (c) 2015 - 2022 Justus Adam
license: BSD3
license-file: LICENSE
build-type: Simple
tested-with:
GHC>=7.8 && <=7.10.2
extra-source-files:
README.md
CHANGELOG.md
test/unit/examples/test-template-partials.txt.mustache
test/unit/examples/test-template.txt.mustache
test/unit/examples/partials/test-partial.txt.mustache
source-repository head
type: git
location: git://github.com/JustusAdam/mustache.git
library
exposed-modules:
Text.Mustache
Text.Mustache.Types
Text.Mustache.Parser
Text.Mustache.Compile
Text.Mustache.Render
other-modules:
Text.Mustache.Internal
Text.Mustache.Internal.Types
Paths_mustache
hs-source-dirs:
src
default-extensions:
LambdaCase
TupleSections
other-extensions:
NamedFieldPuns
OverloadedStrings
LambdaCase
TupleSections
TemplateHaskell
QuasiQuotes
ghc-options: -Wall
build-depends:
aeson
, base >=4.7 && <5
, bytestring
, containers
, directory
, filepath
, mtl >=2.2.1
, parsec
, scientific
, template-haskell
, text
, th-lift
, unordered-containers
, vector
default-language: Haskell2010
executable haskell-mustache
main-is: Main.hs
other-modules:
Paths_mustache
hs-source-dirs:
app
ghc-options: -threaded -Wall
build-depends:
aeson
, base >=4.7 && <5
, bytestring
, cmdargs
, filepath
, mustache
, text
, yaml
default-language: Haskell2010
test-suite language-specifications
type: exitcode-stdio-1.0
main-is: Language.hs
other-modules:
Paths_mustache
hs-source-dirs:
test/integration
build-depends:
aeson
, base >=4.7 && <5
, base-unicode-symbols
, bytestring
, filepath
, hspec
, lens
, mustache
, tar
, text
, unordered-containers
, wreq
, yaml
, zlib
default-language: Haskell2010
test-suite unit-tests
type: exitcode-stdio-1.0
main-is: Spec.hs
other-modules:
Paths_mustache
hs-source-dirs:
test/unit
build-depends:
aeson
, base >=4.7 && <5
, bytestring
, directory
, filepath
, hspec
, mustache
, process
, temporary
, text
, unordered-containers
, yaml
default-language: Haskell2010

View file

@ -0,0 +1,197 @@
{-|
Module : $Header$
Description : Basic functions for dealing with mustache templates.
Copyright : (c) Justus Adam, 2015
License : BSD3
Maintainer : dev@justus.science
Stability : experimental
Portability : POSIX
= How to use this library
This module exposes some of the most convenient functions for dealing with mustache
templates.
== Compiling with automatic partial discovery
The easiest way of compiling a file and its potential includes (called partials)
is by using the 'automaticCompile' function.
@
main :: IO ()
main = do
let searchSpace = [".", "./templates"]
templateName = "main.mustache"
compiled <- automaticCompile searchSpace templateName
case compiled of
Left err -> print err
Right template -> return () -- this is where you can start using it
@
The @searchSpace@ encompasses all directories in which the compiler should
search for the template source files.
The search itself is conducted in order, from left to right.
Should your search space be only the current working directory, you can use
'localAutomaticCompile'.
The @templateName@ is the relative path of the template to any directory
of the search space.
'automaticCompile' not only finds and compiles the template for you, it also
recursively finds any partials included in the template as well,
compiles them and stores them in the 'partials' hash attached to the resulting
template.
The compiler will throw errors if either the template is malformed
or the source file for a partial or the template itself could not be found
in any of the directories in @searchSpace@.
== Substituting
In order to substitute data into the template it must be an instance of the 'ToMustache'
typeclass or be of type 'Value'.
This libray tries to imitate the API of <https://hackage.haskell.org/package/aeson aeson>
by allowing you to define conversions of your own custom data types into 'Value',
the type used internally by the substitutor via typeclass and a selection of
operators and convenience functions.
=== Example
@
data Person = { age :: Int, name :: String }
instance ToMustache Person where
toMustache person = object
[ "age" ~> age person
, "name" ~> name person
]
@
The values to the left of the '~>' operator has to be of type 'Text', hence the
@OverloadedStrings@ can becomes very handy here.
Values to the right of the '~>' operator must be an instance of the 'ToMustache'
typeclass. Alternatively, if your value to the right of the '~>' operator is
not an instance of 'ToMustache' but an instance of 'ToJSON' you can use the
'~=' operator, which accepts 'ToJSON' values.
@
data Person = { age :: Int, name :: String, address :: Address }
data Address = ...
instance ToJSON Address where
...
instance ToMustache Person where
toMustache person = object
[ "age" ~> age person
, "name" ~> name person
, "address" ~= address person
]
@
All operators are also provided in a unicode form, for those that, like me, enjoy
unicode operators.
== Manual compiling
You can compile templates manually without requiring the IO monad at all, using
the 'compileTemplate' function. This is the same function internally used by
'automaticCompile' and does not check if required partial are present.
More functions for manual compilation can be found in the 'Text.Mustache.Compile'
module. Including helpers for finding lists of partials in templates.
Additionally the 'compileTemplateWithCache' function is exposed here which you
may use to automatically compile a template but avoid some of the compilation
overhead by providing already compiled partials as well.
== Fundamentals
This library builds on three important data structures/types.
['Value'] A data structure almost identical to Data.Aeson.Value extended with
lambda functions which represents the data the template is being filled with.
['ToMustache'] A typeclass for converting arbitrary types to 'Value', similar
to Data.Aeson.ToJSON but with support for lambdas.
['Template'] Contains the 'STree', the syntax tree, which is basically a
list of text blocks and mustache tags. The 'name' of the template and its
'partials' cache.
=== Compiling
During the compilation step the template file is located, read, then parsed in a single
pass ('compileTemplate'), resulting in a 'Template' with an empty 'partials' section.
Subsequenty the 'STree' of the template is scanned for included partials, any
present 'TemplateCache' is queried for the partial ('compileTemplateWithCache'),
if not found it will be searched for in the @searchSpace@, compiled and
inserted into the template's own cache as well as the global cache for the
compilation process.
Internally no partial is compiled twice, as long as the names stay consistent.
Once compiled templates may be used multiple times for substitution or as
partial for other templates.
Partials are not being embedded into the templates during compilation, but during
substitution, hence the 'partials' cache is vital to the template even after
compilation has been completed. Any non existent partial in the cache will
rsubstitute to an empty string.
=== Substituting
-}
{-# LANGUAGE LambdaCase #-}
module Text.Mustache
(
-- * Compiling
-- ** Automatic
automaticCompile, localAutomaticCompile
-- ** Manually
, compileTemplateWithCache, compileTemplate, Template(..)
-- * Rendering
-- ** Generic
, substitute, checkedSubstitute
-- ** Specialized
, substituteValue, checkedSubstituteValue
-- ** In Lambdas
, substituteNode, substituteAST, catchSubstitute
-- * Data Conversion
, ToMustache, toMustache, integralToMustache, object, (~>), (~=)
-- ** Utilities for lambdas
, overText
) where
import Text.Mustache.Compile
import Text.Mustache.Render
import Text.Mustache.Types
import qualified Data.Text as T
-- | Creates a 'Lambda' which first renders the contained section and then applies the supplied function
overText :: (T.Text -> T.Text) -> Value
overText f = toMustache $ fmap (f . snd) . catchSubstitute . substituteAST

View file

@ -0,0 +1,213 @@
{-|
Module : $Header$
Description : Basic functions for dealing with mustache templates.
Copyright : (c) Justus Adam, 2015
License : BSD3
Maintainer : dev@justus.science
Stability : experimental
Portability : POSIX
-}
{-# OPTIONS_GHC -fno-warn-missing-fields #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Text.Mustache.Compile
( automaticCompile, localAutomaticCompile, TemplateCache, compileTemplateWithCache
, compileTemplate, cacheFromList, getPartials, mustache, embedTemplate, embedSingleTemplate
) where
import Control.Arrow ((&&&))
import Control.Monad
import Control.Monad.Except
import Control.Monad.State
import Data.Bool
import Data.HashMap.Strict as HM
import Data.Text hiding (concat, find, map, uncons)
import qualified Data.Text.IO as TIO
import Language.Haskell.TH (Exp, Loc, Q, loc_filename,
loc_start, location)
import Language.Haskell.TH.Quote (QuasiQuoter (QuasiQuoter),
quoteExp)
import qualified Language.Haskell.TH.Syntax as THS
import System.Directory
import System.FilePath
import Text.Mustache.Parser
import Text.Mustache.Types
import Text.Parsec.Error
import Text.Parsec.Pos
import Text.Printf
{-|
Compiles a mustache template provided by name including the mentioned partials.
The same can be done manually using 'getFile', 'mustacheParser' and 'getPartials'.
This function also ensures each partial is only compiled once even though it may
be included by other partials including itself.
A reference to the included template will be found in each including templates
'partials' section.
-}
automaticCompile :: [FilePath] -> FilePath -> IO (Either ParseError Template)
automaticCompile searchSpace = compileTemplateWithCache searchSpace mempty
-- | Compile the template with the search space set to only the current directory
localAutomaticCompile :: FilePath -> IO (Either ParseError Template)
localAutomaticCompile = automaticCompile ["."]
{-|
Compile a mustache template providing a list of precompiled templates that do
not have to be recompiled.
-}
compileTemplateWithCache :: [FilePath]
-> TemplateCache
-> FilePath
-> IO (Either ParseError Template)
compileTemplateWithCache searchSpace templates initName =
runExceptT $ evalStateT (compile' initName) $ flattenPartials templates
where
compile' :: FilePath
-> StateT
(HM.HashMap String Template)
(ExceptT ParseError IO)
Template
compile' name' = do
templates' <- get
case HM.lookup name' templates' of
Just template -> return template
Nothing -> do
rawSource <- lift $ getFile searchSpace name'
compiled@(Template { ast = mSTree }) <-
lift $ ExceptT . pure $ compileTemplate name' rawSource
foldM
(\st@(Template { partials = p }) partialName -> do
nt <- compile' partialName
modify (HM.insert partialName nt)
return (st { partials = HM.insert partialName nt p })
)
compiled
(getPartials mSTree)
-- | Flatten a list of Templates into a single 'TemplateCache'
cacheFromList :: [Template] -> TemplateCache
cacheFromList = flattenPartials . fromList . fmap (name &&& id)
-- | Compiles a 'Template' directly from 'Text' without checking for missing partials.
-- the result will be a 'Template' with an empty 'partials' cache.
compileTemplate :: String -> Text -> Either ParseError Template
compileTemplate name' = fmap (flip (Template name') mempty) . parse name'
{-|
Find the names of all included partials in a mustache STree.
Same as @join . fmap getPartials'@
-}
getPartials :: STree -> [FilePath]
getPartials = join . fmap getPartials'
{-|
Find partials in a single Node
-}
getPartials' :: Node Text -> [FilePath]
getPartials' (Partial _ p) = return p
getPartials' (Section _ n) = getPartials n
getPartials' (InvertedSection _ n) = getPartials n
getPartials' _ = mempty
flattenPartials :: TemplateCache -> TemplateCache
flattenPartials m = foldrWithKey (insertWith (\_ b -> b)) m m
{-|
@getFile searchSpace file@ iteratively searches all directories in
@searchSpace@ for a @file@ returning it if found or raising an error if none
of the directories contain the file.
This trows 'ParseError's to be compatible with the internal Either Monad of
'compileTemplateWithCache'.
-}
getFile :: [FilePath] -> FilePath -> ExceptT ParseError IO Text
getFile [] fp = throwError $ fileNotFound fp
getFile (templateDir : xs) fp =
lift (doesFileExist filePath) >>=
bool
(getFile xs fp)
(lift $ TIO.readFile filePath)
where
filePath = templateDir </> fp
-- |
-- Compile a mustache 'Template' at compile time. Usage:
--
-- > {-# LANGUAGE QuasiQuotes #-}
-- > import Text.Mustache.Compile (mustache)
-- >
-- > foo :: Template
-- > foo = [mustache|This is my inline {{ template }} created at compile time|]
--
-- Partials are not supported in the QuasiQuoter
mustache :: QuasiQuoter
mustache = QuasiQuoter {quoteExp = \unprocessedTemplate -> do
l <- location
compileTemplateTH (fileAndLine l) unprocessedTemplate }
-- |
-- Compile a mustache 'Template' at compile time providing a search space for any partials. Usage:
--
-- > {-# LANGUAGE TemplateHaskell #-}
-- > import Text.Mustache.Compile (embedTemplate)
-- >
-- > foo :: Template
-- > foo = $(embedTemplate ["dir", "dir/partials"] "file.mustache")
--
embedTemplate :: [FilePath] -> FilePath -> Q Exp
embedTemplate searchSpace filename = do
template <- either (fail . ("Parse error in mustache template: " ++) . show) pure =<< THS.runIO (automaticCompile searchSpace filename)
let possiblePaths = do
fname <- (filename:) . HM.keys . partials $ template
path <- searchSpace
pure $ path </> fname
mapM_ addDependentRelativeFile =<< THS.runIO (filterM doesFileExist possiblePaths)
THS.lift template
-- |
-- Compile a mustache 'Template' at compile time. Usage:
--
-- > {-# LANGUAGE TemplateHaskell #-}
-- > import Text.Mustache.Compile (embedSingleTemplate)
-- >
-- > foo :: Template
-- > foo = $(embedSingleTemplate "dir/file.mustache")
--
-- Partials are not supported in embedSingleTemplate
embedSingleTemplate :: FilePath -> Q Exp
embedSingleTemplate filePath = do
addDependentRelativeFile filePath
compileTemplateTH filePath =<< THS.runIO (readFile filePath)
fileAndLine :: Loc -> String
fileAndLine loc = loc_filename loc ++ ":" ++ (show . fst . loc_start $ loc)
compileTemplateTH :: String -> String -> Q Exp
compileTemplateTH filename unprocessed =
either (fail . ("Parse error in mustache template: " ++) . show) THS.lift $ compileTemplate filename (pack unprocessed)
addDependentRelativeFile :: FilePath -> Q ()
addDependentRelativeFile = THS.qAddDependentFile <=< THS.runIO . makeAbsolute
-- ERRORS
fileNotFound :: FilePath -> ParseError
fileNotFound fp = newErrorMessage (Message $ printf "Template file '%s' not found" fp) (initialPos fp)

View file

@ -0,0 +1,41 @@
{-|
Module : $Header$
Description : Types and conversions
Copyright : (c) Justus Adam, 2015
License : BSD3
Maintainer : dev@justus.science
Stability : experimental
Portability : POSIX
escapeXML and xmlEntities curtesy to the tagsoup library.
-}
module Text.Mustache.Internal (uncons, escapeXMLText) where
import Data.Char (ord)
import qualified Data.IntMap as IntMap
import qualified Data.Text as T
uncons :: [α] -> Maybe (α, [α])
uncons [] = Nothing
uncons (x:xs) = return (x, xs)
escapeXMLText :: T.Text -> T.Text
escapeXMLText = T.pack . escapeXML . T.unpack
escapeXML :: String -> String
escapeXML = concatMap $ \x -> IntMap.findWithDefault [x] (ord x) mp
where mp = IntMap.fromList [(ord b, "&"++a++";") | (a,[b]) <- xmlEntities]
xmlEntities :: [(String, String)]
xmlEntities =
[ ("quot", "\"")
, ("#39", "'")
, ("amp" , "&")
, ("lt" , "<")
, ("gt" , ">")
]

View file

@ -0,0 +1,412 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
module Text.Mustache.Internal.Types where
import Control.Arrow
import Control.Monad.RWS hiding (lift)
import qualified Data.Aeson as Aeson
#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.KeyMap as KM
#endif
import Data.Int (Int8, Int16, Int32, Int64)
import Data.Foldable (toList)
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as HS
import qualified Data.Map as Map
import Data.Scientific
import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import Data.Text
import qualified Data.Text.Lazy as LT
import qualified Data.Vector as V
import Data.Word (Word8, Word16, Word32, Word64)
import Language.Haskell.TH.Lift (deriveLift)
import Language.Haskell.TH.Syntax
import Numeric.Natural (Natural)
-- | Type of errors we may encounter during substitution.
data SubstitutionError
= VariableNotFound [Key] -- ^ The template contained a variable for which there was no data counterpart in the current context
| InvalidImplicitSectionContextType String -- ^ When substituting an implicit section the current context had an unsubstitutable type
| InvertedImplicitSection -- ^ Inverted implicit sections should never occur
| SectionTargetNotFound [Key] -- ^ The template contained a section for which there was no data counterpart in the current context
| PartialNotFound FilePath -- ^ The template contained a partial for which there was no data counterpart in the current context
| DirectlyRenderedValue Value -- ^ A complex value such as an Object or Array was directly rendered into the template (warning)
deriving (Show)
tellError :: SubstitutionError -> SubM ()
tellError e = SubM $ tell ([e], [])
tellSuccess :: Text -> SubM ()
tellSuccess s = SubM $ tell ([], [s])
newtype SubM a = SubM { runSubM' :: RWS (Context Value, TemplateCache) ([SubstitutionError], [Text]) () a } deriving (Monad, Functor, Applicative, MonadReader (Context Value, TemplateCache))
runSubM :: SubM a -> Context Value -> TemplateCache -> ([SubstitutionError], [Text])
runSubM comp ctx cache = snd $ evalRWS (runSubM' comp) (ctx, cache) ()
shiftContext :: Context Value -> SubM a -> SubM a
shiftContext = local . first . const
-- | Search for a key in the current context.
--
-- The search is conducted inside out mening the current focus
-- is searched first. If the key is not found the outer scopes are recursively
-- searched until the key is found, then 'innerSearch' is called on the result.
search :: [Key] -> SubM (Maybe Value)
search [] = return Nothing
search (key:nextKeys) = (>>= innerSearch nextKeys) <$> go
where
go = asks fst >>= \case
Context parents focus -> do
let searchParents = case parents of
(newFocus: newParents) -> shiftContext (Context newParents newFocus) $ go
_ -> return Nothing
case focus of
Object o ->
case HM.lookup key o of
Just res -> return $ Just res
_ -> searchParents
_ -> searchParents
-- | Searches nested scopes navigating inward. Fails if it encunters something
-- other than an object before the key is expended.
innerSearch :: [Key] -> Value -> Maybe Value
innerSearch [] v = Just v
innerSearch (y:ys) (Object o) = HM.lookup y o >>= innerSearch ys
innerSearch _ _ = Nothing
-- | Syntax tree for a mustache template
type STree = ASTree Text
type ASTree α = [Node α]
-- | Basic values composing the STree
data Node α
= TextBlock α
| Section DataIdentifier (ASTree α)
| InvertedSection DataIdentifier (ASTree α)
| Variable Bool DataIdentifier
| Partial (Maybe α) FilePath
deriving (Show, Eq)
-- | Kinds of identifiers for Variables and sections
data DataIdentifier
= NamedData [Key]
| Implicit
deriving (Show, Eq)
-- | A list-like structure used in 'Value'
type Array = V.Vector Value
-- | A map-like structure used in 'Value'
type Object = HM.HashMap Text Value
-- | Source type for constructing 'Object's
type Pair = (Text, Value)
-- | Representation of stateful context for the substitution process
data Context α = Context { ctxtParents :: [α], ctxtFocus :: α }
deriving (Eq, Show, Ord)
-- | Internal value representation
data Value
= Object !Object
| Array !Array
| Number !Scientific
| String !Text
| Lambda (STree -> SubM STree)
| Bool !Bool
| Null
instance Show Value where
show (Lambda _) = "Lambda function"
show (Object o) = show o
show (Array a) = show a
show (String s) = show s
show (Number n) = show n
show (Bool b) = show b
show Null = "null"
listToMustache' :: ToMustache ω => [ω] -> Value
listToMustache' = Array . V.fromList . fmap toMustache
integralToMustache :: Integral ω => ω -> Value
integralToMustache = toMustache . toInteger
-- | Conversion class
class ToMustache ω where
toMustache :: ω -> Value
listToMustache :: [ω] -> Value
listToMustache = listToMustache'
instance ToMustache Float where
toMustache = Number . fromFloatDigits
instance ToMustache Double where
toMustache = Number . fromFloatDigits
instance ToMustache Integer where
toMustache = Number . fromInteger
instance ToMustache Natural where
toMustache = integralToMustache
instance ToMustache Int where
toMustache = integralToMustache
instance ToMustache Word where
toMustache = integralToMustache
instance ToMustache Int8 where
toMustache = integralToMustache
instance ToMustache Int16 where
toMustache = integralToMustache
instance ToMustache Int32 where
toMustache = integralToMustache
instance ToMustache Int64 where
toMustache = integralToMustache
instance ToMustache Word8 where
toMustache = integralToMustache
instance ToMustache Word16 where
toMustache = integralToMustache
instance ToMustache Word32 where
toMustache = integralToMustache
instance ToMustache Word64 where
toMustache = integralToMustache
instance ToMustache Char where
toMustache = toMustache . (:[])
listToMustache = String . pack
instance ToMustache Value where
toMustache = id
instance ToMustache Bool where
toMustache = Bool
instance ToMustache () where
toMustache = const Null
instance ToMustache ω => ToMustache (Maybe ω) where
toMustache (Just w) = toMustache w
toMustache Nothing = Null
instance ToMustache Text where
toMustache = String
instance ToMustache LT.Text where
toMustache = String . LT.toStrict
instance ToMustache Scientific where
toMustache = Number
instance ToMustache α => ToMustache [α] where
toMustache = listToMustache
instance ToMustache ω => ToMustache (Seq.Seq ω) where
toMustache = listToMustache' . toList
instance ToMustache ω => ToMustache (V.Vector ω) where
toMustache = Array . fmap toMustache
instance (ToMustache ω) => ToMustache (Map.Map Text ω) where
toMustache = mapInstanceHelper id
instance (ToMustache ω) => ToMustache (Map.Map LT.Text ω) where
toMustache = mapInstanceHelper LT.toStrict
instance (ToMustache ω) => ToMustache (Map.Map String ω) where
toMustache = mapInstanceHelper pack
mapInstanceHelper :: ToMustache v => (a -> Text) -> Map.Map a v -> Value
mapInstanceHelper conv =
toMustache
. Map.foldrWithKey
(\k -> HM.insert (conv k) . toMustache)
HM.empty
instance ToMustache ω => ToMustache (HM.HashMap Text ω) where
toMustache = Object . fmap toMustache
instance ToMustache ω => ToMustache (HM.HashMap LT.Text ω) where
toMustache = hashMapInstanceHelper LT.toStrict
instance ToMustache ω => ToMustache (HM.HashMap String ω) where
toMustache = hashMapInstanceHelper pack
hashMapInstanceHelper :: ToMustache v => (a -> Text) -> HM.HashMap a v -> Value
hashMapInstanceHelper conv =
toMustache
. HM.foldrWithKey
(\k -> HM.insert (conv k) . toMustache)
HM.empty
instance ToMustache (STree -> SubM STree) where
toMustache = Lambda
instance ToMustache Aeson.Value where
toMustache (Aeson.Object o) = Object $ fmap toMustache
#if MIN_VERSION_aeson(2,0,0)
(KM.toHashMapText o)
#else
o
#endif
toMustache (Aeson.Array a) = Array $ fmap toMustache a
toMustache (Aeson.Number n) = Number n
toMustache (Aeson.String s) = String s
toMustache (Aeson.Bool b) = Bool b
toMustache Aeson.Null = Null
instance ToMustache ω => ToMustache (HS.HashSet ω) where
toMustache = listToMustache' . HS.toList
instance ToMustache ω => ToMustache (Set.Set ω) where
toMustache = listToMustache' . Set.toList
instance (ToMustache α, ToMustache β) => ToMustache (α, β) where
toMustache (a, b) = toMustache [toMustache a, toMustache b]
instance (ToMustache α, ToMustache β, ToMustache γ)
=> ToMustache (α, β, γ) where
toMustache (a, b, c) = toMustache [toMustache a, toMustache b, toMustache c]
instance (ToMustache α, ToMustache β, ToMustache γ, ToMustache δ)
=> ToMustache (α, β, γ, δ) where
toMustache (a, b, c, d) = toMustache
[ toMustache a
, toMustache b
, toMustache c
, toMustache d
]
instance ( ToMustache α
, ToMustache β
, ToMustache γ
, ToMustache δ
, ToMustache ε
) => ToMustache (α, β, γ, δ, ε) where
toMustache (a, b, c, d, e) = toMustache
[ toMustache a
, toMustache b
, toMustache c
, toMustache d
, toMustache e
]
instance ( ToMustache α
, ToMustache β
, ToMustache γ
, ToMustache δ
, ToMustache ε
, ToMustache ζ
) => ToMustache (α, β, γ, δ, ε, ζ) where
toMustache (a, b, c, d, e, f) = toMustache
[ toMustache a
, toMustache b
, toMustache c
, toMustache d
, toMustache e
, toMustache f
]
instance ( ToMustache α
, ToMustache β
, ToMustache γ
, ToMustache δ
, ToMustache ε
, ToMustache ζ
, ToMustache η
) => ToMustache (α, β, γ, δ, ε, ζ, η) where
toMustache (a, b, c, d, e, f, g) = toMustache
[ toMustache a
, toMustache b
, toMustache c
, toMustache d
, toMustache e
, toMustache f
, toMustache g
]
instance ( ToMustache α
, ToMustache β
, ToMustache γ
, ToMustache δ
, ToMustache ε
, ToMustache ζ
, ToMustache η
, ToMustache θ
) => ToMustache (α, β, γ, δ, ε, ζ, η, θ) where
toMustache (a, b, c, d, e, f, g, h) = toMustache
[ toMustache a
, toMustache b
, toMustache c
, toMustache d
, toMustache e
, toMustache f
, toMustache g
, toMustache h
]
-- | A collection of templates with quick access via their hashed names
type TemplateCache = HM.HashMap String Template
-- | Type of key used for retrieving data from 'Value's
type Key = Text
{-|
A compiled Template with metadata.
-}
data Template = Template
{ name :: String
, ast :: STree
, partials :: TemplateCache
} deriving (Show)
deriveLift ''DataIdentifier
deriveLift ''Node
deriveLift ''Template
-- Data.HashMap 0.2.17.0 introduces its own Lift instance
#if !MIN_VERSION_unordered_containers(0,2,17)
instance Lift TemplateCache where
#if MIN_VERSION_template_haskell(2,16,0)
liftTyped m = [|| HM.fromList $$(liftTyped $ HM.toList m) ||]
#else
lift m = [| HM.fromList $(lift $ HM.toList m) |]
#endif
#endif
--Data.Text 1.2.4.0 introduces its own Lift Text instance
#if !MIN_VERSION_text(1,2,4)
instance Lift Text where
lift = lift . unpack
#endif

View file

@ -0,0 +1,311 @@
{-|
Module : $Header$
Description : Basic functions for dealing with mustache templates.
Copyright : (c) Justus Adam, 2015
License : BSD3
Maintainer : dev@justus.science
Stability : experimental
Portability : POSIX
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Text.Mustache.Parser
(
-- * Generic parsing functions
parse, parseWithConf
-- * Configurations
, MustacheConf(..), defaultConf
-- * Parser
, Parser, MustacheState
-- * Mustache Constants
, sectionBegin, sectionEnd, invertedSectionBegin, unescape2, unescape1
, delimiterChange, nestingSeparator
) where
import Control.Monad
import Data.Char (isAlphaNum, isSpace)
import Data.List (nub)
import Data.Monoid ((<>))
import Data.Text as T (Text, null, pack)
import Prelude as Prel
import Text.Mustache.Types
import Text.Parsec as P hiding (endOfLine, parse)
-- | Initial configuration for the parser
data MustacheConf = MustacheConf
{ delimiters :: (String, String)
}
-- | User state for the parser
data MustacheState = MustacheState
{ sDelimiters :: (String, String)
, textStack :: Text
, isBeginngingOfLine :: Bool
, currentSectionName :: Maybe DataIdentifier
}
data ParseTagRes
= SectionBegin Bool DataIdentifier
| SectionEnd DataIdentifier
| Tag (Node Text)
| HandledTag
-- | @#@
sectionBegin :: Char
sectionBegin = '#'
-- | @/@
sectionEnd :: Char
sectionEnd = '/'
-- | @>@
partialBegin :: Char
partialBegin = '>'
-- | @^@
invertedSectionBegin :: Char
invertedSectionBegin = '^'
-- | @{@ and @}@
unescape2 :: (Char, Char)
unescape2 = ('{', '}')
-- | @&@
unescape1 :: Char
unescape1 = '&'
-- | @=@
delimiterChange :: Char
delimiterChange = '='
-- | @.@
nestingSeparator :: Char
nestingSeparator = '.'
-- | @!@
comment :: Char
comment = '!'
-- | @.@
implicitIterator :: Char
implicitIterator = '.'
-- | Cannot be a letter, number or the nesting separation Character @.@
isAllowedDelimiterCharacter :: Char -> Bool
isAllowedDelimiterCharacter =
not . Prel.or . sequence
[ isSpace, isAlphaNum, (== nestingSeparator) ]
allowedDelimiterCharacter :: Parser Char
allowedDelimiterCharacter =
satisfy isAllowedDelimiterCharacter
-- | Empty configuration
emptyState :: MustacheState
emptyState = MustacheState ("", "") mempty True Nothing
-- | Default configuration (delimiters = ("{{", "}}"))
defaultConf :: MustacheConf
defaultConf = MustacheConf ("{{", "}}")
initState :: MustacheConf -> MustacheState
initState (MustacheConf { delimiters }) = emptyState { sDelimiters = delimiters }
setIsBeginning :: Bool -> Parser ()
setIsBeginning b = modifyState (\s -> s { isBeginngingOfLine = b })
-- | The parser monad in use
type Parser = Parsec Text MustacheState
(<<) :: Monad m => m b -> m a -> m b
(<<) = flip (>>)
endOfLine :: Parser String
endOfLine = do
r <- optionMaybe $ char '\r'
n <- char '\n'
return $ maybe id (:) r [n]
{-|
Runs the parser for a mustache template, returning the syntax tree.
-}
parse :: FilePath -> Text -> Either ParseError STree
parse = parseWithConf defaultConf
-- | Parse using a custom initial configuration
parseWithConf :: MustacheConf -> FilePath -> Text -> Either ParseError STree
parseWithConf = P.runParser parseText . initState
parseText :: Parser STree
parseText = do
(MustacheState { isBeginngingOfLine }) <- getState
if isBeginngingOfLine
then parseLine
else continueLine
appendStringStack :: String -> Parser ()
appendStringStack t = modifyState (\s -> s { textStack = textStack s <> pack t})
continueLine :: Parser STree
continueLine = do
(MustacheState { sDelimiters = ( start@(x:_), _ )}) <- getState
let forbidden = x : "\n\r"
many (noneOf forbidden) >>= appendStringStack
(try endOfLine >>= appendStringStack >> setIsBeginning True >> parseLine)
<|> (try (string start) >> switchOnTag >>= continueFromTag)
<|> (try eof >> finishFile)
<|> (anyChar >>= appendStringStack . (:[]) >> continueLine)
flushText :: Parser STree
flushText = do
s@(MustacheState { textStack = text }) <- getState
putState $ s { textStack = mempty }
return $ if T.null text
then []
else [TextBlock text]
finishFile :: Parser STree
finishFile =
getState >>= \case
(MustacheState {currentSectionName = Nothing}) -> flushText
(MustacheState {currentSectionName = Just name}) ->
parserFail $ "Unclosed section " <> show name
parseLine :: Parser STree
parseLine = do
(MustacheState { sDelimiters = ( start, _ ) }) <- getState
initialWhitespace <- many (oneOf " \t")
let handleStandalone = do
tag <- switchOnTag
let continueNoStandalone = do
appendStringStack initialWhitespace
setIsBeginning False
continueFromTag tag
standaloneEnding = do
try (skipMany (oneOf " \t") >> (eof <|> void endOfLine))
setIsBeginning True
case tag of
Tag (Partial _ name) ->
( standaloneEnding >>
continueFromTag (Tag (Partial (Just (pack initialWhitespace)) name))
) <|> continueNoStandalone
Tag _ -> continueNoStandalone
_ ->
( standaloneEnding >>
continueFromTag tag
) <|> continueNoStandalone
(try (string start) >> handleStandalone)
<|> (try eof >> appendStringStack initialWhitespace >> finishFile)
<|> (appendStringStack initialWhitespace >> setIsBeginning False >> continueLine)
continueFromTag :: ParseTagRes -> Parser STree
continueFromTag (SectionBegin inverted name) = do
textNodes <- flushText
state@(MustacheState
{ currentSectionName = previousSection }) <- getState
putState $ state { currentSectionName = return name }
innerSectionContent <- parseText
let sectionTag =
if inverted
then InvertedSection
else Section
modifyState $ \s -> s { currentSectionName = previousSection }
outerSectionContent <- parseText
return (textNodes <> [sectionTag name innerSectionContent] <> outerSectionContent)
continueFromTag (SectionEnd name) = do
(MustacheState
{ currentSectionName }) <- getState
case currentSectionName of
Just name' | name' == name -> flushText
Just name' -> parserFail $ "Expected closing sequence for \"" <> show name <> "\" got \"" <> show name' <> "\"."
Nothing -> parserFail $ "Encountered closing sequence for \"" <> show name <> "\" which has never been opened."
continueFromTag (Tag tag) = do
textNodes <- flushText
furtherNodes <- parseText
return $ textNodes <> return tag <> furtherNodes
continueFromTag HandledTag = parseText
switchOnTag :: Parser ParseTagRes
switchOnTag = do
(MustacheState { sDelimiters = ( _, end )}) <- getState
choice
[ SectionBegin False <$> (try (char sectionBegin) >> genParseTagEnd mempty)
, SectionEnd
<$> (try (char sectionEnd) >> genParseTagEnd mempty)
, Tag . Variable False
<$> (try (char unescape1) >> genParseTagEnd mempty)
, Tag . Variable False
<$> (try (char (fst unescape2)) >> genParseTagEnd (return $ snd unescape2))
, Tag . Partial Nothing
<$> (try (char partialBegin) >> spaces >> (noneOf (nub end) `manyTill` try (spaces >> string end)))
, return HandledTag
<< (try (char delimiterChange) >> parseDelimChange)
, SectionBegin True
<$> (try (char invertedSectionBegin) >> genParseTagEnd mempty >>= \case
n@(NamedData _) -> return n
_ -> parserFail "Inverted Sections can not be implicit."
)
, return HandledTag << (try (char comment) >> manyTill anyChar (try $ string end))
, Tag . Variable True
<$> genParseTagEnd mempty
]
where
parseDelimChange = do
(MustacheState { sDelimiters = ( _, end )}) <- getState
spaces
delim1 <- allowedDelimiterCharacter `manyTill` space
spaces
delim2 <- allowedDelimiterCharacter `manyTill` try (spaces >> string (delimiterChange : end))
when (delim1 == mempty || delim2 == mempty)
$ parserFail "Tags must contain more than 0 characters"
oldState <- getState
putState $ oldState { sDelimiters = (delim1, delim2) }
genParseTagEnd :: String -> Parser DataIdentifier
genParseTagEnd emod = do
(MustacheState { sDelimiters = ( start, end ) }) <- getState
let nEnd = emod <> end
disallowed = nub $ nestingSeparator : start <> end
parseOne :: Parser [Text]
parseOne = do
one <- noneOf disallowed
`manyTill` lookAhead
(try (spaces >> void (string nEnd))
<|> try (void $ char nestingSeparator))
others <- (char nestingSeparator >> parseOne)
<|> (const mempty <$> (spaces >> string nEnd))
return $ pack one : others
spaces
(try (char implicitIterator) >> spaces >> string nEnd >> return Implicit)
<|> (NamedData <$> parseOne)

View file

@ -0,0 +1,248 @@
{-|
Module : $Header$
Description : Functions for rendering mustache templates.
Copyright : (c) Justus Adam, 2015
License : BSD3
Maintainer : dev@justus.science
Stability : experimental
Portability : POSIX
-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Text.Mustache.Render
(
-- * Substitution
substitute, substituteValue
-- * Checked substitution
, checkedSubstitute, checkedSubstituteValue, SubstitutionError(..)
-- * Working with Context
, Context(..), search, innerSearch, SubM, substituteNode, substituteAST, catchSubstitute
-- * Util
, toString
) where
import Control.Arrow (first, second)
import Control.Monad
import Data.Foldable (for_)
import Data.HashMap.Strict as HM hiding (keys, map)
import Data.Maybe (fromMaybe)
import Data.Scientific (floatingOrInteger)
import Data.Text as T (Text, isSuffixOf, pack,
replace, stripSuffix)
import qualified Data.Vector as V
import Prelude hiding (length, lines, unlines)
import Control.Monad.Reader
import Control.Monad.Writer
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import Text.Mustache.Internal
import Text.Mustache.Internal.Types
import Text.Mustache.Types
{-|
Substitutes all mustache defined tokens (or tags) for values found in the
provided data structure.
Equivalent to @substituteValue . toMustache@.
-}
substitute :: ToMustache k => Template -> k -> Text
substitute t = substituteValue t . toMustache
{-|
Substitutes all mustache defined tokens (or tags) for values found in the
provided data structure and report any errors and warnings encountered during
substitution.
This function always produces results, as in a fully substituted/rendered template,
it never halts on errors. It simply reports them in the first part of the tuple.
Sites with errors are usually substituted with empty string.
The second value in the tuple is a template rendered with errors ignored.
Therefore if you must enforce that there were no errors during substitution
you must check that the error list in the first tuple value is empty.
Equivalent to @checkedSubstituteValue . toMustache@.
-}
checkedSubstitute :: ToMustache k => Template -> k -> ([SubstitutionError], Text)
checkedSubstitute t = checkedSubstituteValue t . toMustache
{-|
Substitutes all mustache defined tokens (or tags) for values found in the
provided data structure.
-}
substituteValue :: Template -> Value -> Text
substituteValue = (snd .) . checkedSubstituteValue
{-|
Substitutes all mustache defined tokens (or tags) for values found in the
provided data structure and report any errors and warnings encountered during
substitution.
This function always produces results, as in a fully substituted/rendered template,
it never halts on errors. It simply reports them in the first part of the tuple.
Sites with errors are usually substituted with empty string.
The second value in the tuple is a template rendered with errors ignored.
Therefore if you must enforce that there were no errors during substitution
you must check that the error list in the first tuple value is empty.
-}
checkedSubstituteValue :: Template -> Value -> ([SubstitutionError], Text)
checkedSubstituteValue template dataStruct =
second T.concat $ runSubM (substituteAST (ast template)) (Context mempty dataStruct) (partials template)
-- | Catch the results of running the inner substitution.
catchSubstitute :: SubM a -> SubM (a, Text)
catchSubstitute = fmap (second (T.concat . snd)) . SubM . hideResults . listen . runSubM'
where
hideResults = censor (\(errs, _) -> (errs, []))
-- | Substitute an entire 'STree' rather than just a single 'Node'
substituteAST :: STree -> SubM ()
substituteAST = mapM_ substituteNode
-- | Main substitution function
substituteNode :: Node Text -> SubM ()
-- subtituting text
substituteNode (TextBlock t) = tellSuccess t
-- substituting a whole section (entails a focus shift)
substituteNode (Section Implicit secSTree) =
asks fst >>= \case
Context parents focus@(Array a)
| V.null a -> return ()
| otherwise -> for_ a $ \focus' ->
let newContext = Context (focus:parents) focus'
in shiftContext newContext $ substituteAST secSTree
Context _ (Object _) -> substituteAST secSTree
Context _ v -> tellError $ InvalidImplicitSectionContextType $ showValueType v
substituteNode (Section (NamedData secName) secSTree) =
search secName >>= \case
Just arr@(Array arrCont) ->
if V.null arrCont
then return ()
else do
Context parents focus <- asks fst
for_ arrCont $ \focus' ->
let newContext = Context (arr:focus:parents) focus'
in shiftContext newContext $ substituteAST secSTree
Just (Bool False) -> return ()
Just Null -> return ()
Just (Lambda l) -> substituteAST =<< l secSTree
Just focus' -> do
Context parents focus <- asks fst
let newContext = Context (focus:parents) focus'
shiftContext newContext $ substituteAST secSTree
Nothing -> tellError $ SectionTargetNotFound secName
-- substituting an inverted section
substituteNode (InvertedSection Implicit _) = tellError InvertedImplicitSection
substituteNode (InvertedSection (NamedData secName) invSecSTree) =
search secName >>= \case
Just (Bool False) -> contents
Just (Array a) | V.null a -> contents
Just Null -> contents
Nothing -> contents
_ -> return ()
where
contents = mapM_ substituteNode invSecSTree
-- substituting a variable
substituteNode (Variable _ Implicit) = asks (ctxtFocus . fst) >>= toString >>= tellSuccess
substituteNode (Variable escaped (NamedData varName)) =
maybe
(tellError $ VariableNotFound varName)
(toString >=> tellSuccess . (if escaped then escapeXMLText else id))
=<< search varName
-- substituting a partial
substituteNode (Partial indent pName) = do
cPartials <- asks snd
case HM.lookup pName cPartials of
Nothing -> tellError $ PartialNotFound pName
Just t ->
let ast' = handleIndent indent $ ast t
in local (second (partials t `HM.union`)) $ substituteAST ast'
showValueType :: Value -> String
showValueType Null = "Null"
showValueType (Object _) = "Object"
showValueType (Array _) = "Array"
showValueType (String _) = "String"
showValueType (Lambda _) = "Lambda"
showValueType (Number _) = "Number"
showValueType (Bool _) = "Bool"
handleIndent :: Maybe Text -> STree -> STree
handleIndent Nothing ast' = ast'
handleIndent (Just indentation) ast' = preface <> content
where
preface = if T.null indentation then [] else [TextBlock indentation]
content = if T.null indentation
then ast'
else reverse $ fromMaybe [] (uncurry (:) . first dropper <$> uncons (reverse fullIndented))
where
fullIndented = fmap (indentBy indentation) ast'
dropper (TextBlock t) = TextBlock $
if ("\n" <> indentation) `isSuffixOf` t
then fromMaybe t $ stripSuffix indentation t
else t
dropper a = a
indentBy :: Text -> Node Text -> Node Text
indentBy indent p@(Partial (Just indent') name')
| T.null indent = p
| otherwise = Partial (Just (indent <> indent')) name'
indentBy indent (Partial Nothing name') = Partial (Just indent) name'
indentBy indent (TextBlock t) = TextBlock $ replace "\n" ("\n" <> indent) t
indentBy _ a = a
-- | Converts values to Text as required by the mustache standard
toString :: Value -> SubM Text
toString (String t) = return t
toString (Number n) = return $ either (pack . show) (pack . show) (floatingOrInteger n :: Either Double Integer)
toString (Lambda l) = do
((), res) <- catchSubstitute $ substituteAST =<< l []
return res
toString e = do
tellError $ DirectlyRenderedValue e
return $ pack $ show e
instance ToMustache (Context Value -> STree -> STree) where
toMustache f = Lambda $ (<$> askContext) . flip f
instance ToMustache (Context Value -> STree -> Text) where
toMustache = lambdaHelper id
instance ToMustache (Context Value -> STree -> LT.Text) where
toMustache = lambdaHelper LT.toStrict
instance ToMustache (Context Value -> STree -> String) where
toMustache = lambdaHelper pack
lambdaHelper :: (r -> Text) -> (Context Value -> STree -> r) -> Value
lambdaHelper conv f = Lambda $ (<$> askContext) . wrapper
where
wrapper :: STree -> Context Value -> STree
wrapper lSTree c = [TextBlock $ conv $ f c lSTree]
instance ToMustache (STree -> SubM Text) where
toMustache f = Lambda (fmap (return . TextBlock) . f)

View file

@ -0,0 +1,110 @@
{-|
Module : $Header$
Description : Types and conversions
Copyright : (c) Justus Adam, 2015
License : BSD3
Maintainer : dev@justus.science
Stability : experimental
Portability : POSIX
-}
module Text.Mustache.Types
(
-- * Types for the Parser / Template
ASTree
, STree
, Node(..)
, DataIdentifier(..)
, Template(..)
, TemplateCache
-- * Types for the Substitution / Data
, Value(..)
, Key
-- ** Converting
, object
, (~>), (), (~=), ()
, ToMustache, toMustache, mFromJSON, integralToMustache
-- ** Representation
, Array, Object, Pair
, SubM, askContext, askPartials
, Context(..)
) where
import Control.Monad.Reader
import qualified Data.Aeson as Aeson
import qualified Data.HashMap.Strict as HM
import Data.Text (Text)
import Text.Mustache.Internal.Types
-- | Convenience function for creating Object values.
--
-- This function is supposed to be used in conjuction with the '~>' and '~=' operators.
--
-- ==== __Examples__
--
-- @
-- data Address = Address { ... }
--
-- instance Address ToJSON where
-- ...
--
-- data Person = Person { name :: String, address :: Address }
--
-- instance ToMustache Person where
-- toMustache (Person { name, address }) = object
-- [ "name" ~> name
-- , "address" ~= address
-- ]
-- @
--
-- Here we can see that we can use the '~>' operator for values that have
-- themselves a 'ToMustache' instance, or alternatively if they lack such an
-- instance but provide an instance for the 'ToJSON' typeclass we can use the
-- '~=' operator.
object :: [Pair] -> Value
object = Object . HM.fromList
-- | Map keys to values that provide a 'ToMustache' instance
--
-- Recommended in conjunction with the `OverloadedStrings` extension.
(~>) :: ToMustache ω => Text -> ω -> Pair
(~>) t = (t, ) . toMustache
{-# INLINEABLE (~>) #-}
infixr 8 ~>
-- | Unicode version of '~>'
() :: ToMustache ω => Text -> ω -> Pair
() = (~>)
{-# INLINEABLE (↝) #-}
infixr 8
-- | Map keys to values that provide a 'ToJSON' instance
--
-- Recommended in conjunction with the `OverloadedStrings` extension.
(~=) :: Aeson.ToJSON ι => Text -> ι -> Pair
(~=) t = (t ~>) . Aeson.toJSON
{-# INLINEABLE (~=) #-}
infixr 8 ~=
-- | Unicode version of '~='
() :: Aeson.ToJSON ι => Text -> ι -> Pair
() = (~=)
{-# INLINEABLE (⥱) #-}
infixr 8
-- | Converts a value that can be represented as JSON to a Value.
mFromJSON :: Aeson.ToJSON ι => ι -> Value
mFromJSON = toMustache . Aeson.toJSON
askContext :: SubM (Context Value)
askContext = asks fst
askPartials :: SubM TemplateCache
askPartials = asks snd

View file

@ -0,0 +1,110 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE UnicodeSyntax #-}
module Main where
import qualified Codec.Archive.Tar as Tar
import qualified Codec.Compression.GZip as GZip
import Control.Applicative ((<$>), (<*>))
import Control.Lens
import Control.Monad
import Data.ByteString.Lazy (toStrict)
import Data.Foldable (for_)
import qualified Data.HashMap.Strict as HM (HashMap, empty,
traverseWithKey)
import Data.List
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Data.Yaml as Y (FromJSON, Value (..), decode,
parseJSON, (.!=), (.:), (.:?))
import Network.Wreq
import System.FilePath
import Test.Hspec
import Text.Mustache
langspecs :: [String]
langspecs =
[ "https://codeload.github.com/andrewthad/spec/legacy.tar.gz/add_list_context_check"
, "https://codeload.github.com/mustache/spec/tar.gz/v1.1.3"
]
data LangSpecFile = LangSpecFile
{ overview :: String
, tests :: [LangSpecTest]
}
data LangSpecTest = LangSpecTest
{ name :: String
, specDescription :: String
, specData :: Y.Value
, template :: T.Text
, expected :: T.Text
, testPartials :: HM.HashMap String T.Text
}
instance FromJSON LangSpecFile where
parseJSON (Y.Object o) = LangSpecFile
<$> o .: "overview"
<*> o .: "tests"
parseJSON _ = mzero
instance FromJSON LangSpecTest where
parseJSON (Y.Object o) = LangSpecTest
<$> o .: "name"
<*> o .: "desc"
<*> o .: "data"
<*> o .: "template"
<*> o .: "expected"
<*> o .:? "partials" .!= HM.empty
parseJSON _ = mzero
getOfficialSpecRelease :: String -> IO [(String, LangSpecFile)]
getOfficialSpecRelease releaseURL = do
res <- get releaseURL
let archive = Tar.read $ GZip.decompress (res ^. responseBody)
return $ Tar.foldEntries handleEntry [] (error . show) archive
where
handleEntry e acc =
case content of
Tar.NormalFile f _
| takeExtension filename `elem` [".yml", ".yaml"]
&& not ("~" `isPrefixOf` takeFileName filename) ->
(filename, fromMaybe (error $ "Error parsing spec file " ++ filename) $ decode $ toStrict f):acc
_ -> acc
where
filename = Tar.entryPath e
content = Tar.entryContent e
testOfficialLangSpec :: [(String, LangSpecFile)] -> Spec
testOfficialLangSpec testfiles =
for_ testfiles $ \(filename, LangSpecFile { tests }) ->
describe ("File: " ++ takeFileName filename) $
for_ tests $ \(LangSpecTest { .. }) ->
it ("Name: " ++ name ++ " Description: " ++ specDescription) $
let
compiled = do
partials' <- HM.traverseWithKey compileTemplate testPartials
template' <- compileTemplate name template
return $ template' { partials = partials' }
in
case compiled of
Left m -> expectationFailure $ show m
Right tmp ->
substituteValue tmp (toMustache specData) `shouldBe` expected
main :: IO ()
main =
void $ do
specs <- mapM getOfficialSpecRelease langspecs
hspec $ mapM_ testOfficialLangSpec specs

260
mustache/test/unit/Spec.hs Normal file
View file

@ -0,0 +1,260 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where
import Control.Applicative ((<$>), (<*>))
import Data.Either
import Data.Function (on)
import Data.Monoid
import qualified Data.Text as T
import System.IO.Unsafe (unsafePerformIO)
import Test.Hspec
import Text.Mustache
import Text.Mustache.Compile
import Text.Mustache.Parser
import Text.Mustache.Types
escaped :: Bool
escaped = True
unescaped :: Bool
unescaped = False
parserSpec :: Spec
parserSpec =
describe "mustacheParser" $ do
let lparse = parse "testsuite"
let returnedOne = return . return
let text = "test12356p0--=-34{}jnv,\n"
it "parses text" $
lparse text `shouldBe` returnedOne (TextBlock text)
it "parses a variable" $
lparse "{{name}}" `shouldBe` returnedOne (Variable escaped (NamedData ["name"]))
it "parses a variable with whitespace" $
lparse "{{ name }}" `shouldBe` returnedOne (Variable escaped (NamedData ["name"]))
it "allows '-' in variable names" $
lparse "{{ name-name }}" `shouldBe`
returnedOne (Variable True (NamedData ["name-name"]))
it "allows '_' in variable names" $
lparse "{{ name_name }}" `shouldBe`
returnedOne (Variable True (NamedData ["name_name"]))
it "parses a variable unescaped with {{{}}}" $
lparse "{{{name}}}" `shouldBe` returnedOne (Variable unescaped (NamedData ["name"]))
it "parses a variable unescaped with {{{}}} with whitespace" $
lparse "{{{ name }}}" `shouldBe`
returnedOne (Variable False (NamedData ["name"]))
it "parses a variable unescaped with &" $
lparse "{{&name}}" `shouldBe` returnedOne (Variable unescaped (NamedData ["name"]))
it "parses a variable unescaped with & with whitespace" $
lparse "{{& name }}" `shouldBe`
returnedOne (Variable False (NamedData ["name"]))
it "parses a partial" $
lparse "{{>myPartial}}" `shouldBe`
returnedOne (Partial (Just "") "myPartial")
it "parses a partial with whitespace" $
lparse "{{> myPartial }}" `shouldBe`
returnedOne (Partial (Just "") "myPartial")
it "parses the an empty section" $
lparse "{{#section}}{{/section}}" `shouldBe`
returnedOne (Section (NamedData ["section"]) mempty)
it "parses the an empty section with whitespace" $
lparse "{{# section }}{{/ section }}" `shouldBe`
returnedOne (Section (NamedData ["section"]) mempty)
it "parses a delimiter change" $
lparse "{{=<< >>=}}<<var>>{{var}}" `shouldBe`
return [Variable True (NamedData ["var"]), TextBlock "{{var}}"]
it "parses a delimiter change with whitespace" $
lparse "{{=<< >>=}}<< var >>{{var}}" `shouldBe`
return [Variable True (NamedData ["var"]), TextBlock "{{var}}"]
it "parses two subsequent delimiter changes" $
lparse "{{=(( ))=}}(( var ))((=-- $-=))--#section$---/section$-" `shouldBe`
return [Variable True (NamedData ["var"]), Section (NamedData ["section"]) []]
it "propagates a delimiter change from a nested scope" $
lparse "{{#section}}{{=<< >>=}}<</section>><<var>>" `shouldBe`
return [Section (NamedData ["section"]) [], Variable escaped (NamedData ["var"])]
it "fails if the tag contains illegal characters" $
lparse "{{#&}}" `shouldSatisfy` isLeft
it "parses a nested variable" $
lparse "{{ name.val }}" `shouldBe` returnedOne (Variable escaped (NamedData ["name", "val"]))
it "parses a variable containing whitespace" $
lparse "{{ val space }}" `shouldBe` returnedOne (Variable escaped (NamedData ["val space"]))
substituteSpec :: Spec
substituteSpec =
describe "substitute" $ do
let toTemplate ast' = Template "testsuite" ast' mempty
it "substitutes a html escaped value for a variable" $
substitute
(toTemplate [Variable escaped (NamedData ["name"])])
(object ["name" ~> ("\" ' < > &" :: T.Text)])
`shouldBe` "&quot; &#39; &lt; &gt; &amp;"
it "substitutes raw value for an unescaped variable" $
substitute
(toTemplate [Variable unescaped (NamedData ["name"])])
(object ["name" ~> ("\" ' < > &" :: T.Text)])
`shouldBe` "\" ' < > &"
it "substitutes a section when the key is present (and an empty object)" $
substitute
(toTemplate [Section (NamedData ["section"]) [TextBlock "t"]])
(object ["section" ~> object []])
`shouldBe` "t"
it "substitutes a section when the key is present (and 'true')" $
substitute
(toTemplate [Section (NamedData ["section"]) [TextBlock "t"]])
(object ["section" ~> True])
`shouldBe` "t"
it "substitutes a section once when the key is present and a singleton list" $
substitute
(toTemplate [Section (NamedData ["section"]) [TextBlock "t"]])
(object ["section" ~> ["True" :: T.Text]])
`shouldBe` "t"
it "substitutes a section twice when the key is present and a list with two items" $
substitute
(toTemplate [Section (NamedData ["section"]) [TextBlock "t"]])
(object ["section" ~> (["True", "False"] :: [T.Text])])
`shouldBe` "tt"
it "substitutes a section twice when the key is present and a list with two\
\ objects, changing the scope to each object" $
substitute
(toTemplate [Section (NamedData ["section"]) [Variable escaped (NamedData ["t"])]])
(object
[ "section" ~>
[ object ["t" ~> ("var1" :: T.Text)]
, object ["t" ~> ("var2" :: T.Text)]
]
])
`shouldBe` "var1var2"
it "substitutes an inverse section when the key is present (and null)" $
substitute
(toTemplate [InvertedSection (NamedData ["section"]) [TextBlock "t"]])
(object ["section" ~> Null])
`shouldBe` "t"
it "does not substitute a section when the key is not present" $
substitute
(toTemplate [Section (NamedData ["section"]) [TextBlock "t"]])
(object [])
`shouldBe` ""
it "does not substitute a section when the key is present (and 'false')" $
substitute
(toTemplate [Section (NamedData ["section"]) [TextBlock "t"]])
(object ["section" ~> False])
`shouldBe` ""
it "does not substitute a section when the key is present (and null)" $
substitute
(toTemplate [Section (NamedData ["section"]) [TextBlock "t"]])
(object ["section" ~> Null])
`shouldBe` ""
it "does not substitute a section when the key is present (and empty list)" $
substitute
(toTemplate [Section (NamedData ["section"]) [TextBlock "t"]])
(object ["section" ~> ([] :: [T.Text])])
`shouldBe` ""
it "substitutes a lambda by applying lambda to contained text" $
substitute
(toTemplate [Section (NamedData ["lambda"]) [TextBlock "t"]])
(object ["lambda" ~> (overText T.toUpper)])
`shouldBe` "T"
it "substitutes a lambda by applying lambda to the nested substitution results" $
substitute
(toTemplate [Section (NamedData ["lambda"]) [TextBlock "t", Variable escaped (NamedData ["inner"])]])
(object [ "lambda" ~> (overText T.toUpper)
, "inner" ~> ("var" :: T.Text)
])
`shouldBe` "TVAR"
it "substitutes a lambda used directly as if applied to empty block" $
substitute
(toTemplate [Variable escaped (NamedData ["lambda"])])
(object ["lambda" ~> (Lambda $ \[] -> return [TextBlock "T"])])
`shouldBe` "T"
it "substitutes a nested section" $
substitute
(toTemplate [Variable escaped (NamedData ["outer", "inner"])])
(object
[ "outer" ~> object ["inner" ~> ("success" :: T.Text)]
, "inner" ~> ("error" :: T.Text)
]
)
`shouldBe` "success"
converterSpec :: Spec
converterSpec =
describe "toMustache" $
it "converts a String" $
toMustache ("My String" :: String) `shouldSatisfy` \case (String "My String") -> True; _ -> False
-- This is a one-off instance to define how we want the Spec to compare templates
instance Eq Template where
(==) = (==) `on` ast
compileTimeSpec :: Spec
compileTimeSpec =
describe "compileTimeCompiling" $ do
it "creates compiled templates from a QuasiQuoter" $
Right [mustache|This {{ template }} was injected at compile time with a quasiquoter|] `shouldBe`
compileTemplate "Template Name" "This {{ template }} was injected at compile time with a quasiquoter"
it "creates compiled templates from an embedded file" $
Right $(embedTemplate ["test/unit/examples"] "test-template.txt.mustache") `shouldBe`
compileTemplate "Template Name" "This {{ template }} was injected at compile time with an embedded file\n"
it "creates compiled templates from a single embedded file" $
Right $(embedSingleTemplate "test/unit/examples/test-template.txt.mustache") `shouldBe`
compileTemplate "Template Name" "This {{ template }} was injected at compile time with an embedded file\n"
it "creates compiled templates from an embedded file containing partials" $
Right $(embedTemplate ["test/unit/examples", "test/unit/examples/partials"] "test-template-partials.txt.mustache") `shouldBe`
unsafePerformIO (automaticCompile ["test/unit/examples", "test/unit/examples/partials"] "test-template-partials.txt.mustache")
main :: IO ()
main = hspec $ do
parserSpec
substituteSpec
converterSpec
compileTimeSpec

View file

@ -0,0 +1 @@
and {{ partials }}

View file

@ -0,0 +1 @@
This {{ template }} was injected at compile time with an embedded file {{> test-partial.txt.mustache }}

View file

@ -0,0 +1 @@
This {{ template }} was injected at compile time with an embedded file