feat(plugin-md-power): add code-tree container and embed syntax, close #567 (#584)

* feat(plugin-md-power): add code-tree container and embed syntax, close #567

* chore: tweak
This commit is contained in:
pengzhanbo 2025-05-02 21:01:25 +08:00 committed by GitHub
parent 3b214f1b58
commit 31e3b41a27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 645 additions and 33 deletions

View File

@ -42,6 +42,7 @@ export const themeGuide = defineNoteConfig({
'card',
'steps',
'file-tree',
'code-tree',
'field',
'tabs',
'timeline',

View File

@ -31,6 +31,7 @@ export const theme: Theme = plumeTheme({
timeline: true,
collapse: true,
chat: true,
codeTree: true,
field: true,
imageSize: 'all',
pdf: true,

View File

@ -0,0 +1,170 @@
---
title: 代码树
icon: stash:side-peek
createTime: 2025/05/02 05:59:44
permalink: /guide/6smvgtbx/
badge: 新
---
## 概述
在 markdown 中,使用 `::: code-tress` 容器,或者使用 `@[code-tree](dir_path)`
可以显示一个带有文件树的代码块区域。
相比于 代码块分组,代码树 可以更加清晰地展示代码文件的组织结构,以及文件的依赖关系。
## 启用
该功能默认不启用,你需要在 `theme` 配置中启用。
```ts title=".vuepress/config.ts"
export default defineUserConfig({
theme: plumeTheme({
markdown: {
codeTree: true, // [!code ++]
}
})
})
```
## 使用
主题提供了 两种使用方式:
### code-tree 容器
````md
::: code-tree title="Project Name" height="400px" entry="filepath"
```lang title="filepath" :active
<!-- code content-->
```
```lang title="filepath"
<!-- code content-->
```
<!-- 更多代码块 -->
:::
````
使用 `::: code-tree` 容器包裹多个代码块。
- 在 `::: code-tree` 后使用 `title="Project Name"` 声明代码树的标题
- 在 `::: code-tree` 后使用 `height="400px"` 声明代码树的高度
- 在 `::: code-tree` 后使用 `entry="filepath"` 声明默认展开的文件路径
- 在代码块 <code>\`\`\` lang</code> 后使用 `title="filepath"` 声明当前代码块的文件路径
- 如果在 `::: code-tree` 未声明 `entry="filepath"`,可以在代码块 <code>\`\`\` lang</code> 后使用 `:active` 声明当前代码块为展开状态
- 如果未指定展开的文件路径,默认展开第一个文件
::: details 代码块上为什么是 `title="filepath"` 而不是 `filepath="filepath"` ?
因为主题已经在 [代码块上提供了标题语法的支持](../code/features.md#代码块标题) ,沿用已有的语法支持
可以减少学习成本。
:::
**输入:**
````md :collapsed-lines
::: code-tree title="Vue App" height="400px" entry="src/main.ts"
```vue title="src/components/HelloWorld.vue"
<template>
<div class="hello">
<h1>Hello World</h1>
</div>
</template>
```
```vue title="src/App.vue"
<template>
<div id="app">
<h3>vuepress-theme-plume</h3>
<HelloWorld />
</div>
</template>
```
```ts title="src/main.ts"
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
```
```json title="package.json"
{
"name": "Vue App",
"scripts": {
"dev": "vite"
}
}
```
:::
````
**输出:**
::: code-tree title="Vue App" height="400px" entry="src/main.ts"
```vue title="src/components/HelloWorld.vue"
<template>
<div class="hello">
<h1>Hello World</h1>
</div>
</template>
```
```vue title="src/App.vue"
<template>
<div id="app">
<h3>vuepress-theme-plume</h3>
<HelloWorld />
</div>
</template>
```
```ts title="src/main.ts"
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
```
```json title="package.json"
{
"name": "Vue App",
"scripts": {
"dev": "vite"
}
}
```
:::
### 从目录导入 code-tree
主题支持通过以下语法从目录导入 `code-tree`:
```md
<!-- 简单导入 -->
@[code-tree](dir_path)
<!-- 添加的配置 -->
@[code-tree title="Project Name" height="400px" entry="filepath"](dir_path)
```
- **dir_path**:
当传入绝对路径,即以 `/` 开头时,从文档站点的 源目录 开始查找。
当传入相对路径时,即以 `.` 开头时,表示相对于当前 markdown 文件。
- **title**: 代码树标题,可选,默认为空
- **height**: 代码树高度,可选,默认为空
- **entry**: 默认展开的文件路径,可选,默认为第一个文件
**输入:**
```md
<!-- 此目录为主题仓库 `docs/.vuepress/notes/` -->
@[code-tree title="Notes 配置" height="400px" entry="index.ts"](/.vuepress/notes)
```
**输出:**
@[code-tree title="Notes 配置" height="400px" entry="index.ts"](/.vuepress/notes)

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { resolveAttrs } from '../src/node/utils/resolveAttrs.js'
import { resolveAttr, resolveAttrs } from '../src/node/utils/resolveAttrs.js'
describe('resolveAttrs(info)', () => {
it('should resolve attrs', () => {
@ -10,6 +10,16 @@ describe('resolveAttrs(info)', () => {
attrs: { a: '1' },
})
expect(resolveAttrs('a=1 b=2 c')).toEqual({
rawAttrs: 'a=1 b=2 c',
attrs: { a: '1', b: '2', c: true },
})
expect(resolveAttrs('a=1 b=true c=false')).toEqual({
rawAttrs: 'a=1 b=true c=false',
attrs: { a: '1', b: true, c: false },
})
expect(resolveAttrs('a="1" b="2"')).toMatchObject({
rawAttrs: 'a="1" b="2"',
attrs: { a: '1', b: '2' },
@ -33,3 +43,13 @@ describe('resolveAttrs(info)', () => {
})
})
})
describe('resolveAttr(info, key)', () => {
it('should resolve attr', () => {
expect(resolveAttr('a="1"', 'a')).toEqual('1')
expect(resolveAttr('a="1"', 'b')).toEqual(undefined)
expect(resolveAttr('a=1', 'a')).toEqual('1')
expect(resolveAttr('a=\'1\'', 'a')).toEqual('1')
expect(resolveAttr('a', 'a')).toEqual(undefined)
})
})

View File

@ -87,6 +87,7 @@
"markdown-it-container": "catalog:prod",
"nanoid": "catalog:prod",
"shiki": "catalog:prod",
"tinyglobby": "catalog:prod",
"tm-grammars": "catalog:prod",
"tm-themes": "catalog:prod",
"vue": "catalog:prod"

View File

@ -9,6 +9,7 @@ const props = defineProps<{
diff?: 'add' | 'remove'
expanded?: boolean
focus?: boolean
filepath?: string
}>()
const activeFileTreeNode = inject<Ref<string>>('active-file-tree-node', ref(''))
@ -23,7 +24,7 @@ function nodeClick() {
if (props.filename === '…' || props.filename === '...')
return
onNodeClick(props.filename, props.type)
onNodeClick(props.filepath || props.filename, props.type)
}
function toggle(ev: MouseEvent) {
@ -47,7 +48,7 @@ function toggle(ev: MouseEvent) {
[type]: true,
focus,
expanded: type === 'folder' ? active : false,
active: type === 'file' ? activeFileTreeNode === filename : false,
active: type === 'file' ? activeFileTreeNode === filepath : false,
diff,
add: diff === 'add',
remove: diff === 'remove',

View File

@ -0,0 +1,159 @@
<script setup lang="ts">
import { onMounted, provide, ref, useTemplateRef, watch } from 'vue'
const props = withDefaults(defineProps<{
title?: string
height?: string
entryFile?: string
}>(), { height: '320px' })
const activeNode = ref(props.entryFile || '')
const isEmpty = ref(true)
const codePanel = useTemplateRef<HTMLDivElement>('codePanel')
provide('active-file-tree-node', activeNode)
provide('on-file-tree-node-click', (filepath: string, type: 'file' | 'folder') => {
if (type === 'file') {
activeNode.value = filepath
}
})
onMounted(() => {
watch(
() => activeNode.value,
() => {
if (codePanel.value) {
const items = Array.from(codePanel.value.querySelectorAll('.code-block-title'))
let hasActive = false
items.forEach((item) => {
if (item.getAttribute('data-title') === activeNode.value) {
item.classList.add('active')
hasActive = true
}
else {
item.classList.remove('active')
}
})
isEmpty.value = !hasActive
}
},
{ immediate: true },
)
})
</script>
<template>
<div class="vp-code-tree">
<div class="code-tree-panel" :style="{ 'max-height': props.height }">
<div v-if="title" class="code-tree-title" :title="title">
<span>{{ title }}</span>
</div>
<div class="vp-file-tree">
<slot name="file-tree" />
</div>
</div>
<div ref="codePanel" class="code-panel" :style="{ height: props.height }">
<slot />
<div v-if="isEmpty" class="code-tree-empty">
<span class="vpi-code-tree-empty" />
</div>
</div>
</div>
</template>
<style>
.vp-code-tree {
width: 100%;
margin: 16px 0;
overflow: hidden;
border: solid 1px var(--vp-c-divider);
border-radius: 6px;
}
@media (min-width: 768px) {
.vp-code-tree {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.vp-code-tree .code-tree-panel {
display: flex;
flex-direction: column;
border-bottom: solid 1px var(--vp-c-divider);
}
@media (min-width: 768px) {
.vp-code-tree .code-tree-panel {
border-right: solid 1px var(--vp-c-divider);
border-bottom: none;
}
}
.vp-code-tree .code-tree-panel .code-tree-title {
height: 40px;
padding: 0 16px;
overflow: hidden;
font-weight: 500;
line-height: 40px;
text-overflow: ellipsis;
white-space: nowrap;
border-bottom: solid 1px var(--vp-c-divider);
}
.vp-code-tree .code-tree-panel .vp-file-tree {
flex: 1 2;
margin: 0;
overflow: auto;
background-color: transparent;
border: none;
border-radius: 0;
}
.vp-code-tree .code-tree-panel .vp-file-tree .vp-file-tree-info.file {
cursor: pointer;
}
.vp-code-tree .code-panel {
grid-column: span 2 / span 2;
}
.vp-code-tree .code-panel [class*="language-"] {
flex: 1 2;
margin: 16px 0 0;
overflow: auto;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.vp-code-tree .code-panel .code-block-title {
display: none;
height: 100%;
}
.vp-code-tree .code-panel .code-block-title.active {
display: flex;
flex-direction: column;
}
.vp-code-tree .code-panel .code-block-title .code-block-title-bar {
margin-top: 0;
border-radius: 0;
}
.vp-code-tree .code-panel .code-tree-empty {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.vp-code-tree .code-panel .code-tree-empty .vpi-code-tree-empty {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256' viewBox='0 0 256 256'%3E%3Cpath fill='%23000' d='m198.24 62.63l15.68-17.25a8 8 0 0 0-11.84-10.76L186.4 51.86A95.95 95.95 0 0 0 57.76 193.37l-15.68 17.25a8 8 0 1 0 11.84 10.76l15.68-17.24A95.95 95.95 0 0 0 198.24 62.63M48 128a80 80 0 0 1 127.6-64.25l-107 117.73A79.63 79.63 0 0 1 48 128m80 80a79.55 79.55 0 0 1-47.6-15.75l107-117.73A79.95 79.95 0 0 1 128 208'/%3E%3C/svg%3E");
width: 128px;
height: 128px;
color: var(--vp-c-default-soft);
}
</style>

View File

@ -0,0 +1,227 @@
/**
* @module CodeTree
*
* code-tree
* ````md
* ::: code-tree title="Project Name" height="400px" entry="filepath"
* ``` lang :active title="filepath"
* ```
* <!-- more code block -->
* :::
* ````
*
* embed syntax
*
* `@[code-tree title="Project Name" height="400px" entry="filepath"](dir_path)`
*/
import type { App, Page } from 'vuepress/core'
import type { Markdown } from 'vuepress/markdown'
import type { CodeTreeOptions } from '../../shared/codeTree.js'
import type { FileTreeIconMode } from '../../shared/fileTree.js'
import type { FileTreeNodeProps } from './fileTree.js'
import path from 'node:path'
import { globSync } from 'tinyglobby'
import { removeLeadingSlash } from 'vuepress/shared'
import { findFile, readFileSync } from '../demo/supports/file.js'
import { createEmbedRuleBlock } from '../embed/createEmbedRuleBlock.js'
import { defaultFile, defaultFolder, getFileIcon } from '../fileIcons/index.js'
import { parseRect } from '../utils/parseRect.js'
import { resolveAttr, resolveAttrs } from '../utils/resolveAttrs.js'
import { stringifyAttrs } from '../utils/stringifyAttrs.js'
import { createContainerPlugin } from './createContainer.js'
const UNSUPPORTED_FILE_TYPES = [
/* image */
'jpg',
'jpeg',
'png',
'gif',
'avif',
'webp',
/* media */
'mp3',
'mp4',
'ogg',
'm3u8',
'm3u',
'flv',
'webm',
'wav',
'flac',
'aac',
/* document */
'pdf',
'doc',
'docx',
'ppt',
'pptx',
'xls',
'xlsx',
]
interface CodeTreeMeta {
title?: string
/**
*
*/
icon?: FileTreeIconMode
/**
*
*/
height?: string
/**
*
*/
entry?: string
}
interface FileTreeNode {
level: number
children?: FileTreeNode[]
filename: string
filepath?: string
}
function parseFileNodes(files: string[]): FileTreeNode[] {
const nodes: FileTreeNode[] = []
for (const file of files) {
const parts = removeLeadingSlash(file).split('/')
let node = nodes
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
const isFile = i === parts.length - 1
let child = node.find(n => n.filename === part)
if (!child) {
child = {
level: i + 1,
filename: part,
filepath: isFile ? file : undefined,
children: isFile ? undefined : [],
}
node.push(child)
}
if (!isFile && child.children)
node = child.children
}
}
return nodes
}
export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions = {}) {
const getIcon = (filename: string, type: 'folder' | 'file', mode?: FileTreeIconMode): string => {
mode ||= options.icon || 'colored'
if (mode === 'simple')
return type === 'folder' ? defaultFolder : defaultFile
return getFileIcon(filename, type)
}
function renderFileTree(nodes: FileTreeNode[], mode?: FileTreeIconMode): string {
return nodes.map((node) => {
const props: FileTreeNodeProps & { filepath?: string } = {
filename: node.filename,
level: node.level,
type: node.children?.length ? 'folder' : 'file',
expanded: true,
filepath: node.filepath,
}
return `<FileTreeNode${stringifyAttrs(props)}>
<template #icon><VPIcon name="${getIcon(node.filename, props.type, mode)}" /></template>
${node.children?.length ? renderFileTree(node.children, mode) : ''}
</FileTreeNode>`
})
.join('\n')
}
createContainerPlugin(md, 'code-tree', {
before: (info, tokens, index) => {
const files: string[] = []
let activeFile: string | undefined
for (
let i = index + 1;
!(
tokens[i].nesting === -1
&& tokens[i].type === 'container_code-tree_close'
);
i++
) {
const token = tokens[i]
if (token.type === 'fence' && token.tag === 'code') {
const fenceInfo = md.utils.unescapeAll(token.info)
const title = resolveAttr(fenceInfo, 'title')
if (title) {
files.push(title)
if (fenceInfo.includes(':active'))
activeFile = title
}
}
}
const { attrs } = resolveAttrs<CodeTreeMeta>(info)
const { title, icon, height, entry } = attrs
const fileTreeNodes = parseFileNodes(files)
const entryFile = activeFile || entry || files[0]
const h = height || String(options.height)
return `<VPCodeTree${stringifyAttrs({ title, entryFile, height: h ? parseRect(h) : undefined })}><template #file-tree>${
renderFileTree(fileTreeNodes, icon)
}</template>`
},
after: () => '</VPCodeTree>',
})
createEmbedRuleBlock(md, {
type: 'code-tree',
syntaxPattern: /^@\[code-tree([^\]]*)\]\(([^)]*)\)/,
meta: ([, info, dir]) => {
const { attrs } = resolveAttrs<CodeTreeMeta>(info)
const h = attrs.height || String(options.height)
return {
title: attrs.title,
entryFile: attrs.entry,
icon: attrs.icon,
height: h ? parseRect(h) : undefined,
dir,
}
},
content: ({ dir, icon, ...props }, _, env) => {
const codeTreeFiles = ((env as any).codeTreeFiles ??= []) as string[]
const root = findFile(app, env, dir)
const files = globSync('**/*', {
cwd: root,
onlyFiles: true,
dot: true,
ignore: ['**/node_modules/**', '**/.DS_Store', '**/.gitkeep'],
}).sort((a, b) => {
const al = a.split('/').length
const bl = b.split('/').length
return bl - al
})
props.entryFile ||= files[0]
const codeContent = files.map((file) => {
const ext = path.extname(file).slice(1)
if (UNSUPPORTED_FILE_TYPES.includes(ext)) {
return ''
}
const filepath = path.join(root, file)
codeTreeFiles.push(filepath)
const content = readFileSync(filepath)
return `\`\`\`${ext || 'txt'} title="${file}"\n${content}\n\`\`\``
}).filter(Boolean).join('\n')
const fileTreeNodes = parseFileNodes(files)
return `<VPCodeTree${stringifyAttrs(props)}><template #file-tree>${
renderFileTree(fileTreeNodes, icon)
}</template>${md.render(codeContent)}</VPCodeTree>`
},
})
}
export function extendsPageWithCodeTree(page: Page): void {
const markdownEnv = page.markdownEnv
const codeTreeFiles = (markdownEnv.codeTreeFiles ?? []) as string[]
if (codeTreeFiles.length)
page.deps.push(...codeTreeFiles)
}

