Home > Net >  Prepend / prefix text to list items without class or id tags
Prepend / prefix text to list items without class or id tags

Time:12-22

I'm working with the Docsify.js markdown parser framework and it automatically creates a sidebar from the headings in a document (unless you manually create a sidebar).

I have some CSS that numbers list elements, but want to convert it to JS as there are rendering issues when classes are added as the page scrolls (ie. adding .active).

Originally, I was trialling using this snippet but it doesn't output it as an auto incrementing hierarchical number system:

The sidebar that is generated is in the following format:

var li = document.getElementsByTagName( 'li' );
for( var i = 0; i < li.length; i   ) {
  var prefix = '1.';
  li[i].innerHTML = prefix   ' Title '   i;
  prefix  ;
}
<aside >
  <div >
    <ul>
      <li>Title 1</li>
      <ul>
        <li>Title 2</li>
        <ul>
          <li>Title 3</li>
          <ul>
            <li>Title 4</li>
            <ul>
              <li>Title 5</li>
              <ul>
                <li>Title 6</li>
              </ul>
            </ul>
          </ul>
        </ul>
      </ul>
      <li>Title 1</li>
      <li>Title 1</li>
      <ul>
        <li>Title 2</li>
        <li>Title 2</li>
        <ul>
          <li>Title 3</li>
          <ul>
            <li>Title 4</li>
            <ul>
              <li>Title 5</li>
              <ul>
                <li>Title 6</li>
              </ul>
            </ul>
          </ul>
        </ul>
      </ul>
    </ul>
  </div>
</aside>

I understand the HTML structure isn't valid with <ul> being a descendant of an <ul> but this is the code that is outputted and I have no control over it.

However, I want to be able to number the headings with sections and sub-sections:

1. Title 1
 1.1. Title 2
  1.1.1. Title 3
   1.1.1.1. Title 4
    1.1.1.1.1. Title 5
     1.1.1.1.1.1. Title 6
2. Title 1
3. Title 1
 3.1. Title 2
 3.2. Title 2
  3.2.1. Title 3
   3.2.1.1. Title 4
    3.2.1.1.1. Title 5
     3.2.1.1.1.1. Title 6

I am struggling to find a way to be able to target the first <li> (or the H1), and then being able to access the next <ul> via .nextElementSibling to continue the loop and prepend the numbering.

As far as I have gotten to at the moment is: document.querySelectorAll( 'div.sidebar-nav > ul' ) and it's not much to go on!

I think I'm really out of my depth for javascript here, and was hoping that I'd be able to get some help on being able to loop through the <li> and <ul> elements to prepend the numbers.

CodePudding user response:

Following is JavaScript to apply nested index numbers. At max there are only 6 header tags, 6 levels, so we can use recursive solution:

let startLevel = 1;
let endLevel = 5;

function indexsify() {
  let children = document.querySelectorAll('#sidebar > ul');
  let numbers = new Array(7).fill(0);
  let depth = 0;

  children.forEach((element, index) => {
    recurse(element,   depth, numbers);
  });
}

function recurse(element, depth, numbers) { //ul
  let children = Array.from(element.children);

  children.forEach((element, index) => {
    if (element.localName.toUpperCase() === 'LI') {
      numbers[depth]  ;
      addNumberString(element, depth, numbers);

    } else if (element.localName.toUpperCase() === 'UL') {
      if (depth < endLevel) {
        recurse(element, depth   1, numbers, startLevel);

        numbers.fill(0, depth   1); //reset all next numbers
      }
    }
  });
}

function addNumberString(element, depth, numbers) {
  let strNumber = "";
  numbers.forEach((num, index) => {
    if (index > depth || index <= startLevel) return;
    strNumber  = `${num}.`;
  });
  element.firstElementChild.innerText = strNumber  
    element.firstElementChild.innerText;
}

indexsify();
ul,
li {
  list-style-type: none;
}
<div id="sidebar">
  <ul>
    <li><a>Home</a></li>
    <ul>
      <li><a>Chapter a</a></li>
      <ul>
        <li><a> Section a</a></li>
        <li><a>Section b</a></li>
      </ul>
      <li><a>Chapter b</a></li>
      <li><a>Chapter c</a></li>
      <ul>
        <li><a>Section a</a></li>
        <li><a>Section b</a></li>
        <ul>
          <li><a>Sub-section a</a></li>
        </ul>
      </ul>
      <li><a>Chapter D</a></li>
    </ul>
  </ul>
</div>


