The question of selecting a template engine raises quite often. And today I want to describe the library we use at work and why do I use that. The library provides a nice way to render dynamic templates.
Firstly, let's make the scope of the task more narrow to reduce the number of potential solutions:
It sets some constraints; for example, we can't use Shakespeare as a powerful library, as it works at compile time. We have to understand that we give up some type checking properties by having this functionality.
ED-E is a nice library that can render data based on the templates with syntax similar to Jinja2. It's widely known so a person who is not aware of Haskell can edit and update templates. And this is good as you can pass the text directly to the i18n team.
It quite safe it does not evaluate arbitrary code at runtime and provide a basic set of filters and predicates; however, you can add your own.
It stateless rendering and applying passes are separated so you can load and compile template ahead of time to reduce application costs.
Unfortunately, there is no full-featured documentation, but everything can be read from the main module haddock and filters module haddock but readme mentions that you can take a look at tests as well.
To use ede
add it to `build-depends` of your library.
To use the library in your module add.
import qualified Text.EDE as EDE
To parse the data, there is a family of functions. parse*
source | includes | syntax | |
---|---|---|---|
parse | pure ByteString | no | default |
parseIO | pure ByteString | directory | default |
parseFile | file content | no | default |
parseFileWith | file content | directory | overridable |
parseWith | pure ByteString | custom | overridable |
In this example, we will use default syntax but will use a template from the file, and now includes so that we will use parseFile
:
import System.Exit (die)
main :: IO ()
main = do
EDE.parseFile "template.tpl" >>= \case
Success x -> undefined x
Failure doc -> die (show doc)
If you run that you'll get an error message file template.tpl doesn't exist.
If you write a wrong template file you'll get a pretty descriptive error, for example:
template.tpl:1:1: error: whitespace significant, expected: "{!", "{{",
end of input, new-line, space
1 | {% sdf
| ^
Now we can actually render something. Let's render email data, that we want to send to some known user with a confirmation code and some additional details. To render the data, you should use render
or renderWith
function, the latter can take additional filters defined in your application, but more on that later.
render
:: Template -- ^ Parsed Template to render.
-> Object -- ^ Bindings to make available in the environment.
-> Result Text -- ^ Render an Object using the supplied Template.
Here Object
is just an type Object
from the aeson
library (i.e. `HashMap Text Value`). You can get it using fromValue :: Value -> Maybe Object
. Or building directly using fromPairs
import Text.EDE ((.=), fromPairs)
import qualified Data.Text as T
import qualified Data.Text.Lazy.IO as TL
main :: IO ()
main = do
EDE.parseFile "template.tpl" >>= \case
EDE.Success tpl -> do
let input = fromPairs
[ "userName" .= ("Alexander" :: T.Text)
, "code" .= ("32167" :: T.Text)
, "purchases" .=
[ "heroes 2"::T.Text
, "starcraft 2"
, "little big adventure 2"
]
]
case EDE.render tpl input of
EDE.Success result -> TL.putStrLn result
EDE.Failure doc -> die (show doc)
EDE.Failure doc -> die (show doc)
And with a template:
Hello {{ userName }},
You've just purchased
<ul>
{% for p in purchases %}
<li> {% if p.last %} and last but not the least{% endif %} {{p.index}}: {{p.value | toUpper}} </li>
{% endfor %}
<ul>
You get:
$ cabal run
Hello Alexander,
You've just purchased
<ul>
<li> 1: HEROES 2 </li>
<li> 2: STARCRAFT 2 </li>
<li> and last but not the least 3: LITTLE BIG ADVENTURE 2 </li>
<ul>
So here we used basic control structures if
and for
with additional properties they add and a filter toUpper
.
If you want to add your own filters you can do that quite easily, for example in our codebase we had:
markdownReplace :: Term
markdownReplace = TLam $ \(TVal (String s)) -> pure $ TVal $ String $ telegramMarkdownV2Escape s
That escaped all markdown characters that Telegram messenger does not accept in a string value.
So I hope that if you need a template library, you'll find this one useful.