#!/usr/bin/env node /** * Fix MkDocs structure by: * 1. Syncing README.md to docs/index.md with necessary path adjustments * 2. Moving overview pages to index.md inside their directories to prevent duplicate navigation entries */ import * as fs from 'fs'; import * as path from 'path'; interface FixResult { message: string; } /** * Find markdown files that have a corresponding directory with the same name, * and move them to index.md inside that directory. */ function fixDuplicateEntries(docsDir: string): FixResult[] { const fixesMade: FixResult[] = []; function walkDir(dir: string): void { let files: string[]; try { files = fs.readdirSync(dir); } catch (err) { console.warn(`Warning: Unable to read directory ${dir}: ${err.message}`); return; } for (const file of files) { const filePath = path.join(dir, file); let stat: fs.Stats; try { stat = fs.statSync(filePath); } catch (err) { // File might have been moved already, skip it continue; } if (stat.isDirectory()) { walkDir(filePath); } else if (file.endsWith('.md')) { const basename = file.slice(0, -3); // Remove .md extension const dirPath = path.join(dir, basename); // Check if there's a directory with the same name if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) { const indexPath = path.join(dirPath, 'index.md'); // Check if index.md already exists in that directory if (!fs.existsSync(indexPath)) { // Move the file to index.md in the directory fs.renameSync(filePath, indexPath); fixesMade.push({ message: `Moved ${path.relative(docsDir, filePath)} -> ${path.relative(docsDir, indexPath)}` }); // Move associated images with pattern basename_* try { const dirFiles = fs.readdirSync(dir); for (const imgFile of dirFiles) { if (imgFile.startsWith(`${basename}_`)) { const imgSrc = path.join(dir, imgFile); try { if (!fs.statSync(imgSrc).isDirectory()) { const imgDest = path.join(dirPath, imgFile); fs.renameSync(imgSrc, imgDest); fixesMade.push({ message: `Moved ${path.relative(docsDir, imgSrc)} -> ${path.relative(docsDir, imgDest)}` }); } } catch (err) { // File might have been moved already, skip it } } } } catch (err) { // Directory might not exist anymore, skip it } // Move exact match images const imgExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg']; for (const ext of imgExtensions) { const imgFile = path.join(dir, `${basename}${ext}`); if (fs.existsSync(imgFile)) { const imgDest = path.join(dirPath, `${basename}${ext}`); fs.renameSync(imgFile, imgDest); fixesMade.push({ message: `Moved ${path.relative(docsDir, imgFile)} -> ${path.relative(docsDir, imgDest)}` }); } } } } } } } walkDir(docsDir); return fixesMade; } /** * Update references in markdown files to point to the new locations. */ function updateReferences(docsDir: string): FixResult[] { const updatesMade: FixResult[] = []; function fixLink(match: string, text: string, link: string, currentDir: string, isIndex: boolean): string { // Skip external links if (link.startsWith('http')) { return match; } // Decode URL-encoded paths for processing let decodedLink: string; try { decodedLink = decodeURIComponent(link); } catch (err) { // If decoding fails, use the original link decodedLink = link; } // 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 if (isIndex && decodedLink.includes('/')) { const pathParts = decodedLink.split('/'); const parentDirName = path.basename(currentDir); // Check if first part matches the parent directory name if (pathParts[0] === parentDirName) { // This is a self-referential path, strip the first part const fixedLink = pathParts.slice(1).join('/'); // Re-encode spaces for URL compatibility before recursing const fixedLinkEncoded = fixedLink.replace(/ /g, '%20'); // Recursively process the fixed link return fixLink(`[${text}](${fixedLinkEncoded})`, text, fixedLinkEncoded, currentDir, isIndex); } } // For any .md link, check if there's a directory with index.md // that should be used instead if (!decodedLink.startsWith('/')) { // Resolve relative to current directory const resolvedPath = path.resolve(currentDir, decodedLink); // Check if this points to a file that should be a directory // Remove .md extension to get the potential directory name if (resolvedPath.endsWith('.md')) { const potentialDir = resolvedPath.slice(0, -3); const potentialIndex = path.join(potentialDir, 'index.md'); // If a directory with index.md exists, update the link if (fs.existsSync(potentialIndex)) { // If we're in an index.md file and linking to a file that's now // in a sibling directory, adjust the path if (isIndex) { // Check if they share the same parent directory if (path.dirname(potentialDir) === path.dirname(currentDir)) { // It's a sibling - just use directory name const dirName = path.basename(potentialDir).replace(/ /g, '%20'); return `[${text}](${dirName}/)`; } } // Calculate relative path from current file to the directory const newPath = path.relative(currentDir, potentialDir).replace(/\\/g, '/').replace(/ /g, '%20'); return `[${text}](${newPath}/)`; } } } // Also handle local references (same directory) if (!decodedLink.includes('/')) { const basename = decodedLink.slice(0, -3); // Remove .md extension const possibleDir = path.join(currentDir, basename); if (fs.existsSync(possibleDir) && fs.statSync(possibleDir).isDirectory()) { // Re-encode spaces for URL compatibility const encodedBasename = basename.replace(/ /g, '%20'); return `[${text}](${encodedBasename}/)`; } } return match; } function walkDir(dir: string): void { let files: string[]; try { files = fs.readdirSync(dir); } catch (err) { console.warn(`Warning: Unable to read directory ${dir}: ${err.message}`); return; } for (const file of files) { const filePath = path.join(dir, file); let stat: fs.Stats; try { stat = fs.statSync(filePath); } catch (err) { // File might have been moved already, skip it continue; } if (stat.isDirectory()) { walkDir(filePath); } else if (file.endsWith('.md')) { let content = fs.readFileSync(filePath, 'utf-8'); const originalContent = content; const isIndex = file === 'index.md'; const currentDir = path.dirname(filePath); // Update markdown links: [text](path.md) const pattern = /\[([^\]]*)\]\(([^)]+\.md)\)/g; content = content.replace(pattern, (match, text, link) => { return fixLink(match, text, link, currentDir, isIndex); }); if (content !== originalContent) { fs.writeFileSync(filePath, content, 'utf-8'); updatesMade.push({ message: `Updated references in ${path.relative(docsDir, filePath)}` }); } } } } walkDir(docsDir); return updatesMade; } /** * Sync README.md to docs/index.md with necessary path adjustments */ function syncReadmeToIndex(projectRoot: string, docsDir: string): FixResult[] { const results: FixResult[] = []; const readmePath = path.join(projectRoot, 'README.md'); const indexPath = path.join(docsDir, 'index.md'); if (!fs.existsSync(readmePath)) { console.warn('README.md not found in project root'); return results; } // Read README content let content = fs.readFileSync(readmePath, 'utf-8'); // Fix image path (./docs/app.png -> app.png) content = content.replace(/src="\.\/docs\/app\.png"/g, 'src="app.png"'); // Fix language links in header content = content.replace(/\[English\]\(\.\/README\.md\)/g, '[English](./index.md)'); content = content.replace(/\.\/docs\/README-ZH_CN\.md/g, './README-ZH_CN.md'); content = content.replace(/\.\/docs\/README-ZH_TW\.md/g, './README-ZH_TW.md'); content = content.replace(/\.\/docs\/README\.ru\.md/g, './README.ru.md'); content = content.replace(/\.\/docs\/README\.ja\.md/g, './README.ja.md'); content = content.replace(/\.\/docs\/README\.it\.md/g, './README.it.md'); content = content.replace(/\.\/docs\/README\.es\.md/g, './README.es.md'); // Fix internal documentation links (./docs/User%20Guide -> ./User%20Guide) content = content.replace(/\.\/docs\/User%20Guide/g, './User%20Guide'); // Write the adjusted content to docs/index.md fs.writeFileSync(indexPath, content, 'utf-8'); results.push({ message: `Synced README.md to docs/index.md with path adjustments` }); return results; } function main(): number { // Get the docs directory const scriptDir = path.dirname(new URL(import.meta.url).pathname); const projectRoot = path.dirname(scriptDir); const docsDir = path.join(projectRoot, 'docs'); // Handle Windows paths (remove leading slash if on Windows) const normalizedProjectRoot = process.platform === 'win32' && projectRoot.startsWith('/') ? projectRoot.substring(1) : projectRoot; const normalizedDocsDir = process.platform === 'win32' && docsDir.startsWith('/') ? docsDir.substring(1) : docsDir; if (!fs.existsSync(normalizedDocsDir)) { console.error(`Error: docs directory not found at ${normalizedDocsDir}`); return 1; } console.log(`Fixing MkDocs structure in ${normalizedDocsDir}`); console.log('-'.repeat(50)); // Sync README.md to docs/index.md const syncResults = syncReadmeToIndex(normalizedProjectRoot, normalizedDocsDir); if (syncResults.length > 0) { console.log('README sync:'); for (const result of syncResults) { console.log(` - ${result.message}`); } console.log(); } // Fix duplicate entries const fixes = fixDuplicateEntries(normalizedDocsDir); if (fixes.length > 0) { console.log('Files reorganized:'); for (const fix of fixes) { console.log(` - ${fix.message}`); } } else { console.log('No duplicate entries found that need fixing'); } console.log(); // Update references const updates = updateReferences(normalizedDocsDir); if (updates.length > 0) { console.log('References updated:'); for (const update of updates) { console.log(` - ${update.message}`); } } else { console.log('No references needed updating'); } console.log('-'.repeat(50)); console.log(`Structure fix complete: ${syncResults.length} README syncs, ${fixes.length} files moved, ${updates.length} files updated`); return 0; } // Run the main function process.exit(main());