Lists are a very common construct in technical documents, which is the kind of material I most often write and review. But getting complex lists to look right is tricky, especially when authoring them in Markdown.

The problem with lists starts early on. All Markdown training material will teach you that lists are written like this:

This is an unordered list:

* First.
* Second.
* Third.

And this is an ordered list:

1. First.
2. Second.
3. Third.

Looks trivial and easily readable in its textual form, right? Unfortunately, this style quickly breaks down as soon as you have multiple people editing the same document or you start nesting lists and code blocks.

From experience, I’ve come up with a style of writing lists that ensures they are always correctly and consistently formatted, and that mistakes are trivial to spot at review time. I find myself repeating these tips and their rationale during PR reviews, so here is a full-fledged explanation for posterity’s sake.

A blog on operating systems, programming languages, testing, build systems, my own software projects and even personal productivity. Specifics include FreeBSD, Linux, Rust, Bazel and EndBASIC.

0 subscribers

Follow @jmmv on Mastodon Follow @jmmv on Twitter RSS feed

Ordered list numbers

When working with a large ordered list, keeping items properly numbered is a task that seems impossible. Oftentimes, I’ve gotten PRs for new content where the item numbers were wrong from the get go. Things like this:

1. First.
3. Third already? I guess the item was moved at the last minute.
2. Second.
2. Second again. Copy/paste error?

Markdown will happily accept this and will render the list with correct sequential numbering (note: it will fix the numbering, not reorder your items!). But… do you think this is good? In my opinion, this looks sloppy—and sloppiness of this kind during a PR review is an indicator that the rest of your change is suspect of more serious problems and deserves thorough scrutiny.

The problems don’t end with the author having to be more careful though. If you try to do the right thing and try to keep the list numbers sequential, you’ll have cases where a trivial addition to a document will result in a large diff because you had to renumber all subsequent entries. Take a look at this change and try to answer the question of “what is this doing?”:

--- before      2022-07-07 05:50:28.439334000 -0700
+++ after       2022-07-07 05:50:48.189334000 -0700
@@ -1,3 +1,4 @@
 1. This item comes first.
-2. This item is in the middle.
-3. This item comes last.
+2. This item is also in the middle.
+3. This item is in the middle.
+4. This item comes last.

This patch is simply adding a new item in position 2, but the change had to end up touching two extra unrelated lines. This is hard to review and pollutes the output of git blame (or whichever equivalent annotation command your VCS provides).

The easiest way out of this problem is to not number your lists sequentially. Instead, always prefix them with 1., like this:

1. First.
1. Second, really.
1. Third. Yes, this works!

Doing so will free you from having to maintain sequential list numbers and will also keep diffs clean. Going back to our previous example, adding an item in position 2 would look like this:

--- before      2022-07-07 05:53:31.549334000 -0700
+++ after       2022-07-07 05:53:33.429334000 -0700
@@ -1,3 +1,4 @@
 1. This item comes first.
+1. This item is also in the middle.
 1. This item is in the middle.
 1. This item comes last.

Clean and to the point.

Nested elements

The other problem when formatting lists comes from complex lists that have more than just one paragraph per item or higher-level blocks within them. Inevitably, people new to Markdown will write things like this:

* This is the first bullet point.

  We try to add a second paragraph, but it may not be within the item.

* This is the second bullet point.

    And we want a code block within it, but this is not a code block!

      Neither is this!

* This is the third bullet point.

  > And we try to add a blockquote like we added a second paragraph, but fail.

The above will not be formatted as you expect. The second paragraph in the first list item may or may not end up rendered as a paragraph within the list item. The code blocks in the second list item may or may not be rendered as a code block, but if they are, they will not be nested. And if you try to nest lists, all bets are off.

These problems don’t look apparent in the raw Markdown text and they are difficult to spot in document previews unless you pay close attention to the rendering. Most people don’t seem to notice when code blocks are wider than they are supposed to be, for example, or when paragraphs are not correctly aligned within their container list items.

Plus it’s not only about the looks. These formatting mistakes break the semantics of the document: instead of having a single list with longer individual elements, you end up with two disjoint lists. I don’t have experience with screen readers, but I’m pretty sure this poses an accessibility problem.

The above problems are fixable, obviously. If we look at the Adding Elements in Lists section of the Markdown Guide website, we’ll find examples on how to correctly nest elements. Their suggestion is this:

* This is the first bullet point.

    This is a second paragraph within the first item.

* This is the second bullet point.

        This is a code block within it.

* This is the third bullet point.

    > This is a blockquote within it.

These work as intended, but to me, they look like a huge mess. If I were reviewing the above, I would have a hard time understanding if the nesting is correct, and to the untrained eye, this just looks wrong. I wouldn’t fault anyone coming up with a PR to “realign” the lines above so that they “look” correct in their textual form.

My suggestion to fix the above is to always indent list item content to multiples of 4 columns, like this:

*   This is the first bullet point.

    This is a continuation paragraph, and it works.

*   This is the second bullet point.

        With a nested code block.

*   This is the third bullet point.

    > With a nested blockquote.

Which works equally well for ordered lists and lists with multiple levels:

*   First bullet point.

    1.  Nested ordered list.

    1.  With multiple items.

            And a code block.

*   Second bullet point.

    > And a blockquote.

Note how there are 3 spaces after the * prefixes and 2 spaces after the 1. prefixes. I’ll agree that this doesn’t look super neat in textual form, but it is consistent, easy to understand, and works all the time.

markdownlint configuration for VSCode

I strongly recommend everyone that authors Markdown content:

  1. to use Visual Studio Code;
  2. to install the markdownlint extension, and
  3. to address all squiggly lines that indicate Markdown formatting mistakes.

If you use these tools, you can apply the following configuration entries to enforce the style suggestions given in this post:

"markdownlint.config": {
    "MD007": { // Unordered list indentation.
        "indent": 4
    },
    "MD029": { // Ordered list item prefix.
        "style": "one"
    },
    "MD030": { // Spaces after list markers.
        "ol_single": 2,
        "ol_multi": 2,
        "ul_single": 3,
        "ul_multi": 3
    }
}

I have yet to set up PR validation automation to run markdownlint directly on the content and fail PR merges on mistakes… but this is an idea I’ve just had as I was writing this 😉