mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-30 18:05:55 +01:00 
			
		
		
		
	fix(docs): try to fix more docs links...
This commit is contained in:
		
							
								
								
									
										7
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,6 +16,7 @@ on: | |||||||
|       - 'requirements-docs.txt' |       - 'requirements-docs.txt' | ||||||
|       - '.github/workflows/deploy-docs.yml' |       - '.github/workflows/deploy-docs.yml' | ||||||
|       - 'scripts/fix-mkdocs-structure.ts' |       - 'scripts/fix-mkdocs-structure.ts' | ||||||
|  |       - 'validate-docs-links.ts' | ||||||
|    |    | ||||||
|   # Allow manual triggering from Actions tab |   # Allow manual triggering from Actions tab | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
| @@ -32,6 +33,7 @@ on: | |||||||
|       - 'requirements-docs.txt' |       - 'requirements-docs.txt' | ||||||
|       - '.github/workflows/deploy-docs.yml' |       - '.github/workflows/deploy-docs.yml' | ||||||
|       - 'scripts/fix-mkdocs-structure.ts' |       - 'scripts/fix-mkdocs-structure.ts' | ||||||
|  |       - 'validate-docs-links.ts' | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   build-and-deploy: |   build-and-deploy: | ||||||
| @@ -111,6 +113,11 @@ jobs: | |||||||
|           test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1) |           test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1) | ||||||
|           echo "✅ Site validation passed" |           echo "✅ Site validation passed" | ||||||
|        |        | ||||||
|  |       - name: Validate Documentation Links | ||||||
|  |         run: | | ||||||
|  |           # Run the TypeScript link validation script | ||||||
|  |           pnpm tsx validate-docs-links.ts | ||||||
|  |        | ||||||
|       # Install wrangler globally to avoid workspace issues |       # Install wrangler globally to avoid workspace issues | ||||||
|       - name: Install Wrangler |       - name: Install Wrangler | ||||||
|         run: | |         run: | | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ | |||||||
|     "chore:update-build-info": "tsx ./scripts/update-build-info.ts", |     "chore:update-build-info": "tsx ./scripts/update-build-info.ts", | ||||||
|     "chore:update-version": "tsx ./scripts/update-version.ts", |     "chore:update-version": "tsx ./scripts/update-version.ts", | ||||||
|     "chore:fix-mkdocs-structure": "tsx ./scripts/fix-mkdocs-structure.ts", |     "chore:fix-mkdocs-structure": "tsx ./scripts/fix-mkdocs-structure.ts", | ||||||
|  |     "docs:validate-links": "tsx ./validate-docs-links.ts", | ||||||
|     "edit-docs:edit-docs": "pnpm run --filter edit-docs edit-docs", |     "edit-docs:edit-docs": "pnpm run --filter edit-docs edit-docs", | ||||||
|     "edit-docs:edit-demo": "pnpm run --filter edit-docs edit-demo", |     "edit-docs:edit-demo": "pnpm run --filter edit-docs edit-demo", | ||||||
|     "test:all": "pnpm test:parallel && pnpm test:sequential", |     "test:all": "pnpm test:parallel && pnpm test:sequential", | ||||||
|   | |||||||
| @@ -109,8 +109,25 @@ function updateReferences(docsDir: string): FixResult[] { | |||||||
|     const updatesMade: FixResult[] = []; |     const updatesMade: FixResult[] = []; | ||||||
|      |      | ||||||
|     function fixLink(match: string, text: string, link: string, currentDir: string, isIndex: boolean): string { |     function fixLink(match: string, text: string, link: string, currentDir: string, isIndex: boolean): string { | ||||||
|         // Skip external links |         // Skip external links, mailto, and special protocols | ||||||
|         if (link.startsWith('http')) { |         if (link.startsWith('http') || link.startsWith('mailto:') || link.startsWith('xmpp:')) { | ||||||
|  |             return match; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Skip anchor-only links | ||||||
|  |         if (link.startsWith('#')) { | ||||||
|  |             return match; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Handle malformed links with nested brackets (e.g., [developers]([url](https://...)) | ||||||
|  |         if (link.includes('[') || link.includes(']')) { | ||||||
|  |             // This is a malformed link, skip it | ||||||
|  |             return match; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Handle links wrapped in angle brackets (e.g., <https://...>) | ||||||
|  |         if (link.startsWith('<') && link.endsWith('>')) { | ||||||
|  |             // This is likely a literal URL that shouldn't be processed | ||||||
|             return match; |             return match; | ||||||
|         } |         } | ||||||
|          |          | ||||||
| @@ -123,6 +140,14 @@ function updateReferences(docsDir: string): FixResult[] { | |||||||
|             decodedLink = link; |             decodedLink = link; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|  |         // Extract anchor if present | ||||||
|  |         let anchorPart = ''; | ||||||
|  |         if (decodedLink.includes('#')) { | ||||||
|  |             const parts = decodedLink.split('#'); | ||||||
|  |             decodedLink = parts[0]; | ||||||
|  |             anchorPart = '#' + parts.slice(1).join('#'); | ||||||
|  |         } | ||||||
|  |          | ||||||
|         // Special case: if we're in index.md and the link starts with the parent directory name |         // Special case: if we're in index.md and the link starts with the parent directory name | ||||||
|         // This happens when a file was converted to index.md and had links to siblings |         // This happens when a file was converted to index.md and had links to siblings | ||||||
|         if (isIndex && decodedLink.includes('/')) { |         if (isIndex && decodedLink.includes('/')) { | ||||||
| @@ -136,7 +161,7 @@ function updateReferences(docsDir: string): FixResult[] { | |||||||
|                 // Re-encode spaces for URL compatibility before recursing |                 // Re-encode spaces for URL compatibility before recursing | ||||||
|                 const fixedLinkEncoded = fixedLink.replace(/ /g, '%20'); |                 const fixedLinkEncoded = fixedLink.replace(/ /g, '%20'); | ||||||
|                 // Recursively process the fixed link |                 // Recursively process the fixed link | ||||||
|                 return fixLink(`[${text}](${fixedLinkEncoded})`, text, fixedLinkEncoded, currentDir, isIndex); |                 return fixLink(`[${text}](${fixedLinkEncoded}${anchorPart})`, text, fixedLinkEncoded + anchorPart, currentDir, isIndex); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
| @@ -161,26 +186,43 @@ function updateReferences(docsDir: string): FixResult[] { | |||||||
|                         if (path.dirname(potentialDir) === path.dirname(currentDir)) { |                         if (path.dirname(potentialDir) === path.dirname(currentDir)) { | ||||||
|                             // It's a sibling - just use directory name |                             // It's a sibling - just use directory name | ||||||
|                             const dirName = path.basename(potentialDir).replace(/ /g, '%20'); |                             const dirName = path.basename(potentialDir).replace(/ /g, '%20'); | ||||||
|                             return `[${text}](${dirName}/)`; |                             return `[${text}](${dirName}/${anchorPart})`; | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                      |                      | ||||||
|                     // Calculate relative path from current file to the directory |                     // Calculate relative path from current file to the directory | ||||||
|                     const newPath = path.relative(currentDir, potentialDir).replace(/\\/g, '/').replace(/ /g, '%20'); |                     const newPath = path.relative(currentDir, potentialDir).replace(/\\/g, '/').replace(/ /g, '%20'); | ||||||
|                     return `[${text}](${newPath}/)`; |                     return `[${text}](${newPath}/${anchorPart})`; | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 // Check if the target file exists | ||||||
|  |                 if (!fs.existsSync(resolvedPath)) { | ||||||
|  |                     // Try to find a similar file by removing special characters from the filename | ||||||
|  |                     const dirPath = path.dirname(resolvedPath); | ||||||
|  |                     const fileName = path.basename(resolvedPath); | ||||||
|  |                      | ||||||
|  |                     // Remove problematic characters and try to find the file | ||||||
|  |                     const cleanFileName = fileName.replace(/[\(\)\\]/g, ''); | ||||||
|  |                     const cleanPath = path.join(dirPath, cleanFileName); | ||||||
|  |                      | ||||||
|  |                     if (fs.existsSync(cleanPath)) { | ||||||
|  |                         // Calculate relative path from current file to the cleaned file | ||||||
|  |                         const newPath = path.relative(currentDir, cleanPath).replace(/\\/g, '/').replace(/ /g, '%20'); | ||||||
|  |                         return `[${text}](${newPath}${anchorPart})`; | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         // Also handle local references (same directory) |         // Also handle local references (same directory) | ||||||
|         if (!decodedLink.includes('/')) { |         if (!decodedLink.includes('/')) { | ||||||
|             const basename = decodedLink.slice(0, -3); // Remove .md extension |             const basename = decodedLink.endsWith('.md') ? decodedLink.slice(0, -3) : decodedLink; | ||||||
|             const possibleDir = path.join(currentDir, basename); |             const possibleDir = path.join(currentDir, basename); | ||||||
|              |              | ||||||
|             if (fs.existsSync(possibleDir) && fs.statSync(possibleDir).isDirectory()) { |             if (fs.existsSync(possibleDir) && fs.statSync(possibleDir).isDirectory()) { | ||||||
|                 // Re-encode spaces for URL compatibility |                 // Re-encode spaces for URL compatibility | ||||||
|                 const encodedBasename = basename.replace(/ /g, '%20'); |                 const encodedBasename = basename.replace(/ /g, '%20'); | ||||||
|                 return `[${text}](${encodedBasename}/)`; |                 return `[${text}](${encodedBasename}/${anchorPart})`; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
| @@ -266,6 +308,20 @@ function syncReadmeToIndex(projectRoot: string, docsDir: string): FixResult[] { | |||||||
|      |      | ||||||
|     // Fix internal documentation links (./docs/User%20Guide -> ./User%20Guide) |     // Fix internal documentation links (./docs/User%20Guide -> ./User%20Guide) | ||||||
|     content = content.replace(/\.\/docs\/User%20Guide/g, './User%20Guide'); |     content = content.replace(/\.\/docs\/User%20Guide/g, './User%20Guide'); | ||||||
|  |     content = content.replace(/\.\/docs\/Script%20API/g, './Script%20API'); | ||||||
|  |     content = content.replace(/\.\/docs\/Developer%20Guide/g, './Developer%20Guide'); | ||||||
|  |      | ||||||
|  |     // Fix specific broken links found in index.md | ||||||
|  |     // These links point to non-existent files, so we need to fix them | ||||||
|  |     content = content.replace(/User%20Guide\/quick-start\.md/g, 'User%20Guide/User%20Guide/'); | ||||||
|  |     content = content.replace(/User%20Guide\/installation\.md/g, 'User%20Guide/User%20Guide/Installation%20&%20Setup/'); | ||||||
|  |     content = content.replace(/User%20Guide\/docker\.md/g, 'User%20Guide/User%20Guide/Installation%20&%20Setup/Docker%20installation/'); | ||||||
|  |     content = content.replace(/User%20Guide\/index\.md/g, 'User%20Guide/User%20Guide/'); | ||||||
|  |     content = content.replace(/Script%20API\/index\.md/g, 'Script%20API/'); | ||||||
|  |     content = content.replace(/Developer%20Guide\/index\.md/g, 'Developer%20Guide/Developer%20Guide/'); | ||||||
|  |     content = content.replace(/Developer%20Guide\/contributing\.md/g, 'Developer%20Guide/Developer%20Guide/'); | ||||||
|  |     content = content.replace(/support\/faq\.md/g, 'User%20Guide/User%20Guide/FAQ/'); | ||||||
|  |     content = content.replace(/support\/troubleshooting\.md/g, 'User%20Guide/User%20Guide/'); | ||||||
|      |      | ||||||
|     // Write the adjusted content to docs/index.md |     // Write the adjusted content to docs/index.md | ||||||
|     fs.writeFileSync(indexPath, content, 'utf-8'); |     fs.writeFileSync(indexPath, content, 'utf-8'); | ||||||
|   | |||||||
							
								
								
									
										366
									
								
								validate-docs-links.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										366
									
								
								validate-docs-links.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,366 @@ | |||||||
|  | #!/usr/bin/env tsx | ||||||
|  |  | ||||||
|  | import { readFileSync, readdirSync, statSync, existsSync } from 'fs'; | ||||||
|  | import { join, relative, dirname, resolve, extname } from 'path'; | ||||||
|  | import { pathToFileURL } from 'url'; | ||||||
|  |  | ||||||
|  | interface LinkValidationResult { | ||||||
|  |   file: string; | ||||||
|  |   line: number; | ||||||
|  |   link: string; | ||||||
|  |   type: 'relative' | 'anchor' | 'absolute' | 'external'; | ||||||
|  |   valid: boolean; | ||||||
|  |   reason?: string; | ||||||
|  |   targetFile?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class DocumentationLinkValidator { | ||||||
|  |   private siteDir: string; | ||||||
|  |   private sourceDir: string; | ||||||
|  |   private results: LinkValidationResult[] = []; | ||||||
|  |   private fileCache: Map<string, string> = new Map(); | ||||||
|  |  | ||||||
|  |   constructor(siteDir: string = './site', sourceDir: string = './docs') { | ||||||
|  |     this.siteDir = resolve(siteDir); | ||||||
|  |     this.sourceDir = resolve(sourceDir); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get all HTML files in the site directory | ||||||
|  |    */ | ||||||
|  |   private getAllHtmlFiles(dir: string): string[] { | ||||||
|  |     const files: string[] = []; | ||||||
|  |     const items = readdirSync(dir); | ||||||
|  |  | ||||||
|  |     for (const item of items) { | ||||||
|  |       const fullPath = join(dir, item); | ||||||
|  |       const stat = statSync(fullPath); | ||||||
|  |  | ||||||
|  |       if (stat.isDirectory()) { | ||||||
|  |         files.push(...this.getAllHtmlFiles(fullPath)); | ||||||
|  |       } else if (item.endsWith('.html')) { | ||||||
|  |         files.push(fullPath); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return files; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get all markdown files in the docs directory | ||||||
|  |    */ | ||||||
|  |   private getAllMarkdownFiles(dir: string): string[] { | ||||||
|  |     const files: string[] = []; | ||||||
|  |     const items = readdirSync(dir); | ||||||
|  |  | ||||||
|  |     for (const item of items) { | ||||||
|  |       const fullPath = join(dir, item); | ||||||
|  |       const stat = statSync(fullPath); | ||||||
|  |  | ||||||
|  |       if (stat.isDirectory()) { | ||||||
|  |         files.push(...this.getAllMarkdownFiles(fullPath)); | ||||||
|  |       } else if (item.endsWith('.md')) { | ||||||
|  |         files.push(fullPath); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return files; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Extract links from HTML content | ||||||
|  |    */ | ||||||
|  |   private extractHtmlLinks(content: string, filePath: string): Array<{link: string, line: number}> { | ||||||
|  |     const links: Array<{link: string, line: number}> = []; | ||||||
|  |     const lines = content.split('\n'); | ||||||
|  |      | ||||||
|  |     // Regex patterns for different types of links | ||||||
|  |     const patterns = [ | ||||||
|  |       /href=["']([^"']+)["']/g,  // href attributes | ||||||
|  |       /src=["']([^"']+)["']/g,   // src attributes (for images, scripts, etc.) | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     lines.forEach((line, index) => { | ||||||
|  |       patterns.forEach(pattern => { | ||||||
|  |         let match; | ||||||
|  |         while ((match = pattern.exec(line)) !== null) { | ||||||
|  |           const link = match[1]; | ||||||
|  |           // Skip external links, mailto, javascript, and data URLs | ||||||
|  |           if (!link.startsWith('http://') &&  | ||||||
|  |               !link.startsWith('https://') &&  | ||||||
|  |               !link.startsWith('mailto:') && | ||||||
|  |               !link.startsWith('javascript:') && | ||||||
|  |               !link.startsWith('data:') && | ||||||
|  |               !link.startsWith('//')) { | ||||||
|  |             links.push({ link, line: index + 1 }); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return links; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Extract links from Markdown content | ||||||
|  |    */ | ||||||
|  |   private extractMarkdownLinks(content: string, filePath: string): Array<{link: string, line: number}> { | ||||||
|  |     const links: Array<{link: string, line: number}> = []; | ||||||
|  |     const lines = content.split('\n'); | ||||||
|  |      | ||||||
|  |     // Regex patterns for markdown links | ||||||
|  |     const patterns = [ | ||||||
|  |       /\[([^\]]*)\]\(([^)]+)\)/g,  // [text](link) | ||||||
|  |       /!\[([^\]]*)\]\(([^)]+)\)/g, //  | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     lines.forEach((line, index) => { | ||||||
|  |       patterns.forEach(pattern => { | ||||||
|  |         let match; | ||||||
|  |         while ((match = pattern.exec(line)) !== null) { | ||||||
|  |           const link = match[2]; | ||||||
|  |           // Skip external links | ||||||
|  |           if (!link.startsWith('http://') &&  | ||||||
|  |               !link.startsWith('https://') &&  | ||||||
|  |               !link.startsWith('mailto:') && | ||||||
|  |               !link.startsWith('//')) { | ||||||
|  |             links.push({ link, line: index + 1 }); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return links; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Validate a single link | ||||||
|  |    */ | ||||||
|  |   private validateLink(link: string, sourceFile: string, isHtml: boolean = true): LinkValidationResult { | ||||||
|  |     const baseResult = { | ||||||
|  |       file: relative(process.cwd(), sourceFile), | ||||||
|  |       link, | ||||||
|  |       line: 0, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Handle anchor links | ||||||
|  |     if (link.startsWith('#')) { | ||||||
|  |       return { | ||||||
|  |         ...baseResult, | ||||||
|  |         type: 'anchor', | ||||||
|  |         valid: true, // We'll assume anchors are valid for now | ||||||
|  |         reason: 'Anchor link (not validated)' | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Handle absolute paths | ||||||
|  |     if (link.startsWith('/')) { | ||||||
|  |       return { | ||||||
|  |         ...baseResult, | ||||||
|  |         type: 'absolute', | ||||||
|  |         valid: false, | ||||||
|  |         reason: 'Absolute paths are not recommended for relative documentation' | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Handle relative links | ||||||
|  |     const sourceDir = dirname(sourceFile); | ||||||
|  |     let targetPath: string; | ||||||
|  |     let anchorPart = ''; | ||||||
|  |  | ||||||
|  |     // Split off anchor if present | ||||||
|  |     const anchorIndex = link.indexOf('#'); | ||||||
|  |     let linkPath = link; | ||||||
|  |     if (anchorIndex > 0) { | ||||||
|  |       linkPath = link.substring(0, anchorIndex); | ||||||
|  |       anchorPart = link.substring(anchorIndex); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Decode URL-encoded characters | ||||||
|  |     linkPath = decodeURIComponent(linkPath); | ||||||
|  |  | ||||||
|  |     if (isHtml) { | ||||||
|  |       // For HTML files in site directory | ||||||
|  |       targetPath = resolve(sourceDir, linkPath); | ||||||
|  |        | ||||||
|  |       // Check if it's a directory link (should have index.html) | ||||||
|  |       if (!linkPath.endsWith('.html') && !linkPath.endsWith('/')) { | ||||||
|  |         // Try with .html extension | ||||||
|  |         const htmlPath = targetPath + '.html'; | ||||||
|  |         if (existsSync(htmlPath)) { | ||||||
|  |           targetPath = htmlPath; | ||||||
|  |         } else { | ||||||
|  |           // Try as directory with index.html | ||||||
|  |           const indexPath = join(targetPath, 'index.html'); | ||||||
|  |           if (existsSync(indexPath)) { | ||||||
|  |             targetPath = indexPath; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } else if (linkPath.endsWith('/')) { | ||||||
|  |         targetPath = join(targetPath, 'index.html'); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       // For markdown files in docs directory | ||||||
|  |       targetPath = resolve(sourceDir, linkPath); | ||||||
|  |        | ||||||
|  |       // MkDocs converts .md to .html, so we need to check both | ||||||
|  |       if (linkPath.endsWith('.md')) { | ||||||
|  |         // Check if the .md file exists | ||||||
|  |         if (!existsSync(targetPath)) { | ||||||
|  |           // Try without .md and with various extensions | ||||||
|  |           const basePath = targetPath.slice(0, -3); | ||||||
|  |           if (existsSync(basePath + '.md')) { | ||||||
|  |             targetPath = basePath + '.md'; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } else if (!extname(linkPath)) { | ||||||
|  |         // No extension, could be a directory or file | ||||||
|  |         if (existsSync(targetPath + '.md')) { | ||||||
|  |           targetPath = targetPath + '.md'; | ||||||
|  |         } else if (existsSync(join(targetPath, 'index.md'))) { | ||||||
|  |           targetPath = join(targetPath, 'index.md'); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const exists = existsSync(targetPath); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       ...baseResult, | ||||||
|  |       type: 'relative', | ||||||
|  |       valid: exists, | ||||||
|  |       reason: exists ? undefined : `Target file not found: ${relative(process.cwd(), targetPath)}`, | ||||||
|  |       targetFile: exists ? relative(process.cwd(), targetPath) : undefined | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Validate all links in HTML files | ||||||
|  |    */ | ||||||
|  |   public validateHtmlFiles(): void { | ||||||
|  |     console.log(`\n🔍 Validating HTML files in ${this.siteDir}...\n`); | ||||||
|  |      | ||||||
|  |     if (!existsSync(this.siteDir)) { | ||||||
|  |       console.error(`❌ Site directory not found: ${this.siteDir}`); | ||||||
|  |       console.log('Please run "mkdocs build" first to generate the site.'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const htmlFiles = this.getAllHtmlFiles(this.siteDir); | ||||||
|  |     console.log(`Found ${htmlFiles.length} HTML files to validate.\n`); | ||||||
|  |  | ||||||
|  |     for (const file of htmlFiles) { | ||||||
|  |       const content = readFileSync(file, 'utf-8'); | ||||||
|  |       const links = this.extractHtmlLinks(content, file); | ||||||
|  |  | ||||||
|  |       for (const { link, line } of links) { | ||||||
|  |         const result = this.validateLink(link, file, true); | ||||||
|  |         result.line = line; | ||||||
|  |         if (!result.valid) { | ||||||
|  |           this.results.push(result); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Validate all links in Markdown files | ||||||
|  |    */ | ||||||
|  |   public validateMarkdownFiles(): void { | ||||||
|  |     console.log(`\n🔍 Validating Markdown files in ${this.sourceDir}...\n`); | ||||||
|  |      | ||||||
|  |     if (!existsSync(this.sourceDir)) { | ||||||
|  |       console.error(`❌ Docs directory not found: ${this.sourceDir}`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const mdFiles = this.getAllMarkdownFiles(this.sourceDir); | ||||||
|  |     console.log(`Found ${mdFiles.length} Markdown files to validate.\n`); | ||||||
|  |  | ||||||
|  |     for (const file of mdFiles) { | ||||||
|  |       const content = readFileSync(file, 'utf-8'); | ||||||
|  |       const links = this.extractMarkdownLinks(content, file); | ||||||
|  |  | ||||||
|  |       for (const { link, line } of links) { | ||||||
|  |         const result = this.validateLink(link, file, false); | ||||||
|  |         result.line = line; | ||||||
|  |         if (!result.valid) { | ||||||
|  |           this.results.push(result); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Print validation results | ||||||
|  |    */ | ||||||
|  |   public printResults(): void { | ||||||
|  |     if (this.results.length === 0) { | ||||||
|  |       console.log('✅ All links are valid!\n'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     console.log(`\n❌ Found ${this.results.length} broken links:\n`); | ||||||
|  |     console.log('=' .repeat(80)); | ||||||
|  |  | ||||||
|  |     // Group results by file | ||||||
|  |     const resultsByFile = new Map<string, LinkValidationResult[]>(); | ||||||
|  |     for (const result of this.results) { | ||||||
|  |       if (!resultsByFile.has(result.file)) { | ||||||
|  |         resultsByFile.set(result.file, []); | ||||||
|  |       } | ||||||
|  |       resultsByFile.get(result.file)!.push(result); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Print results grouped by file | ||||||
|  |     for (const [file, fileResults] of resultsByFile) { | ||||||
|  |       console.log(`\n📄 ${file}`); | ||||||
|  |       console.log('-'.repeat(80)); | ||||||
|  |        | ||||||
|  |       for (const result of fileResults) { | ||||||
|  |         console.log(`  Line ${result.line}: ${result.link}`); | ||||||
|  |         if (result.reason) { | ||||||
|  |           console.log(`    ⚠️  ${result.reason}`); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     console.log('\n' + '='.repeat(80)); | ||||||
|  |     console.log(`\nTotal: ${this.results.length} broken links found.`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get validation results for programmatic use | ||||||
|  |    */ | ||||||
|  |   public getResults(): LinkValidationResult[] { | ||||||
|  |     return this.results; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Run full validation | ||||||
|  |    */ | ||||||
|  |   public validate(): boolean { | ||||||
|  |     this.results = []; | ||||||
|  |      | ||||||
|  |     // Validate built HTML files | ||||||
|  |     this.validateHtmlFiles(); | ||||||
|  |      | ||||||
|  |     // Also validate source markdown files for better debugging | ||||||
|  |     this.validateMarkdownFiles(); | ||||||
|  |      | ||||||
|  |     this.printResults(); | ||||||
|  |      | ||||||
|  |     return this.results.length === 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Main execution | ||||||
|  | if (import.meta.url === pathToFileURL(process.argv[1]).href) { | ||||||
|  |   const validator = new DocumentationLinkValidator(); | ||||||
|  |   const isValid = validator.validate(); | ||||||
|  |    | ||||||
|  |   // Exit with error code if links are broken | ||||||
|  |   process.exit(isValid ? 0 : 1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { DocumentationLinkValidator }; | ||||||
		Reference in New Issue
	
	Block a user