长文没有目录?做了个 Markdown 标题提取生成 TOC 的工具

举报
静水流深-云海 发表于 2026/10/18 08:18:42 2026/10/18
【摘要】 长 Markdown 没有目录不便跳读,做了个标题提取生成 TOC 的工具。讲了必须先把代码块挖掉否则注释行会被误判为标题、锚点按 GitHub 规则生成且各平台有差异、重复标题要加数字后缀、标题里的 Markdown 标记需清除、以及缩进基准应取最小标题级别。

写技术文章写到两千字,回头发现没法跳读。手动编目录麻烦,而且改了标题之后目录还得跟着改,迟早对不上。

做了个提取工具:粘进 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. 这种编号的目录,适合正式文档。

还有个功能是插入回原文。生成的目录一键插到文章开头,不用手动复制粘贴,插入位置可选。

最后是链接有效性提示。生成的锚点跟平台规则有关,导出时提示"发布后请点几个链接验证",避免目录全是死链。

还有个功能是层级校验。大纲里出现 #### 却没有 ###,说明层级可能写错了,工具提示一下。另外支持空标题过滤:只有 # 没有文字的行跳过,不然目录里会出现一个空链接。最后是选项:要不要包含一级标题,有的文章一级标题就是文章名,放进目录没意义。
另外支持生成不带链接的纯文本目录,有些平台不需要锚点链接,只要层级列表。还有个细节是目录里要不要包含序号,正式文档通常需要。
还有个细节是支持统计标题数量,文章分了几节一目了然。另外支持导出时选择要不要带链接,有些场合只需要纯文字的目录。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。