diff --git a/.changeset/file-tree-improvements.md b/.changeset/file-tree-improvements.md new file mode 100644 index 0000000..4a00cc8 --- /dev/null +++ b/.changeset/file-tree-improvements.md @@ -0,0 +1,13 @@ +--- +"rspress-plugin-file-tree": patch +--- + +Improve file tree parser and UI: + +- Support both 2-space and 4-space indentation formats +- Support comments after filenames (any text after the filename is treated as comment) +- Support `#`, `//`, `<--`, `-->` and other comment styles +- Skip leading `.` line (current directory marker) +- Add HTML file icon support +- Empty directories now default to collapsed state +- Add `...` ellipsis support for omitted content diff --git a/packages/rspress-plugin-file-tree/src/components/FileTree/FileTree.module.less b/packages/rspress-plugin-file-tree/src/components/FileTree/FileTree.module.less index 2b33ae2..c490023 100644 --- a/packages/rspress-plugin-file-tree/src/components/FileTree/FileTree.module.less +++ b/packages/rspress-plugin-file-tree/src/components/FileTree/FileTree.module.less @@ -8,5 +8,6 @@ border-radius: var(--rp-radius); border: var(--rp-code-block-border, 1px solid var(--rp-c-divider-light)); box-shadow: var(--rp-code-block-shadow, none); + background-color: var(--rp-code-block-bg); padding: 8px; } diff --git a/packages/rspress-plugin-file-tree/src/components/FileTree/FileTreeItem.module.less b/packages/rspress-plugin-file-tree/src/components/FileTree/FileTreeItem.module.less index 0a2e522..c840101 100644 --- a/packages/rspress-plugin-file-tree/src/components/FileTree/FileTreeItem.module.less +++ b/packages/rspress-plugin-file-tree/src/components/FileTree/FileTreeItem.module.less @@ -51,6 +51,15 @@ text-overflow: ellipsis; } +.comment { + margin-left: 8px; + color: var(--rp-c-text-2); + font-style: italic; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .children { display: flex; flex-direction: column; diff --git a/packages/rspress-plugin-file-tree/src/components/FileTree/FileTreeItem.tsx b/packages/rspress-plugin-file-tree/src/components/FileTree/FileTreeItem.tsx index 7eba987..aeb4810 100644 --- a/packages/rspress-plugin-file-tree/src/components/FileTree/FileTreeItem.tsx +++ b/packages/rspress-plugin-file-tree/src/components/FileTree/FileTreeItem.tsx @@ -12,7 +12,9 @@ interface FileTreeItemProps { const INDENT_SIZE = 12; export const FileTreeItem: React.FC = ({ node, depth }) => { - const [expanded, setExpanded] = useState(true); + // Default to collapsed if directory has no children + const hasChildren = node.type === 'directory' && node.children.length > 0; + const [expanded, setExpanded] = useState(hasChildren); const icon = useMemo(() => { if (node.type === 'directory') { @@ -57,6 +59,7 @@ export const FileTreeItem: React.FC = ({ node, depth }) => { /> {node.name} + {node.comment && {node.comment}} {isDirectory && node.children.length > 0 && expanded && ( diff --git a/packages/rspress-plugin-file-tree/src/components/languages.ts b/packages/rspress-plugin-file-tree/src/components/languages.ts index f568a58..b2a9e0d 100644 --- a/packages/rspress-plugin-file-tree/src/components/languages.ts +++ b/packages/rspress-plugin-file-tree/src/components/languages.ts @@ -101,4 +101,14 @@ export const SUPPORTED_LANGUAGES: LanguageDefinition[] = [ }, ], }, + { + id: 'html', + icons: [ + { + name: 'html', + content: () => import('material-icon-theme/icons/html.svg?raw'), + matcher: /^.*\.html?$/, + }, + ], + }, ]; diff --git a/packages/rspress-plugin-file-tree/src/components/tree-parser/tree-parser.ts b/packages/rspress-plugin-file-tree/src/components/tree-parser/tree-parser.ts index 3e28ce8..c341e6a 100644 --- a/packages/rspress-plugin-file-tree/src/components/tree-parser/tree-parser.ts +++ b/packages/rspress-plugin-file-tree/src/components/tree-parser/tree-parser.ts @@ -11,17 +11,24 @@ import { ParsedTree, TreeNode } from './types'; * └── another.ts */ export function parseTreeContent(content: string): ParsedTree { - const lines = content.split('\n').filter((line) => line.trim()); + let lines = content.split('\n').filter((line) => line.trim()); + + // Skip leading "." line (current directory marker) + if (lines.length > 0 && lines[0].trim() === '.') { + lines = lines.slice(1); + } + const nodes: TreeNode[] = []; const stack: { node: TreeNode; indent: number }[] = []; for (const line of lines) { const indent = calculateIndent(line); const fullName = extractName(line); - // Split name and potential comment - // e.g. "file.ts // comment" -> name="file.ts", comment="comment" - const commentMatch = fullName.match(/^(.*?)(?:\s*\/\/\s*(.*))?$/); - const name = commentMatch ? commentMatch[1].trim() : fullName; + + // Extract name and comment + // Rule: filename ends when we detect a valid file/directory name pattern, + // everything after (with leading spaces) is treated as comment + const { name, comment } = extractNameAndComment(fullName); if (!name) continue; @@ -32,6 +39,7 @@ export function parseTreeContent(content: string): ParsedTree { type: isDirectory ? 'directory' : 'file', children: [], extension: isDirectory ? undefined : getExtension(name), + comment, }; // Find parent node by popping items with equal or greater indent @@ -56,7 +64,7 @@ export function parseTreeContent(content: string): ParsedTree { /** * Calculate indent level from line - * Each level is typically 4 characters: "│ " or " " + * Supports both 4-character ("│ ") and 2-character ("│ ") indent patterns */ function calculateIndent(line: string): number { let indent = 0; @@ -65,13 +73,21 @@ function calculateIndent(line: string): number { while (i < line.length) { const char = line[i]; - // Check for "│ " pattern (vertical line + 3 spaces) + // Check for "│ " pattern (vertical line + 3 spaces) - 4-char indent if (char === '│' && line.substring(i, i + 4) === '│ ') { indent++; i += 4; continue; } + // Check for "│ " pattern (vertical line + 1 space) - 2-char indent + // Must check this AFTER 4-char to avoid partial matches + if (char === '│' && line[i + 1] === ' ') { + indent++; + i += 2; + continue; + } + // Check for 4 spaces if (line.substring(i, i + 4) === ' ') { indent++; @@ -79,6 +95,13 @@ function calculateIndent(line: string): number { continue; } + // Check for 2 spaces (must come after 4-space check) + if (line.substring(i, i + 2) === ' ') { + indent++; + i += 2; + continue; + } + // Check for branch characters (├── or └──) if (char === '├' || char === '└') { if ( @@ -108,27 +131,127 @@ function extractName(line: string): string { .trim(); } +/** + * Extract filename/dirname and comment from a line + * + * Rules for detecting end of filename: + * 1. Files have extensions: name.ext (e.g., file.ts, config.json) + * 2. Hidden files start with dot: .gitignore, .env + * 3. Directories end with / or have no extension + * 4. Special names: ... (ellipsis for omitted content) + * 5. Everything after the name (separated by 2+ spaces) is comment + * + * Note: Filenames can contain spaces (e.g., "0. file.ts"), so we use + * 2+ consecutive spaces as the delimiter between name and comment. + */ +function extractNameAndComment(fullName: string): { + name: string; + comment: string | undefined; +} { + const trimmed = fullName.trim(); + if (!trimmed) { + return { name: '', comment: undefined }; + } + + // Special case: "..." or similar ellipsis patterns (standalone) + if (/^\.{2,}$/.test(trimmed)) { + return { name: trimmed, comment: undefined }; + } + + // Strategy: Find the last occurrence of a file extension pattern, + // then check if there's content after it (separated by 2+ spaces) + + // First, try to split by 2+ spaces (common comment delimiter) + const doubleSpaceMatch = trimmed.match(/^(.+?)\s{2,}(.+)$/); + if (doubleSpaceMatch) { + const potentialName = doubleSpaceMatch[1].trim(); + const potentialComment = doubleSpaceMatch[2].trim(); + + // Verify the name part looks like a valid file/directory + if (isValidName(potentialName)) { + return { name: potentialName, comment: potentialComment }; + } + } + + // If no double-space delimiter, check if the whole thing is just a name + // For files with extensions or directories, we can be more lenient + // Look for pattern: name.ext followed by single space and non-extension content + const singleSpaceMatch = trimmed.match( + /^(.+?\.[a-zA-Z0-9]+)\s+([^.].*)$/ + ); + if (singleSpaceMatch) { + const potentialName = singleSpaceMatch[1].trim(); + const potentialComment = singleSpaceMatch[2].trim(); + return { name: potentialName, comment: potentialComment }; + } + + // Check for hidden files followed by comment + const hiddenFileMatch = trimmed.match(/^(\.[^\s]+)\s+(.+)$/); + if (hiddenFileMatch) { + return { + name: hiddenFileMatch[1].trim(), + comment: hiddenFileMatch[2].trim(), + }; + } + + // Check for directory name followed by single space and comment + // Directory names don't have extensions, so look for "word space non-word-start" + // Also handles names starting with numbers like "2. components" + const dirCommentMatch = trimmed.match(/^([\w][\w.\s-]*?)\s+([^a-zA-Z0-9].*)$/); + if (dirCommentMatch) { + const potentialName = dirCommentMatch[1].trim(); + // Make sure it's not a file with extension + if (!/\.[a-zA-Z0-9]+$/.test(potentialName)) { + return { + name: potentialName, + comment: dirCommentMatch[2].trim(), + }; + } + } + + // No comment detected, return the whole thing as name + return { name: trimmed, comment: undefined }; +} + +/** + * Check if a string looks like a valid file/directory name + */ +function isValidName(name: string): boolean { + if (!name) return false; + + // Ends with / (explicit directory) + if (name.endsWith('/')) return true; + + // Has a file extension + if (/\.[a-zA-Z0-9]+$/.test(name)) return true; + + // Hidden file/directory (starts with dot) + if (name.startsWith('.')) return true; + + // Looks like a directory name (no extension, word characters) + if (/^[\w\s.-]+$/.test(name)) return true; + + return false; +} + /** * Check if name represents a directory * - Ends with / * - Has no extension */ function isDirectoryName(name: string): boolean { - // Strip comments if any (though name passed here usually already has them, let's be safe if logic changes) - const cleanName = name.split(/\s+\/\//)[0].trim(); - - if (cleanName.endsWith('/')) return true; + if (name.endsWith('/')) return true; - const lastPart = cleanName.split('/').pop() || cleanName; + const lastPart = name.split('/').pop() || name; - // Use a more robust check: files usually have extensions. - // Directories usually don't. - // Exception: Dotfiles (.gitignore) are files. - // exception: names with dots but known extensions are files. + // Special case: ellipsis is not a directory + if (/^\.{2,}$/.test(lastPart)) { + return false; + } // If it starts with a dot, it's a file (hidden file), e.g., .gitignore if (lastPart.startsWith('.')) { - return false; // Treat as file + return false; } // If it has an extension (e.g. foo.ts, bar.config.js), it's a file diff --git a/packages/rspress-plugin-file-tree/src/components/tree-parser/types.ts b/packages/rspress-plugin-file-tree/src/components/tree-parser/types.ts index 5d38cdb..eb57f8e 100644 --- a/packages/rspress-plugin-file-tree/src/components/tree-parser/types.ts +++ b/packages/rspress-plugin-file-tree/src/components/tree-parser/types.ts @@ -3,6 +3,7 @@ export interface TreeNode { type: 'file' | 'directory'; children: TreeNode[]; extension?: string; + comment?: string; } export interface ParsedTree { diff --git a/packages/rspress-plugin-file-tree/tests/parser.spec.ts b/packages/rspress-plugin-file-tree/tests/parser.spec.ts index a380006..a72336b 100644 --- a/packages/rspress-plugin-file-tree/tests/parser.spec.ts +++ b/packages/rspress-plugin-file-tree/tests/parser.spec.ts @@ -30,12 +30,7 @@ test('Should parse normal input', () => { [ { "children": [], - "extension": "", - "name": ".", - "type": "file", - }, - { - "children": [], + "comment": undefined, "extension": "ts", "name": "rspress.config.ts", "type": "file", @@ -46,6 +41,7 @@ test('Should parse normal input', () => { "children": [ { "children": [], + "comment": undefined, "extension": "tsx", "name": "FileTreeRender.tsx", "type": "file", @@ -54,111 +50,130 @@ test('Should parse normal input', () => { "children": [ { "children": [], + "comment": undefined, "extension": "tsx", "name": "Expand.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "FileIcon.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "Tree.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeContext.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeFile.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeFolder.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeFolderIcon.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeIndents.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeStatusIcon.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "less", "name": "index.less", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "index.tsx", "type": "file", }, ], + "comment": undefined, "extension": undefined, "name": "Tree", "type": "directory", }, { "children": [], + "comment": undefined, "extension": "ts", "name": "helpers.ts", "type": "file", }, { "children": [], + "comment": undefined, "extension": "ts", "name": "presets.ts", "type": "file", }, ], + "comment": undefined, "extension": undefined, "name": "components", "type": "directory", }, { "children": [], + "comment": undefined, "extension": "ts", "name": "index.ts", "type": "file", }, { "children": [], + "comment": undefined, "extension": "ts", "name": "parser.ts", "type": "file", }, ], + "comment": undefined, "extension": undefined, "name": "src", "type": "directory", }, { "children": [], + "comment": undefined, "extension": "json", "name": "tsconfig.json", "type": "file", @@ -195,6 +210,7 @@ test('Should parse input with comments', () => { [ { "children": [], + "comment": "// Rspress config", "extension": "ts", "name": "rspress.config.ts", "type": "file", @@ -205,6 +221,7 @@ test('Should parse input with comments', () => { "children": [ { "children": [], + "comment": "// The file tree render entry", "extension": "tsx", "name": "FileTreeRender.tsx", "type": "file", @@ -213,111 +230,130 @@ test('Should parse input with comments', () => { "children": [ { "children": [], + "comment": undefined, "extension": "tsx", "name": "Expand.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "FileIcon.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "Tree.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeContext.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeFile.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeFolder.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeFolderIcon.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeIndents.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeStatusIcon.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "less", "name": "index.less", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "index.tsx", "type": "file", }, ], + "comment": undefined, "extension": undefined, "name": "Tree", "type": "directory", }, { "children": [], + "comment": undefined, "extension": "ts", "name": "helpers.ts", "type": "file", }, { "children": [], + "comment": undefined, "extension": "ts", "name": "presets.ts", "type": "file", }, ], + "comment": "// Shared components", "extension": undefined, "name": "components", "type": "directory", }, { "children": [], + "comment": undefined, "extension": "ts", "name": "index.ts", "type": "file", }, { "children": [], + "comment": "// Parse string input to tree structure", "extension": "ts", "name": "parser.ts", "type": "file", }, ], + "comment": undefined, "extension": undefined, "name": "src", "type": "directory", }, { "children": [], + "comment": undefined, "extension": "json", "name": "tsconfig.json", "type": "file", @@ -349,11 +385,37 @@ test('Should parse input with spaces', () => { │ ├── index.ts │ └── parser.ts // Parse string input to tree structure └── 1. tsconfig.json +`; + + // Alternative format with 2-space indentation (should produce same result) + const input2SpaceIndent = ` +├── 0. rspress.config.ts // Rspress config +├── -1. src +│ ├── 2. components // Shared components +│ │ ├── FileTreeRender.tsx // The file tree render entry +│ │ ├── Tree +│ │ │ ├── Expand.tsx +│ │ │ ├── FileIcon.tsx +│ │ │ ├── Tree.tsx +│ │ │ ├── TreeContext.tsx +│ │ │ ├── TreeFile.tsx +│ │ │ ├── TreeFolder.tsx +│ │ │ ├── TreeFolderIcon.tsx +│ │ │ ├── TreeIndents.tsx +│ │ │ ├── TreeStatusIcon.tsx +│ │ │ ├── index.less +│ │ │ └── index.tsx +│ │ ├── helpers.ts +│ │ └── presets.ts +│ ├── index.ts +│ └── parser.ts // Parse string input to tree structure +└── 1. tsconfig.json `; expect(parseTreeContent(input).nodes).toMatchInlineSnapshot(` [ { "children": [], + "comment": "// Rspress config", "extension": "ts", "name": "0. rspress.config.ts", "type": "file", @@ -364,6 +426,7 @@ test('Should parse input with spaces', () => { "children": [ { "children": [], + "comment": "// The file tree render entry", "extension": "tsx", "name": "FileTreeRender.tsx", "type": "file", @@ -372,115 +435,465 @@ test('Should parse input with spaces', () => { "children": [ { "children": [], + "comment": undefined, "extension": "tsx", "name": "Expand.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "FileIcon.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "Tree.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeContext.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeFile.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeFolder.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeFolderIcon.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeIndents.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "TreeStatusIcon.tsx", "type": "file", }, { "children": [], + "comment": undefined, "extension": "less", "name": "index.less", "type": "file", }, { "children": [], + "comment": undefined, "extension": "tsx", "name": "index.tsx", "type": "file", }, ], + "comment": undefined, "extension": undefined, "name": "Tree", "type": "directory", }, { "children": [], + "comment": undefined, "extension": "ts", "name": "helpers.ts", "type": "file", }, { "children": [], + "comment": undefined, "extension": "ts", "name": "presets.ts", "type": "file", }, ], + "comment": "// Shared components", "extension": undefined, "name": "2. components", "type": "directory", }, { "children": [], + "comment": undefined, "extension": "ts", "name": "index.ts", "type": "file", }, { "children": [], + "comment": "// Parse string input to tree structure", "extension": "ts", "name": "parser.ts", "type": "file", }, ], + "comment": undefined, "extension": undefined, "name": "-1. src", "type": "directory", }, { "children": [], + "comment": undefined, "extension": "json", "name": "1. tsconfig.json", "type": "file", }, ] `); + + // Both formats should produce the same result + expect(parseTreeContent(input2SpaceIndent).nodes).toEqual( + parseTreeContent(input).nodes + ); +}); + +test('Should parse 2-space and 4-space indentation identically', () => { + // 4-space indentation format (standard) + const input4Space = ` +├── docs +│ ├── index.md +│ ├── api +│ │ ├── index.md +│ │ ├── theme +│ │ │ ├── index.md +│ │ │ ├── component.mdx +│ │ │ ├── utils.mdx +`; + + // 2-space indentation format (alternative) + const input2Space = ` +├── docs +│ ├── index.md +│ ├── api +│ │ ├── index.md +│ │ ├── theme +│ │ │ ├── index.md +│ │ │ ├── component.mdx +│ │ │ ├── utils.mdx +`; + + const result4Space = parseTreeContent(input4Space).nodes; + const result2Space = parseTreeContent(input2Space).nodes; + + // Both should produce identical results + expect(result2Space).toEqual(result4Space); + + // Verify the structure is correct + expect(result4Space).toMatchInlineSnapshot(` + [ + { + "children": [ + { + "children": [], + "comment": undefined, + "extension": "md", + "name": "index.md", + "type": "file", + }, + { + "children": [ + { + "children": [], + "comment": undefined, + "extension": "md", + "name": "index.md", + "type": "file", + }, + { + "children": [ + { + "children": [], + "comment": undefined, + "extension": "md", + "name": "index.md", + "type": "file", + }, + { + "children": [], + "comment": undefined, + "extension": "mdx", + "name": "component.mdx", + "type": "file", + }, + { + "children": [], + "comment": undefined, + "extension": "mdx", + "name": "utils.mdx", + "type": "file", + }, + ], + "comment": undefined, + "extension": undefined, + "name": "theme", + "type": "directory", + }, + ], + "comment": undefined, + "extension": undefined, + "name": "api", + "type": "directory", + }, + ], + "comment": undefined, + "extension": undefined, + "name": "docs", + "type": "directory", + }, + ] + `); +}); + +test('Should parse space-only indentation without tree characters', () => { + const input = ` +docs +├── advanced.mdx +└── advanced + ├── _meta.json + └── nested +`; + + expect(parseTreeContent(input).nodes).toMatchInlineSnapshot(` + [ + { + "children": [ + { + "children": [], + "comment": undefined, + "extension": "mdx", + "name": "advanced.mdx", + "type": "file", + }, + { + "children": [ + { + "children": [], + "comment": undefined, + "extension": "json", + "name": "_meta.json", + "type": "file", + }, + { + "children": [], + "comment": undefined, + "extension": undefined, + "name": "nested", + "type": "directory", + }, + ], + "comment": undefined, + "extension": undefined, + "name": "advanced", + "type": "directory", + }, + ], + "comment": undefined, + "extension": undefined, + "name": "docs", + "type": "directory", + }, + ] + `); +}); + +test('Should parse both // and # style comments', () => { + const input = ` +docs +├── advanced.mdx +└── advanced + ├── _meta.json # hello world + └── utils.ts // hello world2 +`; + + const result = parseTreeContent(input).nodes; + + // Check that comments are parsed correctly (comments include the markers) + const advancedDir = result[0].children[1]; + expect(advancedDir.name).toBe('advanced'); + + const metaJson = advancedDir.children[0]; + expect(metaJson.name).toBe('_meta.json'); + expect(metaJson.comment).toBe('# hello world'); + + const utilsTs = advancedDir.children[1]; + expect(utilsTs.name).toBe('utils.ts'); + expect(utilsTs.comment).toBe('// hello world2'); +}); + +test('Should parse arrow style comments (<--, -->, <-, ->)', () => { + const input = ` +├── docs +│ └── index.mdx <-- "@rspress/core/theme" +├── theme +│ └── index.tsx <-- "@rspress/core/theme-original" +└── rspress.config.ts +`; + + const result = parseTreeContent(input).nodes; + + // docs/index.mdx - comment includes the arrow + const docsDir = result[0]; + expect(docsDir.name).toBe('docs'); + const indexMdx = docsDir.children[0]; + expect(indexMdx.name).toBe('index.mdx'); + expect(indexMdx.comment).toBe('<-- "@rspress/core/theme"'); + + // theme/index.tsx - comment includes the arrow + const themeDir = result[1]; + expect(themeDir.name).toBe('theme'); + const indexTsx = themeDir.children[0]; + expect(indexTsx.name).toBe('index.tsx'); + expect(indexTsx.comment).toBe('<-- "@rspress/core/theme-original"'); + + // rspress.config.ts (no comment) + const configTs = result[2]; + expect(configTs.name).toBe('rspress.config.ts'); + expect(configTs.comment).toBeUndefined(); +}); + +test('Should treat any text after filename as comment', () => { + const input = ` +├── file1.ts // slash comment +├── file2.ts # hash comment +├── file3.ts <-- left arrow comment +├── file4.ts --> right arrow comment +├── file5.ts any text here is comment +└── file6.ts (note: this is also a comment) +`; + + const result = parseTreeContent(input).nodes; + + // All text after filename is treated as comment (including markers) + expect(result[0].comment).toBe('// slash comment'); + expect(result[1].comment).toBe('# hash comment'); + expect(result[2].comment).toBe('<-- left arrow comment'); + expect(result[3].comment).toBe('--> right arrow comment'); + expect(result[4].comment).toBe('any text here is comment'); + expect(result[5].comment).toBe('(note: this is also a comment)'); +}); + +test('Should skip leading . line (current directory marker)', () => { + const input = ` +. +├── docs +│ └── index.mdx <-- "@rspress/core/theme" +├── theme +│ └── index.tsx <-- "@rspress/core/theme-original" +└── rspress.config.ts +`; + + const result = parseTreeContent(input).nodes; + + // Should have 3 top-level items (docs, theme, rspress.config.ts) + // The leading "." should be skipped + expect(result.length).toBe(3); + + // docs directory + const docsDir = result[0]; + expect(docsDir.name).toBe('docs'); + expect(docsDir.type).toBe('directory'); + expect(docsDir.children[0].name).toBe('index.mdx'); + expect(docsDir.children[0].comment).toBe('<-- "@rspress/core/theme"'); + + // theme directory + const themeDir = result[1]; + expect(themeDir.name).toBe('theme'); + expect(themeDir.type).toBe('directory'); + expect(themeDir.children[0].name).toBe('index.tsx'); + expect(themeDir.children[0].comment).toBe('<-- "@rspress/core/theme-original"'); + + // rspress.config.ts + const configTs = result[2]; + expect(configTs.name).toBe('rspress.config.ts'); + expect(configTs.type).toBe('file'); +}); + +test('Should parse .html files and ... ellipsis correctly', () => { + const input = ` +doc_build +├── static +│ ├── main.js +│ └── ... +├── index.html +├── about.html +├── posts +│ ├── hello-world.html +│ └── ... +`; + + const result = parseTreeContent(input).nodes; + + // doc_build is the root directory + const docBuild = result[0]; + expect(docBuild.name).toBe('doc_build'); + expect(docBuild.type).toBe('directory'); + + // static directory + const staticDir = docBuild.children[0]; + expect(staticDir.name).toBe('static'); + expect(staticDir.type).toBe('directory'); + + // main.js + const mainJs = staticDir.children[0]; + expect(mainJs.name).toBe('main.js'); + expect(mainJs.type).toBe('file'); + expect(mainJs.extension).toBe('js'); + + // ... ellipsis (should be treated as file, not directory) + const ellipsis1 = staticDir.children[1]; + expect(ellipsis1.name).toBe('...'); + expect(ellipsis1.type).toBe('file'); + + // index.html + const indexHtml = docBuild.children[1]; + expect(indexHtml.name).toBe('index.html'); + expect(indexHtml.type).toBe('file'); + expect(indexHtml.extension).toBe('html'); + + // about.html + const aboutHtml = docBuild.children[2]; + expect(aboutHtml.name).toBe('about.html'); + expect(aboutHtml.type).toBe('file'); + expect(aboutHtml.extension).toBe('html'); + + // posts directory + const postsDir = docBuild.children[3]; + expect(postsDir.name).toBe('posts'); + expect(postsDir.type).toBe('directory'); + + // hello-world.html + const helloWorldHtml = postsDir.children[0]; + expect(helloWorldHtml.name).toBe('hello-world.html'); + expect(helloWorldHtml.type).toBe('file'); + expect(helloWorldHtml.extension).toBe('html'); + + // ... ellipsis in posts + const ellipsis2 = postsDir.children[1]; + expect(ellipsis2.name).toBe('...'); + expect(ellipsis2.type).toBe('file'); });