为静态站点添加目录功能
errol发表于2024-12-08 | 分类为 编程 | 标签为博客静态站点jsmarkdown目录

最近阅读自己的文章时有感,没有目录的体验真的很糟糕。

首先是汲取信息方面,缺少目录就意味着失去了一种概览全文的重要途径,想要进一步了解文章只能从头看到尾;其次是操作方面,缺少目录说明没有内容导航,如体现为无法使用锚点跳转,只能依靠滚动&滑动的方式查看文章内容。

当文章较长时,无论是从哪个方面,阅读文章似乎都会变为一种令人厌烦的事情,此乃大忌,所以还是觉得给文章加上目录为好。

本着尽量不改动代码的原则,所以决定在前端层面实现该功能,类似于插件,无非就是新增个外部js,并同步修改生成html文件的模板。

一、方法设计思路

注,本文是基于Markdown文本转换的dom实现方法。

Markdown是一种轻量级标记语言,使用纯文本格式编写,具有轻量化、易读易写的特点,如下一段的md文本。

# 节点1

## 节点2

### 节点3

### 节点4

## 节点5

示例中展示了一组标题元素,其文体结构呈扁平化,且层次分明

规则很明显,相邻&处于同一层次的节点,必定是兄弟节点,如“节点3”与“节点4”、“节点2”与“节点5”;也必定是上一层次节点的子节点,如“节点3”与“节点4”是“节点2”的子节点,“节点2”与“节点5”是“节点1”的子节点。

因此,可利用这种具有层次关系的标题元素,作为生成目录的依据

在转为dom后,也依然保持这样的规律。这很好理解,一般情况下,Markdown相关的转换器,只会根据md文本标题元素中“#”的数量,一比一转换为对应的heading标签,而不会做此外的任何改变,如“# 节点1”将转为<h1>节点1</h1>,"## 节点2"将转为<h2>节点二</h2>,以此类推。

所以,现在只需设计一个自上往下循环heading标签,并找出其全部子节点(包括子节点的子节点)的方法。

如上述md转为dom节点后,应该为:

# 省略了一些获取的步骤...
[h1, h2, h3, h3, h2]

调用方法后,应该得到如下数据:

h1是最外层的节点,其子节点为两个h2;两个h3都是第一个h2节点的子节点。

[
    {
        h1, 
        children: [
            {h2, children: [{h3, h3}]}, 
            {h2}
        ]
    }
]

二、方法实现

分为两个步骤来完成,generateTree()用于创建最外层的节点,makeChildren()用于创建某个节点的子节点。

实现方式也较简单,只是一些朴实无华的递归和循环。

1、generateTree()
// nodes为doc文档中获取的heading元素数组
function generateTree(nodes) {
    const tree = [];
    while (nodes.length > 0) {
        const current = nodes.pop();
        current.level = 1;
        tree.push({
            text: current._node.textContent,
            children: makeChildren(current, nodes),
            ...current
        });
    }
    return tree;
}
2、makeChildren()
function makeChildren(current, nodes) {
    const children = [];
    while (nodes.length > 0) {
        const node = nodes[nodes.length - 1];
        // num为h标签的序号;当遇到大于当前节点的节点时,表明下个节点node并非上个节点current的子节点,结束循环
        if (node.num <= current.num) {
            return children;
        }
        // 移除节点
        nodes.pop();
        node.level = current.level + 1;
        children.push({
            text: node._node.textContent,
            children: makeChildren(node, nodes),
            ...node
        });
    }
    return children;
}

三、使用方法

1、获取dom节点

以下假设“.markdown-body > .content”是由某个markdown转换的dom元素。

const mbc = document.querySelector('.markdown-body > .content');
const children = mbc.children;
const hChildren = [];
const reg = /^H\d$/;
for (let child of children) {
    // 使用正则表达式筛选出heading元素。
    if (reg.test(child.tagName)) {
        hChildren.push({
            _node: child,
            num: +child.tagName.slice(1)
        });
    }
}

// 将数组反转,以供后续使用pop()取出数据
hChildren.reverse();
2、根据dom节点生成树

将步骤1中得到的数据传入方法。

const tree = generateTree(hChildren);

以文章某云滑动认证码逆向为例,其部分内容截图如下:

image

图1 《某云滑动认证码逆向》- 截图1

image

图2 《某云滑动认证码逆向》- 截图2

调用方法后应该得到如下结果:

image

图3 树形结构数据

之后再通过该数据生成目录即可。

image

图4 使用树形结构数据生成的文章目录


之前总觉得文章页面缺了些什么,仔细想想目录就是其中一个,现在整体看上去充实了很多,也比较实用。

说起来,这个功能其实很早就纳入开发计划中,只是久而久之忘记了,以至于今日才落地实现。

以上。

返回