1
使用Markdown编辑编写文章时,我们会使用h1-h6标签来定义章节标题,但Markdown生成的文章中的h1-h6标签是平行结构的,
并不是一颗树状结构,此时就需要我们手动去解析这些h标签,并根据他们直接的规律生成一目录树

文章效果
文章效果

文章dom结构
文章dom结构

最终生成的目录结构
最终生成的目录结构

思路

  1. 获取文章中所有的h1~h6标签
  2. 比较h标签的数字,从当前h标签开始判断,如果后面的h标签数字比自己大则当做自己的子孙级,遇到h标签数字比自己小或和自己一样的则立即停止

假如我们获取到的h标签是这样的:

var hEles = [
      'h4',
      'h6',
      'h3',
      'h4',
      'h4',
      'h1',
      'h2',
      'h3',
      'h3',
      'h3',
      'h3',
      'h2',
      'h3',
      'h3'
];

则我们首先需要将其转换成这样:

var arr2 = [
     {hLevel: 4}, {hLevel: 6}, {hLevel: 3}, {hLevel: 4},
     {hLevel: 4}, {hLevel: 1}, {hLevel: 2}, {hLevel: 3},
     {hLevel: 3}, {hLevel: 3}, {hLevel: 3}, {hLevel: 2},
     {hLevel: 3}, {hLevel: 3}
];

再转换成树状:

var res = [
     { hLevel: 4, level: 1, children: [ {hLevel: 6, level: 2} ] },
     { hLevel: 3,, level: 1, children: [ {hLevel: 4, level: 2}, {hLevel: 4, level: 2} ] },
     {
       hLevel: 1,
       level: 1,
       children: [
          {
             hLevel: 2,
             level: 2
             children: [ {hLevel: 3, level: 3}, {hLevel: 3, level: 3}, {hLevel: 3, level: 3}, {hLevel: 3, level: 3} ]
          },
          {
             hLevel: 2,
             level: 2,
             children: [ {hLevel: 3, level: 3}, {hLevel: 3, level: 3} ]
          }
       ]
     }
 ];

1、代码实现方案一

function toTree(flatArr){
  var tree = [];
  var copyArr = flatArr.map(function (item) { return item; });

  // 根据指定级别查找该级别的子孙级,并删除掉已经查找到的子孙级
  var getChildrenByLevel = function (currentLevelItem, arr, level) {
    if(!currentLevelItem){
      return;
    }
    // 将level值转成负数,再进行比较
    var minusCurrentLevel = -currentLevelItem.hLevel;
    var children = [];
    for(var i = 0, len = arr.length; i < len; i++){
      var levelItem = arr[i];
      if(-levelItem.hLevel < minusCurrentLevel){
        children.push(levelItem);
      }else { // 只找最近那些子孙级
        break;
      }
    }
    // 从数组中删除已经找到的那些子孙级,以免影响到其他子孙级的查找
    if(children.length > 0){
      arr.splice(0, children.length);
    }
    return children;
  }

  var getTree = function (result, arr, level) {
    // 首先将数组第一位移除掉,并添加到结果集中
    var currentItem = arr.shift();

    currentItem.level = level;
    result.push(currentItem);
    while (arr.length > 0){
      if(!currentItem){
        return;
      }
      // 根据当前级别获取它的子孙级
      var children = getChildrenByLevel(currentItem, arr, level);
      // 如果当前级别没有子孙级则开始下一个
      if(children.length == 0){
        currentItem = arr.shift();
        currentItem.level = level;
        if(currentItem){
          result.push(currentItem);
        }
        continue;
      }
      currentItem.children = [];
      // 查找到的子孙级继续查找子孙级
      getTree(currentItem.children, children, level + 1);
    }
  }
  getTree(tree, copyArr, 1);

  return tree;
}

测试一下:

var arr2 = [
     {hLevel: 4}, {hLevel: 6}, {hLevel: 3}, {hLevel: 4},
     {hLevel: 4}, {hLevel: 1}, {hLevel: 2}, {hLevel: 3},
     {hLevel: 3}, {hLevel: 3}, {hLevel: 3}, {hLevel: 2},
     {hLevel: 3}, {hLevel: 3}
];
console.log(toTree(arr));

image.png

2、代码实现方案二:栈思想实现(2021-08-30)

