user3787031
user3787031

Reputation: 177

Numbering h1,h2 headings, nested documents that reset counter

I'm looking to add numbers to headers like below. The problem is that there are documents that may be infinitely nested and need to be counted independently. In the original document, the headings are text only, with no numbered prefix.

<div id="text_output">
  <h1>Document 1</h1>
    <h2>1 Heading</h2>
      <h3>1.1 Subheading</h3>
      <h3>1.2 Subheading</h3>
        <div class="text_output_expansion">
          <h1>Document 2</h1>
            <h2>1 Heading</h2>
              <h3>1.1 Subheading</h3>
            <h2>2 Heading</h2>
              <h3>2.1 Subheading</h3>
        </div>
      <h3>1.3 Subheading</h3>
    <h2>2 Heading</h2>
      <h3>2.1 Subheading</h3>
      <h3>2.2 Subheading</h3>
        <div class="text_output_expansion">
          <h1>Document 3</h1>
            <h2>1 Heading</h2>
              <h3>1.1 Subheading</h3>
            <h2>2 Heading</h2>
              <h3>2.1 Subheading</h3>
                <div class="text_output_expansion">
                  <h1>Document 4</h1>
                    <h2>1 Heading</h2>
                      <h3>1.1 Subheading</h3>
                    <h2>2 Heading</h2>
                      <h3>2.1 Subheading</h3>
                </div>
              <h3>2.2 Subheading</h3>
        </div>
      <h3>2.3 Subheading</h3>
    <h2>3 Heading</h2>
      <h3>3.1 Subheading</h3>
      <h3>3.2 Subheading</h3>
</div>

