SMS Channel Conformance Spec
Conformance tests for the Norq SMS compiler. The SMS compiler takes Markdown input (with optional YAML frontmatter) and produces a plain-text body string.
Each example block specifies input markdown and one or more expected output
fragments (substrings that must appear in SmsOutput.body).
Format:
- Opening fence: 32 backticks followed by
example channel=sms - 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 serde_json::Value::Null as data and
checks that every expected fragment appears as a substring of the body.
Plain Text Output
Basic paragraph
A plain paragraph renders as its text content followed by a double newline. Leading and trailing whitespace is trimmed from the final body.
Your order has been shipped.
.
Your order has been shipped.Multiple paragraphs
Multiple paragraphs are emitted sequentially. Each paragraph appends \n\n
after its text, so consecutive paragraphs are separated by a blank line.
First paragraph.
Second paragraph.
.
First paragraph.
.
Second paragraph.Headings
Headings render as plain text — the Markdown level markers (#, ##, etc.)
are discarded. The text content is emitted followed by \n\n, the same as a
paragraph. No uppercase transformation is applied.
# Order Confirmed
.
Order Confirmed## Shipment Update
Your package left the facility.
.
Shipment Update
.
Your package left the facility.Inline Formatting
Bold stripped
Bold markers (**text** or __text__) are stripped. Only the inner text
is emitted.
Your order **#12345** is ready.
.
Your order #12345 is ready.Italic stripped
Italic markers (*text* or _text_) are stripped. Only the inner text
is emitted.
Please _confirm_ your appointment.
.
Please confirm your appointment.Inline code stripped
Inline code backtick markers are stripped; only the code text is kept.
Run `git pull` to update.
.
Run git pull to update.Links render as link text only
Inline links render as their visible text. The URL is discarded (use
:::action to expose a URL in the body).
Visit [our website](https://example.com) for more.
.
Visit our website for more.Lists
Unordered list
Each unordered list item is prefixed with - and followed by a newline.
There is no extra blank line between items.
- Item one
- Item two
- Item three
.
- Item one
.
- Item two
.
- Item threeOrdered list
Each ordered list item is prefixed with N. (1-based index) and followed
by a newline.
1. First step
2. Second step
3. Third step
.
1. First step
.
2. Second step
.
3. Third stepTask list items
Checked task list items are prefixed with the ✅ emoji (U+2705) and
unchecked items with the ☐ ballot box (U+2610), after the list bullet.
- [x] Account created
- [ ] Profile completed
.
✅ Account created
.
☐ Profile completedDirectives
:::action — link as "Label: url"
:::action extracts the first link from its content and renders it as
Label: url\n\n. The label is the link text; the URL is the href. If no link
is present the directive emits nothing.
:::action
[Track package](https://example.com/track)
:::
.
Track package: https://example.com/track:::highlight — star prefix
:::highlight content is prefixed with the ⭐ star emoji (U+2B50) and a
space, then \n\n. Inline formatting inside the directive is stripped to
plain text before the prefix is applied.
:::highlight
Special announcement
:::
.
⭐ Special announcement:::footer — appended after a newline
:::footer children are recursively emitted after a leading newline separator.
The footer appears at the end of the body.
Your order shipped.
:::footer
Reply STOP to unsubscribe.
:::
.
Your order shipped.
.
Reply STOP to unsubscribe.:::fields — key-value lines
:::fields renders each paragraph child as a trimmed text line followed by a
newline. An extra \n is appended after all field lines.
:::fields
Status: Shipped
Carrier: FedEx
:::
.
Status: Shipped
.
Carrier: FedEx:::columns — stacked sequentially
:::columns has no visual meaning in SMS. Each :::col child's content is
emitted sequentially as plain text.
:::columns
::: col
Left content
:::
::: col
Right content
:::
:::
.
Left content
.
Right content:::header and :::hero — ignored
:::header and :::hero are silently ignored in SMS output. They produce no
text.
:::header
App Name
:::
Your subscription is confirmed.
.
Your subscription is confirmed.:::hero

:::
Order confirmed.
.
Order confirmed.:::raw — ignored
:::raw blocks are silently ignored in SMS; their content is never emitted.
:::raw
<html>some raw HTML</html>
:::
.Control Flow
:::if — null data takes else branch
When data is null, condition paths resolve to null/falsy, so :::if blocks
take the :::else branch (if present) or produce no output.
:::if user.is_premium
Premium content here.
:::else
Standard content here.
:::
.
Standard content here.:::each — empty loop produces no output
When the iterable is null or missing, :::each produces no output.
:::each items as item
- {{item.name}}
:::
After the list.
.
After the list.Expression Interpolation
Basic path expression
{{path}} expressions resolve the data path. With null data, an unresolvable
path produces an empty string.
Hello there!
.
Hello there!Pipes applied to expressions
Pipe functions transform expression values. The upper pipe converts the
resolved string to uppercase.
Status: CONFIRMED
.
Status: CONFIRMEDEmoji Shortcodes
Emoji shortcodes (e.g., :wave:, :tada:) are expanded to their Unicode
equivalents in the final body. Shortcode expansion runs after all other
compilation, on the trimmed output.
Hello :wave: welcome!
:::highlight
Special announcement :tada:
:::
.
Hello 👋 welcome!
.
⭐ Special announcement 🎉Blockquote
Blockquote children are rendered recursively as plain text. The > marker is
not emitted.
> This is a quoted message.
.
This is a quoted message.Images
Standalone images are silently ignored in SMS output.

Order confirmed.
.
Order confirmed.Thematic Breaks
Thematic breaks (***, ---, ___) are silently ignored in SMS output.
First section.
---
Second section.
.
First section.
.
Second section.Section Directive
:::section content passes through as plain text.
::: section {bg="#f9f9f9"}
Hello from section
:::
.
Hello from sectionSegment Calculation
The compiler calculates SmsOutput.segments and SmsOutput.encoding from the
body text. These fields are informational only — they do not affect the body
string.
GSM-7 encoding applies when every character in the body belongs to the
GSM-7 basic charset or extended charset (^{}\\[~]|€). A single GSM-7 message
holds 160 characters (or 153 per segment when multi-part). Extended characters
count as 2 slots each.
UCS-2 encoding applies when any character falls outside GSM-7. A single UCS-2 message holds 70 characters (67 per segment when multi-part).
These segment rules follow the GSMA SMS standard and are not testable via the
spec conformance runner (which checks body text only), but are validated by
the unit tests in sms.rs.