长文没有目录?做了个 Markdown 标题提取生成 TOC 的工具
写技术文章写到两千字,回头发现没法跳读。手动编目录麻烦,而且改了标题之后目录还得跟着改,迟早对不上。
做了个提取工具:粘进 Markdown,自动读出各级标题,生成带锚点链接的目录。重点是跳过代码块里的注释行——这是最容易误判的地方。
一、先把代码块挖掉
function stripCode(text) {
return text.replace(/```[\s\S]*?```/g, (m) => m.replace(/[^\n]/g, ' '));
}
反引号包裹的代码块里,# 开头在 Python、Shell、YAML 里都是注释,会被当成标题。处理办法是把代码块里所有非换行字符替换成空格,这样字符位置不变(不影响行号),但内容不会被正则匹配到。
用空格替换而不是直接删掉,是为了保持文本长度一致,万一后面要按位置定位也不出错。
二、提取标题
function parseHeadings(md) {
const clean = stripCode(md);
const out = [];
const re = /^(#{1,6})\s+(.+?)\s*#*\s*$/gm;
let m;
while ((m = re.exec(clean)) !== null) {
const text = m[2].replace(/[*`_]/g, '').trim();
out.push({ level: m[1].length, text, slug: slugify(text) });
}
return out;
}
标题行是行首一到六个 # 加空格。行尾可能还有 ATX 风格的闭合 #,正则里用 \s*#*\s*$ 吃掉。标题里的 **、反引号、_ 这些 Markdown 标记要去掉,不然生成的目录文字里带着星号。
三、生成锚点
function slugify(s) {
return s
.toLowerCase()
.trim()
.replace(/[^\w\u4e00-\u9fff\s-]/g, '')
.replace(/\s+/g, '-');
}
规则参考 GitHub:转小写、去掉标点、空格换成横线。中文字符保留,因为现代平台基本都支持中文锚点。
四、重复标题和缩进
function dedupeSlugs(headings) {
const seen = new Map();
return headings.map((h) => {
const count = seen.get(h.slug) || 0;
seen.set(h.slug, count + 1);
return count ? { ...h, slug: `${h.slug}-${count}` } : h;
});
}
function toToc(headings) {
if (!headings.length) return '';
const min = Math.min(...headings.map((h) => h.level));
return headings
.map((h) => `${' '.repeat(h.level - min)}- [${h.text}](#${h.slug})`)
.join('\n');
}
两个同名标题会生成同一个锚点,第二个链接就跳不动了。按出现次数加 -1、-2 后缀,跟 GitHub 的行为一致。
缩进基准取标题里最小的级别,这样文章只用 ## 和 ### 时,目录不会凭空多出一级缩进。
五、踩过的坑
第一个坑是代码块里的注释被当成标题。 前面说的,这是最典型的误判。一篇贴了几段 Shell 命令的文章,目录里会冒出一堆 # 安装依赖 这种东西。
第二个坑是各平台锚点规则不同。 GitHub、掘金、自建博客的生成规则都有差异,特别是标点和中文的处理。工具按 GitHub 规则生成,并在界面上说明,换个平台可能要微调。
第三个坑是重复标题。 前面说的,必须加后缀去重。
第四个坑是标题里的 Markdown 标记没清掉。 目录里出现 **加粗标题**,链接文字看着很怪。
第五个坑是缩进基准。 不从最小级别算的话,通篇用 ### 的文章生成的目录会缩进好几层。
第六个坑是行尾闭合的 #。 ATX 风格标题 ## 标题 ## 后面的 # 如果不处理,会变成标题文字的一部分。
六、总结
生成目录这件事,正则本身不难,难的是"别把不是标题的东西当标题"。代码块处理掉之后,剩下的基本就干净了。
后来加了折叠层级选项。有些文章标题层级很深,生成的目录也跟着深,反而不好读。允许设置最多显示几级,深层自动省略。
另外支持编号目录。除了默认的无序列表,也能生成带 1.、1.1. 这种编号的目录,适合正式文档。
还有个功能是插入回原文。生成的目录一键插到文章开头,不用手动复制粘贴,插入位置可选。
最后是链接有效性提示。生成的锚点跟平台规则有关,导出时提示"发布后请点几个链接验证",避免目录全是死链。
还有个功能是层级校验。大纲里出现 #### 却没有 ###,说明层级可能写错了,工具提示一下。另外支持空标题过滤:只有 # 没有文字的行跳过,不然目录里会出现一个空链接。最后是选项:要不要包含一级标题,有的文章一级标题就是文章名,放进目录没意义。
另外支持生成不带链接的纯文本目录,有些平台不需要锚点链接,只要层级列表。还有个细节是目录里要不要包含序号,正式文档通常需要。
还有个细节是支持统计标题数量,文章分了几节一目了然。另外支持导出时选择要不要带链接,有些场合只需要纯文字的目录。
- 点赞
- 收藏
- 关注作者
评论(0)