I'll post a CSS solution, but I'm just struggling to get this working in javascript instead. The idea is so that I can copy and paste the document with the numbers intact (CSS solution won't allow that). Admittedly my javascript isn't great, but my struggle seems to be how js loops through the elements from top to bottom.

Upvotes: 2

Views: 174

Answers (3)

user3787031
user3787031

Reputation: 177

Did another recursive option. I think Ajax has the best answer tho!

function numberHeadings(element) {
var h1_counter = 0;
var h2_counter = 0;
var h3_counter = 0;
var h4_counter = 0;
var h5_counter = 0;
var h6_counter = 0;
element.find(".text_output_expansion").each(function () {
  numberHeadings($(this));
});

element.find("h1:not([numberHeadings=complete]),h2:not([numberHeadings=complete]),h3:not([numberHeadings=complete]),h4:not([numberHeadings=complete]),h5:not([numberHeadings=complete]),h6:not([numberHeadings=complete])").each(function () {
  switch ($(this).prop("tagName")) {
    case "H1":
      h1_counter += 1;
      h2_counter = 0;
      h3_counter = 0;
      h4_counter = 0;
      h5_counter = 0;
      h6_counter = 0;
      $(this).text(h1_counter.toString() + ". " + $(this).text());
      $(this).attr("numberHeadings", "complete");
      break;
    case "H2":
      h2_counter += 1;
      h3_counter = 0;
      h4_counter = 0;
      h5_counter = 0;
      h6_counter = 0;
      $(this).text(h1_counter.toString() + "." + h2_counter.toString() + ". " + $(this).text());
      $(this).attr("numberHeadings", "complete");
      break;
    case "H3":
      h3_counter += 1;
      h4_counter = 0;
      h5_counter = 0;
      h6_counter = 0;
      $(this).text(h1_counter.toString() + "." + h2_counter.toString() + "." + h3_counter.toString() + ". " + $(this).text());
      $(this).attr("numberHeadings", "complete");
      break;
    case "H4":
      h4_counter += 1;
      h5_counter = 0;
      h6_counter = 0;
      $(this).text(h1_counter.toString() + "." + h2_counter.toString() + "." + h3_counter.toString() + "." + h4_counter.toString() + ". " + $(this).text());
      $(this).attr("numberHeadings", "complete");
      break;
    case "H5":
      h5_counter += 1;
      h6_counter = 0;
      $(this).text(h1_counter.toString() + "." + h2_counter.toString() + "." + h3_counter.toString() + "." + h4_counter.toString() + "." + h5_counter.toString() + ". " + $(this).text());
      $(this).attr("numberHeadings", "complete");
      break;
    case "H6":
      h6_counter += 1;
      $(this).text(h1_counter.toString() + "." + h2_counter.toString() + "." + h3_counter.toString() + "." + h4_counter.toString() + "." + h5_counter.toString() + "." + h6_counter.toString() + ". " + $(this).text());
      $(this).attr("numberHeadings", "complete");
      break;
  }
})

};

Upvotes: 0

Ajax1234
Ajax1234

Reputation: 71451

You can recursively traverse the structure by grouping on h tag blocks:

var div = document.createElement("div");
div.innerHTML = `<div id="text_output">
  <h1>Document</h1>
    <h2>Heading</h2>
      <h3>Subheading</h3>
      <h3>Subheading</h3>
        <div class="text_output_expansion">
          <h1>Document</h1>
            <h2>Heading</h2>
              <h3>Subheading</h3>
            <h2>Heading</h2>
              <h3>Subheading</h3>
        </div>
      <h3>Subheading</h3>
    <h2>Heading</h2>
      <h3>Subheading</h3>
      <h3>Subheading</h3>
        <div class="text_output_expansion">
          <h1>Document</h1>
            <h2>Heading</h2>
              <h3>Subheading</h3>
            <h2>Heading</h2>
              <h3>Subheading</h3>
                <div class="text_output_expansion">
                  <h1>Document</h1>
                    <h2>Heading</h2>
                      <h3>Subheading</h3>
                    <h2>Heading</h2>
                      <h3>Subheading</h3>
                </div>
              <h3>Subheading</h3>
        </div>
      <h3>Subheading</h3>
    <h2>Heading</h2>
      <h3>Subheading</h3>
      <h3>Subheading</h3>
</div>`
document.querySelector('body').appendChild(div)

function number_headings(root){
    var h1 = 1;
    function label_headings(nodes, p){
       var [groups, group, h] = [[], [], null]
       for (var i of nodes){
          if (i.tagName[0].toLowerCase() != 'h' || (h != null && i.tagName != h.tagName)){
             group.push(i)
          }
          else if (h === null){
             h = i;
          }
          else{
             groups.push({node:h, block:group.slice()});
             group = [];
             h = i;
          }
       }
       if (h != null){
          groups.push({node:h, block:group.slice()});
       }
       var c = 1;
       for ({node:n, block:b} of groups){
           if (n.tagName === 'H1'){
              n.textContent = `${n.textContent} ${h1}`;
              h1++;
           }
           else{
              n.textContent = `${(p === null ? c.toString() : p+'.'+c.toString())} ${n.textContent}`;
           }
           label_headings(b, n.tagName === 'H1' ? null : (p === null ? '':p+'.')+c.toString());
           for (var k of b.filter(function(x){return x.nodeType === 1 && x.tagName[0] != "H"})){
              label_headings(Array.from(k.childNodes).filter(function(x){return x.nodeType === 1}), null);
           }
           c++;
       }
    }
    label_headings(Array.from(root.childNodes).filter(function(x){return x.nodeType === 1}))
}
number_headings(document.querySelector('#text_output'), null)

Upvotes: 1

user3787031
user3787031

Reputation: 177

One solution is to use CSS counters to append text to the headers instead. Unfortunately, the weakness (or strength) of this approach is that it is visible to the user, but it cannot be copy pasted from the browser, nor is it easy to get the value if you need it elsewhere in your script.

#text_output {
  counter-reset: h2
}

#text_output h2 {
  counter-reset: h3
}

#text_output .text_output_expansion .card-body{
  counter-reset: h2
}

#text_output .text_output_expansion .card-body h2 {
  counter-reset: h3
}

#text_output h2:before {
  counter-increment: h2;
  content: counter(h2) ". "
}

#text_output h3:before,
h3.md-focus.md-heading:before {
  counter-increment: h3;
  content: counter(h2) "."counter(h3) ". "
}

Upvotes: 1

Related Questions