Norq

WhatsApp Channel Conformance Spec

Conformance tests for the Norq WhatsApp compiler. The WhatsApp compiler takes Markdown input (with optional YAML frontmatter) and produces a WhatsAppOutput with message_type and payload fields.

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

Format:

  • Opening fence: 32 backticks followed by example channel=whatsapp
  • 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 serializes WhatsAppOutput.payload using serde_json::to_string and checks that every expected fragment appears as a substring of that JSON string.


Message Types

The WhatsApp compiler selects one of four message types based on content:

Condition message_type Payload shape
:::action with links present interactive {type, body, action, [footer]}
:::hero or :::media with image image {link, [caption]}
mode: template in frontmatter template {type, template, variable_mapping}
Otherwise text {body}

Interactive takes priority over image; image takes priority over text.


Text Messages

Plain paragraph

A plain paragraph compiles to a text message with a body string field.

Hello from WhatsApp.
.
"body":"Hello from WhatsApp."

Multiple paragraphs joined with newline

Multiple paragraphs are joined with \n in the body string. The JSON payload encodes the newline as \n.

First line.
 
Second line.
.
First line.
.
Second line.

WhatsApp Text Formatting

WhatsApp uses its own markup language rather than HTML. The compiler converts Markdown inline formatting to WhatsApp-native equivalents.

Bold → *text*

Markdown bold (**text** or __text__) maps to WhatsApp bold: single asterisks wrapping the text (*text*).

Your order **#12345** is ready.
.
*#12345*

Italic → _text_

Markdown italic (*text* or _text_) maps to WhatsApp italic: single underscores wrapping the text (_text_).

Please _confirm_ your appointment.
.
_confirm_

Strikethrough → ~text~

Markdown strikethrough (~~text~~) maps to WhatsApp strikethrough: single tildes wrapping the text (~text~).

~~Old price~~ New price
.
~Old price~

Inline code → ```code```

Markdown inline code (`code`) maps to WhatsApp monospace: triple backticks wrapping the text.

Run `git pull` to update.
.
```git pull```

WhatsApp does not support hyperlinks in message body text. Inline links render as their visible text; the URL is discarded. Use :::action to surface URLs as interactive buttons.

Visit [our website](https://example.com) for more.
.
Visit our website for more.

Headings

Headings render as WhatsApp bold text (*text*). WhatsApp has no heading concept, so all heading levels map to the same bold treatment.

# Order Confirmed
.
*Order Confirmed*
## Shipment Update
 
Your package is on the way.
.
*Shipment Update*
.
Your package is on the way.

:::header Directive

:::header content is wrapped in WhatsApp bold (*text*) and prepended to the body. It is treated as a bold section header, not as a push title.

:::header
Order Update
:::
 
Your order is on the way.
.
*Order Update*
.
Your order is on the way.

:::highlight Directive

:::highlight content is wrapped in WhatsApp bold (*text*) and included in the body. Unlike SMS which uses a ⭐ prefix, WhatsApp uses bold formatting.

:::highlight
Special announcement
:::
.
*Special announcement*

:::callout Directive

:::callout content is prefixed with the ⚠ warning emoji (U+26A0) and included in the body.

:::callout
Your account is at risk
:::
.
⚠ Your account is at risk

:::fields Directive

:::fields renders each paragraph child as a *Key:* Value line. The key (text before the first colon) is wrapped in WhatsApp bold; the value (text after the colon) is plain.

:::fields
Order: ORD-123
Status: Shipped
:::
.
*Order:* ORD-123
.
*Status:* Shipped

:::columns Directive

:::columns stacks each :::col child's content as sequential lines in the body. The column layout has no visual meaning in WhatsApp.

:::columns
::: col
Left content
:::
::: col
Right content
:::
:::
.
Left content
.
Right content

Blockquote

Blockquote paragraph children are prefixed with > in the WhatsApp body.

> This is an important note.
.
> This is an important note.

Lists

Unordered list

Unordered list items are prefixed with - and joined with \n.

- Item one
- Item two
.
- Item one
.
- Item two

Ordered list

Ordered list items are prefixed with N. (1-based index).

1. First step
2. Second step
.
1. First step
.
2. Second step

Task list items

Checked task list items are prefixed with the ✅ emoji (U+2705) and unchecked items with ☐ (U+2610), after the list bullet.

- [x] Account created
- [ ] Profile completed
.
✅ Account created
.
☐ Profile completed

Interactive Messages (Buttons)

When :::action contains one or more links, the compiler produces an interactive message rather than a text message. WhatsApp interactive messages have a type, body, and action.buttons array. An optional footer field is included if :::footer is present.

Basic interactive message

Your order has shipped.
 
:::action
[Track Order](https://example.com/track)
:::
.
"type":"button"
.
"body":{"text":
.
Track Order

Button structure

Each button in the action.buttons array has type: "reply", a unique reply.id (e.g., "btn-0"), and a reply.title with the link text.

Ready to start?
 
:::action
[Get Started](https://example.com/start)
:::
.
"type":"reply"
.
"id":"btn-0"
.
"title":"Get Started"

Maximum 3 buttons

WhatsApp allows at most 3 reply buttons. When :::action contains more than 3 links, only the first 3 are emitted.

:::action
[Btn1](https://example.com/1) [Btn2](https://example.com/2) [Btn3](https://example.com/3) [Btn4](https://example.com/4)
:::
.
"btn-0"
.
"btn-1"
.
"btn-2"

:::footer content is extracted as plain text and placed in a footer.text field in the interactive payload.

Message body.
 
:::action
[Confirm](https://example.com/confirm)
:::
 
:::footer
Powered by SuprSend
:::
.
"footer":{"text":"Powered by SuprSend"}

Image Messages

When :::hero or :::media contains an image and no :::action buttons are present, the compiler produces an image message with a link field. If body text is present it is included as caption.

:::hero
![Product banner](https://example.com/banner.jpg)
:::
.
"link":"https://example.com/banner.jpg"
:::hero
![Product banner](https://example.com/banner.jpg)
:::
 
Check out our latest collection.
.
"link":"https://example.com/banner.jpg"
.
"caption":"Check out our latest collection."

Emoji Shortcodes

Emoji shortcodes (e.g., :wave:, :tada:) are expanded to Unicode in the final body string. Expansion runs after all compilation on the trimmed text.

Hello :wave: welcome!
.
Hello 👋 welcome!

:::footer and :::action content is excluded from the body text field. They are handled separately as button metadata and footer text.

Your shipment is on its way.
 
:::footer
Terms apply
:::
.
Your shipment is on its way.

Template Mode

When frontmatter contains mode: template, the compiler produces a template message type for the WhatsApp Business API. Template mode requires template_name and template_language frontmatter fields.

Template payloads contain a template object with name, language.code, and components (positional parameter arrays), plus a variable_mapping that maps variable positions to data paths.

:::each and :::list directives are forbidden in template mode.

Basic template payload

---
mode: template
template_name: order_update
template_language: en
---
 
Your order is ready.
.
"type":"template"
.
"name":"order_update"
.
"code":"en"

Dynamic Value Escaping

Expression values containing WhatsApp formatting characters (*, _, ~, `) are escaped with a backslash to prevent unintended formatting. Static text (non-expression) is not escaped.

With null data, expression paths resolve to empty strings and produce no output. The static surrounding text is unaffected.

Hello there!
.
Hello there!