Comark syntax
A template is a Markdown file with YAML frontmatter. Comark adds four things to Markdown: block components, inline components, spans with attributes, and merge tags. The comark package parses all of it; the ir package then decides what is allowed in an email and resolves every merge tag against your Go type.
---
template: order.shipped
family: orders
subject: "Order {{order_id}} is on its way"
preheader: "Arriving by {{eta}}."
---
::statement{eyebrow="Order {{order_id}}"}
# On its way.
Hi {{first_name | there}}, your order will reach you by {{eta}}.
:button[Track delivery]{href="{{track_url}}"}
::
::breakdown
| Order total | {{total}} |
| --- | ---: |
| [Reference]{.small .muted} | [`{{reference}}`]{.small .muted} |
::
Top-level content is a stack of modules: each block component is one, and each run of loose Markdown between components becomes an implicit letter. The layout sets the spacing between modules, so a template never sets margins.
Start with frontmatter
Frontmatter is a YAML mapping between two --- lines, and only counts when the first --- is the very first line of the file. The keys are described in Frontmatter.
A leading UTF-8 byte order mark is ignored and CRLF line endings are accepted everywhere. Invalid UTF-8 anywhere in the file is a parse error at its position (invalid utf-8), and a frontmatter block with no closing line is frontmatter: missing closing ---.
Open a block component
A block component opens with a line of two or more colons and a name, optionally followed by attributes, and closes with a line of exactly the same number of colons:
::stories
:::story{eyebrow="Stays" href="https://example.com/journal" tone="sea"}
Four guesthouses on the Nevis Peak trail
:::
::
- The name matches
[A-Za-z][A-Za-z0-9_-]*. Module names and aliases are matched case-insensitively. - The opener line holds nothing but the colons, the name and the attributes (trailing whitespace is fine). Up to three leading spaces are allowed.
- The closer is a line of exactly the opener's colon count. A line with a different count does not close it.
- Nest a component inside another by giving it more colons (
:::storyinside::stories). When two open components have the same count, the innermost one closes first. - A component can interrupt a paragraph, so you don't need a blank line before
::name. - A closer inside an open fenced code block is part of the code, and a component that is never closed ends at the end of the document.
- Components nest at most 32 deep (
components nested deeper than 32).
A top-level component that is not a module is unknown module "stats". The full list, with aliases such as k1 for statement, is in Modules.
Pass props
Attributes on the opener are one way to give a module its settings. For longer values, put a props block at the start of the component instead. It has two forms, and both are read into the same set of keys as the attributes:
::statement
---
eyebrow: "Weekly payout · {{period}}"
tone: orange
---
# Payout sent.
::
::quote
```yaml [props]
cite: "Dwayne Liburd · Brimstone Woodcraft"
```
"Every board leaves Old Road with the grain facing the sea."
::
- The
---form must be the first line after the opener; the fence form must be the component's first child, with the info string exactlyyaml [props]. - Props must be a YAML mapping. A YAML error is a parse error on its own line, prefixed
props:(for exampleprops: did not find expected ',' or ']'). A---block with no closing line isprops: missing closing ---, and using both forms isprops: given twice. - String props can hold merge tags, like attribute values. Integers (
height: 360) and booleans (headless: true) are plain YAML. - A key given both as an attribute and as a prop is an error:
statement: "eyebrow" set twice.
Write attributes
An attribute list is {…} on one line:
attrs = "{" ws* (item ws*)* "}" ; "{{" never starts attributes
item = "." name | "#" name | [":"] key [ "=" value ]
name = [A-Za-z_][A-Za-z0-9_:-]*
value = '"' … '"' | "'" … "'" | bare
| Form | Example | Meaning |
|---|---|---|
key=value | height=360 | A bare value, up to the next space or }. |
key="value" | eyebrow="Order {{order_id}}" | A quoted value. \" (or \') and \\ are the only escapes. |
key | headless | A flag: a boolean that is on. |
:key="path" | :order="order" | A binding: the value is a data path, not text. See Merge tags and bindings. |
.class, #id | .muted | Classes are for spans. Modules accept neither. |
Every non-binding value is text, so it can hold merge tags. Attributes never span lines, and a list is at most 4096 bytes. A list whose closing } is on the same line but which is invalid is a positioned parse error: duplicate attribute "a", duplicate #id, binding :order needs a value, expected a value after =.
{ never closes on its line, for example because of a missing closing quote in ::statement{eyebrow="x}, is not a component at all. The line becomes text, and the errors that follow point at the content instead, typically letter: h1 is not allowed here. When a component seems to vanish, check the quotes on its opener.A module reads only the attributes it knows. Anything else, including an #id or .class, is <module>: unknown attribute "x".
Leave slots alone
A line #name (no space after #) directly inside a component opens a slot. The parser supports slots, but the email compiler reserves them for later: every slot is the compile error slots are reserved. # Heading, with a space, stays a heading.
Add inline components
Inline components sit inside a paragraph and come in three shapes: :name[label]{attrs}, :name[label] and :name{attrs}. The label is inline Markdown.
An inline component is recognised only when the character before the colon is the start of the line, whitespace, or punctuation other than :, and when the name is followed directly by [ or {. So 2:14 pm, Note:the and https://example.com are ordinary text.
The email compiler knows two inline components, :button and :link, described in Actions and spans. Any other name is unknown inline component ":note".
Style text with spans
[text]{.class …} is a span: a run of inline Markdown with a style. It is a span only when ] is followed directly by {; otherwise [text] is handled by the normal link parser. The allowed classes are muted, teal, mono, small and bold — see Actions and spans.
Keep attributes off other elements
Comark can attach {attrs} directly after a link, image, emphasis, strong or code span, but the email compiler accepts attributes only on spans and inline components. Anything else is attributes are only allowed on spans and inline components (D21). Wrap the text in a span instead:
`{{reference}}`{.small .muted}
[`{{reference}}`]{.small .muted}
An invalid {…} after one of these elements stays literal text, so prose such as *no*{today's the day} still parses. And {{x}} right after a link is a merge tag, never an attribute list.
Insert merge tags
A merge tag is {{path}} or {{path | fallback}}:
Hi {{first_name | there}}, {{driver.first_name}} is on the way.
Your code is {{code | "not set"}}.
Write \{{name}} to show the braces.
- A path is dot-separated segments of
[a-z_][a-z0-9_]*: lower case, digits and underscores. Spaces around the path and the fallback are allowed. - The fallback is bare text, or quoted with
"…"when it contains|,}or leading spaces. A bare fallback may not contain{{: the tag becomes malformed and the text after it is read as a new tag (D33). Use a quoted fallback for that. Inside a quoted fallback,\"and\\are escapes. - A tag never spans lines.
\{{is a literal{{.- Anything that starts with
{{but is not a valid tag ({{ Name }},{{name}) stays literal text and is reported as the warningmalformed merge tag "{{ Name }}". It does not fail the compile, so check warnings — see Compile errors.
Tags work in prose, headings, table cells, code spans (`{{reference}}`), link destinations ([secure your account]({{security_url}})), autolinks, attribute values, props and frontmatter strings. The tag parser runs before emphasis, so underscores in a path never start italics.
Which names a path can use, the fallback rules and how URL values are escaped are in Merge tags and bindings.
Build tables
GFM tables are allowed inside breakdown, compare and table, one table per module. A table anywhere else is letter: table is not allowed here (with the module's name). Cells are inline Markdown: spans, strong, code spans and links all work, and column alignment (---:) carries through to the email.
The table parser splits cells on every bare |, including one inside a merge tag. In a cell, write the fallback separator as \|:
::breakdown
| Recipient | {{first_name \| there}} |
| --- | ---: |
| Total | {{total}} |
::
In a body row, a bare | splits the tag across two cells: the email shows the broken text and the compiler warns malformed merge tag "{{name". In the header row it adds a column, so the delimiter row no longer matches and the table is not parsed at all: the module then reports breakdown: expected one table. \| is accepted as the separator everywhere, not only in tables.
Know what is rejected
The parser reads all of CommonMark and GFM; the compiler turns down whatever has no place in an email. Each of these is a compile error at its position:
| You wrote | Error | Use instead |
|---|---|---|
| Raw HTML, inline or block | raw html is not allowed | Modules, spans and Markdown. |
 in prose | images are not allowed in prose; use ::image | ::image, ::map or the image of a tile. |
~~text~~ | strikethrough is not allowed | — |
[text](url "title") | link titles are not supported | [text](url) |
--- between modules | use ::divider instead of --- | ::divider |
| A blockquote, a code block, or a heading the module doesn't take | letter: blockquote is not allowed here | ::quote, ::code, or a ::statement heading. |
| A list item with two paragraphs or a nested list | letter: list items must be one paragraph | One paragraph per item. |
--- directly under a line of text is a CommonMark setext heading, not a rule: the text becomes an h2, and in loose Markdown that is letter: h2 is not allowed here. Leave a blank line before --- if you meant frontmatter or props, and use ::divider for a rule.Next steps
- Frontmatter for the header keys.
- Modules for every component and what it may contain.
- Compile errors for reading what the compiler reports.