djot.v GitHub

Extensions

Syntax the parser reads in addition to djot’s. Every extension is off in the djot profile and is switched on by itself. For djot’s own syntax, see the djot syntax reference.

Each section ends with the properties that the extension changes.

Callouts

Setting: ext_callouts.

A block quote whose first line is a header [!kind] is a callout, as in Obsidian.

> [!note] Backlinks
> A backlink is a link seen from its target.
<div class="callout" data-callout="note">
<div class="callout-title">Backlinks</div>
<div class="callout-content">
<p>A backlink is a link seen from its target.</p>
</div>
</div>
Open in the playground

The header

The header is tested on the line that opens a quote, and only there. After the quote prefix it is:

  1. [!;
  2. a kind: one or more ASCII letters, digits, - or _;
  3. ];
  4. optionally a fold marker, + or -;
  5. then the end of the line, or a space or tab and the title.

The kind is kept as written. The title is the rest of the line, parsed as inline content.

> [!warning]- Do *not* rename
> The fold marker makes it a `details` element.
<details class="callout" data-callout="warning">
<summary class="callout-title">Do <strong>not</strong> rename</summary>
<div class="callout-content">
<p>The fold marker makes it a <code>details</code> element.</p>
</div>
</details>
Open in the playground

A later line of an open quote is not tested.

> a
> [!note] T
<blockquote>
<p>a
[!note] T</p>
</blockquote>
Open in the playground

What is not a header

