@@ -11,17 +11,24 @@ import { ParsedTree, TreeNode } from './types';
1111 * └── another.ts
1212 */
1313export function parseTreeContent ( content : string ) : ParsedTree {
14- const lines = content . split ( '\n' ) . filter ( ( line ) => line . trim ( ) ) ;
14+ let lines = content . split ( '\n' ) . filter ( ( line ) => line . trim ( ) ) ;
15+
16+ // Skip leading "." line (current directory marker)
17+ if ( lines . length > 0 && lines [ 0 ] . trim ( ) === '.' ) {
18+ lines = lines . slice ( 1 ) ;
19+ }
20+
1521 const nodes : TreeNode [ ] = [ ] ;
1622 const stack : { node : TreeNode ; indent : number } [ ] = [ ] ;
1723
1824 for ( const line of lines ) {
1925 const indent = calculateIndent ( line ) ;
2026 const fullName = extractName ( line ) ;
21- // Split name and potential comment
22- // e.g. "file.ts // comment" -> name="file.ts", comment="comment"
23- const commentMatch = fullName . match ( / ^ ( .* ?) (?: \s * \/ \/ \s * ( .* ) ) ? $ / ) ;
24- const name = commentMatch ? commentMatch [ 1 ] . trim ( ) : fullName ;
27+
28+ // Extract name and comment
29+ // Rule: filename ends when we detect a valid file/directory name pattern,
30+ // everything after (with leading spaces) is treated as comment
31+ const { name, comment } = extractNameAndComment ( fullName ) ;
2532
2633 if ( ! name ) continue ;
2734
@@ -32,6 +39,7 @@ export function parseTreeContent(content: string): ParsedTree {
3239 type : isDirectory ? 'directory' : 'file' ,
3340 children : [ ] ,
3441 extension : isDirectory ? undefined : getExtension ( name ) ,
42+ comment,
3543 } ;
3644
3745 // Find parent node by popping items with equal or greater indent
@@ -56,7 +64,7 @@ export function parseTreeContent(content: string): ParsedTree {
5664
5765/**
5866 * Calculate indent level from line
59- * Each level is typically 4 characters: "│ " or " "
67+ * Supports both 4-character ( "│ ") and 2-character ("│ ") indent patterns
6068 */
6169function calculateIndent ( line : string ) : number {
6270 let indent = 0 ;
@@ -65,20 +73,35 @@ function calculateIndent(line: string): number {
6573 while ( i < line . length ) {
6674 const char = line [ i ] ;
6775
68- // Check for "│ " pattern (vertical line + 3 spaces)
76+ // Check for "│ " pattern (vertical line + 3 spaces) - 4-char indent
6977 if ( char === '│' && line . substring ( i , i + 4 ) === '│ ' ) {
7078 indent ++ ;
7179 i += 4 ;
7280 continue ;
7381 }
7482
83+ // 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 ] === ' ' ) {
86+ indent ++ ;
87+ i += 2 ;
88+ continue ;
89+ }
90+
7591 // Check for 4 spaces
7692 if ( line . substring ( i , i + 4 ) === ' ' ) {
7793 indent ++ ;
7894 i += 4 ;
7995 continue ;
8096 }
8197
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 ;
103+ }
104+
82105 // Check for branch characters (├── or └──)
83106 if ( char === '├' || char === '└' ) {
84107 if (
@@ -108,27 +131,127 @@ function extractName(line: string): string {
108131 . trim ( ) ;
109132}
110133
134+ /**
135+ * Extract filename/dirname and comment from a line
136+ *
137+ * Rules for detecting end of filename:
138+ * 1. Files have extensions: name.ext (e.g., file.ts, config.json)
139+ * 2. Hidden files start with dot: .gitignore, .env
140+ * 3. Directories end with / or have no extension
141+ * 4. Special names: ... (ellipsis for omitted content)
142+ * 5. Everything after the name (separated by 2+ spaces) is comment
143+ *
144+ * Note: Filenames can contain spaces (e.g., "0. file.ts"), so we use
145+ * 2+ consecutive spaces as the delimiter between name and comment.
146+ */
147+ function extractNameAndComment ( fullName : string ) : {
148+ name : string ;
149+ comment : string | undefined ;
150+ } {
151+ const trimmed = fullName . trim ( ) ;
152+ if ( ! trimmed ) {
153+ return { name : '' , comment : undefined } ;
154+ }
155+
156+ // Special case: "..." or similar ellipsis patterns (standalone)
157+ if ( / ^ \. { 2 , } $ / . test ( trimmed ) ) {
158+ return { name : trimmed , comment : undefined } ;
159+ }
160+
161+ // Strategy: Find the last occurrence of a file extension pattern,
162+ // then check if there's content after it (separated by 2+ spaces)
163+
164+ // First, try to split by 2+ spaces (common comment delimiter)
165+ const doubleSpaceMatch = trimmed . match ( / ^ ( .+ ?) \s { 2 , } ( .+ ) $ / ) ;
166+ if ( doubleSpaceMatch ) {
167+ const potentialName = doubleSpaceMatch [ 1 ] . trim ( ) ;
168+ const potentialComment = doubleSpaceMatch [ 2 ] . trim ( ) ;
169+
170+ // Verify the name part looks like a valid file/directory
171+ if ( isValidName ( potentialName ) ) {
172+ return { name : potentialName , comment : potentialComment } ;
173+ }
174+ }
175+
176+ // If no double-space delimiter, check if the whole thing is just a name
177+ // For files with extensions or directories, we can be more lenient
178+ // Look for pattern: name.ext followed by single space and non-extension content
179+ const singleSpaceMatch = trimmed . match (
180+ / ^ ( .+ ?\. [ a - z A - Z 0 - 9 ] + ) \s + ( [ ^ . ] .* ) $ /
181+ ) ;
182+ if ( singleSpaceMatch ) {
183+ const potentialName = singleSpaceMatch [ 1 ] . trim ( ) ;
184+ const potentialComment = singleSpaceMatch [ 2 ] . trim ( ) ;
185+ return { name : potentialName , comment : potentialComment } ;
186+ }
187+
188+ // Check for hidden files followed by comment
189+ const hiddenFileMatch = trimmed . match ( / ^ ( \. [ ^ \s ] + ) \s + ( .+ ) $ / ) ;
190+ if ( hiddenFileMatch ) {
191+ return {
192+ name : hiddenFileMatch [ 1 ] . trim ( ) ,
193+ comment : hiddenFileMatch [ 2 ] . trim ( ) ,
194+ } ;
195+ }
196+
197+ // Check for directory name followed by single space and comment
198+ // Directory names don't have extensions, so look for "word space non-word-start"
199+ // Also handles names starting with numbers like "2. components"
200+ const dirCommentMatch = trimmed . match ( / ^ ( [ \w ] [ \w . \s - ] * ?) \s + ( [ ^ a - z A - Z 0 - 9 ] .* ) $ / ) ;
201+ if ( dirCommentMatch ) {
202+ const potentialName = dirCommentMatch [ 1 ] . trim ( ) ;
203+ // Make sure it's not a file with extension
204+ if ( ! / \. [ a - z A - Z 0 - 9 ] + $ / . test ( potentialName ) ) {
205+ return {
206+ name : potentialName ,
207+ comment : dirCommentMatch [ 2 ] . trim ( ) ,
208+ } ;
209+ }
210+ }
211+
212+ // No comment detected, return the whole thing as name
213+ return { name : trimmed , comment : undefined } ;
214+ }
215+
216+ /**
217+ * Check if a string looks like a valid file/directory name
218+ */
219+ function isValidName ( name : string ) : boolean {
220+ if ( ! name ) return false ;
221+
222+ // Ends with / (explicit directory)
223+ if ( name . endsWith ( '/' ) ) return true ;
224+
225+ // Has a file extension
226+ if ( / \. [ a - z A - Z 0 - 9 ] + $ / . test ( name ) ) return true ;
227+
228+ // Hidden file/directory (starts with dot)
229+ if ( name . startsWith ( '.' ) ) return true ;
230+
231+ // Looks like a directory name (no extension, word characters)
232+ if ( / ^ [ \w \s . - ] + $ / . test ( name ) ) return true ;
233+
234+ return false ;
235+ }
236+
111237/**
112238 * Check if name represents a directory
113239 * - Ends with /
114240 * - Has no extension
115241 */
116242function isDirectoryName ( name : string ) : boolean {
117- // Strip comments if any (though name passed here usually already has them, let's be safe if logic changes)
118- const cleanName = name . split ( / \s + \/ \/ / ) [ 0 ] . trim ( ) ;
119-
120- if ( cleanName . endsWith ( '/' ) ) return true ;
243+ if ( name . endsWith ( '/' ) ) return true ;
121244
122- const lastPart = cleanName . split ( '/' ) . pop ( ) || cleanName ;
245+ const lastPart = name . split ( '/' ) . pop ( ) || name ;
123246
124- // Use a more robust check: files usually have extensions.
125- // Directories usually don't.
126- // Exception: Dotfiles (.gitignore) are files.
127- // exception: names with dots but known extensions are files.
247+ // Special case: ellipsis is not a directory
248+ if ( / ^ \. { 2 , } $ / . test ( lastPart ) ) {
249+ return false ;
250+ }
128251
129252 // If it starts with a dot, it's a file (hidden file), e.g., .gitignore
130253 if ( lastPart . startsWith ( '.' ) ) {
131- return false ; // Treat as file
254+ return false ;
132255 }
133256
134257 // If it has an extension (e.g. foo.ts, bar.config.js), it's a file
0 commit comments