Skip to content
Domphy

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

MarkdownDomphy 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"] }] }.

[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.

[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

![A diagram](/img/diagram.png "Figure 1")
{ 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",
  }],
}
  • code holds the raw, un-escaped source text when no highlighter is supplied — @domphy/core escapes it once at render time, so markdown does not pre-escape it.
  • dataLanguage is Domphy's camelCase form of the data-language attribute, set to the fence's language identifier.
  • class is 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>).