Spaces:
Running
Running
| /** | |
| * Documentation Link Validator | |
| * | |
| * This script validates that all linked files in documentation exist. | |
| * It checks markdown files for internal links and verifies the targets exist. | |
| */ | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| // Configuration | |
| const docsDir = path.join(__dirname, ".."); | |
| const documentationFiles = [ | |
| "README.md", | |
| ".github/CONTRIBUTING.md", | |
| ".github/CODE_OF_CONDUCT.md", | |
| ".github/SECURITY.md", | |
| ".github/PULL_REQUEST_TEMPLATE.md", | |
| ".github/ISSUE_TEMPLATE/bug_report.md", | |
| ".github/ISSUE_TEMPLATE/feature_request.md", | |
| ".github/ISSUE_TEMPLATE/security_vulnerability.md", | |
| ]; | |
| // Regex patterns for finding internal links | |
| const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; | |
| const htmlLinkRegex = /<a[^>]+href="([^"]+)"[^>]*>/g; | |
| function validateFile(filePath) { | |
| try { | |
| const content = fs.readFileSync(filePath, "utf8"); | |
| const relativePath = path.relative(docsDir, filePath); | |
| const issues = []; | |
| // Find all markdown links | |
| let match; | |
| while (true) { | |
| match = markdownLinkRegex.exec(content); | |
| if (match === null) break; | |
| const linkText = match[1]; | |
| const linkTarget = match[2]; | |
| // Skip external links, anchors, and email links | |
| if ( | |
| linkTarget.startsWith("http") || | |
| linkTarget.startsWith("mailto:") || | |
| linkTarget.startsWith("#") || | |
| linkTarget.startsWith("www.") | |
| ) { | |
| continue; | |
| } | |
| // Resolve the target path | |
| const targetPath = path.resolve(path.dirname(filePath), linkTarget); | |
| // Check if target exists | |
| if (!fs.existsSync(targetPath)) { | |
| issues.push({ | |
| type: "missing-file", | |
| linkText, | |
| linkTarget, | |
| targetPath: path.relative(docsDir, targetPath), | |
| }); | |
| } | |
| } | |
| // Find all HTML links | |
| while (true) { | |
| match = htmlLinkRegex.exec(content); | |
| if (match === null) break; | |
| const linkTarget = match[1]; | |
| // Skip external links and anchors | |
| if ( | |
| linkTarget.startsWith("http") || | |
| linkTarget.startsWith("mailto:") || | |
| linkTarget.startsWith("#") | |
| ) { | |
| continue; | |
| } | |
| // Resolve the target path | |
| const targetPath = path.resolve(path.dirname(filePath), linkTarget); | |
| // Check if target exists | |
| if (!fs.existsSync(targetPath)) { | |
| issues.push({ | |
| type: "missing-file", | |
| linkText: "HTML link", | |
| linkTarget, | |
| targetPath: path.relative(docsDir, targetPath), | |
| }); | |
| } | |
| } | |
| return { file: relativePath, issues }; | |
| } catch (error) { | |
| return { | |
| file: path.relative(docsDir, filePath), | |
| error: error.message, | |
| }; | |
| } | |
| } | |
| function main() { | |
| console.log("π Validating documentation links...\n"); | |
| let totalIssues = 0; | |
| let totalFiles = 0; | |
| for (const docFile of documentationFiles) { | |
| const filePath = path.join(docsDir, docFile); | |
| if (!fs.existsSync(filePath)) { | |
| console.log(`β File not found: ${docFile}`); | |
| continue; | |
| } | |
| totalFiles++; | |
| const result = validateFile(filePath); | |
| if (result.error) { | |
| console.log(`β Error reading ${result.file}: ${result.error}`); | |
| continue; | |
| } | |
| if (result.issues.length > 0) { | |
| console.log(`β ${result.file} has ${result.issues.length} issue(s):`); | |
| result.issues.forEach((issue) => { | |
| console.log( | |
| ` β’ Missing target: "${issue.linkTarget}" (${issue.linkText})`, | |
| ); | |
| console.log(` Expected: ${issue.targetPath}`); | |
| }); | |
| totalIssues += result.issues.length; | |
| } else { | |
| console.log(`β ${result.file} - All links valid`); | |
| } | |
| } | |
| console.log(`\nπ Summary:`); | |
| console.log(` Files checked: ${totalFiles}`); | |
| console.log(` Issues found: ${totalIssues}`); | |
| if (totalIssues > 0) { | |
| console.log( | |
| `\nβ Documentation validation failed with ${totalIssues} issue(s)`, | |
| ); | |
| process.exit(1); | |
| } else { | |
| console.log(`\nβ All documentation links are valid!`); | |
| process.exit(0); | |
| } | |
| } | |
| if (require.main === module) { | |
| main(); | |
| } | |
| module.exports = { validateFile }; | |