文章目录
我们在查看各种文档时,经常能够看见侧边的目录项
解析文章目录
不难看出,目录的每一项都是正文的某个标题(h1~h6)
前端拿到文章内容时,解析出文章内的大小标题,将其作为则边栏的目录索引
解析的方法很简单,直接读取对应的 dom 元素,遍历其所有子元素,提取出 h1~h6 标签就可以了
typescript
const doc = document.getElementById("article");
const titles = [];
doc.childNodes.forEach(e => {
if (/h\d/i.test(e.nodeName)) {
titles.push(;
}
});
// #article 元素内的h1~h6标签
console.log(titles);
此时我们只需要遍历这个列表就可以生成一个简易的目录结构了
通过设置a.href为对应标题的id,还可以精确定位到对应标题的位置
根据不同的nodeName设置li的class,就可以在css中设置不同的样式了
vue
<template>
<ul>
<li v-for="item in titles" :class="item.nodeName">
<a :href="`#${item.id}`">{{ item.innerHTML }}</a>
</li>
</ul>
</template>
大功告成了,这样其实就已经足够使用了
不过这个列表是一维的,我们知道h1~h6相当于父子级的关系,我们如果想要解析成一个树形结构,该如何操作呢?
例如:
html
<h1>一级标题</h1>
<h2>二级标题</h2>
<h2>二级标题</h2>
<h1>一级标题</h1>
<h2>二级标题</h2>
<h3>三级标题</h3>
<h3>三级标题</h3>
<h1>一级标题</h1>
转换成:
html
<ul>
<li>一级标题</li>
<ul>
<li>二级标题</li>
<li>二级标题</li>
</ul>
<li>一级标题</li>
<ul>
<li>二级标题</li>
<ul>
<li>三级标题</li>
<li>三级标题</li>
</ul>
</ul>
<li>一级标题</li>
</ul>
解析文章目录-树形结构
对于树形结构,常常要用到递归
递归的精髓就在于,将问题分解为规模更小的相同问题
我们先声明一下类型:
typescript
interface Title {
el: HTMLElement;
children: Title[];
}
我们用el属性存储标题元素对象,并且将他的所有子标题集存储在children属性中,children的类型等同于自己
这便有了一个初步的数据结构,接下来我们将解析出的文章一级列表,再次解析为该树形结构
如:
html
<h1>一级标题</h1>
<h2>二级标题</h2>
<h2>二级标题</h2>
<h1>一级标题</h1>
<h2>二级标题</h2>
<h3>三级标题</h3>
<h3>三级标题</h3>
<h1>一级标题</h1>
我们把目光聚焦在第一个h1,它有两个二级标题作为子集,它转换后的结构应该类似于
typescript
{
el: <h1>一级标题</h1>,
children: [
{
el: <h2>二级标题</h2>
children: [],
},
{
el: <h2>二级标题</h2>
children: [],
},
]
}
也就是说,第一个h1标签与第二个h1标签之间,所有的标题元素都是h1标签的子集!
我们先确定一下入参和返回值,声明一个函数:
typescript
// 这里的入参接受一个一维的Title数组,将会返回一个Title类型树形结构
const r = (list: { el: HtmlElement }[]): Title[] => {
const result: Title = [];
...
return result;
}
假设入参为:
typescript
const list =[
{ el: <h1>一级标题</h1> },
{ el: <h2>二级标题</h2> },
{ el: <h2>二级标题</h2> },
{ el: <h1>一级标题</h1> },
{ el: <h2>二级标题</h2> },
{ el: <h3>三级标题</h3> },
]
那么第一个h1标签的子集就相当于:
typescript
list.slice(1, 4);
这个slice的参数都可以计算得出,只需要预先知道这个h1的index:
-
对于参数一,相当于这个h1的index加1
-
对于参数二,相当于下一个h1的index,如果没有则是列表的长度
我们将截取出来的子集作为参数递归下去,便可完成树形结构的转换
接下来,上代码:
typescript
// 这里的入参接受一个一维的Title数组,将会返回一个Title类型树形结构
const r = (list: { el: HtmlElement }[]): Title[] => {
const result: Title = [];
// 一级标题 > 二级标题 > 三级标题…… 所以这里用了Math.min,这里提取了标题元素标签中的第二个字符作为对比
// 这里找出入参的最大标题级别
const min = Math.min(...list.map(item => +item.el.nodeName.slice(1)));
// 遍历的时候过滤了一下,这里只需要处理 直接子集 ,而孙、曾孙级等等交给函数递归下去就行了
for (const item of list.filter(item => +item.el.nodeName.slice(1) === min)) {
const start = list.indexOf(item) + 1; // 当前索引加1
let end = list.slice(start).findIndex(sub => sub.el.nodeName == item.el.nodeName); // 下一个的索引
end = end == -1 ? list.length : end // 如果没有则是列表的长度
result.push({
el: item.el,
children: r(list.slice(start, end)), // 将截取出来的子集递归下去
});
}
return result;
}
完整代码
typescript
export const resolveArticleTitles = <T extends Element>(doc: T) => {
interface Title {
el: HTMLElement;
children: Title[];
}
const titles: { el: HtmlElement }[] = [];
doc.childNodes.forEach(e => {
if (/h\d/i.test(e.nodeName)) {
titles.push({
el: e as HTMLElement
});
}
});
const r = (list: { el: HtmlElement }[]) => {
const result: Title[] = [];
const min = Math.min(...list.map(item => +item.el.nodeName.slice(1)));
for (const item of list.filter(item => +item.el.nodeName.slice(1) === min)) {
const start = list.indexOf(item) + 1;
let end = list.slice(start).findIndex(sub => sub.el.nodeName == item.el.nodeName);
end = end == -1 ? list.length : end
result.push({
el: item.el,
children: r(list.slice(start, end),
});
}
return result;
};
return r(titles);
};
Comments | 0条评论