77 */
88
99import { MarkdownTreeParser } from '../lib/markdown-parser.js' ;
10- import fs from 'fs/promises' ;
11- import path from 'path' ;
12- import { fileURLToPath } from 'url' ;
10+ import fs from 'node: fs/promises' ;
11+ import path from 'node: path' ;
12+ import { fileURLToPath } from 'node: url' ;
1313
1414const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
1515const packagePath = path . join ( __dirname , '..' , 'package.json' ) ;
@@ -22,6 +22,7 @@ const PATTERNS = {
2222 LEVEL_2_HEADING : / ^ # # / ,
2323 TOC_LINK : / \[ ( [ ^ \] ] + ) \] \( \. \/ ( [ ^ # ) ] + ) (?: # [ ^ ) ] * ) ? \) / ,
2424 LEVEL_2_TOC_ITEM : / ^ { 2 } [ - * ] \[ / ,
25+ EMAIL : / ^ [ a - z A - Z 0 - 9 . _ % + - ] + @ [ a - z A - Z 0 - 9 . - ] + \. [ a - z A - Z ] { 2 , } $ / ,
2526} ;
2627
2728const LIMITS = {
@@ -46,6 +47,7 @@ const MESSAGES = {
4647 USAGE_SEARCH : '❌ Usage: md-tree search <file> <selector>' ,
4748 USAGE_STATS : '❌ Usage: md-tree stats <file>' ,
4849 USAGE_TOC : '❌ Usage: md-tree toc <file>' ,
50+ USAGE_CHECK_LINKS : '❌ Usage: md-tree check-links <file>' ,
4951 INDEX_NOT_FOUND : 'index.md not found in' ,
5052 NO_MAIN_TITLE : 'No main title found in index.md' ,
5153 NO_SECTION_FILES : 'No section files found in TOC' ,
@@ -138,6 +140,7 @@ Commands:
138140 search <file> <selector> Search using CSS-like selectors
139141 stats <file> Show document statistics
140142 toc <file> Generate table of contents
143+ check-links <file> Verify that links are reachable
141144 version Show version information
142145 help Show this help message
143146
@@ -146,6 +149,7 @@ Options:
146149 --level, -l <number> Heading level to work with
147150 --format, -f <json|text> Output format (default: text)
148151 --max-level <number> Maximum heading level for TOC (default: 3)
152+ --recursive, -r Recursively check linked markdown files
149153
150154Examples:
151155 md-tree list README.md
@@ -212,7 +216,9 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
212216
213217 if ( suggestions . length > 0 ) {
214218 console . log ( '\n💡 Did you mean one of these?' ) ;
215- suggestions . forEach ( ( h ) => console . log ( ` - "${ h . text } "` ) ) ;
219+ for ( const h of suggestions ) {
220+ console . log ( ` - "${ h . text } "` ) ;
221+ }
216222 }
217223
218224 process . exit ( 1 ) ;
@@ -286,12 +292,12 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
286292
287293 console . log ( `\n🌳 Document structure for ${ path . basename ( filePath ) } :\n` ) ;
288294
289- headings . forEach ( ( heading ) => {
295+ for ( const heading of headings ) {
290296 const indent = ' ' . repeat ( heading . level - 1 ) ;
291297 const icon =
292298 heading . level === 1 ? '📁' : heading . level === 2 ? '📄' : '📃' ;
293299 console . log ( `${ indent } ${ icon } ${ heading . text } ` ) ;
294- } ) ;
300+ }
295301 }
296302
297303 async searchNodes ( filePath , selector , format = 'text' ) {
@@ -342,11 +348,11 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
342348
343349 if ( Object . keys ( stats . headings . byLevel ) . length > 0 ) {
344350 console . log ( ' By level:' ) ;
345- Object . entries ( stats . headings . byLevel )
346- . sort ( ( [ a ] , [ b ] ) => parseInt ( a ) - parseInt ( b ) )
347- . forEach ( ( [ level , count ] ) => {
348- console . log ( ` Level ${ level } : ${ count } ` ) ;
349- } ) ;
351+ for ( const [ level , count ] of Object . entries ( stats . headings . byLevel ) . sort (
352+ ( [ a ] , [ b ] ) => Number . parseInt ( a ) - Number . parseInt ( b )
353+ ) ) {
354+ console . log ( ` Level ${ level } : ${ count } ` ) ;
355+ }
350356 }
351357
352358 console . log ( `💻 Code blocks: ${ stats . codeBlocks } ` ) ;
@@ -371,6 +377,60 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
371377 console . log ( toc ) ;
372378 }
373379
380+ async checkLinks ( filePath , recursive = false , visited = new Set ( ) ) {
381+ const resolvedPath = path . resolve ( filePath ) ;
382+ if ( visited . has ( resolvedPath ) ) return ;
383+ visited . add ( resolvedPath ) ;
384+
385+ const content = await this . readFile ( resolvedPath ) ;
386+ const tree = await this . parser . parse ( content ) ;
387+ const links = this . parser . selectAll ( tree , 'link' ) ;
388+
389+ console . log (
390+ `\n🔗 Checking ${ links . length } links in ${ path . basename ( resolvedPath ) } :`
391+ ) ;
392+
393+ for ( const link of links ) {
394+ const url = link . url ;
395+ if ( ! url || url . startsWith ( '#' ) ) {
396+ continue ;
397+ }
398+
399+ // Show email links but mark as skipped
400+ if ( url . startsWith ( 'mailto:' ) || PATTERNS . EMAIL . test ( url ) ) {
401+ console . log ( `⏭️ ${ url } (email - skipped)` ) ;
402+ continue ;
403+ }
404+
405+ if ( / ^ h t t p s ? : \/ \/ / i. test ( url ) ) {
406+ try {
407+ const res = await globalThis . fetch ( url , { method : 'HEAD' } ) ;
408+ if ( res . ok ) {
409+ console . log ( `✅ ${ url } ` ) ;
410+ } else {
411+ console . log ( `❌ ${ url } (${ res . status } )` ) ;
412+ }
413+ } catch ( err ) {
414+ console . log ( `❌ ${ url } (${ err . message } )` ) ;
415+ }
416+ } else {
417+ const target = path . resolve (
418+ path . dirname ( resolvedPath ) ,
419+ url . split ( '#' ) [ 0 ]
420+ ) ;
421+ try {
422+ await fs . access ( target ) ;
423+ console . log ( `✅ ${ url } ` ) ;
424+ if ( recursive && / \. m d $ / i. test ( target ) ) {
425+ await this . checkLinks ( target , true , visited ) ;
426+ }
427+ } catch {
428+ console . log ( `❌ ${ url } (file not found)` ) ;
429+ }
430+ }
431+ }
432+ }
433+
374434 parseArgs ( ) {
375435 const args = process . argv . slice ( 2 ) ;
376436
@@ -384,6 +444,7 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
384444 level : 2 ,
385445 format : 'text' ,
386446 maxLevel : 3 ,
447+ recursive : false ,
387448 } ;
388449
389450 // Parse flags
@@ -394,14 +455,16 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
394455 options . output = args [ i + 1 ] ;
395456 i ++ ; // skip next arg
396457 } else if ( arg === '--level' || arg === '-l' ) {
397- options . level = parseInt ( args [ i + 1 ] ) || 2 ;
458+ options . level = Number . parseInt ( args [ i + 1 ] ) || 2 ;
398459 i ++ ; // skip next arg
399460 } else if ( arg === '--format' || arg === '-f' ) {
400461 options . format = args [ i + 1 ] || 'text' ;
401462 i ++ ; // skip next arg
402463 } else if ( arg === '--max-level' ) {
403- options . maxLevel = parseInt ( args [ i + 1 ] ) || 3 ;
464+ options . maxLevel = Number . parseInt ( args [ i + 1 ] ) || 3 ;
404465 i ++ ; // skip next arg
466+ } else if ( arg === '--recursive' || arg === '-r' ) {
467+ options . recursive = true ;
405468 } else if ( ! arg . startsWith ( '-' ) ) {
406469 filteredArgs . push ( arg ) ;
407470 }
@@ -441,7 +504,7 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
441504 console . error ( MESSAGES . USAGE_EXTRACT_ALL ) ;
442505 process . exit ( 1 ) ;
443506 }
444- const level = args [ 2 ] ? parseInt ( args [ 2 ] ) : options . level ;
507+ const level = args [ 2 ] ? Number . parseInt ( args [ 2 ] ) : options . level ;
445508 await this . extractAllSections ( args [ 1 ] , level , options . output ) ;
446509 }
447510
@@ -493,6 +556,14 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
493556 await this . generateTOC ( args [ 1 ] , options . maxLevel ) ;
494557 }
495558
559+ async handleCheckLinksCommand ( args , options ) {
560+ if ( args . length < 2 ) {
561+ console . error ( MESSAGES . USAGE_CHECK_LINKS ) ;
562+ process . exit ( 1 ) ;
563+ }
564+ await this . checkLinks ( args [ 1 ] , options . recursive ) ;
565+ }
566+
496567 async run ( ) {
497568 const { command, args, options } = this . parseArgs ( ) ;
498569
@@ -531,6 +602,9 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
531602 case 'toc' :
532603 await this . handleTocCommand ( args , options ) ;
533604 break ;
605+ case 'check-links' :
606+ await this . handleCheckLinksCommand ( args , options ) ;
607+ break ;
534608 default :
535609 console . error ( `${ MESSAGES . ERROR } Unknown command: ${ command } ` ) ;
536610 console . log ( 'Run "md-tree help" for usage information.' ) ;
@@ -685,9 +759,9 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
685759
686760 // Create a map of section names to filenames for quick lookup
687761 const sectionMap = new Map ( ) ;
688- sectionFiles . forEach ( ( file ) => {
762+ for ( const file of sectionFiles ) {
689763 sectionMap . set ( file . headingText . toLowerCase ( ) , file . filename ) ;
690- } ) ;
764+ }
691765
692766 // Start with title and TOC heading
693767 let toc = `# ${ mainTitle . text } \n\n## Table of Contents\n\n` ;
@@ -778,9 +852,9 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
778852
779853 // Create a map of section names to filenames for quick lookup
780854 const sectionMap = new Map ( ) ;
781- sectionFiles . forEach ( ( file ) => {
855+ for ( const file of sectionFiles ) {
782856 sectionMap . set ( file . headingText . toLowerCase ( ) , file . filename ) ;
783- } ) ;
857+ }
784858
785859 // Start with title and TOC heading, preserving original spacing
786860 let toc = `# ${ mainTitle } \n\n## Table of Contents\n\n` ;
@@ -789,9 +863,9 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
789863 toc += `- [${ mainTitle } ](#table-of-contents)\n` ;
790864
791865 // Add links for each section
792- sectionFiles . forEach ( ( file ) => {
866+ for ( const file of sectionFiles ) {
793867 toc += ` - [${ file . headingText } ](./${ file . filename } )\n` ;
794- } ) ;
868+ }
795869
796870 return toc ;
797871 }
0 commit comments