rspress-plugin-file-tree 0.1.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +84 -0
  4. package/dist/components/Tree/Expand.d.ts +14 -0
  5. package/dist/components/Tree/Expand.js +104 -0
  6. package/dist/components/Tree/FileIcon.d.ts +8 -0
  7. package/dist/components/Tree/FileIcon.js +12 -0
  8. package/dist/components/Tree/FileTreeRender.d.ts +11 -0
  9. package/dist/components/Tree/FileTreeRender.js +7 -0
  10. package/dist/components/Tree/Tree.d.ts +20 -0
  11. package/dist/components/Tree/Tree.js +52 -0
  12. package/dist/components/Tree/TreeContext.d.ts +9 -0
  13. package/dist/components/Tree/TreeContext.js +7 -0
  14. package/dist/components/Tree/TreeFile.d.ts +14 -0
  15. package/dist/components/Tree/TreeFile.js +26 -0
  16. package/dist/components/Tree/TreeFolder.d.ts +14 -0
  17. package/dist/components/Tree/TreeFolder.js +37 -0
  18. package/dist/components/Tree/TreeFolderIcon.d.ts +8 -0
  19. package/dist/components/Tree/TreeFolderIcon.js +12 -0
  20. package/dist/components/Tree/TreeIndents.d.ts +6 -0
  21. package/dist/components/Tree/TreeIndents.js +10 -0
  22. package/dist/components/Tree/TreeStatusIcon.d.ts +9 -0
  23. package/dist/components/Tree/TreeStatusIcon.js +13 -0
  24. package/dist/components/helpers.d.ts +5 -0
  25. package/dist/components/helpers.js +35 -0
  26. package/dist/components/presets.d.ts +2 -0
  27. package/dist/components/presets.js +4 -0
  28. package/dist/index.d.ts +6 -0
  29. package/dist/index.js +31 -0
  30. package/dist/parser.d.ts +7 -0
  31. package/dist/parser.js +37 -0
  32. package/doc_build/static/search_index.aad48136.json +1 -0
  33. package/docs/index.md +26 -0
  34. package/image.png +0 -0
  35. package/package.json +44 -0
  36. package/rspress.config.ts +13 -0
  37. package/src/components/Tree/Expand.tsx +149 -0
  38. package/src/components/Tree/FileIcon.tsx +41 -0
  39. package/src/components/Tree/FileTreeRender.tsx +16 -0
  40. package/src/components/Tree/Tree.tsx +112 -0
  41. package/src/components/Tree/TreeContext.tsx +18 -0
  42. package/src/components/Tree/TreeFile.tsx +69 -0
  43. package/src/components/Tree/TreeFolder.tsx +108 -0
  44. package/src/components/Tree/TreeFolderIcon.tsx +40 -0
  45. package/src/components/Tree/TreeIndents.tsx +26 -0
  46. package/src/components/Tree/TreeStatusIcon.tsx +45 -0
  47. package/src/components/Tree/index.less +178 -0
  48. package/src/components/helpers.ts +42 -0
  49. package/src/components/presets.ts +5 -0
  50. package/src/index.ts +50 -0
  51. package/src/parser.ts +50 -0
  52. package/tsconfig.json +8 -0
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { buildClassName } from '../presets';
3
+
4
+ interface Props {
5
+ count: number;
6
+ }
7
+
8
+ const TreeIndents: React.FC<Props> = ({ count }) => {
9
+ if (count === 0) return null;
10
+
11
+ return (
12
+ <>
13
+ {[...new Array(count)].map((_, index) => (
14
+ <span
15
+ className={buildClassName('indent')}
16
+ key={`indent-${index}`}
17
+ style={{
18
+ left: `calc(-1.875rem * ${index + 1} + 0.75rem)`,
19
+ }}
20
+ ></span>
21
+ ))}
22
+ </>
23
+ );
24
+ };
25
+
26
+ export default TreeIndents;
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { buildClassName } from '../presets';
3
+
4
+ export interface TreeStatusIconProps {
5
+ color?: string;
6
+ width?: number;
7
+ height?: number;
8
+ active?: boolean;
9
+ }
10
+
11
+ const defaultProps = {
12
+ width: 12,
13
+ height: 12,
14
+ active: false,
15
+ };
16
+
17
+ const TreeStatusIcon: React.FC<TreeStatusIconProps> = ({
18
+ color,
19
+ width,
20
+ height,
21
+ active,
22
+ }: TreeStatusIconProps & typeof defaultProps) => {
23
+ return (
24
+ <svg
25
+ className={buildClassName('folder-status-icon')}
26
+ viewBox="0 0 24 24"
27
+ width={width}
28
+ height={height}
29
+ stroke="currentColor"
30
+ strokeWidth="1.5"
31
+ strokeLinecap="round"
32
+ strokeLinejoin="round"
33
+ fill="none"
34
+ shapeRendering="geometricPrecision"
35
+ >
36
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
37
+ {!active && <path d="M12 8v8" />}
38
+ <path d="M8 12h8" />
39
+ </svg>
40
+ );
41
+ };
42
+
43
+ TreeStatusIcon.defaultProps = defaultProps;
44
+ TreeStatusIcon.displayName = 'GeistTreeStatusIcon';
45
+ export default TreeStatusIcon;
@@ -0,0 +1,178 @@
1
+ @rp-file-tree: ~'rp-file-tree';
2
+
3
+ @rp-tree-color-light: #000;
4
+ @rp-tree-color-dark: #fafafa;
5
+
6
+ @rp-tree-status-bg-light: #fff;
7
+ @rp-tree-status-bg-dark: #000;
8
+
9
+ @rp-tree-indent-bg-light: #eaeaea;
10
+ @rp-tree-indent-bg-dark: #333;
11
+
12
+ .@{rp-file-tree} {
13
+ &[data-dark='true'] {
14
+ --rp-tree-color: @rp-tree-color-dark;
15
+ --rp-tree-status-bg: @rp-tree-status-bg-dark;
16
+ --rp-tree-indent-bg: @rp-tree-indent-bg-dark;
17
+ }
18
+
19
+ &[data-dark='false'] {
20
+ --rp-tree-color: @rp-tree-color-light;
21
+ --rp-tree-status-bg: @rp-tree-status-bg-light;
22
+ --rp-tree-indent-bg: @rp-tree-indent-bg-light;
23
+ }
24
+
25
+ padding-left: 1.625rem;
26
+
27
+ &-expand {
28
+ &-container {
29
+ padding: 0;
30
+ margin: 0;
31
+ overflow: hidden;
32
+
33
+ &-expanded {
34
+ height: auto;
35
+ visibility: visible;
36
+ }
37
+ }
38
+ }
39
+
40
+ &-indent {
41
+ position: absolute;
42
+ top: 50%;
43
+ transform: translateY(-50%);
44
+ width: 1px;
45
+ height: 100%;
46
+ background-color: var(--rp-tree-indent-bg);
47
+ margin-left: -1px;
48
+ }
49
+
50
+ &-folder {
51
+ cursor: pointer;
52
+ line-height: 1;
53
+ user-select: none;
54
+
55
+ &-names {
56
+ display: flex;
57
+ height: 1.75rem;
58
+ align-items: center;
59
+ position: relative;
60
+
61
+ & > :global(.@{rp-file-tree}-indent) {
62
+ position: absolute;
63
+ top: 50%;
64
+ transform: translateY(-50%);
65
+ width: 1px;
66
+ height: 100%;
67
+ background-color: var(--rp-tree-indent-bg);
68
+ margin-left: -1px;
69
+ }
70
+ }
71
+
72
+ &-icon {
73
+ margin-right: 0.3rem;
74
+
75
+ & > svg {
76
+ color: var(--rp-tree-color);
77
+ }
78
+ }
79
+
80
+ &-status {
81
+ position: absolute;
82
+ left: calc(-1.125rem);
83
+ top: 50%;
84
+ transform: translate(-50%, -50%);
85
+ width: 0.875rem;
86
+ height: 0.875rem;
87
+ z-index: 10;
88
+ background-color: var(--rp-tree-status-bg);
89
+
90
+ & > svg {
91
+ color: var(--rp-tree-color);
92
+ }
93
+ }
94
+
95
+ &-status,
96
+ &-icon {
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ }
101
+
102
+ &-name {
103
+ transition: opacity 100ms ease 0ms;
104
+ white-space: nowrap;
105
+ font-size: 0.875rem;
106
+ color: var(--rp-tree-color);
107
+
108
+ &:hover {
109
+ opacity: 0.7;
110
+ }
111
+ }
112
+
113
+ &-extra {
114
+ font-size: 0.75rem;
115
+ align-self: baseline;
116
+ padding-left: 4px;
117
+ color: #888;
118
+ }
119
+
120
+ &-content {
121
+ display: flex;
122
+ flex-direction: column;
123
+ height: auto;
124
+ }
125
+ }
126
+
127
+ &-file {
128
+ cursor: pointer;
129
+ line-height: 1;
130
+ user-select: none;
131
+
132
+ &-names {
133
+ display: flex;
134
+ height: 1.75rem;
135
+ align-items: center;
136
+ position: relative;
137
+
138
+ & > :global(.@{rp-file-tree}-indent) {
139
+ position: absolute;
140
+ top: 50%;
141
+ transform: translateY(-50%);
142
+ width: 1px;
143
+ height: 100%;
144
+ background-color: var(--rp-tree-indent-bg);
145
+
146
+ margin-left: -1px;
147
+ }
148
+ }
149
+
150
+ &-icon {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ margin-right: 0.3rem;
154
+
155
+ & > svg {
156
+ color: var(--rp-tree-color);
157
+ }
158
+ }
159
+
160
+ &-name {
161
+ transition: opacity 100ms ease 0ms;
162
+ color: var(--rp-tree-color);
163
+ white-space: nowrap;
164
+ font-size: 0.875rem;
165
+
166
+ &:hover {
167
+ opacity: 0.7;
168
+ }
169
+ }
170
+
171
+ &-extra {
172
+ font-size: 0.75rem;
173
+ align-self: baseline;
174
+ padding-left: 4px;
175
+ color: #888;
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,42 @@
1
+ import React, { type ReactNode } from 'react';
2
+
3
+ export const sortChildren = (
4
+ children: ReactNode | undefined,
5
+ folderComponentType: React.ElementType,
6
+ ) => {
7
+ return React.Children.toArray(children).sort((a, b) => {
8
+ if (!React.isValidElement(a) || !React.isValidElement(b)) return 0;
9
+ if (a.type !== b.type) return a.type !== folderComponentType ? 1 : -1;
10
+ return `${a.props.name}`.charCodeAt(0) - `${b.props.name}`.charCodeAt(0);
11
+ });
12
+ };
13
+
14
+ export const makeChildPath = (name: string, parentPath?: string) => {
15
+ if (!parentPath) return name;
16
+ return `${parentPath}/${name}`;
17
+ };
18
+
19
+ export const stopPropagation = (event: React.MouseEvent) => {
20
+ event.stopPropagation();
21
+ event.nativeEvent.stopImmediatePropagation();
22
+ };
23
+
24
+ export const setChildrenProps = (
25
+ children: ReactNode | undefined,
26
+ props: Record<string, unknown>,
27
+ targetComponents: Array<React.ElementType> = [],
28
+ ): ReactNode | undefined => {
29
+ if (React.Children.count(children) === 0) return [];
30
+ const allowAll = targetComponents.length === 0;
31
+ const clone = (child: React.ReactElement, props = {}) =>
32
+ React.cloneElement(child, props);
33
+
34
+ return React.Children.map(children, (item) => {
35
+ if (!React.isValidElement(item)) return item;
36
+ if (allowAll) return clone(item, props);
37
+
38
+ const isAllowed = targetComponents.find((child) => child === item.type);
39
+ if (isAllowed) return clone(item, props);
40
+ return item;
41
+ });
42
+ };
@@ -0,0 +1,5 @@
1
+ export const presetClassName = 'rp-file-tree';
2
+
3
+ export function buildClassName(fragment?: string) {
4
+ return fragment?.length ? `${presetClassName}-${fragment}` : presetClassName;
5
+ }
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ import path from 'node:path';
2
+
3
+ import {
4
+ PresetConfigMutator,
5
+ RemarkCodeBlockToGlobalComponentPluginFactory,
6
+ } from 'rspress-plugin-devkit';
7
+
8
+ import { parseInput } from './parser';
9
+
10
+ import type { RspressPlugin } from '@rspress/shared';
11
+
12
+ interface RspressPluginFileTreeOptions {
13
+ initialExpandDepth?: number;
14
+ }
15
+
16
+ export default function rspressPluginFileTree(
17
+ options: RspressPluginFileTreeOptions = {},
18
+ ): RspressPlugin {
19
+ const { initialExpandDepth = 0 } = options;
20
+
21
+ const remarkFileTree = new RemarkCodeBlockToGlobalComponentPluginFactory({
22
+ components: [
23
+ {
24
+ lang: 'tree',
25
+ componentPath: path.join(
26
+ __dirname,
27
+ './components/Tree/FileTreeRender.tsx',
28
+ ),
29
+ propsProvider(code) {
30
+ return {
31
+ tree: parseInput(code),
32
+ initialExpandDepth,
33
+ };
34
+ },
35
+ },
36
+ ],
37
+ });
38
+
39
+ return {
40
+ name: 'rspress-plugin-file-tree',
41
+ config(config) {
42
+ return new PresetConfigMutator(config).disableMdxRs().toConfig();
43
+ },
44
+ markdown: {
45
+ remarkPlugins: [remarkFileTree.remarkPlugin],
46
+ globalComponents: remarkFileTree.mdxComponents,
47
+ },
48
+ builderConfig: remarkFileTree.builderConfig,
49
+ };
50
+ }
package/src/parser.ts ADDED
@@ -0,0 +1,50 @@
1
+ type TreeItem = {
2
+ type: 'file' | 'directory';
3
+ name: string;
4
+ files?: TreeItem[];
5
+ };
6
+
7
+ function countLeadingSpaces(line: string): number {
8
+ const matches = line.match(/^(\s*\│\s*)*/);
9
+ if (!matches) return 0;
10
+ return matches[0].length;
11
+ }
12
+
13
+ export function parseInput(input: string): TreeItem[] {
14
+ const lines = input.split('\n').filter((line) => line.trim());
15
+ const tree: TreeItem[] = [];
16
+ const stack: { level: number; item: TreeItem }[] = [];
17
+
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const line = lines[i];
20
+
21
+ if (line === '.') continue;
22
+
23
+ const level = countLeadingSpaces(line);
24
+
25
+ const name = line.trim().split(' ').slice(-1)[0];
26
+
27
+ const nextLine = lines[i + 1] || '';
28
+ const nextLineLevel = countLeadingSpaces(nextLine);
29
+ const type = nextLineLevel > level ? 'directory' : 'file';
30
+ const item: TreeItem =
31
+ type === 'directory' ? { type, name, files: [] } : { type, name };
32
+
33
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) {
34
+ stack.pop();
35
+ }
36
+
37
+ if (stack.length === 0) {
38
+ tree.push(item);
39
+ } else {
40
+ const parentItem = stack[stack.length - 1].item;
41
+ parentItem.files?.push(item);
42
+ }
43
+
44
+ if (item.type === 'directory') {
45
+ stack.push({ level, item });
46
+ }
47
+ }
48
+
49
+ return tree;
50
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "src",
4
+ "outDir": "dist"
5
+ },
6
+ "include": ["src"],
7
+ "extends": "../../tsconfig.base.json"
8
+ }