Header counter

Now that we have this fabulous outline feature, which will allow me transfer a substantial part of my notes to Obsidian, I would like to have the headers and subheaders numbered, like

  1. header
    1.1. subheader
    1.1.1. subsubheader
  2. header
    etc.

For Typora someone once pointed me to the css as per the attachment here. It does not work in Obsidian. Can anyone tell me what to change in there?
Header-counter.css (7.7 KB)

5 Likes

I think its possible to get this working in Obsidian, but Obsidian’s interface has nothing in common with Typora, so complex CSS for one is astronomically unlikely to work for the other. A CSS developer would start over from scratch, rather than try to adapt this CSS.

Happily, I’m a CSS developer. I’ll take a crack at it tomorrow.

1 Like

Hey Daniel, thanks a lot for your encouraging reply.

I would like to ask for your help to have a crack at a completely new code set for header numbering in both the content of a note and in the outline.

Hey @Klaas, Here is CSS which will get things working in the outline pane:

/* List styling for outline pane */

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline {
    counter-reset: h1;
}

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.pane-clickable-item {
    counter-reset: h2;
}

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.outline-heading-children > div > div.pane-clickable-item {
    counter-reset: h3;
}


body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.outline-heading-children > div > div.outline-heading-children > div > div.pane-clickable-item {
    counter-reset: h4;
}

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.outline-heading-children > div > div.outline-heading-children > div > div.outline-heading-children > div > div.pane-clickable-item {
    counter-reset: h5;
}

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.outline-heading-children > div > div.outline-heading-children > div > div.outline-heading-children > div > div.outline-heading-children > div > div.pane-clickable-item {
    counter-reset: h6;
}

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.pane-clickable-item:before {
    counter-increment: h1;
    content: counter(h1) ". ";
}

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.outline-heading-children > div > div.pane-clickable-item:before {
    counter-increment: h2;
    content: counter(h1) "." counter(h2) ". ";
}

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.outline-heading-children > div > div.outline-heading-children > div > div.pane-clickable-item:before {
    counter-increment: h3;
    content: counter(h1) "." counter(h2) "." counter(h3) ". ";
}

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.outline-heading-children > div > div.outline-heading-children > div > div.outline-heading-children > div > div.pane-clickable-item:before {
    counter-increment: h4;
    content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". ";
}

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.outline-heading-children > div > div.outline-heading-children > div > div.outline-heading-children > div > div.outline-heading-children > div > div.pane-clickable-item:before {
    counter-increment: h5;
    content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". ";
}

body > div > div.horizontal-main-container > div > div.workspace-split.mod-horizontal.mod-right-split > div.workspace-tabs > div.workspace-leaf > div > div.view-content > div.outline > div > div.outline-heading-children > div > div.outline-heading-children > div > div.outline-heading-children > div > div.outline-heading-children > div > div.outline-heading-children > div > div.pane-clickable-item:before {
    counter-increment: h6;
    content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". ";
}

This should work for any Obsidian theme of your choice.

Getting things working in a preview pane is proving harder, though. I think it might be impossible with the way Obsidian’s interface’s markup is set up. But if the developers can get header level information to be placed on the div.markdown-preview-section elements in div.markdown-preview-views, it should be no problem from there.

1 Like

Thanks a lot for that, Daniel. It works like a dream. I have asked Licat if it is possible to get header level information to be placed on the div.markdown-preview-section elements in div.markdown-preview-view.

1 Like

Daniel, I just received a reply from @Silver about this, and she replied:

Unfortunately to keep the output HTML clean, I don’t think we’ll add classes to them

I have asked her to explain, since in Typora it is possible to have css code for the header count in the note AND support HTML.

She replied:

Looking at it some more, it looks like it’s not going to work in Obsidian preview, because of the headings not being on the same level. In Typora the headings ( h1 , h2 , etc.) are on the same level. I think it’s due to how CSS counter works, but I hope I’m wrong and someone can come up with a solution for you.

The .markdown-preview-section wrapper for each paragraph is for optimization and scroll sync purposes, so unfortunately we can’t remove those. Daniel suggests that we put heading info on that element, so instead of h1 we have .markdown-preview-section.heading1 which put all the headings on the same level, making the solution work.

