vitepress自定义代码组
前言
vitepress中的markdown扩展中的代码组(code-group)在多文件或多语言显示时非常好用,比如代码是下面这样:
显示出来就会是这样:
这简单方便颜值高,但是,我点其中一个go语言标签页时,想要其他同类型的go语言标签页也能一起激活,怎么办?
源码追踪
网页F12打开开发者模式,定位代码组的Elements元素,发现代码组都包裹在class="vp-code-group"的元素里面。在vitepress源码上搜索,找到src/client/app/composables/codeGroups.ts文件有相关匹配。其代码如下:
import { inBrowser } from 'vitepress'
export function useCodeGroups() {
if (inBrowser) {
window.addEventListener('click', (e) => {
const el = e.target as HTMLInputElement
if (el.matches('.vp-code-group input')) {
// input <- .tabs <- .vp-code-group
const group = el.parentElement?.parentElement
const i = Array.from(group?.querySelectorAll('input') || []).indexOf(el)
const current = group?.querySelector('div[class*="language-"].active')
const next = group?.querySelectorAll(
'div[class*="language-"]:not(.language-id)'
)?.[i]
if (current && next && current !== next) {
current.classList.remove('active')
next.classList.add('active')
}
}
})
}
}
结合元素,可以知道,代码是通过匹配.vp-code-group input元素来知道和切换代码组的标签页的。再通过移除掉旧的激活的标签页代码块div[class*="language-"].active元素中的active,添加active class 到点击的标签页对应的代码块元素上。这样实现了代码组中切换标签页,对应的代码块跟着联动。
实现原理
实现原理就是修改vitepress源码中src/client/app/composables/codeGroups.ts这个文件中的点击逻辑。开玩笑啦,除非是向vitepress提交一个pr,但是这种交互不一定能采纳,整个流程下来到能用上也是很慢。那怎么在vitepress项目上改动这部分逻辑到能用上这个多代码组同语言甚至是同文件名的同时切换呢?
翻阅vitepress官方文档,并没有找到答案。但是,找到扩展默认主题-注册全家组件 这部分文档。觉得可以通过这个来实现。
目录及文件创建
首先,我们在vitepress项目中的.vitepress目录下新建theme目录,并在该目录里面新建index.ts文件。对应源码目录结构,我们再在该目录下新建composables目录,composables目录里面新建codeGroups.ts文件。
相关代码实现
.vitepress/theme/index.ts文件内容:
import DefaultTheme from 'vitepress/theme'
import { useCodeGroups } from './composables/codeGroups'
export default {
extends: DefaultTheme,
enhanceApp(ctx) {
useCodeGroups()
}
}
.vitepress/theme/composables/codeGroups.ts文件内容:
import { inBrowser } from 'vitepress'
export function useCodeGroups() {
if (inBrowser) {
window.addEventListener('click', (e) => {
const el = e.target as HTMLInputElement
if (el.matches('.vp-code-group input')) {
const group = el.parentElement?.parentElement
const i = Array.from(group?.querySelectorAll('input') || []).indexOf(el)
// tabname/filename
const tabname = el.nextElementSibling?.textContent
// language
const block = group?.querySelectorAll('div[class*="language-"]:not(.language-id)')?.[i]
const classList = Array.from(block?.classList || [])
const language = classList.find(v => v.startsWith('language-'))
Array.prototype.forEach.call(document ?.querySelectorAll('.vp-code-group'), (group, gidx) => {
// ------------ tabname/filename ---------------------
Array.from(group.querySelectorAll('input')).forEach((fel, fidx) => {
if (fel?.nextElementSibling?.textContent === tabname) {
if (fel !== el) fel.click()
}
})
// ------------ tabname/filename ---------------------
// ------------ language ---------------------
const current = group?.querySelector('div[class*="language-"].active')
Array.from(group?.querySelectorAll('div[class*="language-"]:not(.language-id)')).forEach((next, nidx) => {
// language
if (next?.classList.contains(language)) {
if (current && next && current !== next) {
current.classList.remove('active')
next.classList.add('active')
}
const fel = group.querySelectorAll('input')[nidx]
if (fel !== el) fel.click()
}
})
// ------------ language ---------------------
})
}
})
}
}
如此,跑起来切换下标签页,交互OK,大功告成!
实现逻辑
实现逻辑简单来讲就是通过点击的标签页,找到该点击标签页的标签名(文件名),和对应的代码块的语言,然后遍历找到所有的同名标签名(文件名)和同语言的代码块,然后激活他们。
当然,同语言同时切换这个场景常见,同标签名(文件名)这个场景不常见,可以把下面这部分代码注释掉。
// ------------ tabname/filename ---------------------
Array.from(group.querySelectorAll('input')).forEach((fel, fidx) => {
if (fel?.nextElementSibling?.textContent === tabname) {
if (fel !== el) fel.click()
}
})
// ------------ tabname/filename ---------------------