From 31e3b41a2706e9adb3470a56da9d8822e88eeb96 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Fri, 2 May 2025 21:01:25 +0800 Subject: [PATCH] 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 --- docs/.vuepress/notes/zh/theme-guide.ts | 1 + docs/.vuepress/theme.ts | 1 + docs/notes/theme/guide/markdown/code-tree.md | 170 +++++++++++++ .../__test__/resolveAttrs.spec.ts | 22 +- plugins/plugin-md-power/package.json | 1 + .../src/client/components/FileTreeNode.vue | 5 +- .../src/client/components/VPCodeTree.vue | 159 ++++++++++++ .../src/node/container/codeTree.ts | 227 ++++++++++++++++++ .../src/node/container/fileTree.ts | 2 +- .../src/node/container/index.ts | 5 + plugins/plugin-md-power/src/node/plugin.ts | 4 + .../src/node/prepareConfigFile.ts | 7 +- .../src/node/utils/resolveAttrs.ts | 11 +- .../plugin-md-power/src/shared/codeTree.ts | 6 + plugins/plugin-md-power/src/shared/plugin.ts | 16 ++ pnpm-lock.yaml | 33 +-- pnpm-workspace.yaml | 1 + theme/src/client/styles/code.css | 4 +- theme/src/node/detector/fields.ts | 1 + theme/src/node/plugins/code.ts | 2 +- 20 files changed, 645 insertions(+), 33 deletions(-) create mode 100644 docs/notes/theme/guide/markdown/code-tree.md create mode 100644 plugins/plugin-md-power/src/client/components/VPCodeTree.vue create mode 100644 plugins/plugin-md-power/src/node/container/codeTree.ts create mode 100644 plugins/plugin-md-power/src/shared/codeTree.ts diff --git a/docs/.vuepress/notes/zh/theme-guide.ts b/docs/.vuepress/notes/zh/theme-guide.ts index 7ee8314c..70859eea 100644 --- a/docs/.vuepress/notes/zh/theme-guide.ts +++ b/docs/.vuepress/notes/zh/theme-guide.ts @@ -42,6 +42,7 @@ export const themeGuide = defineNoteConfig({ 'card', 'steps', 'file-tree', + 'code-tree', 'field', 'tabs', 'timeline', diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 2ca89ef2..5e2b56b9 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -31,6 +31,7 @@ export const theme: Theme = plumeTheme({ timeline: true, collapse: true, chat: true, + codeTree: true, field: true, imageSize: 'all', pdf: true, diff --git a/docs/notes/theme/guide/markdown/code-tree.md b/docs/notes/theme/guide/markdown/code-tree.md new file mode 100644 index 00000000..66ad41f6 --- /dev/null +++ b/docs/notes/theme/guide/markdown/code-tree.md @@ -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 + +``` + +```lang title="filepath" + +``` + +::: +```` + +使用 `::: code-tree` 容器包裹多个代码块。 + +- 在 `::: code-tree` 后使用 `title="Project Name"` 声明代码树的标题 +- 在 `::: code-tree` 后使用 `height="400px"` 声明代码树的高度 +- 在 `::: code-tree` 后使用 `entry="filepath"` 声明默认展开的文件路径 +- 在代码块 \`\`\` lang 后使用 `title="filepath"` 声明当前代码块的文件路径 +- 如果在 `::: code-tree` 未声明 `entry="filepath"`,可以在代码块 \`\`\` lang 后使用 `: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" + +``` + +```vue title="src/App.vue" + +``` + +```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" + +``` + +```vue title="src/App.vue" + +``` + +```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 + +@[code-tree title="Notes 配置" height="400px" entry="index.ts"](/.vuepress/notes) +``` + +**输出:** + +@[code-tree title="Notes 配置" height="400px" entry="index.ts"](/.vuepress/notes) diff --git a/plugins/plugin-md-power/__test__/resolveAttrs.spec.ts b/plugins/plugin-md-power/__test__/resolveAttrs.spec.ts index 4a3343e2..a3fa44ab 100644 --- a/plugins/plugin-md-power/__test__/resolveAttrs.spec.ts +++ b/plugins/plugin-md-power/__test__/resolveAttrs.spec.ts @@ -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) + }) +}) diff --git a/plugins/plugin-md-power/package.json b/plugins/plugin-md-power/package.json index fb219aad..7d0926b6 100644 --- a/plugins/plugin-md-power/package.json +++ b/plugins/plugin-md-power/package.json @@ -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" diff --git a/plugins/plugin-md-power/src/client/components/FileTreeNode.vue b/plugins/plugin-md-power/src/client/components/FileTreeNode.vue index 4c98f286..c84b4592 100644 --- a/plugins/plugin-md-power/src/client/components/FileTreeNode.vue +++ b/plugins/plugin-md-power/src/client/components/FileTreeNode.vue @@ -9,6 +9,7 @@ const props = defineProps<{ diff?: 'add' | 'remove' expanded?: boolean focus?: boolean + filepath?: string }>() const activeFileTreeNode = inject>('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', diff --git a/plugins/plugin-md-power/src/client/components/VPCodeTree.vue b/plugins/plugin-md-power/src/client/components/VPCodeTree.vue new file mode 100644 index 00000000..b256c0bc --- /dev/null +++ b/plugins/plugin-md-power/src/client/components/VPCodeTree.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/plugins/plugin-md-power/src/node/container/codeTree.ts b/plugins/plugin-md-power/src/node/container/codeTree.ts new file mode 100644 index 00000000..40924860 --- /dev/null +++ b/plugins/plugin-md-power/src/node/container/codeTree.ts @@ -0,0 +1,227 @@ +/** + * @module CodeTree + * + * code-tree 容器 + * ````md + * ::: code-tree title="Project Name" height="400px" entry="filepath" + * ``` lang :active title="filepath" + * ``` + * + * ::: + * ```` + * + * 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 ` + + ${node.children?.length ? renderFileTree(node.children, mode) : ''} +` + }) + .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(info) + const { title, icon, height, entry } = attrs + const fileTreeNodes = parseFileNodes(files) + const entryFile = activeFile || entry || files[0] + const h = height || String(options.height) + return `` + }, + after: () => '', + }) + + createEmbedRuleBlock(md, { + type: 'code-tree', + syntaxPattern: /^@\[code-tree([^\]]*)\]\(([^)]*)\)/, + meta: ([, info, dir]) => { + const { attrs } = resolveAttrs(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 `${md.render(codeContent)}` + }, + }) +} + +export function extendsPageWithCodeTree(page: Page): void { + const markdownEnv = page.markdownEnv + const codeTreeFiles = (markdownEnv.codeTreeFiles ?? []) as string[] + if (codeTreeFiles.length) + page.deps.push(...codeTreeFiles) +} diff --git a/plugins/plugin-md-power/src/node/container/fileTree.ts b/plugins/plugin-md-power/src/node/container/fileTree.ts index c030a4ad..8180853c 100644 --- a/plugins/plugin-md-power/src/node/container/fileTree.ts +++ b/plugins/plugin-md-power/src/node/container/fileTree.ts @@ -16,7 +16,7 @@ interface FileTreeAttrs { icon?: FileTreeIconMode } -interface FileTreeNodeProps { +export interface FileTreeNodeProps { filename: string comment?: string focus?: boolean diff --git a/plugins/plugin-md-power/src/node/container/index.ts b/plugins/plugin-md-power/src/node/container/index.ts index 4a3bd13f..eb7facde 100644 --- a/plugins/plugin-md-power/src/node/container/index.ts +++ b/plugins/plugin-md-power/src/node/container/index.ts @@ -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) diff --git a/plugins/plugin-md-power/src/node/plugin.ts b/plugins/plugin-md-power/src/node/plugin.ts index fb20acb1..a0c0fb42 100644 --- a/plugins/plugin-md-power/src/node/plugin.ts +++ b/plugins/plugin-md-power/src/node/plugin.ts @@ -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) }, } } diff --git a/plugins/plugin-md-power/src/node/prepareConfigFile.ts b/plugins/plugin-md-power/src/node/prepareConfigFile.ts index f95e097d..77bf767e 100644 --- a/plugins/plugin-md-power/src/node/prepareConfigFile.ts +++ b/plugins/plugin-md-power/src/node/prepareConfigFile.ts @@ -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)`) diff --git a/plugins/plugin-md-power/src/node/utils/resolveAttrs.ts b/plugins/plugin-md-power/src/node/utils/resolveAttrs.ts index 69749603..262e614e 100644 --- a/plugins/plugin-md-power/src/node/utils/resolveAttrs.ts +++ b/plugins/plugin-md-power/src/node/utils/resolveAttrs.ts @@ -1,6 +1,6 @@ import { camelCase } from '@pengzhanbo/utils' -const RE_ATTR_VALUE = /(?:^|\s+)(?[\w-]+)(?:=\s*(?['"])(?.+?)\k)?(?:\s+|$)/ +const RE_ATTR_VALUE = /(?:^|\s+)(?[\w-]+)(?:=(?['"])(?.+?)\k|=(?\S+))?(?:\s+|$)/ export function resolveAttrs = Record>(info: string): { attrs: T @@ -18,7 +18,8 @@ export function resolveAttrs = Record // 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 = Record return { attrs: attrs as T, rawAttrs } } + +export function resolveAttr(info: string, key: string): string | undefined { + const pattern = new RegExp(`(?:^|\\s+)${key}(?:=(?['"])(?.+?)\\k|=(?\\S+))?(?:\\s+|$)`) + const groups = info.match(pattern)?.groups + return groups?.valueWithQuote || groups?.valueWithoutQuote +} diff --git a/plugins/plugin-md-power/src/shared/codeTree.ts b/plugins/plugin-md-power/src/shared/codeTree.ts new file mode 100644 index 00000000..96c2ca5a --- /dev/null +++ b/plugins/plugin-md-power/src/shared/codeTree.ts @@ -0,0 +1,6 @@ +import type { FileTreeIconMode } from './fileTree' + +export interface CodeTreeOptions { + icon?: FileTreeIconMode + height?: string | number +} diff --git a/plugins/plugin-md-power/src/shared/plugin.ts b/plugins/plugin-md-power/src/shared/plugin.ts index 4bbceb68..ced08c89 100644 --- a/plugins/plugin-md-power/src/shared/plugin.ts +++ b/plugins/plugin-md-power/src/shared/plugin.ts @@ -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 语法 */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d259b5a2..27c3e62c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6af9013c..ecff8b43 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 diff --git a/theme/src/client/styles/code.css b/theme/src/client/styles/code.css index 6b617d27..81f13d65 100644 --- a/theme/src/client/styles/code.css +++ b/theme/src/client/styles/code.css @@ -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); diff --git a/theme/src/node/detector/fields.ts b/theme/src/node/detector/fields.ts index d51d8662..cd6985a9 100644 --- a/theme/src/node/detector/fields.ts +++ b/theme/src/node/detector/fields.ts @@ -47,6 +47,7 @@ export const MARKDOWN_POWER_FIELDS: (keyof MarkdownPowerPluginOptions)[] = [ 'caniuse', 'codeSandbox', 'codeTabs', + 'codeTree', 'codepen', 'demo', 'fileTree', diff --git a/theme/src/node/plugins/code.ts b/theme/src/node/plugins/code.ts index c1bc2e74..86060661 100644 --- a/theme/src/node/plugins/code.ts +++ b/theme/src/node/plugins/code.ts @@ -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 `
${icon ? `` : ''}${title}
${code}
` + return `
${icon ? `` : ''}${title}
${code}
` }, twoslash: isPlainObject(twoslashOptions) ? {