Anything else after the ] leaves the line as djot reads it. This keeps links, spans and reference definitions that begin with [!.

> [!note](x)
<blockquote>
<p><a href="x">!note</a></p>
</blockquote>
Open in the playground
> [!no te] T
<blockquote>
<p>[!no te] T</p>
</blockquote>
Open in the playground

The body

The following lines of the quote are the body, parsed as blocks. No blank line is needed after the header. A line without > directly after the header closes the callout, because the header opens no paragraph for it to continue.

> [!note] T
after
<div class="callout" data-callout="note">
<div class="callout-title">T</div>
<div class="callout-content">
</div>
</div>
<p>after</p>
Open in the playground

Inside the body, a paragraph continues onto a line without > as in any quote.

> [!note] T
> para
lazy
<div class="callout" data-callout="note">
<div class="callout-title">T</div>
<div class="callout-content">
<p>para
lazy</p>
</div>
</div>
Open in the playground

Output

Obsidian’s classes. data-callout holds the kind. With a fold marker the outer element is details, open for + and closed for -. The tree keeps a node of its own, with the kind, the fold marker, the title and the body.

Properties

What turning this on changes, against djot. Every property not listed keeps its status.

Propertydjotwith this extension
Text inside a block quote parses as it would at top level.provedbrokenA quote whose first line is a callout header is a callout, and the header is not part of its content.

Custom tag names

Setting: ext_tags.

A div and a span can have a name, which becomes the element.

Press :kbd[Ctrl+C] to copy.
<p>Press <kbd>Ctrl+C</kbd> to copy.</p>
Open in the playground
::: details
Hidden until opened.
:::
<details>
<p>Hidden until opened.</p>
</details>
Open in the playground

This extension changes how existing djot documents parse: in djot the word after a div fence is a class, and :name[ is text before a bracket.

A div’s name

The word after a div’s opening fence is its name. Which lines open a div does not change. A fence with no word opens an unnamed div.

{.note}
::: aside
A named div with a class.
:::
<aside class="note">
<p>A named div with a class.</p>
</aside>
Open in the playground

A span’s name

:name[ opens a named span. The name is made of the characters a symbol :name: may contain. The span closes at the first ] that arrives while it is the innermost open bracket, and nothing after the ] is waited for.

:kbd[x](u)
<p><kbd>x</kbd>(u)</p>
Open in the playground

Attributes attach as they do to any inline.

:kbd[Esc]{.key}
<p><kbd class="key">Esc</kbd></p>
Open in the playground

Where :name[ is not a named span

After an escaped colon, or when the colon directly follows a letter, a digit or a colon.

\:kbd[a] and a:kbd[b]
<p>:kbd[a] and a:kbd[b]</p>
Open in the playground

When it never closes.

:kbd[a
<p>:kbd[a</p>
Open in the playground

Output

The name is the element name when it is made of ASCII letters, digits and hyphens, starts with a letter, and is not an element whose content a browser reads differently (a void element such as br, or script, style, textarea, title, iframe and similar). Otherwise the element is div or span and the name is in a data-tag attribute.

:script[x] and :a_b[y]
<p><span data-tag="script">x</span> and <span data-tag="a_b">y</span></p>
Open in the playground

Properties

What turning this on changes, against djot. Every property not listed keeps its status.

Propertydjotwith this extension
When delimiters overlap, the first opener that gets closed wins, and a closer takes the closest open opener. Exactly one reading follows these rules, and the parser gives it.provedconditionalFor a paragraph without a character an extension claims: `[[` with wikilinks, `$` with dollar math, `:` with tags, `%` with holes, a hyphen when the delete delimiter is wider than one, or a parenthesis used as a delimiter.

Dollar math

Setting: ext_dollar_math.

Math between dollar signs, as in pandoc’s Markdown and on GitHub. It is independent of djot’s own math syntax, $`x`, which stays available.

The area is $\pi r^2$.
<p>The area is <span class="math inline">\(\pi r^2\)</span>.</p>
Open in the playground

Two dollars give display math.

$$\int_0^1 x\,dx$$
<p><span class="math display">\[\int_0^1 x\,dx\]</span></p>
Open in the playground

Inline math

One $ opens inline math when the character after it is not whitespace and not a backtick. The next $ closes it when the character before it is not whitespace and the character after it is not a digit. Otherwise the opener is text.

It costs $5 and $10.
<p>It costs $5 and $10.</p>
Open in the playground
To split $100 in half, we calculate $100/2$.
<p>To split $100 in half, we calculate <span class="math inline">\(100/2\)</span>.</p>
Open in the playground

Inside the math, a backslash and the character after it are kept as written, and that character cannot close.

$a\$b$
<p><span class="math inline">\(a\$b\)</span></p>
Open in the playground

The content is not parsed as inline syntax.

$a*b*c$
<p><span class="math inline">\(a*b*c\)</span></p>
Open in the playground

Display math

Two $ open display math, closed by the next $$. The whitespace and digit conditions do not apply. Three or more $ in a row are text.

$$x$ and $$$y$$$
<p>$$x$ and $$$y$$$</p>
Open in the playground

The GitHub form

A $ directly before a code span and another directly after it make inline math whose content is the code span’s.

$`x_1`$
<p><span class="math inline">\(x_1\)</span></p>
Open in the playground

Priority

A closer has to arrive before the end of the paragraph, heading or table cell. Once math closes, it wins over emphasis, links and spans that overlap it.

*a $b* c$
<p>*a <span class="math inline">\(b* c\)</span></p>
Open in the playground

A $ inside a code span is not an opener, and \$ is a literal dollar.

`a $b` c$ and \$x$
<p><code>a $b</code> c$ and $x$</p>
Open in the playground

Properties

What turning this on changes, against djot. Every property not listed keeps its status.

Propertydjotwith this extension
When delimiters overlap, the first opener that gets closed wins, and a closer takes the closest open opener. Exactly one reading follows these rules, and the parser gives it.provedconditionalFor a paragraph without a character an extension claims: `[[` with wikilinks, `$` with dollar math, `:` with tags, `%` with holes, a hyphen when the delete delimiter is wider than one, or a parenthesis used as a delimiter.

Holes

Setting: ext_holes.

A hole is an expression in a document, for a consumer to evaluate: the document is a template and the hole is where a value goes, as in an article template literal (Crichton and Krishnamurthi, “A Core Calculus for Documents”, POPL 2024). The parser records the expression as written and never reads it. What language it is in, how it is evaluated, and whether its value is a node, a list or an option are the consumer’s.

Today I am going shopping for %`List.length items` things.
<p>Today I am going shopping for <code data-hole="">List.length items</code> things.</p>
Open in the playground

The HTML renderer shows a hole nobody evaluated as its expression, so it is not lost silently.

A hole is a code span

A % directly before a code span makes it a hole, as a $ makes it math. Everything djot says of the code span holds of the hole: its content is read verbatim, backslashes and braces included, a longer backtick run lets it hold backticks, and a single space is removed on each side when it starts or ends with a backtick.

%`{r with x = 1}` and %``f `A`` and %`print_string "\n"`
<p><code data-hole="">{r with x = 1}</code> and <code data-hole="">f `A</code> and <code data-hole="">print_string "\n"</code></p>
Open in the playground

A hole may cross a line break, which is part of the expression, and one the paragraph ends inside runs to its end, as a code span does.

a %`f
x` b %`g y
<p>a <code data-hole="">f
x</code> b <code data-hole="">g y</code></p>
Open in the playground

An empty expression is still a hole, for the consumer to reject.

%``
<p><code data-hole=""></code></p>
Open in the playground

Attributes after a hole are the hole’s, as they are a code span’s. A raw format is not: as after math, {=html} is text.

%`x`{.c} and %`y`{=html}
<p><code data-hole="" class="c">x</code> and <code data-hole="">y</code>{=html}</p>
Open in the playground

What is not a hole

Only a % immediately before the backticks prefixes them, and \% writes one that does not.

a % `x` and \%`y` and %{.c}
<p>a % <code>x</code> and %<code>y</code> and <span class="c">%</span></p>
Open in the playground

A % inside a code span, an autolink or a link destination is not a prefix.

``%`x`` and [a](%`u`)
<p><code>%`x</code> and <a href="%`u`">a</a></p>
Open in the playground

Priority

A hole opens at its backticks and closes at the matching run, as a code span does, so whatever it holds is not markup and what opens first wins.

*a %`b* c` and `d %`e` f`
<p>*a <code data-hole="">b* c</code> and <code>d %</code>e<code> f</code></p>
Open in the playground

Text

A hole’s text, from which a heading’s identifier and an empty reference’s label are made, is its expression, as math’s is its formula.

# Total %`n`
<section id="Total-n">
<h1>Total <code data-hole="">n</code></h1>
</section>
Open in the playground

Blocks

There is no block syntax. A raw block in format hole is a raw block to the parser, kept exactly, blank lines and indentation included, and a consumer that evaluates holes may read it as one. Like any raw block in a format the HTML renderer does not know, it renders as nothing.

Items:

``` =hole
ul (List.map item items)
```
<p>Items:</p>
Open in the playground

Properties

What turning this on changes, against djot. Every property not listed keeps its status.

Propertydjotwith this extension
When delimiters overlap, the first opener that gets closed wins, and a closer takes the closest open opener. Exactly one reading follows these rules, and the parser gives it.provedconditionalFor a paragraph without a character an extension claims: `[[` with wikilinks, `$` with dollar math, `:` with tags, `%` with holes, a hyphen when the delete delimiter is wider than one, or a parenthesis used as a delimiter.

Keyed blocks

Setting: ext_keyed.

A colon pairs a label with one block.

status: in progress
<dl class="keyed">
<dt>status</dt>
<dd>
<p>in progress</p>
</dd>
</dl>
Open in the playground

When nothing follows the colon, the block is the one on the following lines.

steps:
- one
- two
<dl class="keyed">
<dt>steps</dt>
<dd>
<ul>
<li>
one
</li>
<li>
two
</li>
</ul>
</dd>
</dl>
Open in the playground

This extension changes how existing djot documents parse: every line it reads as a key is a paragraph line in djot.

Where a colon makes a key

A text line is split at its first colon that is

  • not escaped;
  • not preceded by whitespace;
  • followed by a space or by the end of the line (a tab does not count);
  • outside every inline construct: not in a code span, a bracket, an attribute or an open delimiter.
see http://example.com: not this colon, but the next
<dl class="keyed">
<dt>see http://example.com</dt>
<dd>
<p>not this colon, but the next</p>
</dd>
</dl>
Open in the playground
`a: b` is code
<p><code>a: b</code> is code</p>
Open in the playground
foo : bar
<p>foo : bar</p>
Open in the playground

The label is one inline element: a run of text, or an emphasis, a link, a code span.

The value

What follows the colon on the key line is inline content, never block syntax.

foo: - bar
<dl class="keyed">
<dt>foo</dt>
<dd>
<p>- bar</p>
</dd>
</dl>
Open in the playground

A line is split once.

foo: bar: baz
<dl class="keyed">
<dt>foo</dt>
<dd>
<p>bar: baz</p>
</dd>
</dl>
Open in the playground

One block

A key takes one block, and its scope ends where that block ends. A paragraph ends at a blank line, a list at a line that is not part of it, a fenced block at its closing fence.

foo:
bar

baz
<dl class="keyed">
<dt>foo</dt>
<dd>
<p>bar</p>
</dd>
</dl>
<p>baz</p>
Open in the playground

Keys nest by line.

foo:
bar: baz
<dl class="keyed">
<dt>foo</dt>
<dd>
<dl class="keyed">
<dt>bar</dt>
<dd>
<p>baz</p>
</dd>
</dl>
</dd>
</dl>
Open in the playground

Where a colon is not a key

Lines that djot reads as something other than text are read that way first: a div fence :::, a definition list marker, a reference definition [x]: u, a footnote definition, a heading.

# foo: bar
<section id="foo:-bar">
<h1>foo: bar</h1>
</section>
Open in the playground

Output

A description list with one term and the class keyed. The tree keeps a node of its own, with the label and the block.

Properties

What turning this on changes, against djot. Every property not listed keeps its status.

Propertydjotwith this extension
Text inside an item of a bullet or ordered list parses as it would at top level, when indented the way the formatter writes it.provedconditionalWith keys on, for items with no block attribute outside a block quote, and that do not end inside a code block or on a blank line.
The same, for the items of a definition list.provedconditionalWith keys on, for items with no block attribute outside a block quote, and that do not end inside a code block or on a blank line.
A line inside a paragraph never starts a new block.provedconjectured
Which blocks a document has, and how they nest, is decided without reading inline syntax.conditionalExcept a table caption, which disappears when its inline content is empty.unknown

List interruption

Setting: ext_list_interrupts. On in the Markdown-like profile.

In djot a list needs a blank line before it. With this setting a list marker on the line after paragraph text starts a list.

Things to do:
- one
- two
<p>Things to do:</p>
<ul>
<li>
one
</li>
<li>
two
</li>
</ul>
Open in the playground

This also lets a sublist follow an item’s text directly.

- fruit
  - apple
  - pear
<ul>
<li>
fruit
<ul>
<li>
apple
</li>
<li>
pear
</li>
</ul>
</li>
</ul>
Open in the playground

An ordered marker starts a list only when its number is 1. Other numbers often end a sentence.

The civil war ended in
1865. And this should not start a list.
<p>The civil war ended in
1865. And this should not start a list.</p>
Open in the playground
The steps are
1. first
2. second
<p>The steps are</p>
<ol>
<li>
first
</li>
<li>
second
</li>
</ol>
Open in the playground

What it costs

A paragraph can no longer be wrapped anywhere: a line that begins with a bullet or with 1. starts a list.

Properties

What turning this on changes, against djot. Every property not listed keeps its status.

Propertydjotwith this extension
A line inside a paragraph never starts a new block.provedbrokenA line that begins with a list marker starts a list.

Setext headings

Setting: ext_setext_headings. On in the Markdown-like profile.

A paragraph followed by a line of = is a level 1 heading.

A heading
=========
<section id="A-heading">
<h1>A heading</h1>
</section>
Open in the playground

A line of two or more - makes a level 2 heading.

A heading
--
<section id="A-heading">
<h2>A heading</h2>
</section>
Open in the playground

A single - is a list marker and does not underline.

A paragraph
-
<p>A paragraph
-</p>
Open in the playground

The underline applies to the paragraph above it, so the heading can span several lines.

A heading
on two lines
===
<section id="A-heading-on-two-lines">
<h1>A heading
on two lines</h1>
</section>
Open in the playground

Without a paragraph above, the same line is what djot reads.

===
<p>===</p>
Open in the playground

What it costs

A paragraph can no longer be wrapped anywhere: a line that is all =, or two or more -, ends the paragraph and changes what it is.

Properties

What turning this on changes, against djot. Every property not listed keeps its status.

Propertydjotwith this extension
A text line that continues a paragraph may leave out the prefixes of the quotes, list items and footnotes around it, and parses as it would with them written.provedconditionalNot for a line that underlines the paragraph: it makes a setext heading.
A line inside a paragraph never starts a new block.provedbrokenA line of = or - under a paragraph makes it a heading.