On this page
- At a glance
- Writing safely
- What markz doesn't read
- Metadata
- Block
- Paragraphs
- Headings
- Blockquotes
- Lists
- Code blocks
- Raw blocks
- Math blocks
- Tables
- Thematic breaks
- Attributes
- Elements
- Comments
- Inline
- Emphasis
- Inline code
- Links and images
- Spans
- Inline math
- Expressions
- Line breaks
- Escapes and references
- Not supported
- Metadata forms
- Block forms
- Inline forms
- Formatters
- Grammar
Syntax
The markz language. It is the Markdown people already write, plus {…} for attributes and
elements, a metadata block, math and ${…} expressions. There is one way to write each thing.
Anything markz doesn't read stays literal text, with a warning that says what to write instead.
This page starts with the whole language at a glance, then explains each construct. The exact rules, with every edge case, are in the grammar.
At a glance
| To write | Write |
|---|---|
| Metadata | key: value lines between --- lines, at the very top |
| A heading | # to ###### and a space: ## Title |
| A heading's id | {#id} on the line above the heading |
| A paragraph | lines of text, with a blank line between paragraphs |
| Emphasis, strong, struck | _emphasis_, **strong**, ~~struck~~ |
| Inline code | `code` |
| A link | [text](url "title") |
| A link to a URL or email | <https://example.com>, <me@example.com> |
| An image |  |
| A list | - item, or 1. item for a numbered one |
| A task | - [ ] to do, - [x] done |
| A quote | > at the start of every line |
| A code block | a ``` fence, with the language after it |
| A table | pipes, with a --- row under the header, and :---: to align |
| A rule | --- |
| A line break | \ at the end of the line |
| Math | $x$ in a line, and $$ fences or $$x$$ alone on a line for a block |
| A value from code | ${name} |
| A class, id or attribute | {.class #id key=value open} above a block, or after a link or image |
| A styled word | [text]{.class} |
| An inline element | [Ctrl]{@kbd} |
| A block element | {@call-out} … {/call-out}, or [label]{@name /} on one line |
| HTML | a ```=html fence |
| A comment | <!-- … --> on lines of its own |
| A symbol as itself | \ before it (\*, \$), or a number (©) |
| A non-breaking space | \ , a backslash and a space |
Writing safely
Three rules keep a document safe. markz warns when one is broken, so none needs remembering.
- Leave a blank line between a list, quote or table and an element line after it. A formatter
would otherwise move the line into the list, quote or table (
element-lazy-line). - Quote a metadata value that YAML would read another way. That is a value holding
:or#, one starting with a symbol, a yes or no word, or a number YAML would read differently (metadata-value). - Give a custom element a hyphen in its name, as
call-out, notcallout(element-name).
What markz doesn't read
Some Markdown forms aren't part of the language. Each stays literal text, with a warning that
names the form to write. The common ones are setext headings, indented code, reference links,
footnotes, raw HTML, bare URLs, two trailing spaces as a line break, *emphasis*, __strong__,
~~~ fences and ___ rules. Not supported lists every cut form, with its
warning code. The other warnings are named with the rule they guard.
Metadata
A few key: value lines that GitHub, YAML and formatters all read without an error.
A document can open with a metadata block, between --- lines, starting at the first character
(what other tools call frontmatter). When a closing --- line follows, everything between is
metadata, and a line markz can't read is a warning. Without a closing line, the first --- is a
rule, and a key: value line after it gets the warning metadata-unclosed. markz parses the
block into doc.metadata.
---
# a comment
title: Sales Report
summary: "Make it yours: Cloudflare, secrets."
order: 2
draft: false
date: 2026-09-26
image:
tags: [svelte, vite]
deploy.name: my-site
---
| Value | Result |
|---|---|
nothing, or null |
null |
true, false |
boolean |
42, -3, 1.5 |
number |
"text" |
string, with JSON's escapes |
'text' |
string, with '' for a quote |
[a, 2, "b, c"] |
a list of values by these same rules |
| anything else | string, as written: Sales Report, 2026-09-26, C# notes, v2 |
- Keys start with a letter or
_, and hold letters, digits,_,-and.. A.is part of the key, as YAML reads it, sodeploy.nameis one key. A repeated key gets the warningmetadata-duplicate-key, and the first one wins. - Comments are lines that start with
#. - Quote a value that YAML would read another way. Otherwise the line gets the warning
metadata-value. That is a value starting with a symbol such as*,@or{, a yes or no word in any case (True,no,off), or a number YAML would read differently (01234,1.10,1e3). Its key is skipped, since any guess could be wrong. In a list, also quote an item that holds,,[or]. - A value holding
:or#is kept as written, with the same warning. YAML would reject the first and cut the second short, sotitle: Issue #42would lose#42elsewhere. - A long list can wrap onto indented lines after its key, as a formatter writes it, and may end with a comma.
- The rest of YAML is out. Other indented lines,
- itemlists and multi-line strings get the warningmetadata-line, and the key they belong to is skipped.
Block
Paragraphs
Lines of text, with a blank line between paragraphs. Inside a quote or a list item, every line
carries its > or its indentation.
Headings
# to ######, a space, and one line of text. Closing #s (## Title ##) are dropped.
Every heading gets an id, so a link can point at it.
{#id}on the line above sets it. The id then survives renaming the heading. If an earlier heading has the same id, both keep it, and the later one gets the warningduplicate-id.- Otherwise it is made from the text, as GitHub makes it. Lowercase, punctuation removed, and
each space a
-:## Café au laitiscafé-au-lait. A repeat is numbered:foo,foo-1,foo-2. So a link to a heading works on GitHub and on the site. The exact steps are in the grammar.
Blockquotes
> starts every line. A line without it ends the quote.
Lists
- item for bullets, 1. item for numbers, and - [ ] task or - [x] task for tasks.
- The first number sets where a numbered list starts.
- A later line of an item is indented to where the item's text starts.
- A blank line between items makes the list loose, with each item a paragraph.
*and+bullets and1)numbers are read too, since formatters write them. A different marker starts a new list.
Code blocks
A fence of three or more backticks, with the language after it. The first word after the fence
is the language, and the rest is metadata for the host. Use a longer fence to show a fence inside.
A fence with no closing line runs to the end of its container, with the warning unclosed-block.
So does a raw or math block.
Raw blocks
A fence whose language is =html is raw output. html() writes what it holds as it is. It is
the only way to put HTML in a document, so HTML is always marked.
```=html
<iframe src="https://w.soundcloud.com/player/?url=…" height="166"></iframe>
```
- It is for embeds, inline SVG, and the
<style>or<script>a page needs. - A raw block for another format (
=latex) is kept in the tree, andhtml()skips it. - A
```cssor```jsfence is code to show, never to run. - Raw blocks are trusted content. See Security.
- GitHub shows a raw block as a code block.
=names a format only in a fence. Inside{…}it has no meaning, so{=html}is text.
Math blocks
$$ fences on lines of their own, or $$E=mc^2$$ alone on a line. markz keeps the TeX and
doesn't typeset it. html() writes <pre><code class="language-math math-display">, and the
page adds KaTeX or Temml. A ```math fence is an ordinary code block.
Tables
Pipes between cells, and a row of --- under the header. :---, :---: and ---: align a
column left, centre or right. The outer pipes are optional.
Thematic breaks
--- on a line of its own. *** is a rule too, since formatters write it on a document's first
line, where --- would open metadata.
Attributes
{…} is markz's one extension syntax. Attributes decorate what Markdown makes, and @name in
them makes an element Markdown has no syntax for.
{#pricing .center}
## Pricing
{.wide width=600} and the [docs](/docs){target=_blank}.
{.striped}
| Plan | Price |
| ---- | ----- |
| Where | Applies to | For |
|---|---|---|
A line holding only {…} |
the next block | heading ids, and classes on tables, lists and code |
| Straight after a link or image, with no space | that link or image | width, class, target, rel |
Straight after [text], with no space |
a span of the text | classes on words, and inline elements |
Lines holding {@name …} and {/name}, or [label]{@name … /} |
an element | wrappers and components |
- Inside the braces:
#id,.classandkey=value, withkey="a value"for spaces. A barekeyis an HTML attribute that is on or off, asopen,hiddenordownload. Classes add up. For any other key, the later value wins. A value may hold${…}. - Bare keys alone count only on an element, a span, a link or an image:
{@details open},[x]{hidden},[file](/a.pdf){download}. On a line of its own,{open}stays text, since{year}there reads as a placeholder. Add a class or an id to use one above a block:{.note open}. - One line. Attributes start and end on the same line.
- A
{…}line decorates the next block, across blank lines, since formatters add one before a heading. With no block after it, it stays text with the warningorphan-attributes. - Anywhere else a
{is text, so{a, b}and{"json": 1}need no escaping. A{…}after a link or[text], or on a line starting{@or{/, that doesn't parse gets the warningattribute-syntax.
Elements
An element is a {…} whose first item is @name, and the name is the element it writes. Elements
are how wrappers and components are written, since a document holds no HTML. Inline elements are
spans.
{@chart-view data="sales" type="bar" /}
{@call-out type="warning"}
Markdown **inside**.
{/call-out}
- A container opens with
{@name attrs}on a line of its own, and closes with{/name}. The lines between are Markdown. - A leaf is
[label]{@name attrs /}or{@name attrs /}on one line. The/makes it a block. Without it,[label]{@name}is a span inside a paragraph. - The name is a block element from the list below, or a custom element: lowercase letters,
digits and
-, with a-in it (call-out). Any other name stays text, with the warningelement-name. A class comes only from.class, so a note is{@div .note}. - Closing:
{/name}closes the innermost open element when the names match. Otherwise it stays text, with the warningelement-close. An element never closed runs to the end of its container, with the warningunclosed-element. - After a list, quote or table, leave a blank line before an element line. Without one, it
gets the warning
element-lazy-line, since a formatter would move it in.
The block elements are those Markdown has no syntax for and that can't run code: div,
section, article, aside, header, footer, nav, main, address, hgroup, search,
details, summary, figure, figcaption, dl, dt and dd.
html() writes the name as the element, with its attributes. What HTML puts in a child element is
written as one:
{@details .proof}
[Show the proof]{@summary /}
Body **here**.
{/details}
<details class="proof">
<summary>Show the proof</summary>
<p>Body <strong>here</strong>.</p>
</details>
Definition lists are elements too:
{@dl}
[Term]{@dt /}
[Definition]{@dd /}
{/dl}
A framework can map names to its own components (call-out to CallOut) and attributes to
their props, in its own pass over the tree.
Comments
<!-- … --> on lines of its own is a note that stays in the source. html() never writes it.
It may span lines, and ends on the line with -->. Text after --> on that line gets the
warning comment-trailing-text. A <!-- after other text on its line is text, with the
warning raw-html.
Inline
Emphasis
_emphasis_, **strong** and ~~strikethrough~~.
_never works inside a word, sosnake_case_namestays text.**may sit inside a word.***,____and~~~are text. Nest with two markers, as_**both**_.*emphasis*is read only where formatters write it: inside_…_, or touching a letter (a*b*c). Anywhere else it stays text, with a warning.*and**between two digits are text, so2*3*4and2**10stay arithmetic.
Inline code
`code`. To show a backtick inside, use more backticks around it: `` a b `` `.
Links and images
[text](url "title") links, and  shows an image. The title is optional,
and a relative URL works: [About](/about).
<https://example.com> and <me@example.com> link the URL or email as its own text. An autolink
needs a scheme or an @.
A {…} straight after the ) sets the link's or image's attributes:
{.wide width=600}.
Spans
[text]{attrs}, with no space between ] and {. html() writes a <span> with the
attributes, or the element @name names.
| Source | HTML |
|---|---|
[hi]{.highlight} |
<span class="highlight">hi</span> |
x[2]{@sup} |
x<sup>2</sup> |
H[2]{@sub}O |
H<sub>2</sub>O |
[new]{@ins} |
<ins>new</ins> |
[text]{@mark} |
<mark>text</mark> |
[Ctrl]{@kbd} |
<kbd>Ctrl</kbd> |
[HTML]{@abbr title="…"} |
<abbr title="…">HTML</abbr> |
- A span may start inside a word, as
H[2]{@sub}O. - The name is an inline element from the list below, or a custom element. Any other name,
spanincluded, leaves the whole[…]{…}as text, with the warningelement-name.
The inline elements are those Markdown has no syntax for and that can't run code: abbr, b,
i, u, s, small, cite, q, dfn, time, data, var, samp, kbd, mark, sub,
sup, ins, bdi, bdo, ruby, rt and rp. There is no @em, @strong, @code or
@del, since _x_, **x**, `x` and ~~x~~ write them: an edit is ~~old~~ [new]{@ins}.
Inline math
$x^2$. The TeX starts and ends with a character other than a space, and a $ followed by a
digit doesn't close it, so costs $5 and $10 stays text. Math is read before emphasis, so
$a_1 * b_2$ needs no escaping. $$x$$ inside a line of text stays text, with the warning
math-delimiter. html() writes <code class="language-math math-inline">.
Expressions
${…} holds a JavaScript expression for the page to evaluate, as in a template literal. markz
keeps the code and never runs it.
- It works in text, in link URLs and in attribute values. Inside code and math it is text.
- It binds tighter than emphasis, so
${a * b * c}is one expression. - Braces, strings and comments inside it are skipped, so
${f({a: 1})}is whole. - A regex isn't recognised. A
}inside one ends the expression early, with the warningexpression-bracket. Write it as}, or move the regex out of the document. \${is a literal${, and an unclosed${is text.html()writes<code class="language-js expression">holding the code. A page evaluates expressions from the tree, never from the HTML.
Line breaks
\ at the end of a line is a hard break. Any other line ending in a paragraph is a soft break,
which the browser shows as a space. A poem ends each line with \, or a site styles it
(Usage).
Escapes and references
- A
\before any ASCII punctuation is that character:\*,\_,\$,\{. \and a space is a non-breaking space, as in10\ km. At the end of a line it is a hard break, since formatters strip trailing spaces.- A number in
&#…;is that character:©,—. Named ones such as©aren't read. Write the character itself. &is ordinary text, andhtml()escapes it.
Not supported
Each of these stays literal text and adds a warning over exactly its characters. The warning's
code is the table's first column, and its instead is the "Write instead" column. The Why
column says what each would cost. Two look-alikes are ordinary prose, so they stay text with no
warning: a lone [x] and a bare {…}.
Metadata forms
| Code | Syntax | Write instead | Why |
|---|---|---|---|
toml-metadata |
TOML metadata (+++) |
a --- metadata block |
One format. |
Block forms
| Code | Syntax | Write instead | Why |
|---|---|---|---|
raw-html |
Raw HTML blocks and inline tags | a ```=html raw block, or elements and attributes |
Seven HTML-block kinds and a tag grammar. HTML stays possible, but only where it's marked. |
setext-heading |
Setext headings (Title over === or ---) |
# Title |
A second heading form. |
indented-code |
Indented code blocks | fenced code | Indentation meaning code is what makes list indentation hard. The indented line is paragraph text, and never a heading or list inside it. |
tilde-fence |
~~~ fences |
a longer backtick fence | One fence character. |
rule-marker |
___, * * * rules |
--- |
One marker. |
trailing-heading-attributes |
Trailing heading attributes (## Title {#id}) |
{#id} on the line above |
A { after a word is text, so this {…} would belong to the word "Title". |
multiline-attributes |
Multi-line attributes | one line | One line keeps a {…} plain to see. |
directive |
Colon directives (:::name … :::, ::name[label], :name[text]) |
{@name} … {/name}, [label]{@name /} or [text]{@name} |
One extension syntax. {…} already holds the attributes, and @name in it makes the element, so colons were a second way. |
element-name |
Element names that aren't elements ({@chart /}, {@note}, [x]{@note}) |
a div or span with a class ({@div .chart /}, [x]{.note}), or a custom element ({@chart-view /}) |
The name is the element it writes, so there is one way to add a class and a name can never be script. |
lazy-line |
Lazy continuation lines (a quoted or listed paragraph continuing without > or indentation) |
> on every line, or indent to the item's content column |
Lazy lines are the main reason CommonMark's block structure depends on context. Formatters already write them out in full. |
Inline forms
| Code | Syntax | Write instead | Why |
|---|---|---|---|
reference-link |
Reference links: [x][y], [x][], [y]: url |
inline links | A link can't be resolved until the whole document is read, which breaks local parsing and streaming. |
footnote |
Footnotes ([^label], [^label]: text) |
a span, such as [text]{.note} |
A reference can't be resolved until the whole document is read, as with reference links. |
bare-url |
Bare URLs (https://…, www.…, me@example.com) |
<https://…> or [text](url) |
GFM's largest construct, and the only one that has to look back at text already emitted: an email is known only at its @, and trailing punctuation is trimmed afterwards. |
relative-autolink |
Relative autolinks (</docs/intro>) |
[About](/about) |
An autolink needs a scheme, and </about> is a closing HTML tag, reported as raw HTML. A link should have real text. |
named-reference |
Named character references (©, &, ) |
the character itself (©, &), or \ for a non-breaking space |
Files are UTF-8, html() escapes & and < itself, and the table of 2,125 names is about 12 KB gzip. |
trailing-spaces |
Two trailing spaces as a line break | \ at end of line |
Invisible syntax. |
underscore-strong |
__strong__ |
**strong** |
One marker. Formatters rewrite it. |
star-emphasis |
*emphasis*, except inside _…_ or touching a letter (Emphasis) |
_emphasis_ |
One marker, and the source of most emphasis edge cases. Formatters rewrite it. |
single-tilde |
~single~ strikethrough |
~~text~~ |
One marker. Formatters rewrite it. |
math-delimiter |
Other math delimiters: $$x$$ inside a line of text, $`x`$ |
$x$, or a $$ block |
One way each: $x$ in a line, and $$ for a block, fenced or alone on its line. Read by the $x$ rule, $$x$$ would lose a dollar at each end with no report. |
inline-attributes |
Attributes after words, inline code or emphasis (word{.x}, _x_{.x}) |
[text]{.x} |
The brackets mark where a span starts, so one way. Keeping { special only after a ) or ] means braces in prose are plain text. |
jsx |
MDX: JSX (a capitalised tag, <Chart />) and bare {…} expressions |
{@name} elements, ${…} |
A { is only attributes where the rules above say so. Any other brace is prose, so a bare {…} stays text without a report. |
Formatters
markz reads what Markdown formatters write, so formatting a document never changes what it means. A formatter rewrites several forms into one, and markz reads both sides.
| A formatter rewrites | to |
|---|---|
*em*, __strong__, ***both*** |
_em_, **strong**, _**both**_ |
~one~ |
~~one~~ |
~~~ fences |
``` fences |
## Title ## |
## Title |
___, and *** after line one |
--- |
Some of what formatters write is read on purpose:
- List markers. Two lists in a row are kept apart by switching the marker,
-then*, or1.then1). So markz reads every bullet and number marker. *emphasis inside_…_or touching a letter, where_can't work.***on a document's first line, where---would open metadata.- A blank line between a
{…}line and the heading after it. - Either quote style in metadata, whichever the formatter is set to.
Formatters leave the cut forms alone (setext headings, indented code, two-space breaks, named references, bare URLs, reference links and raw HTML). For those, markz's warning is the only signal.
A formatter doesn't know elements, so it reads an element line as paragraph text. Straight after
a list, a quote or a table, that text continues the list item, the quote or the table to the
formatter, which moves it in. That is why markz warns (element-lazy-line) when the blank line
before it is missing.
Grammar
grammar.md states the language this page explains. Each construct there has the
same id, under the same part, with its exact rules. A form cut above has no rule there. The tests
hold the two pages to each other.