Cariad's Code Blog

A Markdown footnote inside an HTML figure inside a Hugo shortcode

by Cariad EcclestonTuesday January 14, 2020 • 6 minute read

It would be easy if you could put Markdown inside HTML block elements. But you can’t, so it isn’t.

Sounds easy, right?

Here’s a photo in a Markdown document I’m working on:

Screenshot of a HTML figure with figcaption.

A <figure> with a very long <figcaption>

I want to shrink the caption down to just “Albert Einstein (1947)", and put the attribution into a footnote linked-to from the caption.

Something a bit like this:

Screenshot of the Markdown footnote example with the fallback paragraph replaced by an anchor on the caption.

A figure with a caption with a footnote

Sounds easy, right? Let’s do it then!

Laying the foundations

<figure> and <figcaption>

Here are the <figure> and <figcaption> elements I’m starting with:

Markdown
<figure>
  <img src="..." />
  <figcaption>
    Albert Einstein (1947). Photograph by Oren Jack Turner, Princeton, NJ. Public Domain, with thanks to WikimediaCommons. https://commons.wikimedia.org/wiki/File:Albert_Einstein_1947.jpg
  </figcaption>
</figure>

I’m omitting the <picture> and <source srcset="..."> elements because oh boy, do they ever add complexity we don’t need right now.

If you can take my word for it, the rendered document looks like this:

Screenshot of a HTML figure with figcaption.

A <figure> with a very long <figcaption>

Footnotes

A footnote in Markdown has two parts: an in-document anchor and end-of-document content:

Markdown
Albert Einstein[^einstein] was fantastic.

[^einstein]: https://en.wikipedia.org/wiki/Albert_Einstein

When we render that example, we get this:

Screenshot of a Markdown footnote.

A rendered Markdown footnote

Okay! Let’s stick a footnote into a figure and see what happens!

You can’t put Markdown inside HTML block elements

This looks like the kind of solution I want, right?

Markdown
<figure>
  <img src="..." />
  <figcaption>
    Albert Einstein (1947).[^einstein]
  </figcaption>
</figure>

[^einstein]: Photograph by Oren Jack Turner, Princeton, NJ. Public Domain, with thanks to WikimediaCommons. https://commons.wikimedia.org/wiki/File:Albert_Einstein_1947.jpg

Nu-uh. That doesn’t work.

The Markdown specification doesn’t allow us to put Markdown inside HTML block elements1.

It’s a pain here and now for this problem, but it’s also a smart idea for preventing ambiguity of if/how the parser should transform the text. I’m cool with this.

So, the caption is rendered literally as-is, without a footnote:

Screenshot of a broken Markdown footnote inside a figure caption.

Markdown parsers render markup inside HTML block elements literally as-is

We need to find another solution.

A little bit of JavaScript

I know, I know. I said the “J” word, and now you’re disappointed because you wanted a pure Markdown solution.

And you know what? Me too, damnit.

Maybe it’s possible if I hook into the parser some way or another… but then I’m just hacking the software, and the markup is still invalid.

So, here’s the plan:

  1. Use Markdown to create the footnote outside of the <figure>.
  2. Use JavaScript to move the footnote inside of the <figure>.
  3. Make sure the document looks good if the JavaScript doesn’t run.

Updated Markdown

Here’s the updated Markdown document:

Markdown
<figure id="einstein-photo" class="has-footnote">
  <img src="..." />
  <figcaption>
    Albert Einstein (1947)
  </figcaption>
</figure>

<em>Albert Einstein (1947)</em> attribution[^einstein]

[^einstein]: Photograph by Oren Jack Turner, Princeton, NJ. Public Domain, with thanks to WikimediaCommons. https://commons.wikimedia.org/wiki/File:Albert_Einstein_1947.jpg

The three things to note are:

  1. The figure has a unique id, which is needed so that our JavaScript can select it.
  2. The figure has a class named “has-footnote”, which is needed for our JavaScript to recognise it as a figure that needs to be updated.
  3. A paragraph has been added directly after the </figure>. I call this the fallback paragraph. You can phrase it however you want, but it must contain the footnote anchor.

The document now looks like this:

Screenshot of the Markdown footnote example with the fallback paragraph visible.

A figure with a caption with a footnote beneath

Without any JavaScript, this is okay. The page is readable, understandable and fine.

But now, let’s add some JavaScript to wrap it up.

New JavaScript

We need to write some JavaScript to:

  1. Wait until the DOM has loaded.
  2. Find all the figure elements with the “has-footnote” class.
  3. For each figure, move the footnote anchor out of its fallback paragraph and into its caption, then remove the paragraph.

I’ve split these three steps into three functions. Let’s go through them bottom-up.

First, the fixFigureFootnote() function fixes the footnote in a single, specific figure:

