packages feed

mjml-hamlet-0.1.0.1: README.md

# mjml-hamlet

[MJML](https://mjml.io) email templates written in Hamlet's syntax and checked
at compile time. Rendering goes through the [`mjml`](https://github.com/kronor-io/mjml/tree/main/mjml) package, so
there is no Node.js involved at build time or at runtime. For what MJML is and
what it is for, see the [repository README](https://github.com/kronor-io/mjml#readme).

An order confirmation: data goes in with `#{...}`, `$forall` repeats a row
per item, `$maybe` adds the tracking button only when there is a link, and
`^{footer}` embeds a piece shared by every email.

```haskell
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE QuasiQuotes #-}

import Data.Text (Text)
import Text.Mjml (MjmlError, Node)
import Text.Mjml.Hamlet (SafeUrl, mjml, renderMjml)

data Order = Order
    { reference :: Text
    , customer :: Text
    , items :: [Item]
    , total :: Text
    , trackingUrl :: Maybe SafeUrl
    }

data Item = Item {name :: Text, quantity :: Int, price :: Text}

confirmation :: Order -> Either MjmlError Text
confirmation order =
    renderMjml
        [mjml|
        <mjml>
          <mj-head>
            <mj-title>Order #{order.reference}
            <mj-preview>Thanks for your order, #{order.customer}
            <mj-attributes>
              <mj-all font-family="Helvetica, Arial, sans-serif">
              <mj-text font-size="15px" line-height="22px" color="#333333">
          <mj-body background-color="#f4f4f4">
            <mj-section padding="12px">
            <mj-section background-color="#ffffff" padding="24px">
              <mj-column>
                <mj-text font-size="22px" font-weight="bold">Thanks, #{order.customer}!
                <mj-text>We have received order #{order.reference} and will tell you when it ships.
            $forall item <- order.items
              <mj-section background-color="#ffffff" padding="0 24px">
                <mj-group>
                  <mj-column width="70%">
                    <mj-text>#{item.quantity} × #{item.name}
                  <mj-column width="30%">
                    <mj-text align="right">#{item.price}
            <mj-section background-color="#ffffff" padding="0 24px 24px">
              <mj-column>
                <mj-divider border-color="#dddddd" border-width="1px">
                <mj-text align="right" font-weight="bold">Total #{order.total}
                $maybe url <- order.trackingUrl
                  <mj-button href=#{url} background-color="#1a73e8">Track your parcel
            ^{footer}
        |]

-- A piece shared by every email, embedded above with ^{footer}.
footer :: [Node]
footer =
    [mjml|
    <mj-section>
      <mj-column>
        <mj-text font-size="12px" color="#888888" align="center">
          You are receiving this email because you placed an order with us.
    |]
```

The result, on a desktop client and on a phone:

![The order confirmation in a desktop mail client](https://raw.githubusercontent.com/kronor-io/mjml/eab39e4062d3a98055c8295a80d50395f502d6f8/docs/confirmation-desktop.png)

![The same email on a phone: narrower, with item names and prices still side by side](https://raw.githubusercontent.com/kronor-io/mjml/eab39e4062d3a98055c8295a80d50395f502d6f8/docs/confirmation-mobile.png)

`mj-group` keeps each item and its price side by side on the phone; plain
columns would stack.

## 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.