mjml-hamlet
MJML email templates written in Hamlet's syntax and checked
at compile time. Rendering goes through the mjml package, so
there is no Node.js involved at build time or at runtime. For what MJML is and
a full example with screenshots, see the repository README.
{-# LANGUAGE QuasiQuotes #-}
import Text.Mjml.Hamlet (mjml, renderMjml)
shipped :: Order -> Either MjmlError Text
shipped order =
renderMjml
[mjml|
<mj-section>
<mj-column>
<mj-text font-size="20px">Your order #{order.reference} is on its way
$forall line <- order.lines
<mj-text>#{line.name} × #{line.quantity}
$maybe url <- order.trackingUrl
<mj-button href=#{url} .primary>Track it
|]
Syntax
As in Hamlet, nesting is given by indentation and there are no closing tags.
<mj-text align="center">Hi is an element; text after the tag on the same
line is its content. Attribute values may be quoted, unquoted, or an
interpolation (href=#{url}). .name adds a class (css-class on MJML
elements, class on HTML ones).
#{expr} interpolates any ToMarkup value, escaped. Expressions are
Hamlet's: variables, application, operators, literals and record fields
(order.lines), but no type annotations or lambdas.
^{expr} embeds what was built elsewhere: nodes (Node, [Node],
Maybe Node) among MJML elements, or blaze Markup inside an ending tag.
$forall pattern <- expr, $if / $elseif / $else,
$maybe pattern <- expr / $nothing, $case expr with nested
$of pattern lines, and $with pattern <- expr work as in Hamlet. $#
starts a comment line.
- Inside an ending tag (
mj-text, mj-button, mj-raw, mj-table, …) the
nested lines are HTML. Consecutive text lines are joined with a space; a
leading \ keeps a line's leading space.
mjmlFile "templates/shipped.mjml" reads the same syntax from a file and
recompiles when the file changes.
A template is a list of nodes. renderMjml renders a whole document rooted at
<mjml> as it is, wraps a template made of <mj-head> and <mj-body> in
<mjml>, and wraps anything else in <mjml><mj-body>, so a template can be
just the sections of an email.
Escaping and injection
#{...} escapes <, >, &, " and ', so an interpolated value cannot
leave its text or attribute. Because MJML copies many attributes into CSS and
some are URLs, interpolated attribute values are also checked for where they
are going:
- URLs (
href, src, background-url, …) take either a single
#{url} of type SafeUrl, which safeUrl only builds for https,
http and mailto URLs with no quotes or spaces in them, or a literal that
fixes the scheme, as in href="https://example.com/o/#{ref}", where the
interpolated part is percent-encoded. Anything else does not compile.
- Typed attributes (colours, units, enumerations) must match their type
when the email is rendered.
- Other attributes that end up in CSS, and
style on HTML elements, must
not contain anything that could add a declaration: ;, braces,
backslashes, comments, url( or @import.
- Event handlers (
onclick, …) cannot be interpolated at all.
A value that fails at render time is dropped, as if the attribute had not been
written. ^{...} embeds markup without escaping and cannot be used in
attribute values.
What does not compile
The checks MJML's validator makes at runtime happen at compile time, with the
template's line and column:
Shipped.hs:12:39: Attribute align has invalid value: middle for type Enum, only accepts left, right, center, justify
- an unknown element, or HTML outside an ending tag;
- an element inside a parent MJML does not allow (
mj-text directly in
mj-section);
- an attribute the element does not declare;
- a literal attribute value of the wrong type (units, colours, enumerations);
- text outside an ending tag.
License
MIT.