快速食用地址: https:// github.com/ahungrynoob/ showdown-toc
背景
什么是toc? table of contents 是markdown中的导航信息,能够通过heading和锚点快速定位。
最近在开发个人博客的时候,希望有一个生成目录导航的功能,目的是做一个像语雀右上角一样的导航栏:
使用的markdown渲染引擎是showdown,找了半天,只找到一个jquery写的toc插件,也不支持nodejs。(很好,又是一个造轮子,开源,刷福报的好机会!)
于是就自己做了一个插件,支持两大功能:
- 会把文章中所有的heading信息,通过闭包的形式传达到上层域
- markdown中写
[toc]
,即可生成toc到markdown的相应位置中。
效果:
使用前:
使用后:
开发过程
汲取和熟悉
首先阅读showdown的wiki,了解extension机制和写法。了解到以下几点:
- extension 分为 lang 和 output两种
- lang 的extension会在markdown一开始的时候(normalize之后)就执行,output会在最后执行。
- 作者还很热心的指出了一些坑(递归调用,不要修改converter):https://github.com/showdownjs/showdown/wiki/extensions#filter-property
思路
老的思路:自己写一个lang插件,在一开始引擎读md的时候,就去收集信息,结果证明:❌
原因: 实践过程中,showdown有缓存机制,只会读取一遍md内容,之后读的都是html的内容,就无法收集toc的元信息了。
新思路:在output阶段中解析html的h1到h6标签,收集heading信息,并替代[toc]
的占位符,输出到html中,结果证明:✅
步骤以及代码:
整个代码的核心是这个正则:/(<h([1-6]).*?id="([^"]*?)".*?>(.+?)</h[1-6]>)|(<p>[toc]</p>)/g
;
这个正则,获取了h1-h6的标题信息以及toc的占位符的位置和次数
第一步:
在这里,showdown-toc会去寻找并按出现次序收集tocItem
和[toc]
// find and collect all headers and [toc] node;
const collection: MetaInfo[] = [];
source.replace(regex, (wholeMatch, _, level, anchor, text) => {
if (wholeMatch === '<p>[toc]</p>') {
collection.push({ type: 'toc' });
} else {
text = text.replace(/<[^>]+>/g, '');
const tocItem = {
anchor,
level: Number(level),
text,
};
// 如果传了闭包的数组参数toc,就会把标题信息推入
if (toc) {
toc.push(tocItem);
}
collection.push({
type: 'header',
...tocItem,
});
}
return '';
});
tocItem长这样:
type TocItem = {
anchor: string; // 锚点
level: number; // 标题级别
text: string; // 标题内容
};
第二步
这里,出现[toc]
之后,会收集这个[toc]
到下个[toc]
之间的标题信息
// calculate toc info
const tocCollection: TocItem[][] = [];
collection.forEach(({ type }, index) => {
if (type === 'toc') {
if (collection[index + 1] && collection[index + 1].type === 'header') {
const headers = [];
const { level: levelToToc } = collection[index + 1] as TocItem;
for (let i = index + 1; i < collection.length; i++) {
if (collection[i].type === 'toc') break;
const { level } = collection[i] as TocItem;
if (level === levelToToc) {
headers.push(collection[i] as TocItem);
}
}
tocCollection.push(headers);
} else {
tocCollection.push([]);
}
}
});
第三步:
这个阶段,会把source中的showdown给我们生成的<p>[toc]</p>
标签替换成我们生成toc标签,也就是ol
和li
标签啦。然后把处理后的source返回给showdown就好了。整个插件也就完成了。
// replace [toc] node in source
source = source.replace(/<p>[toc]</p>[n]*/g, () => {
const headers = tocCollection.shift();
if (headers && headers.length) {
const str = `<ol>${headers
.map(({ text, anchor }) => `<li><a href="#${anchor}">${text}</a></li>`)
.join('')}</ol>n`;
return str;
}
return '';
});
总结
整个插件很简单,但是中间也遇到了不少坑,出现过思路的碰撞。实现的主要步骤逻辑如下:
- 寻找和收集h1-h6以及
[toc]
的出现次数和次序 - 计算两次
[toc]
之间的标题 - 用我们自己生成toc标签去替换
<p>[toc]</p>
占位标签,输出成html字符串。
欢迎pr
项目地址:https://github.com/ahungrynoob/showdown-toc