BlogCode

How to add different ordered list types to markdown

Written by Codemzy on May 27th, 2022

One thing I always found missing from markdown was different list types, specifically, letter and roman numeral lists. In this blog post, I'll add these list types to the popular MarkedJS markdown compiler.

If you're writing text-heavy content for your website, like a blog post, markdown is a great option. I'm writing this post in markdown right now.

Back in the day, I'd write all my content in HTML. But who wants to wrap everything in <p> and <h2> tags? And even worse, if you add a bunch of CSS classes, and happen to update something, you’ve got a load of posts to go through and update. Nightmare!

That's why people use markdown.

You can write like you usually would, add a few characters here and there for a header or a list, and convert it to HTML before putting it on a webpage.

So far, so fabulous.

Lists in markdown

Want to add a list to your content? That's no problem with markdown. Out of the box, markdown supports two types of lists:

  1. Ordered Lists
  • Unordered Lists
1. This is
2. how you write
3. an ordered list
4. in markdown
  1. This is
  2. how you write
  3. an ordered list
  4. in markdown
- This is
- how you write
- an unordered list
- in markdown.
  • This is
  • how you write
  • an unordered list
  • in markdown.

You can indent lists too, which is pretty cool. Let's face it, writing indented lists in HTML is a brain ache. All those <ol> inside <li> inside <ul> and you soon lose track of your lists.

In markdown, add a few spaces, and like magic, the list is indented. And it’s all pretty clear and easy to read.

1. This is
2. how you write
   - an indented list
   - in markdown
  1. This is
  2. how you write
    • an indented list
    • in markdown

But there is a shortcoming with lists in markdown. And that's when you need to use different ordered list types. For example, this won't work:

a) A 
b) Letter
c) List

a) A b) Letter c) List

πŸ‘† That's not how I wanted this list to look!

Writing something technical, or want to get fancy with your lists? You can't. In some situations, this limitation could be a dealbreaker. So how can you add different types of lists?

There are a couple of options.

Use HTML

You can go back to writing boring old HTML. Yep, the thing we used markdown to avoid!

Writing HTML is the most obvious option, but it's not my favourite. You lose some of the benefits of markdown, e.g you need to add HTML tags manually and your lists aren't so readable. But it gets the job done.

The good news is, that the rest of your blog post can still be in markdown. Because markdown accepts HTML input, you don't have to re-write your whole post in HTML just for one unusual element.

<ol type="a">
 <li>This is</li>
 <li>a letter</li>
 <li>list</li>
</ol>
  1. This is
  2. a letter
  3. list

Nice, it works!

But at what cost? The cost of writing in HTML, and if that’s a cost you are willing to pay, you can stop here!

And what I will say is, if you're only going to need different types of lists in a few specific posts, adding them with HTML is the best way to do it.

But if you need other types of lists regularly, why not add ordered list types to your markdown (instead of writing them in HTML).

Extend lists in markdown

Now we get to the fun part!

For my static blog builder, I'm using MarkedJS.

And the great news is, it's extendable.

To champion the single-responsibility and open/closed principles, we have tried to make it relatively painless to extend Marked.

- MarkedJS Extending Marked

At first, I tried creating a custom list parser, but since we want all the existing features of lists, just different types, I extended the list output instead.

It does rule out being able to write a list like:

A.
B.
C.

But I feel this is an ok compromise since if I wrote a dramatic sentence like:

A. GOAT. EATING. MY. DINNER.

I wouldn't want it to turn into a list.

I feel like a good compromise if I need a different type of list is to tell markdown in the first item.

Like:

1. a.
1. This is my letter list
2. I add an extra 1. at the start, you don't have to...
3. But if you don't this would be number 4 but become number 3

You might be wondering why there's an extra number 1. The answer is that the first list item will get removed when specifying a different list type, so I think it's more readable if the numbers match up when the first item is removed.

In MarkedJS, the renderer defines the HTML output of a given token. We can override the default output of the list token like this:

import { marked } from 'marked';

// Override function
const renderer = {
  list(body, ordered, start) {
    return false;
  },
};

marked.use({ renderer });

The list token gets (string body, boolean ordered, number start) arguments.

If you return false, it falls back to the default behaviour, so currently, this code does nothing.

To keep the default behaviour, we only want to change the output if the first list item matches a new list identifier, in this case, a., to represent a letter list.

This sounds like a job for regex!

// Override function
const renderer = {
  list(body, ordered, start) {
    // return letter lists
    if (ordered && body.match(/^<li>a\.<\/li>/)) {
      return body.replace(/^<li>a\.<\/li>(.*)/gms, `<ol type="a">$1</ol>`);
    }
    return false;
  },
};

The body only contains the content of our list, so not the <ol> wrapping tags. In the if statement, we check if the list is an ordered list, and then use some regex to check the first item in the list.

The regex looks at the start (^) of the string. It checks if the first list item contains a. (and only that), with body.match(/^<li>a\.<\/li>/). If it does, we're going to return a custom response, if it doesn't, we'll return false so that the default marked list behaviour can continue.

