/home/thomas

phd student and nlp researcher

Automatic em dash conversion in Eleventy

I recently decided to migrate this blog from (an extremely outdated version of) Jekyll to Eleventy. This process was pretty smooth, although there were some hiccups. One of the more annoying differences is that the default Markdown renderer in Eleventy doesn't convert repeated dashes into en and em dashes. Although I found posts seeming to suggest that this is easily supported, I could not, for the life of me, get it to work.

Now, I know what you're thinking. Who, besides ChatGPT, uses em dashes in 2026? Well, I do. I was introduced to the wonders of the em dash by one of my PhD supervisors, early on in my PhD. Unfortunately, this was just before LLMs started being abused for plagiarism, and when everybody started associating this poor piece of typography with AI slop. If you—like me—are an em dash hold-out, then this post is for you.

The problem

Em dashes are really annoying to type, at least on Swedish QWERTY keyboards. So, a lot of writing tools convert --- into "—" for you. This is true in LaTeX, and it was true in the Markdown renderer I used in Jekyll. It is not, however, true for Eleventy's default renderer: markdown-it. There might be plugins for this—I couldn't find any.

Luckily, Eleventy and markdown-it make it quite easy to extend the renderer. I had previously done this by adding a plugin to allow me to use footnotes in Markdown:

eleventyConfig.amendLibrary("md", (mdLib) => {
    mdLib.use(footnote_plugin);
});

But this extension just involved adding an already-existing plugin. Luckily, adding your own modifications is pretty straightforward!

The solution

The mdLib.core.ruler.after() function allows us to add post-processing steps when rendering our Markdown. Enabling post-processing essentially boils down to adding a call to this function:

eleventyConfig.amendLibrary("md", (mdLib) => {
    mdLib.use(footnote_plugin);
    mdLib.core.ruler.after("inline", "dash_substitution", removeDashes);
});

Of course, we need to actually implement this new removeDashes() function. This function is passed a state argument that we need to modify. The state is structured as a tree, and to perform our conversion we need to traverse while searching for repeated dashes:

const removeDashes = (state) => {
	for (const token of state.tokens) {
		if (token.type !== "inline" || !token.children) {
			continue;
		}
		for (const child of token.children) {
			if (child.type !== "text") {
				continue;
			}
			child.content = child.content
				.replace(/---/g, "—")
				.replace(/--/g, "–");
		}
	}
};

Line #7 is important! We (well, I) don't want to replace dashes in code blocks. Doing so would prove very annoying if a reader (probably, me) wants to copy-paste commands into the terminal [1]. By checking that what we are processing is text, we can avoid this problem (as you can see in this post). And that's it. Now you can continue the fight to reclaim our beloved em dash!


  1. I have read many times that this is something you should avoid. I have also read that you should never pipe curl to sh. Yet, these anti-patterns seem to be the way of the world these days. It feels safer than giving an LLM agent a carte blanche to rm -rf your home directory, which is apparently something people do. ↩︎

Written on January 11, 2026