feat(plugin-md-power): add diff syntax in file-tree container, close #577 (#578)

* feat(plugin-md-power): add diff syntax in file-tree container, close #577

* chore: tweak
This commit is contained in:
pengzhanbo 2025-04-29 20:24:57 +08:00 committed by GitHub
parent 1bc33acb2a
commit 10708c97b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 114 additions and 51 deletions

View File

@ -18,18 +18,20 @@ permalink: /guide/markdown/file-tree/
- 通过加粗文件名或目录名来突出显示,例如 `**README.md**`
- 通过在名称后添加更多文本来为文件或目录添加注释
- 通过在名称前添加 `++``--` 来标记文件或目录为 **新增** 或 **删除**
- 使用 `...``…` 作为名称来添加占位符文件和目录。
- 在 `:::file-tree` 后添加 `icon="simple"` 或 添加 `icon="colored"` 可以切换为简单图标或彩色图标,默认为彩色图标。
- 在 `:::file-tree` 后添加 `title="xxxx"` 可以为文件树添加标题。
**输入:**
```md
```md /++/ /--/
::: file-tree
- docs
- .vuepress
- config.ts
- page1.md
- ++ config.ts
- -- page1.md
- README.md
- theme 一个 **主题** 目录
- client
@ -55,8 +57,8 @@ permalink: /guide/markdown/file-tree/
- docs
- .vuepress
- config.ts
- page1.md
- ++ config.ts
- -- page1.md
- README.md
- theme 一个 **主题** 目录
- client

View File

