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:
-
[!; -
a kind: one or more ASCII letters, digits,
-or_; -
]; -
optionally a fold marker,
+or-; - 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.
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 [!.
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.
| Property | djot | with this extension |
|---|---|---|
| Text inside a block quote parses as it would at top level. | proved | brokenA 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.
::: 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.
Attributes attach as they do to any inline.
Where :name[ is not a named span
After an escaped colon, or when the colon directly follows a letter, a digit or a colon.
When it never closes.
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.
| Property | djot | with 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. | proved | conditionalFor 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.
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.
The content is not parsed as inline syntax.
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.
The GitHub form
A $ directly before a code span and another directly after it make
inline math whose content is the code span’s.
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 $ inside a code span is not an opener, and \$ is a literal dollar.
Properties
What turning this on changes, against djot. Every property not listed keeps its status.
| Property | djot | with 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. | proved | conditionalFor 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.
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.
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.
Properties
What turning this on changes, against djot. Every property not listed keeps its status.
| Property | djot | with 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. | proved | conditionalFor 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
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.
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.
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.
| Property | djot | with 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. | proved | conditionalWith 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. | proved | conditionalWith 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. | proved | conjectured |
| 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.
| Property | djot | with this extension |
|---|---|---|
| A line inside a paragraph never starts a new block. | proved | brokenA 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 line of two or more - makes a level 2 heading.
A single - is a list marker and does not underline.
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.
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.
| Property | djot | with 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. | proved | conditionalNot for a line that underlines the paragraph: it makes a setext heading. |
| A line inside a paragraph never starts a new block. | proved | brokenA line of = or - under a paragraph makes it a heading. |
Wikilinks
Setting: ext_wikilinks.
A wikilink is a target between double brackets, as in Obsidian.
A | separates the target from an alias, which is the text shown.
See [[Backlinks|the other direction]].
<p>See <a href="Backlinks">the other direction</a>.</p>
Open in the playground
A ! in front makes it an embed, as it makes a link an image.
Where it opens and closes
A wikilink opens at a [ that comes directly after another [, and closes
at the first ]] on the same line. A single ] is part of the target.
It does not cross a line break. One that is not closed by the end of its line is text.
Target and alias
The region is split at its first |. Both parts are kept as written:
nothing is trimmed, a backslash is not decoded, and the alias is not parsed
as inline content.
An empty target is not a wikilink.
What it changes in djot
A [ at the start of a link’s text is read as a wikilink opener. So a link
whose text begins with a bracket reads differently.
In a table, the row is split into cells first, so a | inside a wikilink
ends the cell.
Output
A wikilink renders as the link it stands for: the target is the href, and
the alias, or the target when there is none, is the text. An embed renders
as an image. The tree keeps a node of its own, with the target, the alias
and whether it is an embed.
Properties
What turning this on changes, against djot. Every property not listed keeps its status.
| Property | djot | with 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. | proved | conditionalFor 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. |