Not sure we want to add that info, as I don’t see any other uses, but let’s see if people ask for it after 0.8.1.

She’s absolutely right. Adding header info the .markdown-preview-sections would actually mean that the information is there twice (once on the section, and once by the fact that the header is in the section), and that is generally a big no-no when you’re developing high-quality software.

@Silver, I can think of three possible alternatives, though the first one is made without much knowledge of how and why things work the way they do.

First alternative is moving the .markdown-preview-section class onto the contents of the current elements, so that you would have, e.g., an h1.markdown-preview-section. This only works if each .markdown-preview-section contains one element. I looked at a bunch, and that seemed to be the case, but maybe it’s not always guaranteed?

Second alternative is a plugin, which the community will likely make just as soon as the appropriate API is publicly available.

Third alternative is to make it possible to add JS in a similar fashion to how obsidian.css lets us add CSS. Though I think this is a horrible idea for security (people sharing dodgy JS) and stability (JS breaking whenever Obsidian is updated).

Thanks Daniel.

As a non-techie it seems that a plug-in is the best alternative: the devs won’t have to change anything, security is not a problem, and implementation of a header counter is a user’s choice.

The CSS part, in any case has been pushed as far as possible with your code. Many thanks for your help with that.

1 Like

Here is a piece of css code that works for preview pane.
I am not a css developer and am currently learning it. The code below is mostly from https://stackoverflow.com/questions/30925063/numbering-with-css-counter-for-headings and I modified a little to make it work in obsidian. Thanks a lot! I love the outline numbering.


However, It only works properly for the embedded block of md files. For non-embedded part, there’re some unknown issues that it fails to reset the counter…
Another issue is embedded contents are counted together with outer ones. How could that be?
Can anyone help? thanks

/*  heading counter  */
body {counter-reset: h1 h2 h3 h4 h5 h6}
h1 {counter-reset: h2}
h2 {counter-reset: h3}
h3 {counter-reset: h4}
h4 {counter-reset: h5}
h5 {counter-reset: h6}
h1:before {counter-increment: h1; content: counter(h1) ". "}
h2:before {counter-increment: h2; content: counter(h1) "." counter(h2) ". "}
h3:before {counter-increment: h3; content: counter(h1) "." counter(h2) "." counter(h3) ". "}
h4:before {counter-increment: h4; content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". "}
h5:before {counter-increment: h5; content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". "}
h6:before {counter-increment: h6; content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". " }
h1.nocount:before, h2.nocount:before, h3.nocount:before, h4.nocount:before, h5.nocount:before, h6.nocount:before {content: ""; counter-increment: none} 
1 Like

This is the same what @tobyp provided me with here and here.

We will have to accept @DanielFlaum’s assessment that at present it is not possible to have a properly working header counter in CSS for Preview.

@tobyp provided a halfway solution: a count for the H1 headers only, and that works.
Here is the code:

/** initialize css counter */
body {
    counter-reset: head-num-1;
}

/* put counter result into H1 headings */
.markdown-source-view .cm-formatting-header-1::after , h1::before {
  counter-increment: head-num-1;
  content: counter(head-num-1) ". ";
}

Hi, this one, even though it only count the h1 headings, has the same issue of mine: embedded md files’ heading will be also counted along with the ones in the outer body. And yes I guess we have to accept the assessment. I’ve also tried many other css code pieces that doesn’t work at all.
I’m really unfamiliar with css or any other front-end technology. Just very curious about the mechanism behind this behavior, what causes it? Is it a bug or a feature? If bug, how could it be and will it be fixed later?

It’s not a bug. If you read @DanielFlaum’s 2 last comments you’ll understand why.

Thank you Klaas for always being patient to me. I read them but because of my knowledge background it is kinda hard to understand and doesn’t make sense to me. Why could that structure design provide significantly better performance? Silver didn’t explain how does the wrapper help… and according to Daniel’s reply does it mean to save some duplicates? But we only render the currently opened files and I personally suppose that each single file will not be that huge to make the memory saving technique necessary. Sorry in advance if my understanding is wrong, my bad. I really should have learnt more before I talk but I just don’t have the time :frowning: Sorry again if my stupid questions and opinions bothered you, and thanks for any explanation helping me understand it.

