Sashmark / Blog

Markdown link to heading by name: pandoc tests

Bobby Huang ·

Under The sample files, the test inputs are introduced. Under Bracket links under markdown, the converted HTML for ihr.md sits in a code block. Pandoc's HTML after one extension was taken out is quoted in Dropping the extension from markdown. The section Plain commonmark leaves brackets pairs a commonmark run with spec line numbers. A run that adds the extension to commonmark is quoted in The extension added to commonmark. Runs for a second file, one carrying a definition line, fill A definition line beats the heading. Two headings sharing one name are covered in Two headings named Notes. Numbered commands fill Steps for a link written by heading name.

Key takeaways

  • Converted output for the main sample file is printed in Bracket links under markdown.
  • Look in Dropping the extension from markdown for that file converted with one extension removed.
  • Spec line numbers next to a commonmark run appear in Plain commonmark leaves brackets.
  • Pandoc runs against ihr-ref.md fill A definition line beats the heading.
  • Numbered steps and their commands make up Steps for a link written by heading name.

A markdown link to heading by name means typing the heading's words in square brackets, with no #id and no URL, and letting the converter work out the target. Three small files went through pandoc to check how that works.

One macOS computer did each conversion once; pandoc --version reported 3.8.3. Spec line numbers refer to CommonMark 0.31.2. Every claim here rests on output captured during this test.

The sample files

The main file, printed with cat ihr.md:

# Rollback steps

Jane Doe reads [Rollback steps] first.

Then she reads [the rollback part][Rollback steps].

## Sign-off

On line 3, the heading's words sit alone inside brackets: [Rollback steps]. Line 5 gives the link its own wording first, then puts the heading's words in a second pair of brackets: [the rollback part][Rollback steps]. The file has no definition line at all.

Two more files cover the corner cases. ihr-ref.md repeats the heading and the first sentence, then adds a definition line that points at example.com. ihr-dup.md holds two headings that both read # Notes. Their contents are printed in the sections A definition line beats the heading and Two headings named Notes.

