vitepress-plugin-file-tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pengzhanbo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # vitepress-plugin-file-tree
2
+
3
+ Render file tree structure in your VitePress site.
4
+
5
+ 在 VitePress 中渲染文件树结构。
6
+
7
+ ## Usage
8
+
9
+ ### With Vitepress-tuck
10
+
11
+ **Installation:**
12
+
13
+ ```bash
14
+ # npm
15
+ npm install -D vitepress-tuck vitepress-plugin-file-tree
16
+ # pnpm
17
+ pnpm add -D vitepress-tuck vitepress-plugin-file-tree
18
+ # yarn
19
+ yarn add -D vitepress-tuck vitepress-plugin-file-tree
20
+ ```
21
+
22
+ **Configuration:**
23
+
24
+ ```ts
25
+ // .vitepress/config.ts
26
+ import fileTree from 'vitepress-plugin-file-tree'
27
+ import { defineConfig } from 'vitepress-tuck'
28
+
29
+ export default defineConfig({
30
+ plugins: [fileTree()],
31
+ })
32
+ ```
33
+
34
+ ```ts
35
+ // .vitepress/theme/index.ts
36
+ import type { Theme } from 'vitepress'
37
+ import enhanceApp from 'virtual:enhance-app'
38
+ import DefaultTheme from 'vitepress/theme'
39
+
40
+ export default {
41
+ extends: DefaultTheme,
42
+ enhanceApp(ctx) {
43
+ enhanceApp(ctx)
44
+ },
45
+ } satisfies Theme
46
+ ```
47
+
48
+ ### With Vitepress
49
+
50
+ **Installation:**
51
+
52
+ ```bash
53
+ # npm
54
+ npm install -D vitepress-plugin-file-tree
55
+ # pnpm
56
+ pnpm add -D vitepress-plugin-file-tree
57
+ # yarn
58
+ yarn add -D vitepress-plugin-file-tree
59
+ ```
60
+
61
+ **Configuration:**
62
+
63
+ ```ts
64
+ // .vitepress/config.ts
65
+ import { defineConfig } from 'vitepress'
66
+ import { fileTreeMarkdownPlugin } from 'vitepress-plugin-file-tree'
67
+
68
+ export default defineConfig({
69
+ markdown: {
70
+ config: (md) => {
71
+ md.use(fileTreeMarkdownPlugin)
72
+ },
73
+ },
74
+ })
75
+ ```
76
+
77
+ ```ts
78
+ // .vitepress/theme/index.ts
79
+ import type { Theme } from 'vitepress'
80
+ import { enhanceAppWithFileTree } from 'vitepress-plugin-file-tree/client'
81
+ import DefaultTheme from 'vitepress/theme'
82
+
83
+ export default {
84
+ extends: DefaultTheme,
85
+ enhanceApp(ctx) {
86
+ enhanceAppWithFileTree(ctx)
87
+ },
88
+ } satisfies Theme
89
+ ```
90
+
91
+ ## Syntax
92
+
93
+ Use `::: file-tree` container to create a file tree.
94
+
95
+ ```md
96
+ ::: file-tree title="Project Structure"
97
+ - src/
98
+ - components/
99
+ - Button.vue
100
+ - Input.vue
101
+ - index.ts
102
+ - package.json
103
+ - README.md
104
+ :::
105
+ ```
106
+
107
+ ### Node Annotations
108
+
109
+ | Syntax | Description |
110
+ | ---------------------- | ---------------------------------------- |
111
+ | `- filename` | Regular file |
112
+ | `- filename/` | Folder (trailing slash) |
113
+ | `- **filename**` | Highlighted file |
114
+ | `- ++ filename` | Added file (diff add, green `+` mark) |
115
+ | `- -- filename` | Removed file (diff remove, red `-` mark) |
116
+ | `- filename # comment` | File with comment |
117
+ | `- …` or `- ...` | Placeholder (non-clickable) |
118
+
119
+ Indentation uses 2 spaces per level. A copy button is included to copy the file tree as command-line style text.
@@ -0,0 +1,51 @@
1
+ import { EnhanceAppContext } from "vitepress/client";
2
+
3
+ //#region src/client/VPFileTree.vue.d.ts
4
+ type __VLS_Props$1 = {
5
+ title?: string;
6
+ text: string;
7
+ };
8
+ declare var __VLS_6: {};
9
+ type __VLS_Slots$1 = {} & {
10
+ default?: (props: typeof __VLS_6) => any;
11
+ };
12
+ declare const __VLS_base$1: import("vue").DefineComponent<__VLS_Props$1, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props$1> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const __VLS_export$1: __VLS_WithSlots$1<typeof __VLS_base$1, __VLS_Slots$1>;
14
+ declare const _default: typeof __VLS_export$1;
15
+ type __VLS_WithSlots$1<T, S> = T & {
16
+ new (): {
17
+ $slots: S;
18
+ };
19
+ };
20
+ //#endregion
21
+ //#region src/client/VPFileTreeNode.vue.d.ts
22
+ type __VLS_Props = {
23
+ type: 'file' | 'folder';
24
+ filename: string;
25
+ level: number;
26
+ diff?: 'add' | 'remove';
27
+ expanded?: boolean;
28
+ focus?: boolean;
29
+ filepath?: string;
30
+ };
31
+ declare var __VLS_1: {}, __VLS_3: {};
32
+ type __VLS_Slots = {} & {
33
+ comment?: (props: typeof __VLS_1) => any;
34
+ } & {
35
+ default?: (props: typeof __VLS_3) => any;
36
+ };
37
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
39
+ declare const _default$1: typeof __VLS_export;
40
+ type __VLS_WithSlots<T, S> = T & {
41
+ new (): {
42
+ $slots: S;
43
+ };
44
+ };
45
+ //#endregion
46
+ //#region src/client/index.d.ts
47
+ declare function enhanceAppWithFileTree({
48
+ app
49
+ }: EnhanceAppContext): void;
50
+ //#endregion
51
+ export { _default as VPFileTree, _default$1 as VPFileTreeNode, enhanceAppWithFileTree };
@@ -0,0 +1,99 @@
1
+ import "../style.css";
2
+ import { createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeProps, normalizeClass, openBlock, ref, renderSlot, toDisplayString, unref, vShow, withDirectives } from "vue";
3
+ import { VPCopyButton } from "vitepress-plugin-toolkit/client";
4
+ //#region src/client/VPFileTree.vue
5
+ const _hoisted_1$1 = { class: "vp-file-tree has-copy" };
6
+ const _hoisted_2$1 = {
7
+ key: 0,
8
+ class: "vp-file-tree-title"
9
+ };
10
+ const _sfc_main = /* @__PURE__ */ defineComponent({
11
+ __name: "VPFileTree",
12
+ props: {
13
+ title: {},
14
+ text: {}
15
+ },
16
+ setup(__props) {
17
+ return (_ctx, _cache) => {
18
+ return openBlock(), createElementBlock("div", _hoisted_1$1, [
19
+ __props.title ? (openBlock(), createElementBlock("p", _hoisted_2$1, toDisplayString(__props.title), 1)) : createCommentVNode("v-if", true),
20
+ createVNode(unref(VPCopyButton), { text: __props.text }, null, 8, ["text"]),
21
+ renderSlot(_ctx.$slots, "default")
22
+ ]);
23
+ };
24
+ }
25
+ });
26
+ //#endregion
27
+ //#region src/client/VPFileTreeNode.vue
28
+ const _hoisted_1 = { class: "vp-file-tree-node" };
29
+ const _hoisted_2 = ["data-filename"];
30
+ const _hoisted_3 = {
31
+ key: 1,
32
+ class: "comment"
33
+ };
34
+ const _hoisted_4 = {
35
+ key: 0,
36
+ class: "group"
37
+ };
38
+ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
39
+ __name: "VPFileTreeNode",
40
+ props: {
41
+ type: {},
42
+ filename: {},
43
+ level: {},
44
+ diff: {},
45
+ expanded: { type: Boolean },
46
+ focus: { type: Boolean },
47
+ filepath: {}
48
+ },
49
+ setup(__props) {
50
+ const activeFileTreeNode = inject("active-file-tree-node", ref(""));
51
+ const onNodeClick = inject("on-file-tree-node-click", () => {});
52
+ const active = ref(__props.expanded);
53
+ function nodeClick() {
54
+ if (__props.filename === "…" || __props.filename === "...") return;
55
+ onNodeClick(__props.filepath || __props.filename, __props.type);
56
+ }
57
+ function toggle(ev) {
58
+ if (__props.type === "folder") {
59
+ if (!ev.target.matches(".comment, .comment *")) {
60
+ active.value = !active.value;
61
+ nodeClick();
62
+ }
63
+ } else nodeClick();
64
+ }
65
+ return (_ctx, _cache) => {
66
+ return openBlock(), createElementBlock("div", _hoisted_1, [createElementVNode("p", mergeProps({
67
+ class: ["vp-file-tree-info", {
68
+ [__props.type]: true,
69
+ focus: __props.focus,
70
+ expanded: __props.type === "folder" ? active.value : false,
71
+ active: __props.type === "file" ? unref(activeFileTreeNode) === __props.filepath : false,
72
+ diff: __props.diff,
73
+ add: __props.diff === "add",
74
+ remove: __props.diff === "remove"
75
+ }],
76
+ style: { "--file-tree-level": -__props.level }
77
+ }, { onClick: toggle }), [
78
+ __props.filename !== "…" && __props.filename !== "..." ? (openBlock(), createElementBlock("span", {
79
+ key: 0,
80
+ class: normalizeClass(["vp-icon", {
81
+ expanded: __props.type === "folder" ? active.value : false,
82
+ [__props.type]: true
83
+ }]),
84
+ "data-filename": __props.filename
85
+ }, null, 10, _hoisted_2)) : createCommentVNode("v-if", true),
86
+ createElementVNode("span", { class: normalizeClass(["name", [__props.type]]) }, toDisplayString(__props.filename), 3),
87
+ _ctx.$slots.comment ? (openBlock(), createElementBlock("span", _hoisted_3, [renderSlot(_ctx.$slots, "comment")])) : createCommentVNode("v-if", true)
88
+ ], 16), __props.type === "folder" ? withDirectives((openBlock(), createElementBlock("div", _hoisted_4, [renderSlot(_ctx.$slots, "default")], 512)), [[vShow, active.value]]) : createCommentVNode("v-if", true)]);
89
+ };
90
+ }
91
+ });
92
+ //#endregion
93
+ //#region src/client/index.ts
94
+ function enhanceAppWithFileTree({ app }) {
95
+ app.component("VPFileTree", _sfc_main);
96
+ app.component("VPFileTreeNode", _sfc_main$1);
97
+ }
98
+ //#endregion
99
+ export { _sfc_main as VPFileTree, _sfc_main$1 as VPFileTreeNode, enhanceAppWithFileTree };
@@ -0,0 +1,51 @@
1
+ import { EnhanceAppContext } from "vitepress/client";
2
+
3
+ //#region src/client/VPFileTree.vue.d.ts
4
+ type __VLS_Props$1 = {
5
+ title?: string;
6
+ text: string;
7
+ };
8
+ declare var __VLS_6: {};
9
+ type __VLS_Slots$1 = {} & {
10
+ default?: (props: typeof __VLS_6) => any;
11
+ };
12
+ declare const __VLS_base$1: import("vue").DefineComponent<__VLS_Props$1, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props$1> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const __VLS_export$1: __VLS_WithSlots$1<typeof __VLS_base$1, __VLS_Slots$1>;
14
+ declare const _default: typeof __VLS_export$1;
15
+ type __VLS_WithSlots$1<T, S> = T & {
16
+ new (): {
17
+ $slots: S;
18
+ };
19
+ };
20
+ //#endregion
21
+ //#region src/client/VPFileTreeNode.vue.d.ts
22
+ type __VLS_Props = {
23
+ type: 'file' | 'folder';
24
+ filename: string;
25
+ level: number;
26
+ diff?: 'add' | 'remove';
27
+ expanded?: boolean;
28
+ focus?: boolean;
29
+ filepath?: string;
30
+ };
31
+ declare var __VLS_1: {}, __VLS_3: {};
32
+ type __VLS_Slots = {} & {
33
+ comment?: (props: typeof __VLS_1) => any;
34
+ } & {
35
+ default?: (props: typeof __VLS_3) => any;
36
+ };
37
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
39
+ declare const _default$1: typeof __VLS_export;
40
+ type __VLS_WithSlots<T, S> = T & {
41
+ new (): {
42
+ $slots: S;
43
+ };
44
+ };
45
+ //#endregion
46
+ //#region src/client/index.d.ts
47
+ declare function enhanceAppWithFileTree({
48
+ app
49
+ }: EnhanceAppContext): void;
50
+ //#endregion
51
+ export { _default as VPFileTree, _default$1 as VPFileTreeNode, enhanceAppWithFileTree };
@@ -0,0 +1,106 @@
1
+ import { defineComponent, inject, mergeProps, ref, unref, useSSRContext } from "vue";
2
+ import { ssrInterpolate, ssrRenderAttr, ssrRenderAttrs, ssrRenderClass, ssrRenderComponent, ssrRenderSlot, ssrRenderStyle } from "vue/server-renderer";
3
+ import { VPCopyButton } from "vitepress-plugin-toolkit/client";
4
+ //#region src/client/VPFileTree.vue
5
+ const _sfc_main = /* @__PURE__ */ defineComponent({
6
+ __name: "VPFileTree",
7
+ __ssrInlineRender: true,
8
+ props: {
9
+ title: {},
10
+ text: {}
11
+ },
12
+ setup(__props) {
13
+ return (_ctx, _push, _parent, _attrs) => {
14
+ _push(`<div${ssrRenderAttrs(mergeProps({ class: "vp-file-tree has-copy" }, _attrs))}>`);
15
+ if (__props.title) _push(`<p class="vp-file-tree-title">${ssrInterpolate(__props.title)}</p>`);
16
+ else _push(`<!---->`);
17
+ _push(ssrRenderComponent(unref(VPCopyButton), { text: __props.text }, null, _parent));
18
+ ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent);
19
+ _push(`</div>`);
20
+ };
21
+ }
22
+ });
23
+ const _sfc_setup$1 = _sfc_main.setup;
24
+ _sfc_main.setup = (props, ctx) => {
25
+ const ssrContext = useSSRContext();
26
+ (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/VPFileTree.vue");
27
+ return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
28
+ };
29
+ //#endregion
30
+ //#region src/client/VPFileTreeNode.vue
31
+ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
32
+ __name: "VPFileTreeNode",
33
+ __ssrInlineRender: true,
34
+ props: {
35
+ type: {},
36
+ filename: {},
37
+ level: {},
38
+ diff: {},
39
+ expanded: { type: Boolean },
40
+ focus: { type: Boolean },
41
+ filepath: {}
42
+ },
43
+ setup(__props) {
44
+ const activeFileTreeNode = inject("active-file-tree-node", ref(""));
45
+ const onNodeClick = inject("on-file-tree-node-click", () => {});
46
+ const active = ref(__props.expanded);
47
+ function nodeClick() {
48
+ if (__props.filename === "…" || __props.filename === "...") return;
49
+ onNodeClick(__props.filepath || __props.filename, __props.type);
50
+ }
51
+ function toggle(ev) {
52
+ if (__props.type === "folder") {
53
+ if (!ev.target.matches(".comment, .comment *")) {
54
+ active.value = !active.value;
55
+ nodeClick();
56
+ }
57
+ } else nodeClick();
58
+ }
59
+ return (_ctx, _push, _parent, _attrs) => {
60
+ _push(`<div${ssrRenderAttrs(mergeProps({ class: "vp-file-tree-node" }, _attrs))}><p${ssrRenderAttrs(mergeProps({
61
+ class: ["vp-file-tree-info", {
62
+ [__props.type]: true,
63
+ focus: __props.focus,
64
+ expanded: __props.type === "folder" ? active.value : false,
65
+ active: __props.type === "file" ? unref(activeFileTreeNode) === __props.filepath : false,
66
+ diff: __props.diff,
67
+ add: __props.diff === "add",
68
+ remove: __props.diff === "remove"
69
+ }],
70
+ style: { "--file-tree-level": -__props.level }
71
+ }, { onClick: toggle }))}>`);
72
+ if (__props.filename !== "…" && __props.filename !== "...") _push(`<span class="${ssrRenderClass([{
73
+ expanded: __props.type === "folder" ? active.value : false,
74
+ [__props.type]: true
75
+ }, "vp-icon"])}"${ssrRenderAttr("data-filename", __props.filename)}></span>`);
76
+ else _push(`<!---->`);
77
+ _push(`<span class="${ssrRenderClass([[__props.type], "name"])}">${ssrInterpolate(__props.filename)}</span>`);
78
+ if (_ctx.$slots.comment) {
79
+ _push(`<span class="comment">`);
80
+ ssrRenderSlot(_ctx.$slots, "comment", {}, null, _push, _parent);
81
+ _push(`</span>`);
82
+ } else _push(`<!---->`);
83
+ _push(`</p>`);
84
+ if (__props.type === "folder") {
85
+ _push(`<div class="group" style="${ssrRenderStyle(active.value ? null : { display: "none" })}">`);
86
+ ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent);
87
+ _push(`</div>`);
88
+ } else _push(`<!---->`);
89
+ _push(`</div>`);
90
+ };
91
+ }
92
+ });
93
+ const _sfc_setup = _sfc_main$1.setup;
94
+ _sfc_main$1.setup = (props, ctx) => {
95
+ const ssrContext = useSSRContext();
96
+ (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/VPFileTreeNode.vue");
97
+ return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
98
+ };
99
+ //#endregion
100
+ //#region src/client/index.ts
101
+ function enhanceAppWithFileTree({ app }) {
102
+ app.component("VPFileTree", _sfc_main);
103
+ app.component("VPFileTreeNode", _sfc_main$1);
104
+ }
105
+ //#endregion
106
+ export { _sfc_main as VPFileTree, _sfc_main$1 as VPFileTreeNode, enhanceAppWithFileTree };
@@ -0,0 +1,194 @@
1
+ @import url("vitepress-plugin-toolkit/styles/copy-button.css");
2
+
3
+ :root {
4
+ --vp-file-tree-bg: var(--vp-code-block-bg);
5
+
6
+ --vp-file-tree-icon-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='%23000' d='m5.157 13.069l4.611-4.685a.546.546 0 0 0 0-.768L5.158 2.93a.55.55 0 0 1 0-.771a.53.53 0 0 1 .759 0l4.61 4.684a1.65 1.65 0 0 1 0 2.312l-4.61 4.684a.53.53 0 0 1-.76 0a.55.55 0 0 1 0-.771'/%3E%3C/svg%3E");
7
+ --vp-file-tree-icon-file: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 32 32'%3E%3Cpath fill='%23c5c5c5' d='M20.414 2H5v28h22V8.586ZM7 28V4h12v6h6v18Z'/%3E%3C/svg%3E");
8
+ --vp-file-tree-icon-folder: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 32 32'%3E%3Cpath fill='%23c09553' d='M27.5 5.5h-9.3l-2.1 4.2H4.4v16.8h25.2v-21Zm0 4.2h-8.2l1.1-2.1h7.1Z'/%3E%3C/svg%3E");
9
+ --vp-file-tree-icon-folder-expanded: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 32 32'%3E%3Cpath fill='%23dcb67a' d='M27.4 5.5h-9.2l-2.1 4.2H4.3v16.8h25.2v-21Zm0 18.7H6.6V11.8h20.8Zm0-14.5h-8.2l1-2.1h7.1v2.1Z'/%3E%3Cpath fill='%23dcb67a' d='M25.7 13.7H.5l3.8 12.8h25.2z'/%3E%3C/svg%3E");
10
+ }
11
+
12
+ .vp-file-tree {
13
+ position: relative;
14
+ max-width: 100%;
15
+ padding: 16px;
16
+ overflow: auto hidden;
17
+ font-size: 14px;
18
+ background-color: var(--vp-file-tree-bg);
19
+ border: solid 1px var(--vp-c-divider);
20
+ border-radius: 8px;
21
+ transition: border 0.25s ease, background-color 0.25s ease;
22
+ }
23
+
24
+ .vp-file-tree .vp-file-tree-title {
25
+ padding-block: 8px;
26
+ padding-inline: 16px;
27
+ margin-block: -16px 8px;
28
+ margin-inline: -16px;
29
+ font-weight: bold;
30
+ color: var(--vp-c-text-1);
31
+ border-bottom: solid 1px var(--vp-c-divider);
32
+ transition: color 0.25s ease, border-color 0.25s ease;
33
+ }
34
+
35
+ .vp-file-tree .vp-file-tree-title + .vp-copy {
36
+ top: calc(45px + 1em) !important;
37
+ }
38
+
39
+ .vp-file-tree .vp-file-tree-info {
40
+ position: relative;
41
+ display: flex;
42
+ gap: 8px;
43
+ align-items: center;
44
+ justify-content: flex-start;
45
+ min-width: max-content;
46
+ height: 28px;
47
+ padding-block: 2px;
48
+ margin-block: 0;
49
+ margin-inline-start: 16px;
50
+ line-height: 24px;
51
+ text-wrap: nowrap;
52
+ }
53
+
54
+ .vp-file-tree .vp-file-tree-info::after {
55
+ position: absolute;
56
+ top: 0;
57
+ right: -16px;
58
+ bottom: 0;
59
+ left: calc(var(--file-tree-level) * 10.5px - 32px);
60
+ z-index: 0;
61
+ display: block;
62
+ pointer-events: none;
63
+ content: "";
64
+ background-color: transparent;
65
+ transition: background-color 0.25s ease;
66
+ }
67
+
68
+ .vp-file-tree .vp-file-tree-info.active::after,
69
+ .vp-file-tree .vp-file-tree-info:not(.diff):hover::after {
70
+ background-color: var(--vp-c-default-soft);
71
+ }
72
+
73
+ .vp-file-tree .vp-file-tree-info.diff::after {
74
+ padding-inline-start: 4px;
75
+ font-size: 1.25em;
76
+ }
77
+
78
+ .vp-file-tree .vp-file-tree-info.diff.add::after {
79
+ color: var(--vp-c-success-1);
80
+ content: "+";
81
+ background-color: var(--vp-c-success-soft);
82
+ }
83
+
84
+ .vp-file-tree .vp-file-tree-info.diff.remove::after {
85
+ padding-inline-start: 6px;
86
+ color: var(--vp-c-danger-1);
87
+ content: "-";
88
+ background-color: var(--vp-c-danger-soft);
89
+ }
90
+
91
+ .vp-file-tree .vp-file-tree-info.folder {
92
+ cursor: pointer;
93
+ }
94
+
95
+ .vp-file-tree .vp-file-tree-info.folder::before {
96
+ position: absolute;
97
+ top: 8px;
98
+ left: -16px;
99
+ display: block;
100
+ width: 12px;
101
+ height: 12px;
102
+ color: var(--vp-c-text-2);
103
+ cursor: pointer;
104
+ content: "";
105
+ background-color: currentcolor;
106
+ -webkit-mask: var(--vp-file-tree-icon-arrow) no-repeat;
107
+ mask: var(--vp-file-tree-icon-arrow) no-repeat;
108
+ -webkit-mask-size: 100% 100%;
109
+ mask-size: 100% 100%;
110
+ transition: color 0.25s ease, transform 0.25s ease;
111
+ transform: rotate(0);
112
+ }
113
+
114
+ .vp-file-tree .vp-file-tree-info.folder.expanded::before {
115
+ transform: rotate(90deg);
116
+ }
117
+
118
+ .vp-file-tree .vp-file-tree-info .name {
119
+ position: relative;
120
+ flex-shrink: 99;
121
+ min-width: 0;
122
+ font-family: var(--vp-font-family-mono);
123
+ }
124
+
125
+ .vp-file-tree .vp-file-tree-info.folder .name {
126
+ color: var(--vp-c-text-1);
127
+ transition: color 0.25s ease;
128
+ }
129
+
130
+ .vp-file-tree .vp-file-tree-info.focus .name {
131
+ padding-inline: 4px;
132
+ margin-block: 0;
133
+ margin-inline: -4px;
134
+ font-weight: bold;
135
+ color: var(--vp-c-bg);
136
+ background-color: var(--vp-c-brand-2);
137
+ border-radius: 4px;
138
+ transition: color 0.25s ease, background-color 0.25s ease;
139
+ }
140
+
141
+ .vp-file-tree .vp-file-tree-info.active .name {
142
+ color: var(--vp-c-brand-1);
143
+ }
144
+
145
+ .vp-file-tree .vp-file-tree-info:not(.focus).folder .name:hover {
146
+ color: var(--vp-c-brand-1);
147
+ }
148
+
149
+ .vp-file-tree .vp-file-tree-info .comment {
150
+ display: inline-block;
151
+ flex: 1 2;
152
+ height: 28px;
153
+ padding-inline: 20px 16px;
154
+ margin-block: -2px;
155
+ line-height: 28px;
156
+ color: var(--vp-c-text-3);
157
+ cursor: auto;
158
+ transition: color 0.25s ease;
159
+ }
160
+
161
+ .vp-file-tree .vp-file-tree-node .group {
162
+ position: relative;
163
+ min-width: max-content;
164
+ margin-inline-start: 10px;
165
+ }
166
+
167
+ .vp-file-tree .vp-file-tree-node .group::before {
168
+ position: absolute;
169
+ top: 0;
170
+ left: -4.5px;
171
+ width: 1px;
172
+ height: 100%;
173
+ content: "";
174
+ background-color: var(--vp-c-divider);
175
+ transition: background-color 0.25s ease;
176
+ }
177
+
178
+ .vp-file-tree .vp-icon {
179
+ width: 1.2em;
180
+ height: 1.2em;
181
+ margin: 0;
182
+ }
183
+
184
+ .vp-file-tree .vp-icon:where(.file) {
185
+ --icon: var(--vp-file-tree-icon-file);
186
+ }
187
+
188
+ .vp-file-tree .vp-icon:where(.folder) {
189
+ --icon: var(--vp-file-tree-icon-folder);
190
+ }
191
+
192
+ .vp-file-tree .vp-icon:where(.expanded.folder) {
193
+ --icon: var(--vp-file-tree-icon-folder-expanded);
194
+ }
@@ -0,0 +1,64 @@
1
+ import { PluginSimple } from "markdown-it";
2
+
3
+ //#region src/node/fileTreePlugin.d.ts
4
+ /**
5
+ * File tree node structure
6
+ *
7
+ * 文件树节点结构
8
+ */
9
+ interface FileTreeNode {
10
+ filename: string;
11
+ comment?: string;
12
+ focus?: boolean;
13
+ expanded?: boolean;
14
+ type: 'folder' | 'file';
15
+ diff?: 'add' | 'remove';
16
+ level: number;
17
+ children: FileTreeNode[];
18
+ }
19
+ /**
20
+ * File tree container attributes
21
+ *
22
+ * 文件树容器属性
23
+ */
24
+ interface FileTreeAttrs {
25
+ title?: string;
26
+ }
27
+ /**
28
+ * Parse raw file tree content to node tree structure
29
+ *
30
+ * 解析原始文件树内容为节点树结构
31
+ *
32
+ * @param content - Raw file tree text content / 文件树的原始文本内容
33
+ * @returns File tree node array / 文件树节点数组
34
+ */
35
+ declare function parseFileTreeRawContent(content: string): FileTreeNode[];
36
+ /**
37
+ * Parse single node info string, extract filename, comment, type, etc.
38
+ *
39
+ * 解析单个节点的 info 字符串,提取文件名、注释、类型等属性
40
+ *
41
+ * @param info - Node description string / 节点描述字符串
42
+ * @returns File tree node props / 文件树节点属性
43
+ */
44
+ declare function parseFileTreeNodeInfo(info: string): Omit<FileTreeNode, 'children' | 'level'>;
45
+ /**
46
+ * @example
47
+ * ```ts
48
+ * import { fileTreeMarkdownPlugin } from 'vitepress-plugin-file-tree'
49
+ * import { defineConfig } from 'vitepress'
50
+ * export default defineConfig({
51
+ * markdown: {
52
+ * config: (md) => {
53
+ * md.use(fileTreeMarkdownPlugin)
54
+ * },
55
+ * },
56
+ * })
57
+ * ```
58
+ */
59
+ declare const fileTreeMarkdownPlugin: PluginSimple;
60
+ //#endregion
61
+ //#region src/node/index.d.ts
62
+ declare const _default: (option?: unknown) => import("vitepress-tuck").VitepressPlugin;
63
+ //#endregion
64
+ export { type FileTreeAttrs, type FileTreeNode, _default as default, fileTreeMarkdownPlugin, parseFileTreeNodeInfo, parseFileTreeRawContent };
@@ -0,0 +1,176 @@
1
+ import { definePlugin } from "vitepress-tuck";
2
+ import { removeTrailingSlash } from "@pengzhanbo/utils";
3
+ import { createContainerSyntaxPlugin, stringifyAttrs } from "vitepress-plugin-toolkit";
4
+ //#region src/node/fileTreePlugin.ts
5
+ /**
6
+ * Parse raw file tree content to node tree structure
7
+ *
8
+ * 解析原始文件树内容为节点树结构
9
+ *
10
+ * @param content - Raw file tree text content / 文件树的原始文本内容
11
+ * @returns File tree node array / 文件树节点数组
12
+ */
13
+ function parseFileTreeRawContent(content) {
14
+ const root = {
15
+ level: -1,
16
+ children: []
17
+ };
18
+ const stack = [root];
19
+ const lines = content.trimEnd().split("\n");
20
+ const spaceLength = lines[0]?.match(/^\s*/)?.[0].length ?? 0;
21
+ for (const line of lines) {
22
+ const match = line.match(/^(\s*)-(.*)$/);
23
+ if (!match) continue;
24
+ const level = Math.floor((match[1].length - spaceLength) / 2);
25
+ const info = match[2].trim();
26
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) stack.pop();
27
+ const parent = stack[stack.length - 1];
28
+ const node = {
29
+ level,
30
+ children: [],
31
+ ...parseFileTreeNodeInfo(info)
32
+ };
33
+ parent.children.push(node);
34
+ stack.push(node);
35
+ }
36
+ return root.children;
37
+ }
38
+ /**
39
+ * Regex for focus marker
40
+ *
41
+ * 高亮标记正则
42
+ */
43
+ const RE_FOCUS = /^\*\*(.*)\*\*(?:$|\s+)/;
44
+ /**
45
+ * Parse single node info string, extract filename, comment, type, etc.
46
+ *
47
+ * 解析单个节点的 info 字符串,提取文件名、注释、类型等属性
48
+ *
49
+ * @param info - Node description string / 节点描述字符串
50
+ * @returns File tree node props / 文件树节点属性
51
+ */
52
+ function parseFileTreeNodeInfo(info) {
53
+ let filename = "";
54
+ let comment = "";
55
+ let focus = false;
56
+ let expanded = true;
57
+ let type = "file";
58
+ let diff;
59
+ if (info.startsWith("++")) {
60
+ info = info.slice(2).trim();
61
+ diff = "add";
62
+ } else if (info.startsWith("--")) {
63
+ info = info.slice(2).trim();
64
+ diff = "remove";
65
+ }
66
+ info = info.replace(RE_FOCUS, (_, matched) => {
67
+ filename = matched;
68
+ focus = true;
69
+ return "";
70
+ });
71
+ if (filename === "" && !focus) {
72
+ const sharpIndex = info.indexOf("#");
73
+ filename = info.slice(0, sharpIndex === -1 ? info.length : sharpIndex).trim();
74
+ info = sharpIndex === -1 ? "" : info.slice(sharpIndex);
75
+ }
76
+ comment = info.trim();
77
+ if (filename.endsWith("/")) {
78
+ type = "folder";
79
+ expanded = false;
80
+ filename = removeTrailingSlash(filename);
81
+ }
82
+ return {
83
+ filename,
84
+ comment,
85
+ focus,
86
+ expanded,
87
+ type,
88
+ diff
89
+ };
90
+ }
91
+ /**
92
+ * Convert file tree to command line text format
93
+ *
94
+ * 将文件树转换为命令行文本格式
95
+ *
96
+ * @param nodes - File tree nodes / 文件树节点
97
+ * @param prefix - Line prefix / 行前缀
98
+ * @returns CMD text / CMD 文本
99
+ */
100
+ function fileTreeToCMDText(nodes, prefix = "") {
101
+ let content = prefix ? "" : ".\n";
102
+ for (let i = 0, l = nodes.length; i < l; i++) {
103
+ const { filename, children } = nodes[i];
104
+ content += `${prefix + (i === l - 1 ? "└── " : "├── ")}${filename}\n`;
105
+ const child = children.filter((n) => n.filename !== "…");
106
+ if (child.length) content += fileTreeToCMDText(child, prefix + (i === l - 1 ? " " : "│ "));
107
+ }
108
+ return content;
109
+ }
110
+ /**
111
+ * @example
112
+ * ```ts
113
+ * import { fileTreeMarkdownPlugin } from 'vitepress-plugin-file-tree'
114
+ * import { defineConfig } from 'vitepress'
115
+ * export default defineConfig({
116
+ * markdown: {
117
+ * config: (md) => {
118
+ * md.use(fileTreeMarkdownPlugin)
119
+ * },
120
+ * },
121
+ * })
122
+ * ```
123
+ */
124
+ const fileTreeMarkdownPlugin = (md) => {
125
+ /**
126
+ * Recursively render file tree nodes
127
+ *
128
+ * 递归渲染文件树节点
129
+ */
130
+ const renderFileTree = (nodes, meta) => nodes.map((node) => {
131
+ const { level, children, filename, comment, focus, expanded, type, diff } = node;
132
+ if (children.length === 0 && type === "folder") children.push({
133
+ level: level + 1,
134
+ children: [],
135
+ filename: "…",
136
+ type: "file"
137
+ });
138
+ const nodeType = children.length > 0 ? "folder" : type;
139
+ const renderedComment = comment ? `<template #comment>${md.renderInline(comment.replaceAll("#", "#"))}</template>` : "";
140
+ return `<VPFileTreeNode${stringifyAttrs({
141
+ expanded: nodeType === "folder" ? expanded : false,
142
+ focus,
143
+ type: nodeType,
144
+ diff,
145
+ filename,
146
+ level
147
+ }, false, ["filename"])}>
148
+ ${renderedComment}${children.length > 0 ? renderFileTree(children, meta) : ""}
149
+ </VPFileTreeNode>`;
150
+ }).join("\n");
151
+ createContainerSyntaxPlugin(md, "file-tree", (tokens, index) => {
152
+ const token = tokens[index];
153
+ const nodes = parseFileTreeRawContent(token.content);
154
+ const meta = token.meta;
155
+ const text = fileTreeToCMDText(nodes).trim();
156
+ return `<VPFileTree${stringifyAttrs({
157
+ title: meta.title,
158
+ text
159
+ })}>${renderFileTree(nodes, meta)}</VPFileTree>`;
160
+ });
161
+ };
162
+ //#endregion
163
+ //#region src/node/index.ts
164
+ var node_default = definePlugin(() => ({
165
+ name: "vitepress-plugin-file-tree",
166
+ client: { enhance: "enhanceAppWithFileTree" },
167
+ markdown: { config: (md) => {
168
+ md.use(fileTreeMarkdownPlugin);
169
+ } },
170
+ vite: {
171
+ optimizeDeps: { exclude: ["@pengzhanbo/utils"] },
172
+ ssr: { noExternal: ["vitepress-plugin-file-tree"] }
173
+ }
174
+ }));
175
+ //#endregion
176
+ export { node_default as default, fileTreeMarkdownPlugin, parseFileTreeNodeInfo, parseFileTreeRawContent };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "vitepress-plugin-file-tree",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "Render file tree structure in your VitePress site.",
6
+ "author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "vitepress",
10
+ "vitepress-plugin",
11
+ "file-tree"
12
+ ],
13
+ "exports": {
14
+ ".": "./dist/node/index.js",
15
+ "./client": {
16
+ "browser": "./dist/client/browser/index.js",
17
+ "default": "./dist/client/ssr/index.js"
18
+ },
19
+ "./style.css": "./dist/client/style.css"
20
+ },
21
+ "module": "./dist/node/index.js",
22
+ "types": "./dist/node/index.d.ts",
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "peerDependencies": {
27
+ "vitepress": "^1.6.4 || ^2.0.0-alpha.17",
28
+ "vue": "^3.5.0"
29
+ },
30
+ "dependencies": {
31
+ "@pengzhanbo/utils": "^3.7.3",
32
+ "@vueuse/core": "^14.3.0",
33
+ "vitepress-plugin-toolkit": "0.1.0",
34
+ "vitepress-tuck": "0.1.0"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "clean": "rimraf --glob ./dist",
41
+ "dev": "pnpm '/(tsdown|copy):watch/'",
42
+ "build": "pnpm tsdown && pnpm copy",
43
+ "copy": "cpx \"src/**/*.css\" dist",
44
+ "copy:watch": "pnpm copy -w",
45
+ "tsdown": "tsdown --config-loader unrun",
46
+ "tsdown:watch": "pnpm tsdown -w"
47
+ }
48
+ }