/** * Documentation Link Fixer * * Reads the audit report generated by audit-doc-links.js and applies fixes * to broken links where a single match was found. * * Usage: * node tools/fix-doc-links.js # Dry-run (preview changes) * node tools/fix-doc-links.js --apply # Apply changes */ const { readFileSync, writeFileSync, existsSync } = require('node:fs'); const { resolve, relative } = require('node:path'); const REPORT_PATH = resolve(__dirname, '.link-audit-report.json'); // Colors for console output const colors = { reset: '\u001B[0m', red: '\u001B[31m', green: '\u001B[32m', yellow: '\u001B[33m', cyan: '\u001B[36m', dim: '\u001B[2m', }; /** * Load the audit report. */ function loadReport() { if (!existsSync(REPORT_PATH)) { console.error(`\n ${colors.red}Error:${colors.reset} No audit report found.`); console.error(` Run 'node tools/audit-doc-links.js' first.\n`); process.exit(1); } try { const content = readFileSync(REPORT_PATH, 'utf-8'); return JSON.parse(content); } catch (error) { console.error(`\n ${colors.red}Error:${colors.reset} Failed to parse audit report.`); console.error(` ${error.message}\n`); process.exit(1); } } /** * Apply a fix to a file by replacing the original link with the suggested fix. */ function applyFix(filePath, originalLink, suggestedFix) { const content = readFileSync(filePath, 'utf-8'); // Create the replacement pattern - we need to match the exact link in markdown syntax // Original might have anchor, so we need to handle that const originalWithoutAnchor = originalLink.split('#')[0]; const suggestedWithoutAnchor = suggestedFix.split('#')[0]; // Also preserve any anchor from the original if the fix doesn't include one let finalFix = suggestedFix; if (originalLink.includes('#') && !suggestedFix.includes('#')) { const anchor = originalLink.slice(originalLink.indexOf('#')); finalFix = suggestedFix + anchor; } // Replace the link - be careful to only replace inside markdown link syntax const linkPattern = new RegExp(`\\]\\(${escapeRegex(originalLink)}\\)`, 'g'); const newContent = content.replace(linkPattern, `](${finalFix})`); if (newContent === content) { return false; // No change made } writeFileSync(filePath, newContent); return true; } /** * Escape special regex characters in a string. */ function escapeRegex(string) { return string.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); } /** * Main function. */ async function main() { const args = process.argv.slice(2); const applyChanges = args.includes('--apply'); console.log('\n Documentation Link Fixer'); console.log(' ========================\n'); if (applyChanges) { console.log(` ${colors.green}APPLYING CHANGES${colors.reset}\n`); } else { console.log(` ${colors.yellow}DRY RUN${colors.reset} - No files will be modified.`); console.log(` Use --apply to apply changes.\n`); } const report = loadReport(); if (report.autoFixable.length === 0) { console.log(` ${colors.green}✓${colors.reset} No auto-fixable links found.\n`); process.exit(0); } console.log(` Found ${report.autoFixable.length} auto-fixable link(s).\n`); const fixed = []; const failed = []; // Group fixes by file for efficiency const byFile = {}; for (const item of report.autoFixable) { if (!byFile[item.sourceFileAbsolute]) { byFile[item.sourceFileAbsolute] = []; } byFile[item.sourceFileAbsolute].push(item); } for (const [filePath, items] of Object.entries(byFile)) { const displayPath = relative(process.cwd(), filePath); console.log(` ${colors.cyan}${displayPath}${colors.reset}`); for (const item of items) { console.log(` Line ${item.line}:`); console.log(` ${colors.dim}Old:${colors.reset} ${item.originalLink}`); console.log(` ${colors.dim}New:${colors.reset} ${item.suggestedFix}`); if (applyChanges) { try { const success = applyFix(filePath, item.originalLink, item.suggestedFix); if (success) { console.log(` ${colors.green}✓ Fixed${colors.reset}`); fixed.push(item); } else { console.log(` ${colors.yellow}⚠ No match found (may have been fixed already)${colors.reset}`); } } catch (error) { console.log(` ${colors.red}✗ Error: ${error.message}${colors.reset}`); failed.push({ ...item, error: error.message }); } } console.log(); } } // Summary console.log(' ' + '─'.repeat(50)); console.log('\n SUMMARY\n'); if (applyChanges) { console.log(` Fixed: ${colors.green}${fixed.length}${colors.reset}`); if (failed.length > 0) { console.log(` Failed: ${colors.red}${failed.length}${colors.reset}`); } console.log(`\n Run 'node tools/audit-doc-links.js' to verify remaining issues.\n`); } else { console.log(` Would fix: ${colors.yellow}${report.autoFixable.length}${colors.reset} link(s)`); console.log(`\n To apply these fixes, run:`); console.log(` node tools/fix-doc-links.js --apply\n`); } process.exit(failed.length > 0 ? 1 : 0); } main().catch((error) => { console.error('Error:', error.message); process.exit(1); });