Skip to content

Commit 06939cb

Browse files
authored
fix(plugin-file-tree): fix 2-space indentation parsing for deeply nested tree structures (#17)
1 parent 01144e1 commit 06939cb

File tree

2 files changed

+113
-17
lines changed

2 files changed

+113
-17
lines changed

packages/rspress-plugin-file-tree/src/components/tree-parser/tree-parser.ts

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ export function parseTreeContent(content: string): ParsedTree {
1818
lines = lines.slice(1);
1919
}
2020

21+
// Detect the indentation mode (2-space or 4-space) for the entire content
22+
const indentSize = detectIndentSize(lines);
23+
2124
const nodes: TreeNode[] = [];
2225
const stack: { node: TreeNode; indent: number }[] = [];
2326

2427
for (const line of lines) {
25-
const indent = calculateIndent(line);
28+
const indent = calculateIndent(line, indentSize);
2629
const fullName = extractName(line);
2730

2831
// Extract name and comment
@@ -62,44 +65,74 @@ export function parseTreeContent(content: string): ParsedTree {
6265
return { nodes, raw: content };
6366
}
6467

68+
/**
69+
* Detect the indentation size used in the content
70+
* Returns 2 for 2-space indentation, 4 for 4-space indentation
71+
*/
72+
function detectIndentSize(lines: string[]): number {
73+
for (const line of lines) {
74+
// Look for space-only indentation before branch characters
75+
const match = line.match(/^( +)[]/);
76+
if (match) {
77+
const spaceCount = match[1].length;
78+
// If we find a line with 2 spaces (not 4), it's 2-space mode
79+
if (spaceCount === 2) {
80+
return 2;
81+
}
82+
// If we find exactly 4 spaces, check if there are any 2-space lines
83+
// to distinguish between "4-space mode" and "2-space mode at level 2"
84+
}
85+
86+
// Also check for "│ " (pipe + 1 space) pattern which indicates 2-space mode
87+
if (/ []/.test(line)) {
88+
return 2;
89+
}
90+
}
91+
92+
// Default to 4-space mode
93+
return 4;
94+
}
95+
6596
/**
6697
* Calculate indent level from line
6798
* Supports both 4-character ("│ ") and 2-character ("│ ") indent patterns
99+
*
100+
* @param line - The line to calculate indent for
101+
* @param indentSize - The detected indent size (2 or 4)
68102
*/
69-
function calculateIndent(line: string): number {
103+
function calculateIndent(line: string, indentSize: number): number {
70104
let indent = 0;
71105
let i = 0;
72106

73107
while (i < line.length) {
74108
const char = line[i];
75109

76110
// Check for "│ " pattern (vertical line + 3 spaces) - 4-char indent
77-
if (char === '│' && line.substring(i, i + 4) === '│ ') {
111+
if (indentSize === 4 && char === '│' && line.substring(i, i + 4) === '│ ') {
78112
indent++;
79113
i += 4;
80114
continue;
81115
}
82116

83117
// Check for "│ " pattern (vertical line + 1 space) - 2-char indent
84-
// Must check this AFTER 4-char to avoid partial matches
85-
if (char === '│' && line[i + 1] === ' ') {
118+
if (indentSize === 2 && char === '│' && line[i + 1] === ' ') {
86119
indent++;
87120
i += 2;
88121
continue;
89122
}
90123

91-
// Check for 4 spaces
92-
if (line.substring(i, i + 4) === ' ') {
93-
indent++;
94-
i += 4;
95-
continue;
96-
}
97-
98-
// Check for 2 spaces (must come after 4-space check)
99-
if (line.substring(i, i + 2) === ' ') {
100-
indent++;
101-
i += 2;
102-
continue;
124+
// Handle space-only indentation based on detected indent size
125+
if (char === ' ') {
126+
if (indentSize === 2 && line.substring(i, i + 2) === ' ') {
127+
indent++;
128+
i += 2;
129+
continue;
130+
}
131+
if (indentSize === 4 && line.substring(i, i + 4) === ' ') {
132+
indent++;
133+
i += 4;
134+
continue;
135+
}
103136
}
104137

105138
// Check for branch characters (├── or └──)

packages/rspress-plugin-file-tree/tests/parser.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,3 +897,66 @@ doc_build
897897
expect(ellipsis2.name).toBe('...');
898898
expect(ellipsis2.type).toBe('file');
899899
});
900+
901+
test('Should parse docs structure with basic directory containing mdx files', () => {
902+
const input = `
903+
docs
904+
├── _meta.json
905+
└── guides
906+
├── _meta.json
907+
└── basic
908+
├── introduction.mdx
909+
├── install.mdx
910+
└── plugin-development.md
911+
`;
912+
913+
const result = parseTreeContent(input).nodes;
914+
915+
// docs is the root directory
916+
const docs = result[0];
917+
expect(docs.name).toBe('docs');
918+
expect(docs.type).toBe('directory');
919+
expect(docs.children.length).toBe(2);
920+
921+
// _meta.json at root level
922+
const metaJson = docs.children[0];
923+
expect(metaJson.name).toBe('_meta.json');
924+
expect(metaJson.type).toBe('file');
925+
expect(metaJson.extension).toBe('json');
926+
927+
// guides directory
928+
const guides = docs.children[1];
929+
expect(guides.name).toBe('guides');
930+
expect(guides.type).toBe('directory');
931+
expect(guides.children.length).toBe(2);
932+
933+
// _meta.json in guides
934+
const guidesMetaJson = guides.children[0];
935+
expect(guidesMetaJson.name).toBe('_meta.json');
936+
expect(guidesMetaJson.type).toBe('file');
937+
expect(guidesMetaJson.extension).toBe('json');
938+
939+
// basic directory (should be a directory, not a file)
940+
const basic = guides.children[1];
941+
expect(basic.name).toBe('basic');
942+
expect(basic.type).toBe('directory');
943+
expect(basic.children.length).toBe(3);
944+
945+
// introduction.mdx
946+
const introduction = basic.children[0];
947+
expect(introduction.name).toBe('introduction.mdx');
948+
expect(introduction.type).toBe('file');
949+
expect(introduction.extension).toBe('mdx');
950+
951+
// install.mdx
952+
const install = basic.children[1];
953+
expect(install.name).toBe('install.mdx');
954+
expect(install.type).toBe('file');
955+
expect(install.extension).toBe('mdx');
956+
957+
// plugin-development.md
958+
const pluginDev = basic.children[2];
959+
expect(pluginDev.name).toBe('plugin-development.md');
960+
expect(pluginDev.type).toBe('file');
961+
expect(pluginDev.extension).toBe('md');
962+
});

0 commit comments

Comments
 (0)