7
7
*/
8
8
9
9
import { 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' ;
13
13
14
14
const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
15
15
const packagePath = path . join ( __dirname , '..' , 'package.json' ) ;
@@ -22,6 +22,7 @@ const PATTERNS = {
22
22
LEVEL_2_HEADING : / ^ # # / ,
23
23
TOC_LINK : / \[ ( [ ^ \] ] + ) \] \( \. \/ ( [ ^ # ) ] + ) (?: # [ ^ ) ] * ) ? \) / ,
24
24
LEVEL_2_TOC_ITEM : / ^ { 2 } [ - * ] \[ / ,
25
+ EMAIL : / ^ [ a - z A - Z 0 - 9 . _ % + - ] + @ [ a - z A - Z 0 - 9 . - ] + \. [ a - z A - Z ] { 2 , } $ / ,
25
26
} ;
26
27
27
28
const LIMITS = {
@@ -46,6 +47,7 @@ const MESSAGES = {
46
47
USAGE_SEARCH : '❌ Usage: md-tree search <file> <selector>' ,
47
48
USAGE_STATS : '❌ Usage: md-tree stats <file>' ,
48
49
USAGE_TOC : '❌ Usage: md-tree toc <file>' ,
50
+ USAGE_CHECK_LINKS : '❌ Usage: md-tree check-links <file>' ,
49
51
INDEX_NOT_FOUND : 'index.md not found in' ,
50
52
NO_MAIN_TITLE : 'No main title found in index.md' ,
51
53
NO_SECTION_FILES : 'No section files found in TOC' ,
@@ -138,6 +140,7 @@ Commands:
138
140
search <file> <selector> Search using CSS-like selectors
139
141
stats <file> Show document statistics
140
142
toc <file> Generate table of contents
143
+ check-links <file> Verify that links are reachable
141
144
version Show version information
142
145
help Show this help message
143
146
@@ -146,6 +149,7 @@ Options:
146
149
--level, -l <number> Heading level to work with
147
150
--format, -f <json|text> Output format (default: text)
148
151
--max-level <number> Maximum heading level for TOC (default: 3)
152
+ --recursive, -r Recursively check linked markdown files
149
153
150
154
Examples:
151
155
md-tree list README.md
@@ -212,7 +216,9 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
212
216
213
217
if ( suggestions . length > 0 ) {
214
218
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
+ }
216
222
}
217
223
218
224
process . exit ( 1 ) ;
@@ -286,12 +292,12 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
286
292
287
293
console . log ( `\n🌳 Document structure for ${ path . basename ( filePath ) } :\n` ) ;
288
294
289
- headings . forEach ( ( heading ) => {
295
+ for ( const heading of headings ) {
290
296
const indent = ' ' . repeat ( heading . level - 1 ) ;
291
297
const icon =
292
298
heading . level === 1 ? '📁' : heading . level === 2 ? '📄' : '📃' ;
293
299
console . log ( `${ indent } ${ icon } ${ heading . text } ` ) ;
294
- } ) ;
300
+ }
295
301
}
296
302
297
303
async searchNodes ( filePath , selector , format = 'text' ) {
@@ -342,11 +348,11 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
342
348
343
349
if ( Object . keys ( stats . headings . byLevel ) . length > 0 ) {
344
350
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
+ }
350
356
}
351
357
352
358
console . log ( `💻 Code blocks: ${ stats . codeBlocks } ` ) ;
@@ -371,6 +377,60 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
371
377
console . log ( toc ) ;
372
378
}
373
379
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
+
374
434
parseArgs ( ) {
375
435
const args = process . argv . slice ( 2 ) ;
376
436
@@ -384,6 +444,7 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
384
444
level : 2 ,
385
445
format : 'text' ,
386
446
maxLevel : 3 ,
447
+ recursive : false ,
387
448
} ;
388
449
389
450
// Parse flags
@@ -394,14 +455,16 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
394
455
options . output = args [ i + 1 ] ;
395
456
i ++ ; // skip next arg
396
457
} else if ( arg === '--level' || arg === '-l' ) {
397
- options . level = parseInt ( args [ i + 1 ] ) || 2 ;
458
+ options . level = Number . parseInt ( args [ i + 1 ] ) || 2 ;
398
459
i ++ ; // skip next arg
399
460
} else if ( arg === '--format' || arg === '-f' ) {
400
461
options . format = args [ i + 1 ] || 'text' ;
401
462
i ++ ; // skip next arg
402
463
} else if ( arg === '--max-level' ) {
403
- options . maxLevel = parseInt ( args [ i + 1 ] ) || 3 ;
464
+ options . maxLevel = Number . parseInt ( args [ i + 1 ] ) || 3 ;
404
465
i ++ ; // skip next arg
466
+ } else if ( arg === '--recursive' || arg === '-r' ) {
467
+ options . recursive = true ;
405
468
} else if ( ! arg . startsWith ( '-' ) ) {
406
469
filteredArgs . push ( arg ) ;
407
470
}
@@ -441,7 +504,7 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
441
504
console . error ( MESSAGES . USAGE_EXTRACT_ALL ) ;
442
505
process . exit ( 1 ) ;
443
506
}
444
- const level = args [ 2 ] ? parseInt ( args [ 2 ] ) : options . level ;
507
+ const level = args [ 2 ] ? Number . parseInt ( args [ 2 ] ) : options . level ;
445
508
await this . extractAllSections ( args [ 1 ] , level , options . output ) ;
446
509
}
447
510
@@ -493,6 +556,14 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
493
556
await this . generateTOC ( args [ 1 ] , options . maxLevel ) ;
494
557
}
495
558
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
+
496
567
async run ( ) {
497
568
const { command, args, options } = this . parseArgs ( ) ;
498
569
@@ -531,6 +602,9 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
531
602
case 'toc' :
532
603
await this . handleTocCommand ( args , options ) ;
533
604
break ;
605
+ case 'check-links' :
606
+ await this . handleCheckLinksCommand ( args , options ) ;
607
+ break ;
534
608
default :
535
609
console . error ( `${ MESSAGES . ERROR } Unknown command: ${ command } ` ) ;
536
610
console . log ( 'Run "md-tree help" for usage information.' ) ;
@@ -685,9 +759,9 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
685
759
686
760
// Create a map of section names to filenames for quick lookup
687
761
const sectionMap = new Map ( ) ;
688
- sectionFiles . forEach ( ( file ) => {
762
+ for ( const file of sectionFiles ) {
689
763
sectionMap . set ( file . headingText . toLowerCase ( ) , file . filename ) ;
690
- } ) ;
764
+ }
691
765
692
766
// Start with title and TOC heading
693
767
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
778
852
779
853
// Create a map of section names to filenames for quick lookup
780
854
const sectionMap = new Map ( ) ;
781
- sectionFiles . forEach ( ( file ) => {
855
+ for ( const file of sectionFiles ) {
782
856
sectionMap . set ( file . headingText . toLowerCase ( ) , file . filename ) ;
783
- } ) ;
857
+ }
784
858
785
859
// Start with title and TOC heading, preserving original spacing
786
860
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
789
863
toc += `- [${ mainTitle } ](#table-of-contents)\n` ;
790
864
791
865
// Add links for each section
792
- sectionFiles . forEach ( ( file ) => {
866
+ for ( const file of sectionFiles ) {
793
867
toc += ` - [${ file . headingText } ](./${ file . filename } )\n` ;
794
- } ) ;
868
+ }
795
869
796
870
return toc ;
797
871
}
0 commit comments