Copyright | (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> |
---|---|
License | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. A copy of the MPL can be found in the LICENSE file or you can obtain it at http://mozilla.org/MPL/2.0/. |
Maintainer | Brendan Hay <brendan.g.hay@gmail.com> |
Stability | experimental |
Portability | non-portable (GHC extensions) |
Safe Haskell | Safe-Inferred |
Language | Haskell2010 |
Synopsis
- data Template
- parse :: ByteString -> Result Template
- parseIO :: FilePath -> ByteString -> IO (Result Template)
- parseFile :: FilePath -> IO (Result Template)
- parseFileWith :: Syntax -> FilePath -> IO (Result Template)
- parseWith :: Monad m => Syntax -> Resolver m -> Text -> ByteString -> m (Result Template)
- type Resolver m = Syntax -> Id -> Delta -> m (Result Template)
- type Id = Text
- includeMap :: Monad m => HashMap Id Template -> Resolver m
- includeFile :: FilePath -> Resolver IO
- render :: Template -> HashMap Text Value -> Result Text
- renderWith :: HashMap Id Term -> Template -> HashMap Text Value -> Result Text
- eitherParse :: ByteString -> Either String Template
- eitherParseFile :: FilePath -> IO (Either String Template)
- eitherParseWith :: (Functor m, Monad m) => Syntax -> Resolver m -> Text -> ByteString -> m (Either String Template)
- eitherRender :: Template -> HashMap Text Value -> Either String Text
- eitherRenderWith :: HashMap Id Term -> Template -> HashMap Text Value -> Either String Text
- data Delta
- data Result a
- eitherResult :: Result a -> Either String a
- result :: (AnsiDoc -> b) -> (a -> b) -> Result a -> b
- success :: Monad m => a -> m (Result a)
- failure :: Monad m => AnsiDoc -> m (Result a)
- fromValue :: Value -> Maybe (HashMap Text Value)
- fromPairs :: [Pair] -> HashMap Text Value
- (.=) :: (KeyValue e kv, ToJSON v) => Key -> v -> kv
- version :: Version
- type Delim = (String, String)
- data Syntax
- delimPragma :: HasSyntax c => Lens' c Delim
- delimInline :: HasSyntax c => Lens' c Delim
- delimComment :: HasSyntax c => Lens' c Delim
- delimBlock :: HasSyntax c => Lens' c Delim
- defaultSyntax :: Syntax
- alternateSyntax :: Syntax
How to use this library
A simple example of parsing and rendering Text
containing a basic conditional
expression and variable interpolation follows.
First the Template
is defined and parsed in the Result
monad:
>>>
tmpl <- parse "{% if var %}\nHello, {{ var }}!\n{% else %}\nnegative!\n{% endif %}\n" :: Result Template
Then an 'HashMap Text Value' is defined containing the environment which will be
available to the Template
during rendering:
>>>
let env = fromPairs [ "var" .= "World" ] :: HashMap Text Value
Note: the fromPairs
function above is a wrapper over Aeson's object
which removes the outer Object
Value
constructor, exposing the underlying HashMap
.
Then, the Template
is rendered using the 'HashMap Text Value' environment:
>>>
render tmpl env :: Result Text
> Success "Hello, World!"
In this manner, Template
s can be pre-compiled to the internal AST and
the cost of parsing can be amortised if the same Template
is rendered multiple times.
Another example, this time rendering a Template
from a file:
import qualified Data.Text.Lazy as Text.Lazy import qualified Text.EDE as EDE main :: IO () main = do r <- eitherParseFile "template.ede" either error print $ r >>= (`eitherRender` env) where env = fromPairs [ "text" .= "Some Text." , "int" .= 1 , "list" .= [5..10] ]
Please see the syntax section for more information about available statements and expressions.
Parsing and Rendering
Parsing and rendering require two separate steps intentionally so that the
more expensive (and potentially impure) action of parsing and resolving
include
s can be embedded and re-used in a pure fashion.
- Parsing tokenises the input and converts it to an internal AST representation,
resolving
include
s using a custom function. The result is a compiled template which can be cached for future use. - Rendering takes a
HashMap
of customFun
s (functions available in the template context), an 'HashMap Text Value' as the binding environment, and a parsedTemplate
to subsitute the values into. The result is a LazyText
value containing the rendered output.
A parsed and compiled template.
Parsing
:: ByteString | Strict |
-> Result Template |
Parse a ByteString
into a compiled Template
.
Because this function is pure and does not resolve include
s,
encountering an include
expression during parsing will result in an Error
.
See parseFile
or parseWith
for mechanisms to deal with include
dependencies.
:: FilePath | Parent directory for relatively pathed includes. |
-> ByteString | Strict |
-> IO (Result Template) |
Parse a ByteString
into a compiled Template
.
This function handles all include
expressions as FilePath
s and performs
recursive loading/parsing.
:: Syntax | Delimiters and parsing options. |
-> FilePath | Path to the template to load and parse. |
-> IO (Result Template) |
See: parseFile
.
:: Monad m | |
=> Syntax | Delimiters and parsing options. |
-> Resolver m | Function to resolve includes. |
-> Text | Strict |
-> ByteString | Strict |
-> m (Result Template) |
Parse a Template
from a Strict ByteString
using a custom function for
resolving include
expressions.
Two custom include
resolvers are supplied:
parseFile
for example, is defined as: parseWith
includeFile
.
Includes
The Resolver
used to resolve include
expressions determines the purity
of Template
parsing.
For example, using the includeFile
Resolver
means parsing is restricted
to IO
, while pre-caching a HashMap
of Template
s and supplying them to
parseWith
using includeMap
offers a pure variant for include
resolution.
type Resolver m = Syntax -> Id -> Delta -> m (Result Template) Source #
A function to resolve the target of an include
expression.
Rendering
:: Template | Parsed |
-> HashMap Text Value | Bindings to make available in the environment. |
-> Result Text |
Render an 'HashMap Text Value' using the supplied Template
.
:: HashMap Id Term | Filters to make available in the environment. |
-> Template | Parsed |
-> HashMap Text Value | Bindings to make available in the environment. |
-> Result Text |
Render an 'HashMap Text Value' using the supplied Template
.
Either Variants
eitherParse :: ByteString -> Either String Template Source #
See: parse
eitherParseWith :: (Functor m, Monad m) => Syntax -> Resolver m -> Text -> ByteString -> m (Either String Template) Source #
See: parseWith
eitherRenderWith :: HashMap Id Term -> Template -> HashMap Text Value -> Either String Text Source #
See: renderWith
Results and Errors
The Result
of a parse
or render
steps can be inspected or analysed using
result
as follows:
>>>
result failure success $ render tmpl env
If you're only interested in dealing with errors as strings, and the positional
information contained in Meta
is not of use you can use the convenience functions
eitherParse
, eitherRender
, or convert a Result
to Either
using eitherResult
.
>>>
either failure success $ eitherParse tmpl
Since there are multiple ways to be at a certain location, Delta
captures
all these alternatives as a single type.
Columns !Int64 !Int64 | ( number of characters , number of bytes ) |
Tab !Int64 !Int64 !Int64 | ( number of characters before the tab , number of characters after the tab , number of bytes ) |
Lines !Int64 !Int64 !Int64 !Int64 | ( number of newlines contained , number of characters since the last newline , number of bytes , number of bytes since the last newline ) |
Directed !ByteString !Int64 !Int64 !Int64 !Int64 | ( current file name , number of lines since the last line directive , number of characters since the last newline , number of bytes , number of bytes since the last newline ) |
Instances
The result of running parsing or rendering steps.
Instances
Foldable Result Source # | |
Defined in Text.EDE.Internal.Types fold :: Monoid m => Result m -> m # foldMap :: Monoid m => (a -> m) -> Result a -> m # foldMap' :: Monoid m => (a -> m) -> Result a -> m # foldr :: (a -> b -> b) -> b -> Result a -> b # foldr' :: (a -> b -> b) -> b -> Result a -> b # foldl :: (b -> a -> b) -> b -> Result a -> b # foldl' :: (b -> a -> b) -> b -> Result a -> b # foldr1 :: (a -> a -> a) -> Result a -> a # foldl1 :: (a -> a -> a) -> Result a -> a # elem :: Eq a => a -> Result a -> Bool # maximum :: Ord a => Result a -> a # minimum :: Ord a => Result a -> a # | |
Traversable Result Source # | |
Alternative Result Source # | |
Applicative Result Source # | |
Functor Result Source # | |
Monad Result Source # | |
Show a => Show (Result a) Source # | |
Show a => AnsiPretty (Result a) Source # | |
:: (AnsiDoc -> b) | Function to apply to the |
-> (a -> b) | Function to apply to the |
-> Result a | The |
-> b |
Perform a case analysis on a Result
.
Input
fromPairs
(or fromValue
) is a wrapper around Aeson's object
function which
safely strips the outer Value
constructor, providing the correct type
signature for input into render
.
It is used in combination with the re-exported .=
as follows:
>>>
render (fromPairs [ "foo" .= "value", "bar" .= 1 ]) :: Template -> Result Text
Version
Syntax
defaultSyntax :: Syntax Source #
The default ED-E syntax.
Delimiters:
- Pragma:
{! ... !}
- Inline:
{{ ... }}
- Comments:
{# ... #}
- Blocks:
{% ... %}
alternateSyntax :: Syntax Source #
An alternate syntax (based on Play/Scala templates) designed to be used when the default is potentially ambiguous due to another encountered smarty based syntax.
Delimiters:
- Inline:
<@ ... @>
- Comments:
@* ... *@
- Blocks:
@( ... )@
Pragmas
Syntax can be modified either via the arguments to parseWith
or alternatively
by specifying the delimiters via an EDE_SYNTAX
pragma.
Note: The pragmas must start on line1. Subsequently encountered pragmas are parsed as textual template contents.
For example:
{! EDE_SYNTAX pragma=("{*", "*}") inline=("#@", "@#") comment=("<#", "#>") block=("$$", "$$") !} {* EDE_SYNTAX block=("#[", "]#") *} ...
Would result in the following syntax:
- Pragmas:
{* ... *}
- Inline:
#@ ... @#
- Comment:
<# ... #>
- Block:
#[ ... ]#
Note: EDE_SYNTAX
pragmas only take effect for the current template, not
child includes. If you want to override the syntax for all templates use parseWith
and custom Syntax
settings.
Expressions
Expressions behave as any simplistic programming language with a variety of prefix, infix, and postifx operators available. (See: Text.EDE.Filters)
A rough overview of the expression grammar:
expression ::= literal | identifier | '|' filter filter ::= identifier identifier ::= [a-zA-Z_]{1}[0-9A-Za-z_']* object ::= '{' pairs '}' pairs ::= string ':' literal | string ':' literal ',' pairs array ::= '[' elements ']' elements ::= literal | literal ',' elements literal ::= object | array | boolean | number | string boolean ::= true | false number ::= integer | double string ::= "char+|escape"
Variables
Variables are substituted directly for their renderable representation.
An error is raised if the varaible being substituted is not a literal type
(ie. an Array
or 'HashMap Text Value') or doesn't exist in the supplied environment.
{{ var }}
Nested variable access is also supported for variables which resolve to an 'HashMap Text Value'. Dot delimiters are used to chain access through multiple nested 'HashMap Text Value's. The right-most accessor must resolve to a renderable type as with the previous non-nested variable access.
{{ nested.var.access }}
Conditionals
A conditional is introduced and completed with the section syntax:
{% if <expr1> %} ... consequent expressions {% elif <expr2> %} ... consequent expressions {% elif <expr3> %} ... consequent expressions {% else %} ... alternate expressions {% endif %}
The boolean result of the expr
determines the branch that is rendered by
the template with multiple (or none) elif branches supported, and the
else branch being optional.
In the case of a literal it conforms directly to the supported boolean or relation logical operators from Haskell. If a variable is singularly used its existence determines the result of the predicate; the exception to this rule is boolean values which will be substituted into the expression if they exist in the supplied environment.
The following logical expressions are supported as predicates in conditional statements with parameters type checked and an error raised if the left and right hand sides are not type equivalent.
And
:&&
Or
:||
Equal
:==
Not Equal
:!=
(See:/=
)Greater
:>
Greater Or Equal
:>=
Less
:<
Less Or Equal
:<=
Negation
:!
(See:not
)
See: Text.EDE.Filters
Case Analysis
To pattern match a literal or variable, you can use the case
statement:
{% case var %} {% when "a" %} .. matched expressions {% when "b" %} .. matched expressions {% else %} .. alternate expressions {% endcase %}
Patterns take the form of variables
, literals
, or the wild-card
'_
' pattern (which matches anything).
Loops
Iterating over an Array
or 'HashMap Text Value' can be acheived using the 'for ... in' section syntax.
Attempting to iterate over any other type will raise an error.
Example:
{% for var in list %} ... iteration expression {% else %} ... alternate expression {% endfor %}
The iteration branch is rendering per item with the else branch being (which is optional)
being rendered if the {{ list }}
variable is empty.
When iterating over an 'HashMap Text Value', a stable sort using key equivalence is applied, Array
s
are unmodified.
The resulting binding within the iteration expression (in this case, {{ var }}
) is
an 'HashMap Text Value' containing the following keys:
key :: Text
: They key if the loop target is an 'HashMap Text Value'value :: a
: The value of the loop targetloop :: Object
: Loop metadata.length :: Int
: Length of the loopindex :: Int
: Index of the iterationindex0 :: Int
: Zero based index of the iterationremainder :: Int
: Remaining number of iterationsremainder0 :: Int
: Zero based remaining number of iterationsfirst :: Bool
: Is this the first iteration?last :: Bool
: Is this the last iteration?odd :: Bool
: Is this an odd iteration?even :: Bool
: Is this an even iteration?
For example:
{% for item in items %} {{ item.index }}:{{ item.value }} {% if !item.last %} {% endif %} {% endfor %}
Will render each item with its (1-based) loop index as a prefix, separated by a blank newline, without a trailing at the end of the document.
Valid loop targets are 'HashMap Text Value's, Array
s, and String
s, with
only 'HashMap Text Value's having an available {{ var.key }}
in scope.
Includes
Includes are a way to reduce the amount of noise in large templates. They can be used to abstract out common snippets and idioms into partials.
If parseFile
or the includeFile
resolver is used, templates will be loaded
using FilePath
s. (This is the default.)
For example:
{% include "/var/tmp/partial.ede" %}
Loads partial.ede
from the file system.
The current environment is made directly available to the included template.
Additional bindings can be created (See: let
) which will be additionally
available only within the include under a specific identifier:
{% include "/var/tmp/partial.ede" with some_number = 123 %}
Includes can also be resolved using pure Resolver
s such as includeMap
,
which will treat the include
expression's identifier as a HashMap
key:
{% include "arbitrary_key" %}
Uses lookup
to find arbitrary_key
in the HashMap
supplied to includeMap
.
Filters
Filters are typed functions which can be applied to variables and literals. An example of rendering a lower cased boolean would be:
{{ true | show | lower }}
The input is on the LHS and chained filters (delimited by the pipe operator |
)
are on the RHS, with filters being applied postfix, left associatively.
See: Text.EDE.Filters
Raw
You can disable template processing for blocks of text using the raw
section:
{% raw %} Some {{{ handlebars }}} or {{ mustache }} or {{ jinja2 }} output tags etc. {% endraw %}
This can be used to avoid parsing expressions which would otherwise be
considered valid ED-E
syntax.
Comments
Comments are ignored by the parser and omitted from the rendered output.
{# singleline comment #}
{# multiline comment #}
Let Expressions
You can also bind an identifier to values which will be available within the following expression scope.
For example:
{% let var = false %} ... {{ var }} ... {% endlet %}
Set
You can also bind an identifier to whole templates which will be available within the following expression scope. The identifier will be available in subsequent template.
For example:
{% set var %} ... {% endset %} ... {{ var }}