目录

如何给MarkDown添加toc文章目录功能nodejs+marked版本


起源

  • 看别人搭建个人站点,好牛逼,不行,自己也来搞个,看别人的文章自带标题引导toc,不行,我也要,故而出现下面的代码。
  • marked是非常流行的markdown解析器,但是它并不自带toc功能。但是,我也想要怎么办呢?于是格式baidu、google、噢啦,结果下了个蛋,这个蛋便是我想要的toc;

toc诞生记

  • 一下我们将从toc的工作原理及如何实现来进行分析
toc 工作原理
  • 首先,toc的工作原理是网页中某个标签有个id字段,然后我们通过一个href=目标ID便可以时页面定位到该元素。
  • 那么我们只要给每个h标签添加对应的ID,然后再额外生成一个toc及一堆的a标签,每个a标签分别对应一个h

然而,问题来了,如何给h添加ID呢?

  • 我们都知道marked有个Renderer成员,负责渲染相关的,而大多数人可能又知道renderer有个code回调方法,是用来方便自定义处理pre 部分的。
  • 其实我们还有一个heading回调,是marked解析到 # 对应的字段时回调的,也就是我们的标题部分。
  • 那么,我们在其渲染标题时增加id给他,而且此时我们还能获取其标题内容作为a标签的展示文本,多友好的效果啊!
编码实现
  • 啥也不说,代码如下

      const highlight = require('highlight.js')
      const marked = require('marked')
      const tocObj = {
          add: function(text, level) {
              var anchor = `#toc${level}${++this.index}`;
              this.toc.push({ anchor: anchor, level: level, text: text });
              return anchor;
          },
          // 使用堆栈的方式处理嵌套的ul,li,level即ul的嵌套层次,1是最外层
          // <ul>
          //   <li></li>
          //   <ul>
          //     <li></li>
          //   </ul>
          //   <li></li>
          // </ul>
          toHTML: function() {
              let levelStack = [];
              let result = '';
              const addStartUL = () => { result += '<ul class="anchor-ul" id="anchor-fix">'; };
              const addEndUL = () => { result += '</ul>\n'; };
              const addLI = (anchor, text) => { result += '<li><a class="toc-link" href="#'+anchor+'">'+text+'<a></li>\n'; };
    
              this.toc.forEach(function (item) {
                  let levelIndex = levelStack.indexOf(item.level);
                  // 没有找到相应level的ul标签,则将li放入新增的ul中
                  if (levelIndex === -1) {
                      levelStack.unshift(item.level);
                      addStartUL();
                      addLI(item.anchor, item.text);
                  } // 找到了相应level的ul标签,并且在栈顶的位置则直接将li放在此ul下
                  else if (levelIndex === 0) {
                      addLI(item.anchor, item.text);
                  } // 找到了相应level的ul标签,但是不在栈顶位置,需要将之前的所有level出栈并且打上闭合标签,最后新增li
                  else {
                      while (levelIndex--) {
                          levelStack.shift();
                          addEndUL();
                      }
                      addLI(item.anchor, item.text);
                  }
              });
              // 如果栈中还有level,全部出栈打上闭合标签
              while(levelStack.length) {
                  levelStack.shift();
                  addEndUL();
              }
              // 清理先前数据供下次使用
              this.toc = [];
              this.index = 0;
              return result;
          },
          toc: [],
          index: 0
      };
    
    
      class MarkUtils {
    
          constructor() {
    
              this.rendererMD = new marked.Renderer();
              this.rendererMD.heading = function(text, level, raw) {
                  var anchor = tocObj.add(text, level);
                  return `<h${level} id=${anchor}>${text}</h${level}>\n`;
              };
              highlight.configure({useBR: true});
              marked.setOptions({
                  renderer: this.rendererMD,
                  headerIds: false,
                  gfm: true,
                  tables: true,
                  breaks: false,
                  pedantic: false,
                  sanitize: false,
                  smartLists: true,
                  smartypants: false,
                  highlight: function (code) {
                      return highlight.highlightAuto(code).value;
                  }
    
              });
    
          }
    
    
    
          async marked(data) {
              if (data){
                  let content = await marked(data);
                  let toc = tocObj.toHTML();
                  return {content:content,toc:toc}
              }else {
                  return null;
              }
          }
      }
    
      module.exports = new MarkUtils();