Heya @left! I’ll try to illuminate as best I can. This is just to satiate curiosity (and writing it all out is a good way for me verify I’ve checked everything I can think of). There’s no pressure to get all this.

First a big picture:

CSS is one of a trio of technologies that combine to make up Web pages. The other two are HTML and JavaScript. These Web technologies apply here because Obsidian is built on https://www.electronjs.org/, and so its user interface is basically a Web page running in a stripped down Web browser.

The three technologies work together to enforce a separation of concerns:

  • When you want to deal with what is on a Web page, then you work with HTML.
  • When you want to deal with what a Web page looks like, then you work with CSS.
  • When you want to deal with what a Web page does, then you work with JavaScript.

(We’re not interested in JavaScript, but I mention it to ensure that the big picture is complete.)


HTML is used to describe a https://en.wikipedia.org/wiki/Tree_structure that makes up what is on a Web page. Here’s a sample I banged together (I put it in an image so we can have pretty colors that make things easier to look at):

That HTML corresponds to this tree:

We call the things in the tree “elements”, and we can speak of each elements’ parent, children, and siblings, since the whole thing is close enough to a family tree style of thing.

Once we have a tree like this, then we can use CSS to specify what things should look like. Here’s a sample:

image

This CSS specifies three rules. Each rule has two parts; a selector, and a list of style attributes.

The selector tells the Web browser which things the rule applies to. For example, the selector nav ul li tells the Web browser to style all the li list elements that are in ul unordered lists, that are in turn in nav navigation sections.

So this CSS would cause the Web browser to render the paragraphs of text in red, the navigation links in green, and the article header in blue. The navigation links’ text would be 1.5 times as big as the text in the paragraphs, and the article header’s text would be twice as big.


A Web developer almost always adds extra information to elements in an HTML tree to give CSS the ability to ask for more specific things. One of these pieces of information is the class. A class is a name the developer specifies, and that name can then be applied to as many elements in the HTML tree as the developer likes. And a single element can also have as many different classes as the developer likes, too.

So a developer could write a selector like p.lead-paragraph to style all the <p class="lead-paragraph">s with slightly larger text. Then a Web page could have as many lead paragraphs as it needs, and each one would be made slightly larger.


So now we have a big picture. Obsidian’s interface is a giant tree. Most things in the tree have various classes to help describe what they are, and the CSS in Obsidian’s themes can specify which elements with which classes in which parts of the tree should look like what.

This is why CSS for other things–other apps or other Web pages–rarely work when you put them in your theme. CSS from elsewhere uses selectors that assume a different tree structure and different classes than the ones Obsidian uses.


Second, why we can’t get numbering to work:

CSS provides a handful of style attributes to set up numbering. One of them–counter-reset–is used to start counting. Another, counter-increment, is used to actually count something.

Both of these attributes work when you specify which counter they apply to. You can have as many counters as you like, and each one has its own name that you choose. Then, in other CSS, you can specify where to display the value of the counter by using its name.

The tricky part is this: We, the writers of the CSS, can’t know in advance how many counters we will need. One Web page might have five separate lists that each need their own counter. And a list could have sub lists, which will each need their own counter, and they could have sub lists too. And a different Web page on our Web site may have a different number of lists.

So in order for all this to work, we have to rely on CSS to handle setting up actual counters for us. All we do is specify patterns in the HTML tree that match when to make a counter.

There are basically two kinds of patterns we can specify: flat ones and nested ones. Which one we use depends on the tree structure we have to work with.

The flat structure is the simplest one, where all the headers are just children of the same parent element, one after another. In this case, we just make six counters (one for each level of header), and the counters are reset each time we go past a header of the next highest level.

The outline pane, though, uses a nested tree structure. Each header has its child headers contained within it.

