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 c341e6a..e4a16a6 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 @@ -18,11 +18,14 @@ export function parseTreeContent(content: string): ParsedTree { lines = lines.slice(1); } + // Detect the indentation mode (2-space or 4-space) for the entire content + const indentSize = detectIndentSize(lines); + const nodes: TreeNode[] = []; const stack: { node: TreeNode; indent: number }[] = []; for (const line of lines) { - const indent = calculateIndent(line); + const indent = calculateIndent(line, indentSize); const fullName = extractName(line); // Extract name and comment @@ -62,11 +65,42 @@ export function parseTreeContent(content: string): ParsedTree { return { nodes, raw: content }; } +/** + * Detect the indentation size used in the content + * Returns 2 for 2-space indentation, 4 for 4-space indentation + */ +function detectIndentSize(lines: string[]): number { + for (const line of lines) { + // Look for space-only indentation before branch characters + const match = line.match(/^( +)[├└]/); + if (match) { + const spaceCount = match[1].length; + // If we find a line with 2 spaces (not 4), it's 2-space mode + if (spaceCount === 2) { + return 2; + } + // If we find exactly 4 spaces, check if there are any 2-space lines + // to distinguish between "4-space mode" and "2-space mode at level 2" + } + + // Also check for "│ " (pipe + 1 space) pattern which indicates 2-space mode + if (/│ [├└]/.test(line)) { + return 2; + } + } + + // Default to 4-space mode + return 4; +} + /** * Calculate indent level from line * Supports both 4-character ("│ ") and 2-character ("│ ") indent patterns + * + * @param line - The line to calculate indent for + * @param indentSize - The detected indent size (2 or 4) */ -function calculateIndent(line: string): number { +function calculateIndent(line: string, indentSize: number): number { let indent = 0; let i = 0; @@ -74,32 +108,31 @@ function calculateIndent(line: string): number { const char = line[i]; // Check for "│ " pattern (vertical line + 3 spaces) - 4-char indent - if (char === '│' && line.substring(i, i + 4) === '│ ') { + if (indentSize === 4 && 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] === ' ') { + if (indentSize === 2 && char === '│' && line[i + 1] === ' ') { indent++; i += 2; continue; } - // Check for 4 spaces - if (line.substring(i, i + 4) === ' ') { - indent++; - i += 4; - continue; - } - - // Check for 2 spaces (must come after 4-space check) - if (line.substring(i, i + 2) === ' ') { - indent++; - i += 2; - continue; + // Handle space-only indentation based on detected indent size + if (char === ' ') { + if (indentSize === 2 && line.substring(i, i + 2) === ' ') { + indent++; + i += 2; + continue; + } + if (indentSize === 4 && line.substring(i, i + 4) === ' ') { + indent++; + i += 4; + continue; + } } // Check for branch characters (├── or └──) diff --git a/packages/rspress-plugin-file-tree/tests/parser.spec.ts b/packages/rspress-plugin-file-tree/tests/parser.spec.ts index a72336b..55b5e69 100644 --- a/packages/rspress-plugin-file-tree/tests/parser.spec.ts +++ b/packages/rspress-plugin-file-tree/tests/parser.spec.ts @@ -897,3 +897,66 @@ doc_build expect(ellipsis2.name).toBe('...'); expect(ellipsis2.type).toBe('file'); }); + +test('Should parse docs structure with basic directory containing mdx files', () => { + const input = ` +docs +├── _meta.json +└── guides + ├── _meta.json + └── basic + ├── introduction.mdx + ├── install.mdx + └── plugin-development.md +`; + + const result = parseTreeContent(input).nodes; + + // docs is the root directory + const docs = result[0]; + expect(docs.name).toBe('docs'); + expect(docs.type).toBe('directory'); + expect(docs.children.length).toBe(2); + + // _meta.json at root level + const metaJson = docs.children[0]; + expect(metaJson.name).toBe('_meta.json'); + expect(metaJson.type).toBe('file'); + expect(metaJson.extension).toBe('json'); + + // guides directory + const guides = docs.children[1]; + expect(guides.name).toBe('guides'); + expect(guides.type).toBe('directory'); + expect(guides.children.length).toBe(2); + + // _meta.json in guides + const guidesMetaJson = guides.children[0]; + expect(guidesMetaJson.name).toBe('_meta.json'); + expect(guidesMetaJson.type).toBe('file'); + expect(guidesMetaJson.extension).toBe('json'); + + // basic directory (should be a directory, not a file) + const basic = guides.children[1]; + expect(basic.name).toBe('basic'); + expect(basic.type).toBe('directory'); + expect(basic.children.length).toBe(3); + + // introduction.mdx + const introduction = basic.children[0]; + expect(introduction.name).toBe('introduction.mdx'); + expect(introduction.type).toBe('file'); + expect(introduction.extension).toBe('mdx'); + + // install.mdx + const install = basic.children[1]; + expect(install.name).toBe('install.mdx'); + expect(install.type).toBe('file'); + expect(install.extension).toBe('mdx'); + + // plugin-development.md + const pluginDev = basic.children[2]; + expect(pluginDev.name).toBe('plugin-development.md'); + expect(pluginDev.type).toBe('file'); + expect(pluginDev.extension).toBe('md'); +});