本篇文章讲述为博客文章添加自动生成目录的功能。
讲述功能之前,我先说下我的博客文章从编写到发布展现给读者的大致过程。我的文章都是在本地用markdown语法编写的,待文章写完后,就直接从网站管理后台上传至服务器,这时读者就可以在我的网站看到我刚发布的文章了。当读者点击某一篇文章时,浏览器会发送一个请求,请求这篇文章获得文章的信息(包括markdown内容),请求成功后,markdown会被转换成html格式(这个转换我用的是Showdown.js库完成的),有了html代码,就可以直接在页面上展示了。
自动生成目录的功能,就是分析博客文章转换后的html代码,得到各个部分的标题,用标题组成目录。
大家可以点击我博客网站的每一篇文章,查看自动生成的目录的效果。
功能代码实现如下。
interface CatalogNode {
title: string; // 目录名称
tag: string; // 标题元素的tag,"h1", "h2", "h3", "h4", "h5", "h6",表示该标题是几级标题
childs: CatalogNode[]; // 该目录下的子目录
id: string; // 标题元素的id属性值,用于跳转到文章指定标题位置
};
/**
* 生成目录
* @param htmlText html内容文本
*/
const generateCatalog = (htmlText: string) => {
const tags = ["h1", "h2", "h3", "h4", "h5", "h6"];
const temEle = document.createElement("div");
temEle.innerHTML = htmlText;
const cmpContainer = [];
const catalogNodes: CatalogNode[] = [];
const existsId: string[] = [];
for (let i = 0; i < temEle.childNodes.length; i++) {
const ele = temEle.childNodes[i] as HTMLElement;
if (ele.tagName && tags.includes(ele.tagName.toLowerCase())) {
const index1 = tags.indexOf(ele.tagName.toLowerCase());
while (cmpContainer.length > 0) {
const index2 = tags.indexOf(cmpContainer[cmpContainer.length - 1].tag);
if (index1 > index2) {
break;
}
cmpContainer.pop();
}
let idStr = ele.id;
if (!idStr) {
idStr = ele.innerText.replaceAll(" ", "-");
// 检测已添加的id
let newIdStr = idStr;
let IdSuffix = 0;
while(existsId.indexOf(newIdStr) !== -1) {
IdSuffix++;
newIdStr = `${idStr}_ ${IdSuffix}`;
}
idStr = newIdStr;
existsId.push(idStr);
ele.setAttribute("id", idStr); // 使用文章名称作为element的属性
}
if (cmpContainer.length > 0) {
const node: CatalogNode = {
title: ele.innerText,
tag: ele.tagName.toLowerCase(),
childs: [],
id: idStr
};
cmpContainer[cmpContainer.length - 1].childs.push(node);
cmpContainer.push(node);
} else {
const rootNode: CatalogNode = {
title: ele.innerText,
tag: ele.tagName.toLowerCase(),
childs: [],
id: idStr
};
catalogNodes.push(rootNode);
cmpContainer.push(rootNode);
}
}
}
return {catalogNodes, htmlText: temEle.innerHTML};
};
generateCatalog
函数接受博客文章的原html代码字符串参数,返回catalogNodes
和htmlText
,catalogNodes
表示所有的目录信息(html源码中标题信息),其中childs
表示一个目录下的子目录。返回的htmlText
(后面称newHtmlText
)是与catalogNodes相对应的新的博客文章的html代码,newHtmlText
中所有的标题都添加了指定id属性值,这个id与catalogNodes
中对应的标题的id字段值相同,这样就可以通过在url后接hash字段跳转到指定的标题位置。
接下来,将newHtmlText
作为文章的渲染代码,通过catalogNodes
生成目录。
下面的代码是react代码,接受catalogNodes
,渲染目录列表。
const renderList = (catalogNodes: CatalogNode[]) => {
if (catalogNodes.length === 0) {
return <></>;
}
return (
<ul className="list-wrap">
{
catalogNodes.map((node) => (
<React.Fragment key={node.id}>
<li className="list">
<a href={`#${node.id}`}>{node.title}</a>
</li>
{renderList(node.childs)}
</React.Fragment>
))
}
</ul>
);
}
<a href={`#${node.id}`}>{node.title}</a>
这句代码将目录的href
设置为文章的对应标题的id
值,这样可以通过点击目录跳转到指定目录位置。
(完)