Syntax Reference
This page documents every markdown syntax construct and the exact Domphy element shape @domphy/press produces for each one. Use it to understand what body contains and how to write patches or styles that target specific elements.
Headings
ATX headings (# through ######) become h1 through h6 with an id attribute derived from the heading text. The walker also appends a header-anchor child (#) so in-page permalinks work; the link is aria-hidden (the TOC is the screen-reader path) and tabIndex: -1 so it is not a second tab stop.
# Level 1
## Level 2
### Level 3// body
[
{
h1: [
"Level 1",
{ a: "#", href: "#level-1", class: "header-anchor", ariaHidden: "true", tabIndex: -1 },
],
id: "level-1",
},
{
h2: [
"Level 2",
{ a: "#", href: "#level-2", class: "header-anchor", ariaHidden: "true", tabIndex: -1 },
],
id: "level-2",
},
{
h3: [
"Level 3",
{ a: "#", href: "#level-3", class: "header-anchor", ariaHidden: "true", tabIndex: -1 },
],
id: "level-3",
},
]The id is the slug produced by defaultSlugify (or your anchorSlugify option). Duplicate heading text gets a numeric suffix: first occurrence keeps the base slug, subsequent ones get -1, -2, and so on. The header-anchor href always matches that id.
# Intro
# Intro[
{
h1: [
"Intro",
{ a: "#", href: "#intro", class: "header-anchor", ariaHidden: "true", tabIndex: -1 },
],
id: "intro",
},
{
h1: [
"Intro",
{ a: "#", href: "#intro-1", class: "header-anchor", ariaHidden: "true", tabIndex: -1 },
],
id: "intro-1",
},
]Paragraphs
Blocks of text become p elements. The children array holds plain-text strings and any inline elements:
A paragraph with **bold**, _italic_, and `code`.[{
p: [
"A paragraph with ",
{ strong: ["bold"] },
", ",
{ em: ["italic"] },
", and ",
{ code: "code" },
".",
],
}]Inline emphasis
| Markdown | Domphy element |
|---|---|
**bold** or __bold__ | { strong: [...children] } |
_italic_ or *italic* | { em: [...children] } |
~~struck~~ | { s: [...children] } |
`inline code` | { code: "text" } |
Emphasis elements can be nested: **_bold italic_** becomes { strong: [{ em: ["bold italic"] }] }.
Links
[Domphy](https://domphy.dev "Homepage"){
a: ["Domphy"],
href: "https://domphy.dev",
title: "Homepage",
target: "_blank",
rel: "noopener noreferrer",
}All attributes emitted by the mdast walker are copied as element properties. Absolute http:///https:// links automatically get target: "_blank" and rel: "noopener noreferrer"; relative and anchor (#...) links do not. Auto-linked bare URLs (enabled by default via GFM) produce the same shape.
Reference-style links
[text][label], collapsed [text][], and shortcut [text] resolve against a [label]: url "title" definition (first definition wins; identifiers match case-insensitively). The walker emits the same a shape as an inline link. The definition node itself is not rendered.
[Domphy][home]
[home]: https://domphy.dev "Homepage"{
a: ["Domphy"],
href: "https://domphy.dev",
title: "Homepage",
target: "_blank",
rel: "noopener noreferrer",
}Script-capable destinations (javascript:, vbscript:, data:text/html) are rewritten to "#" — the same sanitizeUrl used for inline links.
Images
{ img: null, src: "/img/diagram.png", alt: "A diagram", title: "Figure 1", loading: "lazy" }The img property is null because images are void elements. The alt text is extracted from the image's inline token children (markup stripped to plain text).
Reference-style images
![alt][label] resolves the same way as a link reference and emits the same img shape as an inline image (including loading: "lazy"). The definition is not rendered.
Unordered lists
- one
- two
- three{
ul: [
{ li: ["one"], _key: 0 },
{ li: ["two"], _key: 1 },
{ li: ["three"], _key: 2 },
],
}Each li element carries a _key number equal to its zero-based position among siblings. This gives Domphy stable keys for list diffing. Items whose content is a tight paragraph (no blank line between items) expose the text directly as li children; items separated by blank lines get a p child inside li.
Ordered lists
1. first
2. second
3. third{
ol: [
{ li: ["first"], _key: 0 },
{ li: ["second"], _key: 1 },
{ li: ["third"], _key: 2 },
],
}Nested lists
- parent
- child one
- child two{
ul: [
{
li: [
"parent",
{
ul: [
{ li: ["child one"], _key: 0 },
{ li: ["child two"], _key: 1 },
],
},
],
_key: 0,
},
],
}The nested list appears as the last child inside its parent li. Ordered and unordered lists can be mixed at any depth.
Blockquotes
> Quoted text.
> Second line.{
blockquote: [
{ p: ["Quoted text. Second line."] },
],
}Blockquotes nest: >> deeply quoted produces { blockquote: [{ blockquote: [{ p: [...] }] }] }.
Fenced code blocks
```ts
const x: number = 42
```{
pre: [{
code: "const x: number = 42\n",
dataLanguage: "ts",
class: "language-ts",
}],
}codeholds the raw, un-escaped source text when no highlighter is supplied —@domphy/coreescapes it once at render time, so markdown does not pre-escape it.dataLanguageis Domphy's camelCase form of thedata-languageattribute, set to the fence's language identifier.classis set to"language-{lang}"so CSS-based highlighters can target it.
When a highlighter is provided, code contains the highlighted output (a string of inner HTML or a DomphyElement) instead of the raw text.
Indented code blocks
Four-space-indented code blocks produce the same pre > code shape but carry no language metadata:
const x = 1;
const y = 2;{
pre: [{ code: "const x = 1;\nconst y = 2;\n" }],
}dataLanguage and class are absent because indented code blocks carry no language annotation.
GFM tables
| Name | Age |
| ----- | --- |
| Alice | 30 |
| Bob | 25 |{
table: [
{
thead: [{
tr: [
{ th: ["Name"] },
{ th: ["Age"] },
],
}],
},
{
tbody: [
{ tr: [{ td: ["Alice"] }, { td: ["30"] }] },
{ tr: [{ td: ["Bob"] }, { td: ["25"] }] },
],
},
],
}The full table > thead/tbody > tr > th/td structure is preserved.
Column alignment
Alignment markers (:-, :-:, -:) add a style object to each cell in that column:
| Left | Center | Right |
|:-----|:------:|------:|
| L | C | R |{ th: ["Left"], style: { textAlign: "left" } }
{ th: ["Center"], style: { textAlign: "center" } }
{ th: ["Right"], style: { textAlign: "right" } }Cells without an alignment marker have no style property.
Horizontal rule
---{ hr: null }hr is null because <hr> is a void element.
Line breaks
A regular newline inside a paragraph becomes a soft break. remark keeps it as a literal \n inside one text node; the walker replaces that \n with a space, so the paragraph is a single string:
line one
line two{ p: ["line one line two"] }Two trailing spaces followed by a newline produce a hard break — a void br element:
line one
line two{ p: ["line one", { br: null }, "line two"] }Task lists
Enabled by default (GFM task lists, via remark-gfm) — no option needed. List items beginning with [ ] (unchecked) or [x] / [X] (checked) get a disabled <input type="checkbox"> prepended:
- [x] Completed item
- [ ] Pending item{
ul: [
{
li: [
{ input: null, type: "checkbox", disabled: true, checked: true },
"Completed item",
],
_key: 0,
},
{
li: [
{ input: null, type: "checkbox", disabled: true },
"Pending item",
],
_key: 1,
},
],
}The checked property is present only on checked items; it is absent (not false) on unchecked ones.
Footnotes
Enabled by default (GFM footnotes, via remark-gfm). A [^label] reference becomes a numbered superscript link; matching [^label]: definitions are collected (not rendered in place) and emitted once at the end of body as a section.footnotes. Numbering follows first-reference order. A second reference to the same note reuses the number and gets id suffix -2. Unused definitions are dropped.
See note.[^1]
[^1]: First footnote with **bold**.[
{
p: [
"See note.",
{
sup: [{
a: "1",
href: "#user-content-fn-1",
id: "user-content-fnref-1",
dataFootnoteRef: true,
"aria-describedby": "footnote-label",
}],
},
],
},
{
section: [
{ h2: "Footnotes", id: "footnote-label", class: "sr-only" },
{
ol: [{
li: [{
p: [
"First footnote with ",
{ strong: ["bold"] },
".",
" ",
{
a: "↩",
href: "#user-content-fnref-1",
dataFootnoteBackref: true,
ariaLabel: "Back to reference 1",
class: "data-footnote-backref",
},
],
}],
id: "user-content-fn-1",
_key: 0,
}],
},
],
class: "footnotes",
dataFootnotes: true,
},
]Ids use the mdast-util-to-hast / GitHub GFM user-content- prefix so the fragment contract matches GFM HTML. A later reference to the same note adds a second backref (↩ + <sup>2</sup>, ariaLabel: "Back to reference 1-2").
Math
createMarkdown({ math: true }) only installs the remark-math plugin so $...$ and $$...$$ are parsed into math/inlineMath nodes — it does not by itself produce the class-wrapped shape below. Pair it with an onCustom handler to render those nodes:
import remarkMath from "remark-math"
import { createMarkdown } from "@domphy/press"
const parser = createMarkdown({
plugins: [remarkMath],
onCustom: (node) => {
if (node.type === "math") return { div: node.value, class: "math math-display" }
if (node.type === "inlineMath") return { span: node.value, class: "math math-inline" }
return null
},
})Without an onCustom handler, math/inlineMath nodes fall through the default walker branch and come out as bare, unwrapped strings. See overview for CDN setup.
Inline math
The formula $E = mc^2$ is famous.{
p: [
"The formula ",
{ span: "E = mc^2", class: "math math-inline" },
" is famous.",
],
}Display math
$$
\int_0^\infty e^{-x}\,dx = 1
$${ div: "\\int_0^\\infty e^{-x}\\,dx = 1\n", class: "math math-display" }The raw LaTeX is stored verbatim. KaTeX (or MathJax) processes .math elements at runtime. Both examples above assume the onCustom handler shown earlier — this is what math: true is meant to be paired with.
Raw HTML
A raw HTML block or inline fragment in the source is markup, so the walker wraps it in rawHtml() from @domphy/core (the explicit HTML opt-in). A bare string child is always text — putting "<figure>…" in the body array would render as escaped characters, not a <figure>.
rawHtml() still runs sanitizeHTMLString: <script> elements, on* handlers, and javascript: / vbscript: / data:text/html URLs are stripped. That is defense in depth, not a sanitizer for untrusted input.
Block HTML
A block of raw HTML is a single rawHtml(...) child in the body array — no div wrapper is added:
<figure>
<img src="/chart.png" alt="Chart">
<figcaption>Monthly visits</figcaption>
</figure>import { rawHtml } from "@domphy/core"
[
rawHtml("<figure>\n <img src=\"/chart.png\" alt=\"Chart\">\n <figcaption>Monthly visits</figcaption>\n</figure>"),
]Inline HTML
Raw HTML inline is a per-fragment operation: each open or close tag fragment is its own rawHtml(...) (no span wrapper). The walker does not reconstruct the nested element tree from raw inline HTML:
Text with <strong>bold</strong> inline.import { rawHtml } from "@domphy/core"
{
p: [
"Text with ",
rawHtml("<strong>"),
"bold",
rawHtml("</strong>"),
" inline.",
],
}For content that needs inline styling, prefer standard markdown emphasis syntax. Reserve raw HTML for block-level elements that have no markdown equivalent (e.g. <figure>, <details>, <video>).