skip to Main Content

I made a JavaScript plugin that made headings into documentation style (also known as hierarchical numbered outlines or multilevel numbered outlines).

You often see these styles in legal documents, technical manuals. For example:

1. Main title
1.1. Sub-title
1.1.1. Sub-sub-title

I’ve now found some time to refactor and actually make the plugin work and robust. But I’m really having issue with injecting and incrementing the numbers.

This is the current code I have:

const scopeHeadingElements = scope.querySelectorAll(levelsRange);
const currentNumbers = {
  h1: headingNumbers.h1,
  h2: headingNumbers.h2,
  h3: headingNumbers.h3,
  h4: headingNumbers.h4,
  h5: headingNumbers.h5,
  h6: headingNumbers.h6
};
scopeHeadingElements.forEach((heading) => {
  const tagName = heading.tagName;
  const headingLevel = parseInt(tagName.substring(1));
  let numberText = '';
  currentNumbers['h' + (headingLevel + 1)]++;
  for (var levelNumber = 1; levelNumber <= 6; levelNumber++) {
    if(levelNumber <= headingLevel) {
      numberText += currentNumbers['h' + levelNumber] + options.separator;
    } else {
      continue;
    }
  }
  heading.innerHTML = `${numberText} ${heading.innerHTML}`;
});

The scopeHeadingElements returns any of the H1-H6 elements in the DOM depending on what the levelRange is. For example, it could be all H1 to H6 elements, or it could be scoped to H3-H4 elements.

The headingNumbers is the starting value of the heading documentation. For example, the page might start with: {h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1}

But if we are splitting documentation over multiple pages, we might want to start page two at: {h1: 12, h2: 1, h3: 5, h4: 8, h5: 1, h6: 10}

What I need this code to do in the loop over the scopeHeadingElements is to be able to prepend the documentation numbering to the innerHTML.


For example, if we have the starting headingNumbers: {h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1} then the following HTML scope should output the following:

<h1>Heading 1</h1> ----> <h1>1. Heading 1</h1>
<h2>Heading 2</h2> ----> <h2>1.1. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>1.1.1. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>1.1.1.1. Heading 4</h4>
<h5>Heading 5</h5> ----> <h5>1.1.1.1.1. Heading 5</h5>
<h6>Heading 6</h6> ----> <h6>1.1.1.1.1.1. Heading 6</h6>

However if the starting headingNumbers were different: {h1: 12, h2: 1, h3: 5, h4: 8, h5: 1, h6: 10} then the following HTML scope should output the following:

<h1>Heading 1</h1> ----> <h1>12. Heading 1</h1>
<h2>Heading 2</h2> ----> <h2>12.1. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>12.1.5. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>12.1.5.8. Heading 4</h4>
<h5>Heading 5</h5> ----> <h5>12.1.5.8.1. Heading 5</h5>
<h6>Heading 6</h6> ----> <h6>12.1.5.8.1.10. Heading 6</h6>

What is most important in all this though, is once the currently processed tag is higher than the existing one then we need to reset that value to 1.

So again the example of {h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1} then the following HTML scope should output the following:

<h1>Heading 1</h1> ----> <h1>1. Heading 1</h1>
<h2>Heading 2</h2> ----> <h2>1.1. Heading 2</h2>
<h2>Heading 2</h2> ----> <h2>1.2. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>1.2.1. Heading 3</h3>
<h3>Heading 3</h3> ----> <h3>1.2.2. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>1.2.2.1. Heading 4</h4>
<h2>Heading 2</h2> ----> <h2>1.3. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>1.3.1. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>1.3.1.1. Heading 4</h4>
<h5>Heading 5</h5> ----> <h5>1.3.1.1.1. Heading 5</h5>
<h6>Heading 6</h6> ----> <h6>1.3.1.1.1.1. Heading 6</h6>
<h6>Heading 6</h6> ----> <h6>1.3.1.1.1.2. Heading 6</h6>

In my above JS code, I just cannot seem to figure out how to get the looping, and incrementing working without it incrementing the wrong heading tag (some attempts increment the H1 value as well as the H2, or the H1, H2, and H3 value when incremeting the H4).