Modify markdown itself: As per the Docsify plugin documentation there is no direct provision to influence the sidebar content. Your plugin uses hook.afterEach(function(html, next) and the sidebar is generated separately. So you are trying to manipulate generated sidebar also. You are trying to do similar operation two times.
Why not use hook.beforeEach(function(content) and manipulate markdown itself. That way you'll have to do the numbering operations only once.
Here is a demo site and the code sandbox link for following sample plugin that manipulates markdown content:

<!DOCTYPE html>
<html>
  <body>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/docsify@4/themes/vue.css"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/docsify/themes/dark.css"
    />
    <div id="app">Please wait...</div>
    <script>
      window.$docsify = {
        el: "#app",
        loadSidebar: true,
        maxLevel: 4,
        subMaxLevel: 5,
        homepage: "readme.md"
      };
    </script>

    <script>
      //plugin
      let myPlugin = function (hook, vm) {
        hook.init(function () {
          //remove '-' before Table of content entries
          let customStyles = document.createElement("style");
          customStyles.type = "text/css";
          customStyles.textContent = `.app-sub-sidebar li::before {
             content: '' !important;
             padding-right: 4px;
             float: left;
           }`;
          document.body.appendChild(customStyles);
        });

        //update markdown content before docsify parsing
        hook.beforeEach(function (content) {
          let lines = content.split("\n");

          let numbers = new Array(6).fill(0);
          let depth = 0;

          lines.forEach((line, index) => {
            let level = getLevel(line);

            //if not a header continue to next line
            if (level === -1) return;

            if (level > depth) {
              depth  ; //increase depth
            } else {
              depth = level; //decrease depth
              numbers.fill(0, depth   1); //set all next depth to 0
            }
            numbers[depth]  ;

            let strNumber = "";
            numbers.forEach((num, index) => {
              if (index > depth || index < startLevel) return;
              strNumber  = `${num}.`;
            });

            if (depth < endLevel) {
              lines[index] =
                levels[level]   strNumber   line.substr(depth   1, line.length);
            }
          });

          //update original content
          content = lines.join("\n");
          return content;
        });

        let levels = ["# ", "## ", "### ", "#### ", "##### ", "###### "];
        let startLevel = 1;
        let endLevel = 4;
        let regEx = new RegExp(`^#{1,${endLevel}}\\s .*`);

        function getLevel(line) {
          if (!regEx.test(line)) return -1; //not a header line
          if (line.startsWith(levels[0])) return 0; //h1
          if (line.startsWith(levels[1])) return 1;
          if (line.startsWith(levels[2])) return 2;
          if (line.startsWith(levels[3])) return 3;
          if (line.startsWith(levels[4])) return 4;
          if (line.startsWith(levels[5])) return 5; //h6
        }
      };

      window.$docsify.plugins = [myPlugin];
    </script>
    <script src="https://cdn.jsdelivr.net/npm/docsify@4"></script>
  </body>
</html>

We need to override default CSS in hook.init(function ()) to remove leading - in table of contents.


Old answer: You can have numbers directly on anchors<a> tags :

.sidebar ul>li {
  counter-increment: item;
}

.sidebar ul>li:first-child {
  counter-reset: item;
}

.sidebar ul>li a::before {
  content: counters(item, ".") " ";
}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify@4/themes/vue.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify/themes/dark.css" />
<div id="app">Please wait...</div>
<script>
  window.$docsify = {
    el: "#app",
    loadSidebar: false,
    homepage: 'https://gist.githubusercontent.com/OnkarRuikar/bb1d986f279dddceea9004a4bde3844b/raw/80fe153d6b8c1bb2b8e7035be7df1bb908779061/readme.md'
  }
</script>
<script src="https://cdn.jsdelivr.net/npm/docsify@4"></script>

If you generate numbers for titles in main section then they may get populated automatically in the sidebar. You can use markdown setting to write the script. Or you can try something like: https://github.com/markbattistella/docsify-autoHeaders

CodePudding user response:

You want to "do something" to every child of the outer UL, depending on its type - either increment the index at the current level and then prepend a label, if it's an LI; or else recurse to the next level, if it's a UL. Given the HTML provided by the OP, this bit of code produces exactly the desired result:

function addLabels(element, prefix) {
    var index = 0;

    Array.from(element.children).forEach(element => {
        if (element.localName.toUpperCase() === 'LI') {
            index  = 1;
            element.innerText = prefix  
                index   '. '   element.innerText;
        } else if (element.localName.toUpperCase() === 'UL') {
            addLabels(element, prefix   index   '.');
        }
    });
}

document.querySelectorAll('div.sidebar-nav > ul').forEach(
    element => addLabels(element, '')
);
  • Related