Within the if statement, we run our regex again but this time in a replace function. A regex group (.*) gets the rest of the list content so that in our replacement we can return an ordered list of type="a" wrapping the list content $1.

If the replacement pattern confuses you, here's a blog post on regex groups with .replace() in Javascript.

Not starting at the start

With the ordered list type, you don't have to start at 1.

5. Five
6. Six
7. Seven
8. Eight
  1. Five
  2. Six
  3. Seven
  4. Eight

Our new type of list should have the same functionality.

5. a.
5. Five (e)
6. Six (f)
7. Seven (g)
8. Eight (h)

And we can easily keep this because the list token receives a start argument for ordered lists. As long as we number the list where we want it to start (e.g. 5 in the list above) we can pass the start argument along.

An integer to start counting from for the list items. Always an Arabic numeral (1, 2, 3, etc.), even when the numbering type is letters or Roman numerals.

- MDN <ol>: The Ordered List element

The start attribute is always a number, so we can keep it that way!

const renderer = {
  list(body, ordered, start) {
    if (ordered && body.match(/^<li>a\.<\/li>/)) {
        return body.replace(/^<li>a\.<\/li>(.*)/gms, `<ol type="a" start="${start}">$1</ol>`);
    }
    return false;
  },
};

Now we can start our letter list wherever in the alphabet we like!

5. a.
5. Five (e)
6. Six (f)
7. Seven (g)
8. Eight (h)
  1. Five (e)
  2. Six (f)
  3. Seven (g)
  4. Eight (h)

Adding other list types

So far we have added lowercase letter list types to markdown, but there are other list types we should add. Let's take a look at how our list support is coming along...

  • 1 for numbers (default) βœ…
  • a for lowercase letters βœ…
  • A for uppercase letters ❌
  • i for lowercase Roman numerals ❌
  • I for uppercase Roman numerals ❌

Hmmm. Ok, less than halfway.

The good news is, there's not a whole lot more code to add to support the other list types. We just need to tweak the regex.

In the if statement, instead of checking if the first item contains a., let's check for all the new list types - by swapping a for [a|A|i|I].

body.match(/^<li>[a|A|i|I]\.<\/li>/)

And in the replacement, we can add a group to get the list type and pop that into our returned HTML. Now that there's more than one group, I'll name them for readability.

For more information on naming regex groups, here's a blog post on regex groups with .replace() in Javascript to explain.

return body.replace(/^<li>(?<type>.)\.<\/li>(?<list>.*)/gms, `<ol type="$<type>" start="${start}">$<list></ol>`);

Now there's a new type group ?<type> and the a has been swapped for . to match whatever the list type is (we're already filtering the exact types in the if statement, so no invalid types will get through).

The group for the list content is now called list ?<list> (nothing else changes here).

Here's the full code:

import { marked } from 'marked';

const renderer = {
  list(body, ordered, start) {
    if (ordered && body.match(/^<li>[a|A|i|I]\.<\/li>/)) {
      return body.replace(/^<li>(?<type>.)\.<\/li>(?<list>.*)/gms, `<ol type="$<type>" start="${start}">$<list></ol>`);
    }
    return false;
  },
};

marked.use({ renderer });

And with that, all of the ordered list types are now supported in markdown.

Examples

Unordered lists βœ…

Unordered lists are supported in markdown by default, we haven't changed anything here.

- This is
- how you write
- an unordered list
- in markdown.
  • This is
  • how you write
  • an unordered list
  • in markdown.

Numbered lists βœ…

Numbered lists are supported in markdown by default, we haven't changed anything here.

1. This is
2. how you write
3. an ordered list
4. in markdown
  1. This is
  2. how you write
  3. an ordered list
  4. in markdown

Lowercase letter lists πŸ†•

Our custom code adds markdown support for lowercase letter a lists.

1. a.
1. Now you have
2. lowercase letter
3. lists
4. in markdown
  1. Now you have
  2. lowercase letter
  3. lists
  4. in markdown

Uppercase letter lists πŸ†•

Our custom code adds markdown support for uppercase letter A lists.

1. A.
1. Now you have
2. uppercase letter
3. lists
4. in markdown
  1. Now you have
  2. uppercase letter
  3. lists
  4. in markdown

Lowercase roman numeral lists πŸ†•

Our custom code adds markdown support for lowercase roman numeral i lists.

1. i.
1. Now you have
2. lowercase roman numeral
3. lists
4. in markdown
  1. Now you have
  2. lowercase roman numeral
  3. lists
  4. in markdown

Uppercase roman numeral lists πŸ†•

Our custom code adds markdown support for uppercase roman numeral I lists.

1. I.
1. Now you have
2. uppercase roman numeral
3. lists
4. in markdown
  1. Now you have
  2. uppercase roman numeral
  3. lists
  4. in markdown

If you're using custom CSS, and run into an issue where uppercase lists display as lowercase, it's actually down to the CSS type selector not being case sensitive (at least in Chrome). It's nothing to do with markdown, and would be a problem even if you wrote the list in HTML. You might need to add a custom class to get around that!