Markdown: improving the table of contents

The following refers to the DEVONthink Markdown renderer! Make sure to select it in the global DT preferences if you want to play around with the TOC as described here.

Edit: I modified the CSS and JS code so that they work with both MD renderers now.

You can add a table of contents to your rendered Markdown document with the {{TOC}} instruction. This will produce a bare-bones list of links to all the headings in your document. {{TOC}}does not take any options, so you will get all headings from # down to ###### in the table of contents (TOC). As it stands, the TOC is not only very detailed, it’s also placed at the top of the content where it scrolls out of sight when you scroll the content. The following sections will explore several ways to modify the default TOC.

Keep the TOC fixed when the document scrolls

If you want to keep the TOC always in sight, even when your document scrolls, here’s some CSS to achieve that.

body {
  margin-left: 2rem;
  width: 40rem;
}
.table-of-contents, .TOC {
  left: 50rem;
  position: fixed;
}

These lines limit the width of the document (body) to 40rem, which stands for 40 times the size of the body's font. If your body element’s font size is 16px, the document will be 640px wide. Alternatively, you could use a percentage value like 80% for the body’s width property.

Now, the .table-of-contents rule address the TOC (which is identified by the class name table-of-contents). It pushes the TOC 50rem to the right and sets its position to fixed. The document itself will always be to the left of the TOC because it is only 40rem wide and the TOC leaves a 50rem wide empty space to its left.

When you scroll an MD document with a TOC and the CSS as shown above, the TOC will stay at the right upper corner so that it is always visible.

Add a heading to the TOC

While the list of links in the TOC might indicate their purpose, a heading would perhaps be nice, too. Again, CSS can easily help with that:

.table-of-contents::before, .TOC::before {
    content: "Table of content";
    font-size: 1.5rem;
    font-weight: bold;
    display: inline-block;
  }

Here, ::before creates a pseudo-element right before the TOC itself. It can be styled as any other element (here with a bold font that is 1.5rem tall). More importantly, you can add a text to this pseudo-element with the content property. The display is set to inline-block to ensure a line break after the heading.

Exclude headings from the TOC

Edit: While the following works, it is not needed at all. @MsLogica shows a far simpler solution below.
To limit the headings that end up in the TOC, you can again use CSS. Let’s first take a look at the rendered TOC. If you inspect it in a browser’s developer tools, you will see something like this:

<ul>
  <li>First level heading</li>
  <ul>
    <li>Second level heading</li>
    <ul>
      <li>Third level heading</li>
      ...
    </ul>
  </ul>
</ul>

If you wanted to exclude all headings including and below the third level, you’d have to do something like