View File

@ -16,7 +16,7 @@ interface FileTreeAttrs {
icon?: FileTreeIconMode
}
interface FileTreeNodeProps {
export interface FileTreeNodeProps {
filename: string
comment?: string
focus?: boolean

View File

@ -6,6 +6,7 @@ import { alignPlugin } from './align.js'
import { cardPlugin } from './card.js'
import { chatPlugin } from './chat.js'
import { codeTabs } from './codeTabs.js'
import { codeTreePlugin } from './codeTree.js'
import { collapsePlugin } from './collapse.js'
import { demoWrapperPlugin } from './demoWrapper.js'
import { fieldPlugin } from './field.js'
@ -51,6 +52,10 @@ export async function containerPlugin(
fileTreePlugin(md, isPlainObject(options.fileTree) ? options.fileTree : {})
}
if (options.codeTree) {
codeTreePlugin(md, app, isPlainObject(options.codeTree) ? options.codeTree : {})
}
if (options.timeline)
timelinePlugin(md)

View File

@ -2,6 +2,7 @@ import type { Plugin } from 'vuepress/core'
import type { MarkdownPowerPluginOptions } from '../shared/index.js'
import { addViteOptimizeDepsInclude } from '@vuepress/helper'
import { isPackageExists } from 'local-pkg'
import { extendsPageWithCodeTree } from './container/codeTree.js'
import { containerPlugin } from './container/index.js'
import { demoPlugin, demoWatcher, extendsPageWithDemo, waitDemoRender } from './demo/index.js'
import { embedSyntaxPlugin } from './embed/index.js'
@ -68,6 +69,9 @@ export function markdownPowerPlugin(
extendsPage: (page) => {
if (options.demo)
extendsPageWithDemo(page)
if (options.codeTree)
extendsPageWithCodeTree(page)
},
}
}

View File

@ -70,11 +70,16 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
enhances.add(`app.component('CanIUseViewer', CanIUse)`)
}
if (options.fileTree) {
if (options.fileTree || options.codeTree) {
imports.add(`import FileTreeNode from '${CLIENT_FOLDER}components/FileTreeNode.vue'`)
enhances.add(`app.component('FileTreeNode', FileTreeNode)`)
}
if (options.codeTree) {
imports.add(`import VPCodeTree from '${CLIENT_FOLDER}components/VPCodeTree.vue'`)
enhances.add(`app.component('VPCodeTree', VPCodeTree)`)
}
if (options.artPlayer) {
imports.add(`import ArtPlayer from '${CLIENT_FOLDER}components/ArtPlayer.vue'`)
enhances.add(`app.component('ArtPlayer', ArtPlayer)`)

View File

@ -1,6 +1,6 @@
import { camelCase } from '@pengzhanbo/utils'
const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w-]+)(?:=\s*(?<quote>['"])(?<value>.+?)\k<quote>)?(?:\s+|$)/
const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w-]+)(?:=(?<quote>['"])(?<valueWithQuote>.+?)\k<quote>|=(?<valueWithoutQuote>\S+))?(?:\s+|$)/
export function resolveAttrs<T extends Record<string, any> = Record<string, any>>(info: string): {
attrs: T
@ -18,7 +18,8 @@ export function resolveAttrs<T extends Record<string, any> = Record<string, any>
// eslint-disable-next-line no-cond-assign
while (matched = info.match(RE_ATTR_VALUE)) {
const { attr, value = true } = matched.groups!
const { attr, valueWithQuote, valueWithoutQuote } = matched.groups!
const value = valueWithQuote || valueWithoutQuote || true
let v = typeof value === 'string' ? value.trim() : value
if (v === 'true')
v = true
@ -31,3 +32,9 @@ export function resolveAttrs<T extends Record<string, any> = Record<string, any>
return { attrs: attrs as T, rawAttrs }
}
export function resolveAttr(info: string, key: string): string | undefined {
const pattern = new RegExp(`(?:^|\\s+)${key}(?:=(?<quote>['"])(?<valueWithQuote>.+?)\\k<quote>|=(?<valueWithoutQuote>\\S+))?(?:\\s+|$)`)
const groups = info.match(pattern)?.groups
return groups?.valueWithQuote || groups?.valueWithoutQuote
}

View File

@ -0,0 +1,6 @@
import type { FileTreeIconMode } from './fileTree'
export interface CodeTreeOptions {
icon?: FileTreeIconMode
height?: string | number
}

View File

@ -1,5 +1,6 @@
import type { CanIUseOptions } from './caniuse.js'
import type { CodeTabsOptions } from './codeTabs.js'
import type { CodeTreeOptions } from './codeTree.js'
import type { FileTreeOptions } from './fileTree.js'
import type { IconsOptions } from './icons.js'
import type { NpmToOptions } from './npmTo.js'
@ -191,6 +192,21 @@ export interface MarkdownPowerPluginOptions {
*/
fileTree?: boolean | FileTreeOptions
/**
*
*
* ```md
* ::: code-tree
* :::
* ```
*
* `@[code-tree](file_path)`
*
*
* @default false
*/
codeTree?: boolean | CodeTreeOptions
/**
* demo
*/

33
pnpm-lock.yaml generated
View File

@ -278,6 +278,9 @@ catalogs:
shiki:
specifier: ^3.3.0
version: 3.3.0
tinyglobby:
specifier: 0.2.13
version: 0.2.13
tm-grammars:
specifier: ^1.23.16
version: 1.23.16
@ -645,6 +648,9 @@ importers:
stylus:
specifier: catalog:dev
version: 0.64.0
tinyglobby:
specifier: catalog:prod
version: 0.2.13
tm-grammars:
specifier: catalog:prod
version: 1.23.16
@ -4217,14 +4223,6 @@ packages:
fault@2.0.1:
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
fdir@6.4.3:
resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fdir@6.4.4:
resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
peerDependencies:
@ -6579,10 +6577,6 @@ packages:
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyglobby@0.2.12:
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
engines: {node: '>=12.0.0'}
tinyglobby@0.2.13:
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
engines: {node: '>=12.0.0'}
@ -9698,7 +9692,7 @@ snapshots:
package-manager-detector: 1.1.0
semver: 7.7.1
tinyexec: 0.3.2
tinyglobby: 0.2.12
tinyglobby: 0.2.13
yaml: 2.7.0
transitivePeerDependencies:
- magicast
@ -10671,7 +10665,7 @@ snapshots:
jsonc-eslint-parser: 2.4.0
pathe: 2.0.3
pnpm-workspace-yaml: 0.3.1
tinyglobby: 0.2.12
tinyglobby: 0.2.13
yaml-eslint-parser: 1.3.0
eslint-plugin-regexp@2.7.0(eslint@9.25.1(jiti@2.4.2)):
@ -10927,10 +10921,6 @@ snapshots:
dependencies:
format: 0.2.2
fdir@6.4.3(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
fdir@6.4.4(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
@ -13515,11 +13505,6 @@ snapshots:
tinyexec@0.3.2: {}
tinyglobby@0.2.12:
dependencies:
fdir: 6.4.3(picomatch@4.0.2)
picomatch: 4.0.2
tinyglobby@0.2.13:
dependencies:
fdir: 6.4.4(picomatch@4.0.2)
@ -13593,7 +13578,7 @@ snapshots:
source-map: 0.8.0-beta.0
sucrase: 3.35.0
tinyexec: 0.3.2
tinyglobby: 0.2.12
tinyglobby: 0.2.13
tree-kill: 1.2.2
optionalDependencies:
postcss: 8.5.3

View File

@ -110,6 +110,7 @@ catalogs:
package-manager-detector: ^1.2.0
picocolors: ^1.1.1
shiki: ^3.3.0
tinyglobby: 0.2.13
tm-grammars: ^1.23.16
tm-themes: ^1.10.5
unplugin: ^2.3.2

View File

@ -115,13 +115,15 @@ html:not([data-theme="dark"]) .vp-code span {
.vp-doc div[class*="language-"].line-numbers-mode .line-numbers {
position: absolute;
top: 0;
bottom: 0;
/* rtl:ignore */
left: 0;
z-index: 3;
width: 32px;
height: fit-content;
min-height: 100%;
padding-top: 20px;
padding-bottom: 20px;
font-family: var(--vp-font-family-mono);
font-size: var(--vp-code-font-size);
line-height: var(--vp-code-line-height);

View File

@ -47,6 +47,7 @@ export const MARKDOWN_POWER_FIELDS: (keyof MarkdownPowerPluginOptions)[] = [
'caniuse',
'codeSandbox',
'codeTabs',
'codeTree',
'codepen',
'demo',
'fileTree',

View File

@ -43,7 +43,7 @@ export function codePlugins(pluginOptions: ThemeBuiltinPlugins): PluginConfig {
langs: uniq([...twoslash ? ['ts', 'js', 'vue', 'json', 'bash', 'sh'] : [], ...langs]),
codeBlockTitle: (title, code) => {
const icon = getIcon(title)
return `<div class="code-block-title"><div class="code-block-title-bar"><span class="title">${icon ? `<VPIcon name="${icon}"/>` : ''}${title}</span></div>${code}</div>`
return `<div class="code-block-title" data-title="${title}"><div class="code-block-title-bar"><span class="title">${icon ? `<VPIcon name="${icon}"/>` : ''}${title}</span></div>${code}</div>`
},
twoslash: isPlainObject(twoslashOptions)
? {