Or other attempts increment too early, and everything starts 1 off.

I tried to add a - 1 to the currentNumbers however if the levelsRange in const scopeHeadingElements = scope.querySelectorAll(levelsRange); is only matching on say, H2 to H4, then the other numbers go negative or to 0.

Essentially, all headings should be given the documentation numbering system, but if they arent included in the "scope" then the HTML isnt affected.

For example, if we had the example again of {h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1}, but for this the scope was limited to H2 to H3 elements:

<h1>Heading 1</h1> ----> <h1>Heading 1</h1>
<h2>Heading 2</h2> ----> <h2>1.1. Heading 2</h2>
<h2>Heading 2</h2> ----> <h2>1.2. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>1.2.1. Heading 3</h3>
<h3>Heading 3</h3> ----> <h3>1.2.2. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>Heading 4</h4>
<h2>Heading 2</h2> ----> <h2>1.3. Heading 2</h2>
<h3>Heading 3</h3> ----> <h3>1.3.1. Heading 3</h3>
<h4>Heading 4</h4> ----> <h4>Heading 4</h4>
<h5>Heading 5</h5> ----> <h5>Heading 5</h5>
<h6>Heading 6</h6> ----> <h6>Heading 6</h6>
<h6>Heading 6</h6> ----> <h6>Heading 6</h6>

2

Answers


  1. I think that’s easier to implement the looping with a stack, rather than with a flat object:

    const level = 6;
    const headingNumbers = {h1: 12, h2: 1, h3: 5, h4: 8, h5: 1, h6: 10};
    
    const headers = [...document.querySelectorAll(Array.from({length: level}, (_, i) => 'h' + (i + 1)).join(','))];
    
    let prevLevel = 0;
    const stack = [];
    for(const header of headers){
      const level = +header.tagName.match(/d+/)[0];
      if(level > prevLevel) {
        const tag = header.tagName.toLowerCase();
        stack.push(headingNumbers[tag] ?? 1);
        delete headingNumbers[tag];
      } else {
        stack.splice(level, stack.length);
        stack[stack.length - 1]++;
      }
      header.insertAdjacentHTML('afterbegin', `<span>${stack.join('.')}</span>&nbsp;`);
      prevLevel = level;
    }
    <h1>Heading 1</h1>
    <h2>Heading 2</h2>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h5>Heading 5</h5>
    <h6>Heading 6</h6>
    <h6>Heading 6</h6>
    <h1>Heading 1</h1>
    <h2>Heading 2</h2>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h5>Heading 5</h5>
    <h6>Heading 6</h6>
    <h6>Heading 6</h6>
    Login or Signup to reply.
  2. You actually don’t need JS for this. This can be achieved just with CSS if you are fine with the browser support (which is 88.75% – for counter-set – at the time of writing: caniuse.com), by using counter-reset, counter-set, counter-increment, :before and content.

    body {
       counter-reset: h1;
     }
    
     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>Heading 1</h1>
    <h2>Heading 2</h2>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h5>Heading 5</h5>
    <h6>Heading 6</h6>
    <h6>Heading 6</h6>

    To start at a different number you would use counter-set:

    body {
       counter-set: h1 5;
     }
    
     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>Heading 1</h1>
    <h2>Heading 2</h2>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h5>Heading 5</h5>
    <h6>Heading 6</h6>
    <h6>Heading 6</h6>

    And whether or not to prefix your headings with the numberings can be controlled using content, which could be done using a distinct class (how depends on the use-case):

    body {
       counter-set: h1 5;
     }
    
     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: "";
     }
    
     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: "";
     }
    
     h5:before {
       counter-increment: h5;
       content:  "";
     }
    
     h6:before {
       counter-increment: h6;
       content:  "";
     }
    <h1>Heading 1</h1>
    <h2>Heading 2</h2>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h2>Heading 2</h2>
    <h3>Heading 3</h3>
    <h4>Heading 4</h4>
    <h5>Heading 5</h5>
    <h6>Heading 6</h6>
    <h6>Heading 6</h6>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search