1+ #!/usr/bin/env node
2+
3+ const fs = require ( 'fs' ) ;
4+ const path = require ( 'path' ) ;
5+ const https = require ( 'https' ) ;
6+ const http = require ( 'http' ) ;
7+ const { URL } = require ( 'url' ) ;
8+
9+ const RESOURCES_DIR = path . join ( __dirname , '../docs/resources' ) ;
10+ const TIMEOUT_MS = 10000 ;
11+ const USER_AGENT = 'Mozilla/5.0 (compatible; LinkChecker/1.0)' ;
12+
13+ class LinkValidator {
14+ constructor ( ) {
15+ this . validLinks = [ ] ;
16+ this . invalidLinks = [ ] ;
17+ this . removedLinks = [ ] ;
18+ }
19+
20+ async validateUrl ( url ) {
21+ return new Promise ( ( resolve ) => {
22+ try {
23+ const urlObj = new URL ( url ) ;
24+ const protocol = urlObj . protocol === 'https:' ? https : http ;
25+
26+ const options = {
27+ hostname : urlObj . hostname ,
28+ port : urlObj . port ,
29+ path : urlObj . pathname + urlObj . search ,
30+ method : 'HEAD' ,
31+ headers : {
32+ 'User-Agent' : USER_AGENT ,
33+ 'Accept' : '*/*'
34+ } ,
35+ timeout : TIMEOUT_MS
36+ } ;
37+
38+ const req = protocol . request ( options , ( res ) => {
39+ const statusCode = res . statusCode ;
40+ if ( statusCode >= 200 && statusCode < 400 ) {
41+ resolve ( { valid : true , status : statusCode } ) ;
42+ } else if ( statusCode >= 300 && statusCode < 400 && res . headers . location ) {
43+ // Follow redirects manually for HEAD requests
44+ this . validateUrl ( res . headers . location ) . then ( resolve ) ;
45+ } else {
46+ resolve ( { valid : false , status : statusCode , error : `HTTP ${ statusCode } ` } ) ;
47+ }
48+ } ) ;
49+
50+ req . on ( 'error' , ( error ) => {
51+ resolve ( { valid : false , error : error . message } ) ;
52+ } ) ;
53+
54+ req . on ( 'timeout' , ( ) => {
55+ req . destroy ( ) ;
56+ resolve ( { valid : false , error : 'Request timeout' } ) ;
57+ } ) ;
58+
59+ req . setTimeout ( TIMEOUT_MS ) ;
60+ req . end ( ) ;
61+ } catch ( error ) {
62+ resolve ( { valid : false , error : error . message } ) ;
63+ }
64+ } ) ;
65+ }
66+
67+ extractLinksFromMarkdown ( content ) {
68+ const linkRegex = / \[ ( [ ^ \] ] + ) \] \( ( [ ^ ) ] + ) \) / g;
69+ const links = [ ] ;
70+ let match ;
71+
72+ while ( ( match = linkRegex . exec ( content ) ) !== null ) {
73+ const text = match [ 1 ] ;
74+ const url = match [ 2 ] ;
75+ const fullMatch = match [ 0 ] ;
76+
77+ // Only process external URLs (not internal links)
78+ if ( url . startsWith ( 'http://' ) || url . startsWith ( 'https://' ) ) {
79+ links . push ( {
80+ text,
81+ url,
82+ fullMatch,
83+ index : match . index
84+ } ) ;
85+ }
86+ }
87+ return links ;
88+ }
89+
90+ async processFile ( filePath ) {
91+ console . log ( `\n📄 Processing: ${ path . basename ( filePath ) } ` ) ;
92+
93+ const content = fs . readFileSync ( filePath , 'utf8' ) ;
94+ const links = this . extractLinksFromMarkdown ( content ) ;
95+
96+ if ( links . length === 0 ) {
97+ console . log ( ' No external links found' ) ;
98+ return ;
99+ }
100+
101+ let modifiedContent = content ;
102+ let hasChanges = false ;
103+
104+ // Process links in reverse order to maintain correct indices when removing
105+ for ( let i = links . length - 1 ; i >= 0 ; i -- ) {
106+ const link = links [ i ] ;
107+ process . stdout . write ( ` 🔍 Checking: ${ link . url } ... ` ) ;
108+
109+ const result = await this . validateUrl ( link . url ) ;
110+
111+ if ( result . valid ) {
112+ console . log ( `✅ Valid (${ result . status || 'OK' } )` ) ;
113+ this . validLinks . push ( { file : filePath , ...link , status : result . status } ) ;
114+ } else {
115+ console . log ( `❌ Invalid (${ result . error } )` ) ;
116+ this . invalidLinks . push ( { file : filePath , ...link , error : result . error } ) ;
117+
118+ // Remove the invalid link from content
119+ const beforeLink = modifiedContent . substring ( 0 , link . index ) ;
120+ const afterLink = modifiedContent . substring ( link . index + link . fullMatch . length ) ;
121+ modifiedContent = beforeLink + afterLink ;
122+ hasChanges = true ;
123+
124+ this . removedLinks . push ( {
125+ file : path . basename ( filePath ) ,
126+ text : link . text ,
127+ url : link . url ,
128+ error : result . error
129+ } ) ;
130+
131+ console . log ( ` 🗑️ Removed invalid link: "${ link . text } "` ) ;
132+ }
133+ }
134+
135+ // Write back the modified content if there were changes
136+ if ( hasChanges ) {
137+ fs . writeFileSync ( filePath , modifiedContent , 'utf8' ) ;
138+ console . log ( ` 💾 File updated with ${ this . removedLinks . filter ( r => r . file === path . basename ( filePath ) ) . length } link(s) removed` ) ;
139+ }
140+ }
141+
142+ async validateAllResources ( ) {
143+ console . log ( '🔗 Link Validator for Resources Folder' ) ;
144+ console . log ( '=====================================\n' ) ;
145+
146+ const files = fs . readdirSync ( RESOURCES_DIR )
147+ . filter ( file => file . endsWith ( '.md' ) )
148+ . map ( file => path . join ( RESOURCES_DIR , file ) ) ;
149+
150+ if ( files . length === 0 ) {
151+ console . log ( 'No markdown files found in resources directory' ) ;
152+ return ;
153+ }
154+
155+ for ( const file of files ) {
156+ await this . processFile ( file ) ;
157+ }
158+
159+ this . printSummary ( ) ;
160+ }
161+
162+ printSummary ( ) {
163+ console . log ( '\n📊 SUMMARY' ) ;
164+ console . log ( '===========' ) ;
165+ console . log ( `✅ Valid links: ${ this . validLinks . length } ` ) ;
166+ console . log ( `❌ Invalid links: ${ this . invalidLinks . length } ` ) ;
167+ console . log ( `🗑️ Links removed: ${ this . removedLinks . length } ` ) ;
168+
169+ if ( this . removedLinks . length > 0 ) {
170+ console . log ( '\n🗑️ REMOVED LINKS:' ) ;
171+ this . removedLinks . forEach ( link => {
172+ console . log ( ` ${ link . file } : "${ link . text } " (${ link . url } )` ) ;
173+ console . log ( ` Reason: ${ link . error } ` ) ;
174+ } ) ;
175+ }
176+
177+ if ( this . invalidLinks . length === 0 ) {
178+ console . log ( '\n🎉 All links are valid!' ) ;
179+ } else {
180+ console . log ( '\n⚠️ Invalid links have been removed from the files.' ) ;
181+ }
182+ }
183+ }
184+
185+ // Run the validator
186+ if ( require . main === module ) {
187+ const validator = new LinkValidator ( ) ;
188+ validator . validateAllResources ( ) . catch ( console . error ) ;
189+ }
190+
191+ module . exports = LinkValidator ;
0 commit comments