feat(docs): add broken link validation script

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-20 21:53:25 +00:00
1 parent 8d43fc1be8
commit 1a46fd0484
3 files changed
+187 -1

No files matched your search

+185
View File
@@ -0,0 +1,185 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, '..');
const IGNORE_DIRS = [
'node_modules',
'.git',
'dist',
'coverage',
'test_logs',
'task-logs',
'reports',
'artifacts',
'bin'
];
const IGNORE_FILES = [
'package-lock.json',
'yarn.lock'
];
function getAllFiles(dirPath, arrayOfFiles) {
const files = fs.readdirSync(dirPath);
arrayOfFiles = arrayOfFiles || [];
files.forEach(function(file) {
if (IGNORE_DIRS.includes(file) || IGNORE_FILES.includes(file)) {
return;
}
const fullPath = path.join(dirPath, file);
if (fs.statSync(fullPath).isDirectory()) {
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
} else {
if (file.endsWith('.md')) {
arrayOfFiles.push(fullPath);
}
}
});
return arrayOfFiles;
}
function slugify(text) {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '') // Remove all non-word chars
.replace(/[\s_-]+/g, '-') // Replace spaces and underscores with -
.replace(/^-+|-+$/g, ''); // Remove leading/trailing -
}
function getAnchors(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const anchors = new Set();
// Markdown headings
const headingRegex = /^(#{1,6})\s+(.*)$/gm;
let match;
while ((match = headingRegex.exec(content)) !== null) {
anchors.add(slugify(match[2]));
}
// HTML anchors <a name="..."> or <a id="...">
const htmlAnchorRegex = /<a\s+(?:name|id)=["']([^"']+)["']/g;
while ((match = htmlAnchorRegex.exec(content)) !== null) {
anchors.add(match[1]);
}
return anchors;
}
const fileAnchorsCache = new Map();
function getFileAnchors(filePath) {
if (!fileAnchorsCache.has(filePath)) {
fileAnchorsCache.set(filePath, getAnchors(filePath));
}
return fileAnchorsCache.get(filePath);
}
function checkLinks(files) {
let brokenLinks = [];
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
const relativeFilePath = path.relative(ROOT_DIR, file);
const processLink = (link) => {
link = link.trim();
// Remove title
if (link.includes(' "') || link.includes(" '")) {
const parts = link.split(/\s+["']/);
link = parts[0];
}
// Remove wrapping < >
if (link.startsWith('<') && link.endsWith('>')) {
link = link.slice(1, -1);
}
validateLink(link, file, relativeFilePath, brokenLinks);
};
// Regex for [text](url)
const inlineLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
let match;
while ((match = inlineLinkRegex.exec(content)) !== null) {
processLink(match[2]);
}
// Regex for [ref]: url
const refLinkRegex = /^\[([^\]]+)\]:\s*(\S+)/gm;
while ((match = refLinkRegex.exec(content)) !== null) {
processLink(match[2]);
}
});
return brokenLinks;
}
function validateLink(link, currentFile, relativeFilePath, brokenLinks) {
if (!link) return;
if (link.startsWith('http://') || link.startsWith('https://') || link.startsWith('mailto:') || link.startsWith('data:')) return;
const [linkPath, anchor] = link.split('#');
let targetPath;
if (!linkPath) {
// Internal anchor link: #header
targetPath = currentFile;
} else if (linkPath.startsWith('/')) {
targetPath = path.join(ROOT_DIR, linkPath);
} else {
targetPath = path.resolve(path.dirname(currentFile), linkPath);
}
if (!fs.existsSync(targetPath)) {
brokenLinks.push({
file: relativeFilePath,
link: link,
target: path.relative(ROOT_DIR, targetPath),
reason: 'File not found'
});
return;
}
// Check anchor if present and if target is a markdown file
if (anchor && targetPath.endsWith('.md')) {
const anchors = getFileAnchors(targetPath);
if (!anchors.has(anchor)) {
// Try decoding URI component just in case
try {
if (anchors.has(decodeURIComponent(anchor))) return;
} catch {
// Ignore decoding errors
}
brokenLinks.push({
file: relativeFilePath,
link: link,
target: path.relative(ROOT_DIR, targetPath),
reason: `Anchor '#${anchor}' not found`
});
}
}
}
console.log('Scanning for markdown files...');
const files = getAllFiles(ROOT_DIR);
console.log(`Found ${files.length} markdown files. Checking links (including anchors)...`);
const brokenLinks = checkLinks(files);
if (brokenLinks.length > 0) {
console.error(`Found ${brokenLinks.length} broken links:`);
brokenLinks.forEach(item => {
console.error(` ${item.file}: '${item.link}' -> ${item.reason}`);
});
process.exit(1);
} else {
console.log('No broken links found.');
}