@ -71,79 +71,93 @@ exports[`fileTree > parseFileTreeRawContent > should work 1`] = `
`;
exports[`fileTreePlugin > should work with default options 1`] = `
"<div class="vp-file-tree"><FileTreeNode expanded type="folder" filename="docs">
<template #icon><VPIcon name="vscode-icons:folder-type-docs" /></template><FileTreeNode type="file" filename="README.md">
"<div class="vp-file-tree"><FileTreeNode expanded type="folder" filename="docs" :level="0">
<template #icon><VPIcon name="vscode-icons:folder-type-docs" /></template><FileTreeNode type="file" filename="README.md" :level="1">
<template #icon><VPIcon name="flat-color-icons:info" /></template>
</FileTreeNode>
<FileTreeNode type="file" filename="foo.md">
<FileTreeNode type="file" filename="foo.md" :level="1">
<template #icon><VPIcon name="vscode-icons:file-type-markdown" /></template>
</FileTreeNode>
</FileTreeNode>
<FileTreeNode expanded type="folder" filename="src">
<template #icon><VPIcon name="vscode-icons:folder-type-src" /></template><FileTreeNode expanded type="folder" filename="client">
<template #icon><VPIcon name="vscode-icons:folder-type-client" /></template><FileTreeNode expanded type="folder" filename="components">
<template #icon><VPIcon name="vscode-icons:folder-type-component" /></template><FileTreeNode focus type="file" filename="Navbar.vue">
<FileTreeNode expanded type="folder" filename="src" :level="0">
<template #icon><VPIcon name="vscode-icons:folder-type-src" /></template><FileTreeNode expanded type="folder" filename="client" :level="1">
<template #icon><VPIcon name="vscode-icons:folder-type-client" /></template><FileTreeNode expanded type="folder" filename="components" :level="2">
<template #icon><VPIcon name="vscode-icons:folder-type-component" /></template><FileTreeNode focus type="file" filename="Navbar.vue" :level="3">
<template #icon><VPIcon name="vscode-icons:file-type-vue" /></template>
</FileTreeNode>
</FileTreeNode>
<FileTreeNode type="file" filename="index.ts">
<FileTreeNode type="file" filename="index.ts" :level="2">
<template #icon><VPIcon name="vscode-icons:file-type-typescript" /></template><template #comment># comment</template>
</FileTreeNode>
</FileTreeNode>
<FileTreeNode expanded type="folder" filename="node">
<template #icon><VPIcon name="vscode-icons:default-folder" /></template><FileTreeNode type="file" filename="index.ts">
<FileTreeNode expanded type="folder" filename="node" :level="1">
<template #icon><VPIcon name="vscode-icons:default-folder" /></template><FileTreeNode type="file" filename="index.ts" :level="2">
<template #icon><VPIcon name="vscode-icons:file-type-typescript" /></template>
</FileTreeNode>
</FileTreeNode>
</FileTreeNode>
<FileTreeNode type="file" filename=".gitignore">
<FileTreeNode type="file" filename=".gitignore" :level="0">
<template #icon><VPIcon name="vscode-icons:file-type-git" /></template>
</FileTreeNode>
<FileTreeNode type="file" filename="package.json">
<FileTreeNode type="file" filename="package.json" :level="0">
<template #icon><VPIcon name="vscode-icons:file-type-node" /></template>
</FileTreeNode></div>
<div class="vp-file-tree"><p class="vp-file-tree-title">files</p><FileTreeNode expanded type="folder" filename="src">
<template #icon><VPIcon name="vscode-icons:folder-type-src" /></template><FileTreeNode expanded type="folder" filename="js">
<template #icon><VPIcon name="vscode-icons:default-folder" /></template><FileTreeNode type="file" filename="…">
<div class="vp-file-tree"><p class="vp-file-tree-title">files</p><FileTreeNode expanded type="folder" filename="src" :level="0">
<template #icon><VPIcon name="vscode-icons:folder-type-src" /></template><FileTreeNode expanded type="folder" filename="js" :level="1">
<template #icon><VPIcon name="vscode-icons:default-folder" /></template><FileTreeNode type="file" filename="…" :level="2">
</FileTreeNode>
</FileTreeNode>
<FileTreeNode type="folder" filename="vue">
<template #icon><VPIcon name="vscode-icons:default-folder" /></template><FileTreeNode type="file" filename="…">
<FileTreeNode type="folder" filename="vue" :level="1">
<template #icon><VPIcon name="vscode-icons:default-folder" /></template><FileTreeNode type="file" filename="…" :level="2">
</FileTreeNode>
</FileTreeNode>
<FileTreeNode type="folder" filename="css">
<template #icon><VPIcon name="vscode-icons:folder-type-css" /></template><FileTreeNode type="file" filename="…">
<FileTreeNode type="folder" filename="css" :level="1">
<template #icon><VPIcon name="vscode-icons:folder-type-css" /></template><FileTreeNode type="file" filename="…" :level="2">
</FileTreeNode>
</FileTreeNode>
</FileTreeNode>
<FileTreeNode type="file" filename="README.md">
<FileTreeNode type="file" filename="README.md" :level="0">
<template #icon><VPIcon name="flat-color-icons:info" /></template>
</FileTreeNode></div>
<div class="vp-file-tree"><FileTreeNode type="file" filename="docs">
<div class="vp-file-tree"><FileTreeNode type="file" filename="docs" :level="0">
<template #icon><VPIcon name="vscode-icons:default-file" /></template>
</FileTreeNode>
<FileTreeNode expanded type="folder" filename="src">
<template #icon><VPIcon name="vscode-icons:default-folder" /></template><FileTreeNode type="file" filename="a.js">
<FileTreeNode expanded type="folder" filename="src" :level="0">
<template #icon><VPIcon name="vscode-icons:default-folder" /></template><FileTreeNode type="file" filename="a.js" :level="1">
<template #icon><VPIcon name="vscode-icons:default-file" /></template>
</FileTreeNode>
<FileTreeNode type="file" filename="b.ts">
<FileTreeNode type="file" filename="b.ts" :level="1">
<template #icon><VPIcon name="vscode-icons:default-file" /></template>
</FileTreeNode>
</FileTreeNode>
<FileTreeNode type="file" filename="README.md">
<FileTreeNode type="file" filename="README.md" :level="0">
<template #icon><VPIcon name="vscode-icons:default-file" /></template>
</FileTreeNode></div>
<div class="vp-file-tree"><FileTreeNode type="file" filename="">
<div class="vp-file-tree"><FileTreeNode type="file" filename="" :level="0">
<template #icon><VPIcon name="vscode-icons:default-file" /></template>
</FileTreeNode>
<FileTreeNode expanded type="folder" filename="">
<template #icon><VPIcon name="vscode-icons:default-folder" /></template><FileTreeNode type="file" filename="">
<FileTreeNode expanded type="folder" filename="" :level="0">
<template #icon><VPIcon name="vscode-icons:default-folder" /></template><FileTreeNode type="file" filename="" :level="1">
<template #icon><VPIcon name="vscode-icons:default-file" /></template>
</FileTreeNode>
</FileTreeNode></div>
<div class="vp-file-tree"><FileTreeNode expanded type="folder" filename="docs" :level="0">
<template #icon><VPIcon name="vscode-icons:folder-type-docs" /></template><FileTreeNode type="file" diff="add" filename="added.md" :level="1">
<template #icon><VPIcon name="vscode-icons:file-type-markdown" /></template>
</FileTreeNode>
<FileTreeNode type="file" diff="remove" filename="remove.md" :level="1">
<template #icon><VPIcon name="vscode-icons:file-type-markdown" /></template>
</FileTreeNode>
</FileTreeNode>
<FileTreeNode type="file" diff="add" filename="src" :level="0">
<template #icon><VPIcon name="vscode-icons:default-file" /></template>
</FileTreeNode>
<FileTreeNode type="file" diff="remove" filename="source" :level="0">
<template #icon><VPIcon name="vscode-icons:default-file" /></template>
</FileTreeNode></div>
<div class="vp-file-tree"></div>
"
`;

View File

