Talk is cheap. Show me your code

JavaScript 遍历文档生成目录结构

一、需求描述

在 Word 中编辑文档的时候,可以在视图中打开导航窗格来查看目录树

类似的,现在需要基于页面上的文章,渲染出一个这样的目录结构

 

在网页上这些标题都是通过 <h1> 这样的标签渲染的,而且段落与标题之间是兄弟节点的关系

所以第一步只需要获取到文章的根节点,然后遍历 <h1> 这样的兄弟节点,就能拿到初步的目录结构

 

但有一种特殊情况需要考虑:

可能文章中的第一个标题并不是 h1,而是更低层级的标题,比如 h3,但在显示上依然需要作为一级标题来展示,因为在 h3 之前没有更大的标题

同样的,在 h1 下面如果先出现了 h3,紧接着又出现了 h2,那么先出现的 h3 实际上和后面的 h2 处于一个层级

也就是说类似这样的结构:

<h3>标题3</h3>
<h4>标题4</h4>
<h1>标题1</h1>
<h2>标题2</h2>
<h1>标题1</h1>
<h4>标题4</h4>
<h3>标题3</h3>
<h2>标题2</h2>

需要展示为:

 

 

二、程序设计

虽然页面上的文章是一棵 DOM 树,但由于标题元素是块级元素,所以实际上需要处理的树节点是平铺的,只有一个层级

也就是说,不管是怎样的文档,最终都能处理成这样的结构:

const article = [
  { tag: 'h3',content: '标题3' },
  { tag: 'p', content: '这里是第一部分的内容' },
  { tag: 'h4', content: '标题4' },
  { tag: 'p', content: '这里是第二部分的内容' },
  { tag: 'p', content: '上面说得很好,接下来再补充一点' },
  { tag: 'h1', content: '标题1' },
  { tag: 'h2', content: '标题2' },
  { tag: 'h1', content: '标题1' },
  { tag: 'p', content: '刚才有一点忘记说了' },
  { tag: 'p', content: '我话讲完,谁赞成,谁反对' },
  { tag: 'h4', content: '标题4' },
  { tag: 'h3', content: '标题3' },
  { tag: 'p', content: '不好意思,你刚才说什么我没听清' },
  { tag: 'h2', content: '标题2' },
  { tag: 'p', content: '现在我再问一次,谁赞成,谁反对' },
]

所以对于文档本身,只需要做一次遍历即可

但是对于文档目录,由于最终计算的是一个相对层级,所以也不太方便使用固定长度的数组来记录层级

所以最终的解决方案是维护一个栈来记录标题的层级关系

在一开始的时候,对于标题节点无论是几级标题,都直接压栈

后面每次处理标题,都和栈尾的标题进行比较,如果当前的标题层级更深,则压入栈内,否则清除栈尾,并比较前一位标题

 

在处理标题层级的同时,还需要另外维护一个记录前缀的栈,这两个栈是映射关系

最终可以通过这两个栈,得到目录的完整文案,甚至是缩进量,所以出参可以这样的结构:

const result = [
  { title: '1 标题', indent: 0 },
  { title: '1.1 标题', indent: 1 },
]

 

 

三、代码实现

function getHeadingList(list) {
  if (!Array.isArray(list)) {
    return;
  }

  const reg = /h(\d)/; // 使用正则来匹配标题节点
  const levelStack = []; // 记录标题层级
  const prefixStack = []; // 记录前缀

  return list.reduce((res, node) => {
    const { tag, content } = node || {};
    const tagSplited = reg.exec(tag);
    if (!tagSplited) return res;

    updateLevelList(levelStack, prefixStack, Number(tagSplited[1]));

    res.push({
      title: `${prefixStack.join('.')} ${content}`,
      indent: prefixStack.length - 1,
    });
    return res;
  }, []);
}

function updateLevelList(levelStack, prefixStack, current) {
  const idx = levelStack.length - 1;
  const lastLevel = levelStack[idx];
  if (!lastLevel || current > lastLevel) {
    // 当前为最深层级,压入栈尾
    levelStack.push(current);
    prefixStack.push(1);
    return;
  }

  if (current === lastLevel) {
    // 层级相等时,只修改前缀
    prefixStack[idx]++;
  } else if (current < lastLevel) {
    // 当前层级更高,先和上一层级对比
    const preIndex = idx - 1;
    const preLevel = levelStack[preIndex];
    if (!preLevel || current > preLevel) {
      // 如果preLevel不存在,则代表当前层级比顶层更高,即 [2, 3, 1] 这种情况
      // 如果preLevel比当前层级更高,即 [1, 3, 2] 这种情况
      prefixStack[idx]++;
      levelStack[idx] = current;
    } else {
      // 删除栈尾,继续递归
      levelStack.splice(idx, 1);
      prefixStack.splice(idx, 1);
      updateLevelList(levelStack, prefixStack, current);
    }
  }
}

 

posted @ 2021-05-29 17:15  Wise.Wrong  阅读(1174)  评论(0编辑  收藏  举报