A link that names the id itself, such as [text](#rollback-steps), has its own guide to heading anchor links. The bracket forms get all the attention in this post.

Bracket links under markdown

The command:

pandoc -f markdown -t html ihr.md

Output:

<h1 id="rollback-steps">Rollback steps</h1>
<p>Jane Doe reads <a href="#rollback-steps">Rollback steps</a>
first.</p>
<p>Then she reads <a href="#rollback-steps">the rollback part</a>.</p>
<h2 id="sign-off">Sign-off</h2>

Both bracket forms came out as a elements, and both carry href="#rollback-steps". In the first link, the heading's words became the link text. In the second, the words the rollback part took that place, and the heading's name only picked the target.

Pandoc split the first paragraph after </a>, and that break is kept as printed.

The behavior comes from a pandoc extension called implicit_header_references. In the pandoc --list-extensions output for the markdown reader, that name carries a +, which marks it as on by default. The auto_identifiers extension carries a + in that same list.

Dropping the extension from markdown

Adding -implicit_header_references to the reader name drops that one extension:

pandoc -f markdown-implicit_header_references -t html ihr.md

Output:

<h1 id="rollback-steps">Rollback steps</h1>
<p>Jane Doe reads [Rollback steps] first.</p>
<p>Then she reads [the rollback part][Rollback steps].</p>
<h2 id="sign-off">Sign-off</h2>

The heading kept id="rollback-steps", yet neither bracket form turned into a link. Both paragraphs print their brackets as plain characters. In this output, a count of href="#rollback-steps" returned 0. Run on the default markdown output, that count returned 2.

Here, an id on the heading did not produce links by itself. The links appeared once the extension was on, and went away once it was off.

Plain commonmark leaves brackets

The command:

pandoc -f commonmark -t html ihr.md

Output:

<h1>Rollback steps</h1>
<p>Jane Doe reads [Rollback steps] first.</p>
<p>Then she reads [the rollback part][Rollback steps].</p>
<h2>Sign-off</h2>

This run wrote no id attribute on either heading. Both paragraph lines match the run with the extension removed, byte for byte.

The spec accounts for the brackets. Lines 7294-7296 name three kinds of reference links: full, collapsed and shortcut. Lines 7297-7299 say a full reference link is link text followed by a link label "that matches a link reference definition elsewhere in the document". Lines 7655-7658 describe a shortcut reference link as a label that matches a definition in the same way. Since ihr.md holds no definition line, neither form had anything to match.

The name implicit_header_references belongs to pandoc. Counting implicit_header across the spec file found 0 lines; the control phrase link reference definition matched 17. In the extension list for the commonmark reader, implicit_header_references carries a -, so it starts off.

The extension added to commonmark

A plus sign switches it on:

pandoc -f commonmark+implicit_header_references -t html ihr.md

Output:

<h1>Rollback steps</h1>
<p>Jane Doe reads <a href="#">Rollback steps</a> first.</p>
<p>Then she reads <a href="#">the rollback part</a>.</p>
<h2>Sign-off</h2>

This time both bracket forms became links. Their target is a bare href="#", though, and the heading line has no id attribute. In this output the extra test counted href="#" on two lines and id= on zero.

Nothing follows the # in either href, so no heading name sits in the target. To put an id on a heading by hand, see the post on custom heading ids. That syntax was never combined with the extension in these runs.

A definition line beats the heading

cat ihr-ref.md printed:

# Rollback steps

Jane Doe reads [Rollback steps] first.

[Rollback steps]: https://example.com/rollback

Its closing line defines a link reference. Its label, Rollback steps, matches the heading's words exactly. The reference links guide explains how to write such lines.

The command:

pandoc -f markdown -t html ihr-ref.md

Output:

<h1 id="rollback-steps">Rollback steps</h1>
<p>Jane Doe reads <a href="https://example.com/rollback">Rollback
steps</a> first.</p>

The link went to https://example.com/rollback, the URL on the definition line. The heading still has its id, and no href in this output points at it. Two further runs fed this file to markdown-implicit_header_references and to plain commonmark. Each of those wrote the same <p> element, carrying the example.com URL.

When a definition line and a heading shared one label here, pandoc 3.8.3 took the definition line.

Two headings named Notes

cat ihr-dup.md printed:

# Notes

First notes.

# Notes

Second notes. See [Notes].

The command:

pandoc -f markdown -t html ihr-dup.md

Output:

<h1 id="notes">Notes</h1>
<p>First notes.</p>
<h1 id="notes-1">Notes</h1>
<p>Second notes. See <a href="#notes">Notes</a>.</p>

The [Notes] link got href="#notes". That value matches the id on the first # Notes heading, even though the link sits in the paragraph under the second one. Why the second heading got the id it did is a separate rule, explained in heading ids with special characters.

Two more runs used this file. With implicit_header_references removed from markdown, [Notes] stayed as bracket text. Under commonmark+implicit_header_references, it became <a href="#">Notes</a>, the same bare # that ihr.md got with that setting.

Steps for a link written by heading name

Inside the sample-file folder, the extra test ran these commands:

cat ihr.md
pandoc -f markdown -t html ihr.md -o ihr-md.html
grep -c 'href="#rollback-steps"' ihr-md.html

To link a heading by its words with pandoc's markdown reader, follow these six steps.

  1. Write the heading, for example # Rollback steps.
  2. Where the link belongs, type the heading's words in square brackets: [Rollback steps]. For different link wording, put that wording first, as in [the rollback part][Rollback steps].
  3. Make sure no definition line shares that label. In this test, ihr-ref.md showed the definition line taking the link.
  4. Pass -f markdown to pandoc; its extension list shows implicit_header_references as on by default. That is how the extra test produced ihr-md.html.
  5. Count the hrefs. On ihr-md.html, grep -c printed 2, matching the two links.
  6. If a heading name repeats, check which heading the href names. Under Two headings named Notes, pandoc aimed [Notes] at the first one.

Where Sashmark fits

Picture Sashmark at version one, should things go to plan: a misspelled word gets fixed right where you are reading it. Untouched lines are meant to reach the disk byte-identical, which is the aim of that clean Round-Trip. Another planned piece, Changes View, should flag whatever shifted between visits, and git stays out of it.

It is in development and has not been released. The app itself has no AI features. See the planned features, the help pages and the roadmap for where each part stands.

More at sashmark.com.

Questions

Is a markdown link to heading by name possible in pandoc?
Numbered commands for that job are laid out in Steps for a link written by heading name.
Why did my bracketed heading name stay as text?
Look at Dropping the extension from markdown and at Plain commonmark leaves brackets.
Which heading does a repeated name point at?
Under Two headings named Notes, the href given to the Notes link is quoted.
What if a reference definition uses the same label?
Pandoc's result for such a file appears in A definition line beats the heading.

What Sashmark plans for v1: Features · Roadmap

View as Markdown