Norq

Email Channel Conformance Spec

Conformance tests for the Norq email compiler. The email compiler takes Markdown input (with YAML frontmatter) and produces MJML output, which is then rendered to HTML by the mrml library. Conformance tests validate the intermediate MJML output, not the final HTML.

Each example block specifies input markdown and one or more expected output fragments (substrings that must appear in the compiled MJML).

Format:

  • Opening fence: 32 backticks followed by example channel=email
  • Input markdown (first section, before the first . separator)
  • . separator line
  • One or more expected fragments, each separated by . lines
  • Closing fence: 32 backticks

The test runner compiles each input using the default EmailTheme with serde_json::Value::Null as data and checks that every expected fragment appears as a substring of the EmailOutput.mjml field.


Document Structure

Frontmatter and MJML boilerplate

Every email template requires a subject field in the YAML frontmatter. The compiler wraps the entire output in an <mjml> document containing <mj-head> (with <mj-attributes> defining defaults for fonts, colors, and button styling) and <mj-body>. The body background-color and width come from the theme.

---
subject: Welcome
---
 
Hello
.
<mjml>
.
<mj-head>
.
<mj-attributes>
.
<mj-text font-size="16px" line-height="1.6" color="#3f3f46" />
.
<mj-body background-color="#fafafa" css-class="email-bg" width="600px">

Preheader

The optional preheader frontmatter field emits an <mj-preview> tag inside <mj-head>, which email clients show as preview text in the inbox list.

---
subject: Order shipped
preheader: Your package is on the way
---
 
Your order has shipped.
.
<mj-preview>Your package is on the way</mj-preview>

Default heading styles

The <mj-head> includes a <mj-style> block defining default heading sizes and weights. These CSS rules apply to all headings in the email body unless overridden by block attributes.

---
subject: Test
---
 