function toTree2 (flatArr) {
    var resultArr = []; // 结果集数组
    var stack = []; // 栈数组
    flatArr.forEach((levelItem, index) => {
      levelItem.children = [];
      if (resultArr.length == 0) { // 第一次循环时没有任何内容,则默认将第一个元素入栈
        levelItem.parentCollector = resultArr; // 存储父级

        stack.push(levelItem);
        resultArr.push(levelItem);
      } else {
        let lastestLeveInStack = stack[stack.length - 1]; // 获取栈顶的元素,即最近如入栈的元素

        if (lastestLeveInStack.level < levelItem.level) { // 遇到子级节点
          levelItem.parentCollector = lastestLeveInStack.children;

          stack.push(levelItem);
          lastestLeveInStack.children.push(levelItem);
        } else if (lastestLeveInStack.level == levelItem.level) {// 遇到同级节点
          levelItem.parentCollector = lastestLeveInStack.parentCollector;

          stack.push(levelItem); // 将当前元素入栈
          lastestLeveInStack.parentCollector.push(levelItem);
        } else {
          let lastestLeveParentCollector = lastestLeveInStack.parentCollector;
          let lastestLeveParentIndex = stack.findIndex(function (leveItem) {
            return leveItem.children === lastestLeveParentCollector;
          });

          if (lastestLeveParentIndex > -1) {
            // 栈顶节点的父级节点
            let lastestLeveParent = stack[lastestLeveParentIndex];

            if (lastestLeveParent.level < levelItem.level) { // 当前节点为栈顶节点父节点的子级
              levelItem.parentCollector = lastestLeveParent.children;

              lastestLeveParent.children.push(levelItem);
              stack.push(levelItem);
              return;
            }

            // 移除栈顶节点父级节点及其以后的所有节点
            stack.splice(lastestLeveParentIndex);

            if (stack.length == 0) { // 如果栈空了则该节点为顶层节点
              levelItem.parentCollector = resultArr;

              stack.push(levelItem);
              resultArr.push(levelItem);
              return;
            }

            lastestLeveInStack = stack[stack.length - 1];
            lastestLeveParent = lastestLeveInStack.parentCollector;

            if (lastestLeveInStack.level < levelItem.level) { // 当前节点为栈顶节点的子节点
              levelItem.parentCollector = lastestLeveInStack.children;
              stack.push(levelItem);
              lastestLeveInStack.children.push(levelItem);
            } else { // 当前节点为栈顶节点的兄弟节点
              let lastestLeveParentCollector = lastestLeveInStack.parentCollector;
              let collector = stack.find(function (leveItem) {
                return leveItem.children === lastestLeveParentCollector;
              });
              // 如果栈顶节点的父级节点的级别比当前节点级别大,则继续往上查找
              while (collector && collector.level >= levelItem.level) {
                lastestLeveParentCollector = collector.parentCollector;
                collector = stack.find(function (leveItem) {
                  return leveItem.children === lastestLeveParentCollector;
                });
              }
              if (!collector) {
                levelItem.parentCollector = resultArr;
                stack.push(levelItem);
                resultArr.push(levelItem);
              } else {
                levelItem.parentCollector = collector.children;
                collector.children.push(levelItem);

                /* let collectorIndex = stack.findIndex(function (item) {
                  return item === collector;
                });
                if (collectorIndex > -1) {
                  stack.splice(collectorIndex);
                } */

                stack.push(levelItem);
              }

            }
          } else {
            console.log('kkkkkkkkkkkkkkk');
          }
        }
      }
    });
    // 移除元素的parentCollector属性
    flatArr.forEach(levelItem => {
      if (levelItem.parentCollector) {
        delete levelItem.parentCollector;
      }
      if (levelItem.children.length == 0) {
        delete levelItem.children;
      }
    });
    return resultArr;
  }

接下来只需要根据这个树状结构生成对应的dom树就可以了

// 根据树状结构数据生成章节目录dom树
function getChapterDomTree(chapterTreeData, parentNode){
  if(!parentNode){
    parentNode = createNodeByHtmlStr('<ul class="markdown-toc-list"></ul>')[0];
  }
  chapterTreeData.forEach(chapterItem => {
    var itemDom = createNodeByHtmlStr('<li><a class="toc-level-' + chapterItem.level + '" href="#' + chapterItem.id + '">' + chapterItem.text + '</a></li>')[0];
    parentNode.appendChild(itemDom);
    if(chapterItem.children){
      var catalogList = createNodeByHtmlStr('<ul class="markdown-toc-list"></ul>')[0];
      itemDom.appendChild(catalogList);
      getChapterDomTree(chapterItem.children, catalogList);
    }
  });

  return parentNode;
}

// 根据html字符串生成dom元素
function createNodeByHtmlStr(htmlStr){
  var div = document.createElement('div');
  div.innerHTML = htmlStr;
  var children = div.children;
  div = null;
  return children;
}


var treeData = toTree([...]);
var domTree = getChapterDomTree(treeData);

heath_learning
1.4k 声望31 粉丝