.table-of-contents ul ul ul, .TOC ul ul` {
  display: none;
}

in your CSS.

Note: This is a crude way to handle it. The MMD6 and the DT renderer differ subtly in what they do with missing heading levels: DT inserts and empty ul element, MMD6 does not. Therefore, the selectors in the above CSS look slightly different for both cases. A more robust solution would have to be written in JavaScript and check which heading levels are present in the document before setting the display property of the unwanted ones to none. Or remove them altogether from the TOC.

IMO, the {{TOC}} should accept parameters to select the maximum heading level appearing in the TOC. It does, as @MsLogica pointed out below.

Highlight the current section in the TOC

If you have a long document, it would be nice if the TOC indicates the section you are currently seeing even if the corresponding heading is not visible currently. However, this is not possible with CSS alone. Some JavaScript is needed, too:

document.addEventListener('DOMContentLoaded', e => {
  const highlightClass = 'highlight-entry'
  const toc = document.querySelector('.table-of-contents, .TOC');
  if (toc) {
    const callback = (entries, observer) => {
      entries.forEach((entry) => {
        const id = entry.target.id;
        const tocLink = toc.querySelector(`a[href="#${id}"]`); 
        if (entry.isIntersecting) {
          const lastHeading = toc.querySelector(`.${highlightClass`);
          lastHeading?.classList.remove(highlightClass);
          tocLink.classList.add(highlightClass);
        }
      });
    };

    // Configure observer options
    const options = {
      root: null, // Use the viewport as the root
      rootMargin: '0px', // No margin around the root
      threshold: 0.1, // Trigger when 10% of the target is visible
    };
    const observer = new IntersectionObserver(callback, options);

    // Start observing the target
    document.querySelectorAll('h1, h2').forEach((heading) => {
      observer.observe(heading);
    });
  }
})

I’m not going to explain all of the code here. Its main purpose is to install an IntersectionObserver for all the headings in the document that are visible in the TOC. This happens at the very end with

 document.querySelectorAll('h1, h2').forEach((heading) => {
      observer.observe(heading);
    });

Here, we are only interested in first and second level headings (h1, h2). Note that you should not include headings in this list that you have removed from the TOC via CSS.
The callback handling IntersectionObserver events is quite straightforward: If an observed heading scrolls into view

  • a previously highlighted TOC entry is un-highlighted, and
  • the current TOC entry is highlighted

To highlight an entry, we add a class to it. Defining the attributes of this class in the CSS like so

.highlight-entry {
  font-width: bold;
}

would make the heading of the currently visible section bold in the TOC.

11 Likes

That seems to be a nice solution. I found the place in the setting where to insert the code, and it works in DTTG. But: I find it much more useful to have an outline in the Edit view than in the rendered view. Is there also a possibility to get a TOC which allows to navigate within a Markdown file when editing it?

There are several possibilites, all of them are already described elsewhere, notably the manual and here.

It should. However, the CSS is not really approriate for small devices.

It works well on an iPad if I change the rem values.

If you want it to work on differently sized devices, I’d go for percent widths. Or have a @media query in the CSS that sets different rem widths depending on device width.

It’s not a very good idea to completely change your post after someone has replied to it. That makes their reply incomprehensible.

That depends on your editor, I’d say. It might be possible in Emacs (where you could write your own function to do that if none exists), BBEdit or VS Code. “Outline” might be a term to look for.

Not in the built-in editor in DT, afaict

1 Like

OK, sorry for my inattentive posting behaviour. But – of course I am only interested to get an outline within DTTG Markdown texts. I want all Markdown features in one place and not being forced to use Obsidian or Ulysses for longer texts.

And now you’re posting as another user?

Anyway, I was not posting about how to do something in DTTG that you can’t do. Instead, I suggested a way to change the appearance of a TOC in the rendered (!) markup. My apologies, if that was not clear from the beginning.

In any case, what is not working with the “Table of content” inspector? No idea if that is available in DTTG, but it certainly is in DT. And it already does what you want.

The issue of “I want to do everything in DT(TG) and never open another app” has already been discussed ad nauseam here.

1 Like

Haha, please calm down a bit. I do not want to do everything in DTTG – I only want to do anything what is reasonable in the Markdown editor of DTTG.

This is really cool, thanks for sharing this! It wouldn’t have even occurred to me that this would be nice to have, but having read through your post I thought “well of course I want that on giant documents” :joy:

You can decide which headings you want including in a TOC by writing the numbers you want included, like so:

{{TOC:1-3}}

(Only include headings 1-3)

Or

{{TOC:1}}

(Only include heading 1)

I’ve also discovered that you can put multiples TOCs in the same document, which means that for a large document where you’ve got a subsection that it would be desirable to make clearer, you can set the subheadings at a lower value than you might normally use (e.g. 4) and make a TOC just for them in the right place. I use this mainly if I want a list to appear under a heading so I can see it in full at a glance, and then each list item is clickable and takes you to the section with the extra description.

(The tip for only including certain headings in a TOC is from Jim and I’m just a gleeful thief.)

3 Likes

Thanks for pointing that out. I had a vague idea but couldn’t find anything in the MMD manual.

And edited my OP accordingly.

@chrillek Thanks for sharing the CSS for the fixed TOC. I really needed this to better see and manage long documents. Placed it to use right away.

@MsLogica Thanks for the tip on writing the numbers to control visible levels. A small tip with big effect.