If you look in the CSS we’re using for the outline, you’ll find counters for each of the six levels of header you can use in your notes, named from h1 to h6. That CSS sets up a counter for the h1s (the biggest headers), and then sets up one counter for each h1 to count the h2s that are inside it. And then, for each of those h2s, it sets up one counter to count the h3s that are inside. And so on down to h6.

This works because CSS is relying on the tree structure of the outline. Each h1 contains h2s, each of which contains h3s. So the CSS works by creating a new counter each time it goes down a level in the tree.

The problem with the preview is that it doesn’t use either of these two structures. It’s a little bit of both:

Screenshot-20200728163821-654x302

The preview is made up of a series of div.markdown-preview-section elements, each one of which contains the interesting stuff (e.g. a header, or a paragraph). (div is short for “division”–it’s a general purpose element).

We can’t use a flat approach in the CSS because the only things we can count are the markdown-preview-sections. Nothing distinguishes one from any other.

And we can’t use a nested approach, because the headers aren’t children of each other. Whenever we write CSS that descends into each .markdown-preview-section, that CSS sets up counters that only work inside that one single .markdown-preview-section. That’s what we want in a nested tree structure, but it doesn’t work here.


This is frustrating, of course, because we as humans can easily see how to count things ourselves. Our problem is that CSS, as a language, doesn’t offer the vocabulary we need to express how to do that.

In order to get counting to work properly, either the preview needs to fully adhere to one structure or the other, or we need to use a tool more sophisticated than CSS (namely, JavaScript).

My first suggestion was to add information to each .markdown-preview-section saying what kind of header it was. That info would be added to each div's class attribute like you see in the screenshot. The CSS could pick up on that, and so we would have a flat structure.

But that’s actually not a good solution, because then information is duplicated, and duplicated information is a good source of bugs. If each .markdown-preview-section says what kind of header it contains, then if then if the header changes from, say, an h1 to an h2, the developers have to make sure that not only does the header itself get changed, but so does the information on the containing markdown-preview-section. This is a great place for someone to make a mistake.


So the thing to do is hold out for the plugin API (application programming interface–its just like a user interface, except that programs use it instead of users). Then hopefully that API will make it possible to write a plugin which does the numbering.


Don’t worry if this doesn’t all make sense. I’m not very good at explanation, and the thing I just tried to explain covers a lot of ground at both a high and low level. But if you picked up just a few separate little things from it, then it’s still a win!


As for what @Silver says about optimization and scroll syncing, I simply can’t comment. Those are topics where only a working knowledge of Obsidian’s code and design can help, and I don’t have that.

4 Likes

This is a great description. I think I mentioned in Discord that it isn’t just that the headings are all siblings without hierarchy, it is that they are all children of identical siblings without hierarchy.

I am sure there is a good reason for this, and like you say, nothing that a plug-in could not attempt to fix.

1 Like

Hi Daniel, Thank you!!! I can’t believe my eyes that you write down such a great and clear tutorial for me (and for organizing your thoughts). It must take your time to do this. Thanks again! It’s so amazing and you are really good at explaining! It helps a lot. I will read it again to make sure I learn more from your work.

1 Like

Hi Daniel, what a fantastic explanation, clear and in simple, layman’s terms. Very impressive. As a layman I do not fully understand it all, but the picture IS getting a bit clearer, esp. because of the images you provide with it.

I am going to bookmark this page and will come back to it many time.
Thanks again for all your help and patience. Who knows, if the Obsidian API becomes reality perhaps you can write that plug-in.

@klaas, I was wondering how the outline was treating you. Thought you’d be happy. :slight_smile: Smart people on this thread.

I presume you mean the header counter in the outline pane: correct?
If so I can state unequivocally that @DanielFlaum’s code is perfect, and, having tried it on 3 or 4 themes, his statement that it will work on all themes has a probability of truth that borders on certainty :wink:

Seriously, his code works well, and as expected. When API becomes reality I hope he will write the plug-in for both outline and note content.

Daniel, I am sorry to bug you again. I just noticed an oddity - see screenshot.
Heading 5.9.5 is correct: it relates to an H3 header.

But both headers 5.9.5.001 are wrong because there is an H4 header (####) in each place.

odd-number