一、需求描述
在 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); } } }