Esc

comark-footnotes

Footnotes

Write [^1] where a note applies and [^1]: anywhere below, or the note itself as ^[text], and get numbered notes at the end of the page.

Playground

The deploy waits when the roof holds more than 12 pigeons[1], unless it's a hotfix[2].


  1. Counted once, from the window, at 9:00. ↩
  2. A hotfix needs two approvals. ↩

Notes are numbered in the order the text first references them and listed in a section at the end. A definition can run over indented lines and hold paragraphs, lists, and code. The nodes are the ones comark/plugins/footnotes writes, so styles and renderers made for it keep working.

A reference is [^label] with no spaces in the label. Its definition starts a line with [^label]:, anywhere in the document:

The duck has a name[^name].

[^name]: Gerald.
md

Notes are numbered in the order they're first referenced, whatever their labels. A note referenced twice lists a link back to each reference.

Lines indented by four spaces continue a definition, as in a list item, so a note can hold several paragraphs, a list, or code. A line that isn't indented continues the paragraph above it:

[^deploy]: Deploys stop on Friday at noon
and start again on Monday.

    Hotfixes still ship:

    ```sh
    deploy --hotfix --approvers 2
    ```
md

Markdown in a note renders as it does anywhere else. The note's links back go at the end of its last paragraph.

^[text] is a note written where it applies, with no label. It's numbered with the rest:

Standup is at 9:30^[Unless the pigeons are counted late.].
md

A reference with no definition stays as written, [^label], and a definition inside code is code. [^text](https://…) is a link.

A reference with no definition, a definition nothing references, and a label defined twice each add a sentence to meta.issues that names the label and says how to fix it. Each one has its own rule in Doctor.

footnotes: [^gone] has no definition; add a "[^gone]: …" line, or remove the reference.
footnotes: [^spare]: is never referenced; add [^spare] where it applies, or remove the definition.
footnotes: [^1]: is defined more than once, and only the first shows; give the others their own labels, or remove them.
txt

Swap the import; the options and the nodes are the same, so the classes footnote-ref, footnotes, footnotes-list, and footnote-backref and the ids fn-label and fnref-label don't change. Three things render differently:

  • A definition inside fenced code stays in the code. Comark's plugin removes every [^label]: line before parsing, code included.
  • Markdown in a note renders. Comark's plugin keeps the note as text, so **bold** stays as written.
  • A note can run over several lines and blocks. Comark's plugin keeps the first line, and its indented lines become a code block in the page.

A reference used twice gets the id fnref-label-2 the second time, where Comark's plugin repeats fnref-label, and its note gets a link back to each reference. A note of several blocks is an li holding p elements, with the links back inside the last one.

Comark's Footnote handler for renderMarkdown reads the same nodes, so it writes a note of one paragraph back to [^label]: …. A note of several blocks comes back as its first block only.

A stream renders notes as they arrive: ^[an unfinished note already shows as a note, and [^label] shows as text until its definition arrives. Blocks Comark reuses from the last parse keep their notes.

The options go under footnotes in comarkUi(), or to footnotes() when you use the plugin alone. They are Comark's, with the same defaults:

Option
label The section's heading. Default "Footnotes"; "" leaves the heading out.
hr Whether a horizontal rule comes before the section. Default true.
backRef The link from a note back to where it's referenced. Default "↩".

comark-ui brings comark-footnotes and registers it. To use it alone, install it and register it instead of comark/plugins/footnotes:

pnpm add comark @comark/html comark-footnotes
import { parseMarkdown } from "comark";
import footnotes from "comark-footnotes";

const document = await parseMarkdown(markdown, {
  plugins: [footnotes()],
});
ts

It has no components, because the notes render as plain HTML. It is named footnotes, like Comark's plugin, so a list holds one or the other. When a list holds both, Comark keeps the first, so remove the import from comark/plugins/footnotes rather than adding this one after it.

It works with registerDefaultPlugins: false and in streaming parses.