# Title
.
h1 { font-size: 32px; font-weight: 700; color: #09090b; margin: 0 0 12px 0; }
.
h2 { font-size: 24px; font-weight: 700; color: #09090b; margin: 0 0 10px 0; }
.
h3 { font-size: 20px; font-weight: 600; color: #09090b; margin: 0 0 8px 0; }

Button defaults in mj-attributes

The <mj-attributes> block sets default button styling: background-color from the theme's buttonColor, text color from buttonTextColor, border-radius of 3px, font-weight 600, font-size 13px, and inner-padding of 10px 25px.

---
subject: Test
---
 
Hello
.
<mj-button background-color="#18181b" color="#fafafa" border-radius="3px" font-weight="600" font-size="13px" inner-padding="10px 25px" align="center" />

Paragraphs

Basic paragraph

A paragraph compiles to a <p> tag inside <mj-text>, wrapped in an <mj-section> with css-class email-content and padding 20px 0. Consecutive text-like nodes (paragraphs, headings, lists, blockquotes, code blocks) are batched into a single TextRun, sharing one <mj-section>.

---
subject: Test
---
 
Hello world
.
<mj-section css-class="email-content" padding="20px 0"><mj-column><mj-text><p>Hello world</p></mj-text></mj-column></mj-section>

Consecutive paragraphs batch into a single section

Multiple consecutive paragraphs are grouped into one TextRun. They share a single <mj-section> and <mj-text>, with each paragraph as a separate <p> tag.

---
subject: Test
---
 
First paragraph
 
Second paragraph
.
<mj-text><p>First paragraph</p><p>Second paragraph</p></mj-text>

Inline formatting

Paragraphs support standard Markdown inline formatting: bold (**text**), italic (*text*), inline code (`code`), and links.

---
subject: Test
---
 
This is **bold** and *italic* and `code` text.
.
<p>This is <strong>bold</strong> and <em>italic</em> and <code>code</code> text.</p>

Links render as <a> tags with the theme's brand color applied as an inline style="color:..." attribute within text runs.

---
subject: Test
---
 
Visit [our site](https://example.com) today.
.
<a href="https://example.com" style="color:#18181b">our site</a>

Headings

Heading levels

Headings h1 through h4 compile to their corresponding HTML heading tags inside <mj-text>. They are batched with adjacent paragraphs into the same TextRun section.

---
subject: Test
---
 
# Heading 1
 
## Heading 2
 
### Heading 3
 
#### Heading 4
.
<h1>Heading 1</h1>
.
<h2>Heading 2</h2>
.
<h3>Heading 3</h3>
.
<h4>Heading 4</h4>

Images

An image is written using standard Markdown syntax: ![alt](url). When an image is the sole meaningful content of a paragraph (ignoring surrounding whitespace), the compiler emits an <mj-image> element. If the image appears inline alongside other text or formatting, it is emitted as an HTML <img> tag inside an <mj-text> block instead.

This section covers only the <mj-image> path — the common case for email templates.

Basic images

A paragraph containing only an image compiles to an <mj-image> wrapped in an <mj-section> and <mj-column>. The default padding is 10px 25px, matching MJML's built-in <mj-image> default.

---
subject: Test
---
 
![Product photo](https://example.com/product.jpg)
.
<mj-image src="https://example.com/product.jpg" alt="Product photo" padding="10px 25px" border="none" />

The border="none" attribute is always emitted to prevent default borders in some email clients.

An empty alt text is allowed but the linter will warn about it (a11y/missing-alt):

---
subject: Test
---
 
![](https://example.com/hero.jpg)
.
<mj-image src="https://example.com/hero.jpg" alt="" padding="10px 25px" border="none" />

Image width

The {width="N"} attribute sets the image display width. Values without a unit are treated as pixels. The compiler appends px if not already present.

---
subject: Test
---
 
![Banner](https://example.com/banner.jpg){width="600"}
.
<mj-image src="https://example.com/banner.jpg" alt="Banner" padding="10px 25px" border="none" width="600px" />

A value with an explicit unit is passed through unchanged:

---
subject: Test
---
 
![Icon](https://example.com/icon.png){width="50px"}
.
width="50px"

Image alignment

The {align="left|center|right"} attribute controls horizontal alignment within the column. Without this attribute, the image inherits the column's default alignment (typically center in MJML).

---
subject: Test
---
 
![Logo](https://example.com/logo.png){align="left"}
.
<mj-image src="https://example.com/logo.png" alt="Logo" padding="10px 25px" border="none" align="left" />

Image padding

The {padding="..."} attribute overrides the default 10px 25px padding. It accepts named tokens (none, compact, normal, spacious) or raw CSS shorthand values ("0", "16 32", "8 16 8 16").

A zero-padding image is flush with the edges of its column:

---
subject: Test
---
 
![Hero](https://example.com/hero.jpg){padding="0"}
.
<mj-image src="https://example.com/hero.jpg" alt="Hero" padding="0px" border="none" />

Named tokens resolve to pixel values:

---
subject: Test
---
 
![Photo](https://example.com/photo.jpg){padding="spacious"}
.
padding="40px 32px"

Image border radius

The {border-radius="N"} attribute rounds the image corners.

---
subject: Test
---
 
![Avatar](https://example.com/avatar.jpg){border-radius="50%" width="80"}
.
<mj-image src="https://example.com/avatar.jpg" alt="Avatar" padding="10px 25px" border="none" width="80px" border-radius="50%" />

Clickable images

A link wrapping an image — [![alt](image-url)](link-url) — compiles to a single <mj-image> with an href attribute. The link's URL becomes the click target; the image's URL is the src.

Clickable images have a default padding of 0 (not 10px 25px), because they typically serve as hero banners or logos where edge-to-edge display is desired.

---
subject: Test
---
 
[![Shop now](https://example.com/banner.jpg)](https://example.com/shop)
.
<mj-image src="https://example.com/banner.jpg" alt="Shop now" padding="0" border="none" href="https://example.com/shop" />

Attributes can be placed on the link. They apply to the generated <mj-image>. Link attributes take priority over image attributes when both are present:

---
subject: Test
---
 
[![Logo](https://example.com/logo.png)](https://example.com){width="200" padding="10"}
.
<mj-image src="https://example.com/logo.png" alt="Logo" padding="10px" border="none" href="https://example.com" width="200px" />

Section wrapping

At the top level of a template (outside any directive), an image paragraph is wrapped in its own <mj-section> with CSS class email-content and padding 10px 25px:

---
subject: Test
---
 
![Hero](https://example.com/hero.jpg)
.
<mj-section css-class="email-content" padding="10px 25px"><mj-column><mj-image src="https://example.com/hero.jpg" alt="Hero" padding="10px 25px" border="none" /></mj-column></mj-section>

Images inside text

When an image appears alongside text in the same paragraph (not as the sole content), it does NOT compile to <mj-image>. Instead, it renders as an HTML <img> tag inside an <mj-text> block. This is because <mj-image> is a block-level MJML element that cannot be mixed with text.

---
subject: Test
---
 
Check out this ![icon](https://example.com/icon.png) inline icon.
.
<img
.
icon

Buttons

Basic button

A link with the {button} attribute compiles to an <mj-button> element wrapped in its own <mj-section>. The default variant is primary, using the theme's buttonColor as background and buttonTextColor as text color.

---
subject: Test
---
 
[Click here](https://example.com/cta){button}
.
<mj-button css-class="norq-btn" align="center" href="https://example.com/cta" background-color="#18181b" color="#fafafa">Click here</mj-button>

Secondary variant

{button.secondary} produces a transparent-background button with a 2px solid border in the theme's secondary color. The text uses secondaryTextColor.

---
subject: Test
---
 
[Cancel](https://example.com/cancel){button.secondary}
.
<mj-button css-class="norq-btn" align="center" href="https://example.com/cancel" background-color="transparent" color="#71717a" border="2px solid #e4e4e7">Cancel</mj-button>

Danger variant

{button.danger} uses the theme's danger color (#ef4444) as background.

---
subject: Test
---
 
[Delete](https://example.com/delete){button.danger}
.
<mj-button css-class="norq-btn" align="center" href="https://example.com/delete" background-color="#ef4444" color="#ffffff">Delete</mj-button>

Success variant

{button.success} uses the theme's success color (#22c55e) as background.

---
subject: Test
---
 
[Confirm](https://example.com/confirm){button.success}
.
background-color="#22c55e" color="#ffffff"

Warning variant

{button.warning} uses the theme's warning color (#f59e0b) as background.

---
subject: Test
---
 
[Caution](https://example.com/caution){button.warning}
.
background-color="#f59e0b" color="#ffffff"

Custom button colors with bg and color

When both bg and color attributes are present on a button, bg sets the background and color sets the text color. This is the standard path for custom button colors.

---
subject: Test
---
 
[Custom](https://example.com){button bg="#111111" color="#ffffff"}
.
background-color="#111111" color="#ffffff"

Legacy color-only semantics

When color is specified alone (without bg), it is treated as the background color for legacy compatibility. The text defaults to white.

---
subject: Test
---
 
[Legacy](https://example.com){button color="#ff0000"}
.
background-color="#ff0000" color="#ffffff"

Full-width button

The {full} attribute or {width="full"} makes the button stretch to 100% of its container width.

---
subject: Test
---
 
[Full Width](https://example.com){button full}
.
width="100%"

Custom border radius

The {radius="N"} attribute overrides the default 3px border-radius. The value is emitted with a px suffix.

---
subject: Test
---
 
[Rounded](https://example.com){button radius="20"}
.
border-radius="20px"

Lists

Unordered list

An unordered list compiles to <ul><li>...</li></ul> inside a text run's <mj-text> block.

---
subject: Test
---
 
- Item one
- Item two
- Item three
.
<ul><li>Item one</li><li>Item two</li><li>Item three</li></ul>

Ordered list

An ordered list compiles to <ol><li>...</li></ol>.

---
subject: Test
---
 
1. First
2. Second
3. Third
.
<ol><li>First</li><li>Second</li><li>Third</li></ol>

Code Blocks

Code block without language

Fenced code blocks without a language specifier compile to a monospace <pre> with a dark background, wrapped in <mj-text>. The code block uses hardcoded styling for email-safe rendering.

---
subject: Test
---
 
```
const x = 42;
```
.
<pre style="background:#2b303b;color:#c0c5ce;padding:16px;border-radius:8px;overflow-x:auto;font-family:monospace;font-size:14px;line-height:1.5"><code>
.
const x = 42;

Dividers

Basic divider

A Markdown thematic break (***) compiles to an <mj-divider /> wrapped in an <mj-section> with padding 10px 25px.

---
subject: Test
---
 
***
.
<mj-section css-class="email-content" padding="10px 25px"><mj-column><mj-divider /></mj-column></mj-section>

Divider with attributes

The {color="..." thickness="..." width="..."} block attributes customize the divider. Color tokens are resolved through the theme. Thickness and width values get a px suffix if no unit is present.

---
subject: Test
---
 
***
{color="#cccccc" thickness="2" width="50%"}
.
<mj-divider width="50%" border-color="#cccccc" border-width="2px" />

Block Attributes

Block attributes use the {key="value"} syntax on the line immediately following a paragraph or heading. They resolve to inline CSS style attributes on the generated HTML element.

Size tokens

The size attribute maps named tokens to pixel font-sizes:

Token Value
xs 12px
sm 14px
md 16px
lg 18px
xl 20px
2xl 24px
3xl 30px
4xl 36px
---
subject: Test
---
 
Large text
{size="lg"}
.
<p style="font-size:18px">Large text</p>
---
subject: Test
---
 
Extra large heading
{size="3xl"}
.
style="font-size:30px"
---
subject: Test
---
 
Huge heading
{size="4xl"}
.
style="font-size:36px"

Color tokens

The color attribute maps named tokens to theme colors:

Token Default value
brand #18181b
muted #71717a
danger #ef4444
success #22c55e
warning #f59e0b

Raw hex values (starting with #) are passed through.

---
subject: Test
---
 
Subdued text here.
{color="muted"}
.
<p style="color:#71717a">Subdued text here.</p>
---
subject: Test
---
 
Error message
{color="danger"}
.
color:#ef4444

Background tokens

The bg attribute maps named tokens to theme background colors:

Token Default value
brand #18181b
card #f4f4f5
highlight #f59e0b

Raw hex values are passed through.

---
subject: Test
---
 
Card text
{bg="card"}
.
background-color:#f4f4f5

Weight tokens

The weight attribute maps to CSS font-weight values:

Token Value
light 300
normal 400
medium 500
semibold 600
bold 700
---
subject: Test
---
 
Bold paragraph text
{weight="bold"}
.
<p style="font-weight:700">Bold paragraph text</p>

Spacing tokens

The spacing attribute controls line-height. Named tokens map to fixed values, and numeric values in the range 0.5-3.0 are passed through directly.

Token Value
tight 1.5
normal 1.6
relaxed 1.8
loose 2.0
---
subject: Test
---
 
Relaxed line spacing
{spacing="relaxed"}
.
<p style="line-height:1.8">Relaxed line spacing</p>

Padding tokens (block-level)

The padding attribute on paragraphs resolves to inline CSS padding. These are the block-level padding tokens (not to be confused with the MJML-level padding tokens used for images and directives).

Token Value
none 0
compact 8px 0
normal 16px 0
spacious 32px 0
---
subject: Test
---
 
Spacious padding paragraph
{padding="spacious"}
.
<p style="padding:32px 0">Spacious padding paragraph</p>

Multiple attributes

Attributes can be combined freely. They compile to a semicolon-separated style attribute.

---
subject: Test
---
 
Styled paragraph
{size="lg" align="center" weight="bold"}
.
style="font-size:18px;text-align:center;font-weight:700"

Directives

:::highlight

The :::highlight directive produces an <mj-section> containing a column with the theme's brand color (#18181b) as background, white text, and hardcoded font-weight="600" and border-radius="8px". The column carries the CSS class email-highlight.

---
subject: Test
---
 
::: highlight
Special offer inside!
:::
.
<mj-column background-color="#18181b" css-class="email-highlight" border-radius="8px" padding="20px 24px"><mj-text align="left" color="#ffffff" font-weight="600">

Custom background via {bg="..."}:

---
subject: Test
---
 
::: highlight {bg="#0000ff"}
Blue highlight
:::
.
background-color="#0000ff" css-class="email-highlight"
.
color="#ffffff" font-weight="600"

:::callout

The :::callout directive produces an <mj-section> with a column using the theme's card color (#f4f4f5) as background. The column carries the CSS class email-card with border-radius="8px".

---
subject: Test
---
 
::: callout
This is a callout box.
:::
.
css-class="email-card" border-radius="8px"
.
<p>This is a callout box.</p>

Custom background color on callout is passed through:

---
subject: Test
---
 
::: callout {bg="#eff6ff"}
Blue callout
:::
.
background-color="#eff6ff" css-class="email-card"

:::action

The :::action directive extracts links from its children and renders each as an <mj-button> in its own <mj-section>. Links can specify a variant ({primary}, {secondary}, {danger}, {success}, {warning}) via attributes. The default variant is primary.

---
subject: Test
---
 
::: action
[Get Started](https://example.com/start)
:::
.
<mj-button css-class="norq-btn" align="center" href="https://example.com/start" background-color="#18181b" color="#fafafa">Get Started</mj-button>

A {danger} variant button uses the danger color:

---
subject: Test
---
 
::: action
[Delete Account](https://example.com/delete){danger}
:::
.
<mj-button css-class="norq-btn" align="center" href="https://example.com/delete" background-color="#ef4444" color="#ffffff">Delete Account</mj-button>

Custom colors inside :::action using bg and color attributes:

---
subject: Test
---
 
::: action
[Custom Button](https://example.com){bg="#111111" color="#ffffff"}
:::
.
background-color="#111111" color="#ffffff"

Full-width button inside :::action:

---
subject: Test
---
 
::: action
[Full Button](https://example.com){full}
:::
.
width="100%"

:::columns + :::col

The :::columns directive produces an <mj-section> containing multiple <mj-column> elements — one per :::col child. Each column can specify width, vertical alignment, and background color.

Two equal columns (no explicit width defaults to MJML auto-sizing):

---
subject: Test
---
 
::: columns
::: col
Left column content
:::
::: col
Right column content
:::
:::
.
<mj-section css-class="email-content"
.
<mj-column>
.
Left column content
.
Right column content

Column with explicit fractional width {width="2/3"} converts to 66.67%:

---
subject: Test
---
 
::: columns
::: col {width="2/3"}
Wide column
:::
::: col {width="1/3"}
Narrow column
:::
:::
.
width="66.67%"
.
width="33.33%"

Column with vertical alignment:

---
subject: Test
---
 
::: columns
::: col {valign="middle"}
Centered vertically
:::
::: col
Other content
:::
:::
.
vertical-align="middle"

Column with background color:

---
subject: Test
---
 
::: columns
::: col {bg="#f0f0f0"}
Tinted column
:::
::: col
Normal column
:::
:::
.
background-color="#f0f0f0"

Mobile stacking control

Columns with {mobile="inline"} emit <mj-group> to prevent stacking.

---
subject: Test
---
 
::: columns {mobile="inline"}
::: col {width="1/3"}
A
:::
::: col {width="1/3"}
B
:::
::: col {width="1/3"}
C
:::
:::
.
<mj-group>

The :::header directive is partitioned out of the content flow and emitted as a separate <mj-section> with the CSS class email-header. Default alignment is center and default padding is 32px 32px 24px 32px. The background defaults to the theme's background color.

---
subject: Test
---
 
::: header
My Company
:::
 
Welcome to our service.
.
<mj-section background-color="#fafafa" css-class="email-header" padding="32px 32px 24px 32px"><mj-column><mj-text padding="0" align="center">
.
My Company

The :::footer directive is partitioned out of the content flow and emitted as a separate <mj-section> with CSS class email-footer. It uses a smaller font size (13px), line-height 1.5, and the theme's secondary text color (#71717a). Default padding is 24px 32px 32px 32px.

---
subject: Test
---
 
::: footer
Unsubscribe | Privacy Policy
:::
.
<mj-section background-color="#fafafa" css-class="email-footer" padding="24px 32px 32px 32px"><mj-column><mj-text padding="0" font-size="13px" line-height="1.5" color="#71717a" align="center">

:::hero

The :::hero directive with a {url="..."} attribute emits an <mj-hero> element with mode="fluid-height" and the URL as background-url. Default padding is 100px 0px and default text color is white.

---
subject: Test
---
 
::: hero {url="https://example.com/banner.jpg"}
Hero headline
:::
.
<mj-hero mode="fluid-height" background-url="https://example.com/banner.jpg" padding="100px 0px"><mj-text padding="0" align="center" color="#ffffff">
.
Hero headline

:::fields

The :::fields directive parses Key: Value lines and produces an HTML table inside <mj-text>. Keys are rendered with font-weight:bold.

---
subject: Test
---
 
::: fields
Status: Shipped
Carrier: FedEx
:::
.
<td style="font-weight:bold;padding:4px 8px">Status</td><td style="padding:4px 8px">Shipped</td>
.
<td style="font-weight:bold;padding:4px 8px">Carrier</td><td style="padding:4px 8px">FedEx</td>

:::social

The :::social directive auto-detects platforms from link URL hostnames and produces <mj-social> with <mj-social-element> children. Each element's name attribute is set to the detected platform, which MJML uses to select the appropriate icon.

Supported platforms: facebook, twitter, x, instagram, linkedin, youtube, github, pinterest, dribbble, medium, snapchat, soundcloud, tumblr, vimeo, xing. Unknown URLs produce name="web".

---
subject: Test
---
 
::: social
[Follow us](https://twitter.com/example)
:::
.
<mj-social-element name="twitter" href="https://twitter.com/example">Follow us</mj-social-element>

Unknown URLs produce the generic web icon:

---
subject: Test
---
 
::: social
[Visit](https://example.com)
:::
.
<mj-social-element name="web" href="https://example.com">Visit</mj-social-element>

X (formerly Twitter) detection:

---
subject: Test
---
 
::: social
[Follow](https://x.com/example)
:::
.
name="x"

The social section wraps elements in <mj-social> with configurable icon size (default 20px), mode (default horizontal), and alignment (default center):

---
subject: Test
---
 
::: social
[GitHub](https://github.com/example)
:::
.
<mj-social font-size="0px" icon-size="20px" mode="horizontal" align="center"

:::centered

The :::centered directive wraps content in an <mj-text> with align="center".

---
subject: Test
---
 
::: centered
Thank you for your purchase.
:::
.
<mj-text align="center">
.
Thank you for your purchase.

:::raw

The :::raw directive passes through HTML content verbatim inside an <mj-raw> tag. The HTML must be inside a fenced code block within the directive.

---
subject: Test
---
 
::: raw
```html
<div class="custom">Raw HTML content</div>
```
:::
.
<mj-raw><div class="custom">Raw HTML content</div></mj-raw>

Theme

Theme colors in mj-head

The default theme colors appear throughout the <mj-head> boilerplate. The brand color is used for link styling and blockquote borders.

---
subject: Test
---
 
Hello
.
a { color: #18181b; text-decoration: none; }
.
blockquote { border-left: 3px solid #18181b;

Frontmatter theme overrides

Frontmatter keys matching theme fields (in camelCase) override the defaults for that template. For example, brandColor in frontmatter overrides the brand color used in links, buttons, and highlight backgrounds.

---
subject: Test
brandColor: "#ff0000"
---
 
Hello
.
a { color: #ff0000; text-decoration: none; }

Padding Resolution (MJML-level)

Directives and images use MJML-level padding attributes (not inline CSS). The same named tokens are available but resolve to different default values depending on context.

Token Value
none 0
compact 10px 20px
normal 24px 32px
spacious 40px 32px

Numeric values are converted to pixel strings. Single values ("16" becomes "16px"), two values ("16 32" becomes "16px 32px"), and four values ("8 16 8 16" becomes "8px 16px 8px 16px") are supported. Three values and non-numeric values fall back to the context's default.

---
subject: Test
---
 
![Photo](https://example.com/photo.jpg){padding="compact"}
.
padding="10px 20px"

Numeric two-value padding:

---
subject: Test
---
 
![Photo](https://example.com/photo.jpg){padding="16 32"}
.
padding="16px 32px"

Section Directive

:::section is shorthand for single-column :::columns + :::col. It compiles to the same MJML output with no boilerplate.

---
subject: Test
---
 
::: section {bg="#f9f9f9" padding="compact"}
Hello from a section
:::
.
background-color="#f9f9f9"
.
Hello from a section

Section with flush attribute:

---
subject: Test
---
 
::: section {bg="#000" flush}
Dark section content
:::
.
background-color="#000"
.
Dark section content