自定义 container_code-group

先看看 vitepress 的源码

js
function createCodeGroup(options, md) {
  return [
    container_plugin,
    'code-group',
    {
      render(tokens, idx) {
        if (tokens[idx].nesting === 1) {
          const name = nanoid$1(5)
          let tabs = ''
          let checked = 'checked'
          for (let i = idx + 1; !(tokens[i].nesting === -1 && tokens[i].type === 'container_code-group_close'); ++i) {
            const isHtml = tokens[i].type === 'html_block'
            if ((tokens[i].type === 'fence' && tokens[i].tag === 'code') || isHtml) {
              const title = extractTitle(
                isHtml ? tokens[i].content : tokens[i].info,
                isHtml
              )
              if (title) {
                const id = nanoid$1(7)
                tabs += `<input type="radio" name="group-${name}" id="tab-${id}" ${checked}><label data-title="${md.utils.escapeHtml(title)}" for="tab-${id}">${title}</label>`
                if (checked && !isHtml)
tokens[i].info += ' active'
                checked = ''
              }
            }
          }
          return `<div class="vp-code-group${getAdaptiveThemeMarker(
            options
          )}"><div class="tabs">${tabs}</div><div class="blocks">
`
        }
        return `</div></div>
`
      }
    }
  ]
}

代码分析

  1. token type 的开始和结束是 container_code-group_opencontainer_code-group_close

  2. 拿到这两个 token 之间的所有 token,然后根据他们 token 的类型来判断是 fence 和 html_block

  3. 获取 title,然后生成 tabs 和 blocks,最后返回 html 字符串

自定义 code_group

  1. 由于我是用 naive-ui 来做的主题,所以这种 tab 切换的效果我是用 naive-uin-tabs 来实现的

  2. 但是 n-tabs 是需要 n-tab-pane 来配合使用的, 所以我需要找到包裹在 container_code-group_opencontainer_code-group_close 里面的 fence 和 html_block token,然后使用 n-tab-pane 包裹这两个类型的 token

  3. 由于本人水平实在有限,没有认真研究 markdown-it 里面的 ruler/token 的用法, (不知道 Ruler.before/after 能不能够实现)

  4. 已下是我变通的实践

ts
/*
 * @Author       : peter peter@qingcongai.com
 * @Date         : 2024-11-30 10:22:15
 * @LastEditors  : peter peter@qingcongai.com
 * @LastEditTime : 2024-12-06 17:35:08
 * @Description  :
 */
import type { MarkdownIt } from './index.ts'

import { extractTitle } from '../../utils/index.ts'

export default (md: MarkdownIt) => {
  md.renderer.rules['container_code-group_open'] = (tokens, idx) => {
    const closeTokenIndex = tokens.findIndex((token, index) => {
      return index > idx && token.type === 'container_code-group_close'
    })
    const childTokens = tokens
      .slice(idx + 1, closeTokenIndex)
      .filter((t) => t.level === tokens[idx].level + 1)
    childTokens.forEach((t) => {
      const isHtml = t.type === 'html_block'
      const title = extractTitle(isHtml ? t.content : t.info, isHtml)
      t.attrSet('tabName', title)
    })
    return `<n-card
      :class="isMobile ? '-mx-3 !w-auto' : ''"
      class="code_group_card"
      embedded
      :bordered="false"
      content-class="!p-0"
    >
      <n-tabs type="line" animated>
    `
  }
  md.renderer.rules['container_code-group_close'] = () => {
    return `</n-tabs>
      </n-card>`
  }
}

代码解释

  1. 找到所有的直接子 token(看上面的源码也就知道只有 fencehtml_block两种)

  2. 根据子 token 来生成 tab title, 并设置给 tokentabName attr

自定义 html_blockfence

tabName attr,就用 n-tab-pane 包裹

html_block

ts
/*
 * @Author       : peter peter@qingcongai.com
 * @Date         : 2024-11-23 10:49:38
 * @LastEditors  : peter peter@qingcongai.com
 * @LastEditTime : 2024-12-03 14:03:29
 * @Description  :
 */
import type { MarkdownIt } from './index.ts'

export default (md: MarkdownIt) => {
  const defaultRender = md.renderer.rules.html_block
  md.renderer.rules.html_block = (...arg) => {
    const [tokens, idx] = arg
    const token = tokens[idx]
    const tabName = token.attrGet('tabName')
    token.attrSet('tabName', '')
    let prev = ''
    let post = ''
    if (tabName) {
      prev = `<n-tab-pane name="${tabName}" tab="${tabName}">`
      post = '</n-tab-pane>'
    }
    return `${prev}${defaultRender!(...arg)}${post}`
  }
}

fence

ts
/*
 * @Author       : peter peter@qingcongai.com
 * @Date         : 2024-11-23 10:49:38
 * @LastEditors  : huchaomin iisa_peter@163.com
 * @LastEditTime : 2024-12-01 23:35:01
 * @Description  :
 */
import { parse } from 'node-html-parser'

import type { MarkdownIt } from './index.ts'

export default (md: MarkdownIt) => {
  const defaultRender = md.renderer.rules.fence
  md.renderer.rules.fence = (...arg) => {
    const result = defaultRender!(...arg)
    const root = parse(result).clone() as unknown as HTMLElement
    root.querySelector('button.copy')?.remove()
    root.classList.remove('vp-adaptive-theme')
    const [tokens, idx] = arg
    const token = tokens[idx]
    const tabName = token.attrGet('tabName')
    token.attrSet('tabName', '')
    let prev = ''
    let post = ''
    if (tabName) {
      prev = `<n-tab-pane name="${tabName}" tab="${tabName}">`
      post = '</n-tab-pane>'
    }
    return `${prev}<FenceWrapper :inCodeGroup=${!!tabName} content="${md.utils
      .escapeHtml(tokens[idx].content)
      .replace(/\/\/ \[!code .*\]/g, '')
      .trim()}">${root.outerHTML}</FenceWrapper>${post}`
  }
}

结果

md
::: code-group
<h1 data-title="我是标题">这里是html_block</h1>

\```ts-vue [frontmatter]
{{ $frontmatter }}
\```

:::
我是标题
frontmatter

这里是html_block