@ -99,6 +99,14 @@ describe('fileTreePlugin', () => {
-
:::
::: file-tree
- docs
- ++ added.md
- -- remove.md
- ++ src
- -- source
:::
::: file-tree
:::
`

View File

@ -1,13 +1,12 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import { FadeInExpandTransition } from '@vuepress/helper/client'
import { inject, ref } from 'vue'
import '@vuepress/helper/transition/fade-in-height-expand.css'
const props = defineProps<{
type: 'file' | 'folder'
filename: string
level: number
diff?: 'add' | 'remove'
expanded?: boolean
focus?: boolean
}>()
@ -49,18 +48,20 @@ function toggle(ev: MouseEvent) {
focus,
expanded: type === 'folder' ? active : false,
active: type === 'file' ? activeFileTreeNode === filename : false,
diff,
add: diff === 'add',
remove: diff === 'remove',
}"
:style="{ '--file-tree-level': -level }"
@click="toggle"
>
<slot name="icon" />
<span class="name" :class="[type]">{{ filename }}</span>
<span v-if="$slots.comment" class="comment"><slot name="comment" /></span>
</p>
<FadeInExpandTransition>
<div v-if="type === 'folder'" v-show="active" class="group">
<slot />
</div>
</FadeInExpandTransition>
<div v-if="type === 'folder'" v-show="active" class="group">
<slot />
</div>
</div>
</template>
@ -100,24 +101,40 @@ function toggle(ev: MouseEvent) {
.vp-file-tree .vp-file-tree-info::after {
position: absolute;
top: 1px;
right: 0;
bottom: 1px;
left: -16px;
top: 0;
right: -16px;
bottom: 0;
left: calc(var(--file-tree-level) * 28px - 32px);
z-index: 0;
display: block;
pointer-events: none;
content: "";
background-color: transparent;
border-radius: 6px;
transition: background-color var(--vp-t-color);
}
.vp-file-tree .vp-file-tree-info.active::after,
.vp-file-tree .vp-file-tree-info:hover::after {
.vp-file-tree .vp-file-tree-info:not(.diff):hover::after {
background-color: var(--vp-c-default-soft);
}
.vp-file-tree .vp-file-tree-info.diff::after {
padding-left: 4px;
font-weight: 600;
}
.vp-file-tree .vp-file-tree-info.diff.add::after {
color: var(--vp-c-success-1);
content: "+";
background-color: var(--vp-c-success-soft);
}
.vp-file-tree .vp-file-tree-info.diff.remove::after {
color: var(--vp-c-danger-1);
content: "-";
background-color: var(--vp-c-danger-soft);
}
.vp-file-tree .vp-file-tree-info.folder {
cursor: pointer;
}

View File

@ -16,6 +16,16 @@ interface FileTreeAttrs {
icon?: FileTreeIconMode
}
interface FileTreeNodeProps {
filename: string
comment?: string
focus?: boolean
expanded?: boolean
type: 'folder' | 'file'
diff?: 'add' | 'remove'
level?: number
}
export function parseFileTreeRawContent(content: string): FileTreeNode[] {
const root: FileTreeNode = { info: '', level: -1, children: [] }
const stack: FileTreeNode[] = [root]
@ -44,12 +54,22 @@ export function parseFileTreeRawContent(content: string): FileTreeNode[] {
const RE_FOCUS = /^\*\*(.*)\*\*(?:$|\s+)/
export function parseFileTreeNodeInfo(info: string) {
export function parseFileTreeNodeInfo(info: string): FileTreeNodeProps {
let filename = ''
let comment = ''
let focus = false
let expanded: boolean | undefined = true
let type: 'folder' | 'file' = 'file'
let diff: 'add' | 'remove' | undefined
if (info.startsWith('++')) {
info = info.slice(2).trim()
diff = 'add'
}
else if (info.startsWith('--')) {
info = info.slice(2).trim()
diff = 'remove'
}
info = info.replace(RE_FOCUS, (_, matched) => {
filename = matched
@ -71,7 +91,7 @@ export function parseFileTreeNodeInfo(info: string) {
filename = removeEndingSlash(filename)
}
return { filename, comment, focus, expanded, type }
return { filename, comment, focus, expanded, type, diff }
}
export function fileTreePlugin(md: Markdown, options: FileTreeOptions = {}) {
@ -85,7 +105,7 @@ export function fileTreePlugin(md: Markdown, options: FileTreeOptions = {}) {
const renderFileTree = (nodes: FileTreeNode[], meta: FileTreeAttrs): string =>
nodes.map((node) => {
const { info, level, children } = node
const { filename, comment, focus, expanded, type } = parseFileTreeNodeInfo(info)
const { filename, comment, focus, expanded, type, diff } = parseFileTreeNodeInfo(info)
const isOmit = filename === '…' || filename === '...' /* fallback */
if (children.length === 0 && type === 'folder') {
@ -99,11 +119,13 @@ export function fileTreePlugin(md: Markdown, options: FileTreeOptions = {}) {
const renderedIcon = !isOmit
? `<template #icon><VPIcon name="${getIcon(filename, nodeType, meta.icon)}" /></template>`
: ''
const props = {
const props: FileTreeNodeProps = {
expanded: nodeType === 'folder' ? expanded : false,
focus,
type: nodeType,
diff,
filename,
level,
}
return `<FileTreeNode${stringifyAttrs(props)}>
${renderedIcon}${renderedComment}${children.length > 0 ? renderFileTree(children, meta) : ''}