之前的博客目录存在一些BUG,如同名标题跳转问题以及正则解析标题可能会将代码块注释也解析成标题。

同名标题跳转问题

这个使用markdown-it-anchor插件在标题渲染成html的时候添加一个唯一id,插件内置唯一id策略当slugify的值相同时候,则返回slugify-{idx}

vue
.use(anchor, { slugify: s => s.trim().toLowerCase().replace(/\s+/g, '-') }) // 标题锚点支持

�所以另一边解析目录结构的时候也要配合解析出id

vue
 const usedIds = new Set()
  for (const line of lines) {
    const match = line.match(/^(#{1,6})\s+(.*)/)
    if (match) {
      const level = match[1].length
      const text = match[2].trim()
      const slug = text.toLowerCase().replace(/\s+/g, '-')
      let uniqueSlug = slug
      let i = 1
      while (usedIds.has(uniqueSlug)) {
        uniqueSlug = `${slug}-${i++}`
      }
      usedIds.add(uniqueSlug)
      const node : TocNode = { level, text, id: uniqueSlug, children: [] }

这样之前用textcotent来做MapKey就可以换成id了。

解析标题问题

直接使用markdown-it解析AST,避免正则再去判断#号开头。

vue
const md = new MarkdownIt()

// 生成 TOC 树结构
const generateTocTree = (markdown : string) : TocNode[] => {
  const tokens = md.parse(markdown, {})
  const toc: TocNode[] = []
  const stack: TocNode[] = []
  const usedIds = new Set<string>()

  for (let i = 0; i < tokens.length; i++) {
    const token = tokens[i]
    if (token.type === 'heading_open') {
      const level = parseInt(token.tag.slice(1)) // e.g. 'h2' -> 2
      const inlineToken = tokens[i + 1]
      if (inlineToken && inlineToken.type === 'inline') {
        const text = inlineToken.content.trim()
        let slug = text.toLowerCase().replace(/\s+/g, '-')
        let uniqueSlug = slug
        let j = 1
        while (usedIds.has(uniqueSlug)) {
          uniqueSlug = `${slug}-${j++}`
        }
        usedIds.add(uniqueSlug)

        const node: TocNode = { level, text, id: uniqueSlug, children: [] }

        while (stack.length && stack[stack.length - 1].level >= level) {
          stack.pop()
        }

        if (stack.length === 0) {
          toc.push(node)
        } else {
          stack[stack.length - 1].children.push(node)
        }

        stack.push(node)
      }
    }
  }

  return toc
}