JavaScript
function fixFigureFootnote(id) {

  // Get the fallback paragraph.
  const para = document.querySelector(`#${id} + p`);

  // Get the footnote anchor within the paragraph.
  const anchor = para.querySelector('sup');

  // Remove the fallback paragraph from the document.
  para.parentNode.removeChild(para);

  // Get the figure caption.
  const c = document.querySelector(`#${id} figcaption`);

  // Append the footnote anchor to the caption.
  c.appendChild(anchor);
}

Next up, the fixFigureFootnotes() function finds and fixes the footnotes in all figures:

JavaScript
function fixFigureFootnotes(className) {
  const selector = `figure.${className}`;
  const figures = document.querySelectorAll(selector);
  for (const figure of figures) {
    fixFigureFootnote(figure.id);
  }
}

Finally, we need to wait for the DOM to finish loading before we go looking for all the figures with the “has-footnote” class.

JavaScript
window.addEventListener('load', function() {
  fixFigureFootnotes('has-footnote');
});

Right! Let’s pull it all together!

All together now

With all of those changes applied, here are our final files:

Markdown
<figure id="einstein-photo" class="has-footnote">
  <img src="..." />
  <figcaption>
    Albert Einstein (1947)
  </figcaption>
</figure>

<em>Albert Einstein (1947)</em> attribution[^einstein]

[^einstein]: Photograph by Oren Jack Turner, Princeton, NJ. Public Domain, with thanks to WikimediaCommons. https://commons.wikimedia.org/wiki/File:Albert_Einstein_1947.jpg
JavaScript
window.addEventListener('load', function() {
  fixFigureFootnotes('has-footnote');
});

function fixFigureFootnotes(className) {
  const selector = `figure.${className}`;
  const figures = document.querySelectorAll(selector);
  for (const figure of figures) {
    fixFigureFootnote(figure.id);
  }
}

function fixFigureFootnote(id) {
  const para = document.querySelector(`#${id} + p`);
  const anchor = para.querySelector('sup');
  para.parentNode.removeChild(para);
  const c = document.querySelector(`#${id} figcaption`);
  c.appendChild(anchor);
}

When the JavaScript runs, the document looks like this:

Screenshot of the Markdown footnote example with the fallback paragraph replaced by an anchor on the caption.

A figure with a caption with a footnote

Ta-da! The fallback paragraph has gone, and the footnote’s anchor is attached to the photo’s caption. Perfect!

And remember, if the JavaScript doesn’t run for any reason, the fallback paragraph keeps the document readable:

Screenshot of the Markdown footnote example with the fallback paragraph visible.

A figure with a caption with a footnote beneath

Making it work with a Hugo shortcode

That’s all great for hand-crafted Markdown, but my endgame is to make this work as automatically as possible in my Hugo websites.

I organise my images into headless page bundles. I won’t go into the detail of that right now, suffice to say it allows me to create an “image” shortcode that needs only a single name as input. Hugo can then reference and build all the image assets and metadata automatically. It’s quite lovely.

Here’s my resource entry for the photo of Einstein:

YAML (content/images/index.md)
name:  "einstein"
src:   "Albert_Einstein_1947.jpg"
title: "Albert Einstein (1947)"
params:
  attribution: "Photograph by Oren Jack Turner, Princeton, NJ. Public Domain, with thanks to Wikimedia Commons ([File:Albert Einstein 1947.jpg](https://commons.wikimedia.org/wiki/File:Albert_Einstein_1947.jpg))"

And here’s my “image” shortcode with the footnotes code added:

Hugo shortcode (shortcodes/image.md)
{{ $name     := .Get "name" }}
{{ $images   := .Site.GetPage "/images" }}
{{ $resource := $images.Resources.GetMatch $name }}

{{ $footnoteID := printf "%s-fn" $name }}

{{ with $resource }}
  <figure id="{{ $name }}-figure" class="has-footnote">
    <figcaption>
      {{ .Title }}
    </figcaption>
  </figure>

  <em>{{ .Title }}</em> attribution[^{{ $footnoteID }}]

  [^{{ $footnoteID }}]: {{ .Params.attribution }}
{{ end }}

I haven’t included the <picture> and <source srcset="..."> elements because they add a ton of complexity that isn’t needed for this example.

The figure’s ID is the image’s ID with -figure appended. The actual value isn’t significant, but that suffix makes it less likely to conflict with another in the DOM.

Likewise, the footnote ID is the image’s ID with -fn appended. As long as the value is unique to this image, it can be anything you want.

Handily, we can inject the footnote’s ID variable into the markup with just Hugo’s {{ }} syntax. Hugo injects its build-time values before sending the document to the Markdown parser; the parser then doesn’t know and doesn’t care that we constructed the anchor from a variable.

And there we go!


  1. Daring Fireball: Markdown Syntax Documentation, for inline HTML ↩︎