| #!/usr/bin/env node | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const docsRoot = path.resolve(__dirname, '../docs'); | |
| function findFiles(dir) { | |
| const results = []; | |
| for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { | |
| const full = path.join(dir, entry.name); | |
| if (entry.isDirectory()) results.push(...findFiles(full)); | |
| else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) results.push(full); | |
| } | |
| return results; | |
| } | |
| function fixLinks(filePath) { | |
| let content = fs.readFileSync(filePath, 'utf-8'); | |
| const fileDir = path.dirname(path.relative(docsRoot, filePath)); | |
| let modified = false; | |
| // Fix links like [text](./section/page.md) that should be [text](../section/page.md) | |
| // The pattern: a link starting with ./ that references a top-level section different from current | |
| content = content.replace(/\]\(\.\/([a-z][\w-]*\/[^\)]*)\)/g, (match, linkPath) => { | |
| // Extract the target section (first path segment) | |
| const targetSection = linkPath.split('/')[0]; | |
| const currentSection = fileDir.split('/')[0]; | |
| // If targeting a different section, need to go up | |
| if (targetSection !== currentSection && targetSection !== '.' && targetSection !== '') { | |
| // Calculate how many levels up we need to go | |
| const depth = fileDir.split('/').length; | |
| const prefix = '../'.repeat(depth); | |
| modified = true; | |
| return `](${prefix}${linkPath})`; | |
| } | |
| // If same section but in a subdirectory, the ./ is correct | |
| return match; | |
| }); | |
| // Also fix links like [text](./page.md) within same directory that reference other sections | |
| // These are links like ./concepts/rest/overview.md from within concepts/rest/ -> should be ./overview.md | |
| // But more commonly they are cross-section links from a page like concepts/catalog.md linking to ./concepts/rest/overview.md | |
| // which should be ./rest/overview.md (since we're already in concepts/) | |
| // Fix self-referencing section prefix: ./concepts/rest/rest-api.md from within concepts/rest/ | |
| content = content.replace(/\]\(\.\/([\w-]+)\/([\w-]+\/[^\)]*)\)/g, (match, firstSeg, rest) => { | |
| const currentSection = fileDir.split('/')[0]; | |
| const currentSubdir = fileDir.split('/')[1] || ''; | |
| if (firstSeg === currentSection) { | |
| // We're in the same section, remove the redundant prefix | |
| modified = true; | |
| return `](./${rest})`; | |
| } | |
| return match; | |
| }); | |
| // Fix absolute-style links starting with ./ from root-level pages | |
| // e.g., from concepts/overview.md, a link ./concepts/rest/overview.md should be ./rest/overview.md | |
| if (modified) { | |
| fs.writeFileSync(filePath, content); | |
| console.log(` Fixed: ${path.relative(docsRoot, filePath)}`); | |
| } | |
| } | |
| // A more thorough approach: recompute all ref links properly | |
| function fixAllLinks(filePath) { | |
| let content = fs.readFileSync(filePath, 'utf-8'); | |
| const relFile = path.relative(docsRoot, filePath); | |
| const fileDir = path.dirname(relFile); // e.g., "concepts" or "concepts/rest" | |
| let modified = false; | |
| // Match markdown links: [text](./path/to/file.md) or [text](./path/to/file.md#anchor) | |
| content = content.replace(/\]\(\.\/([\w-]+(?:\/[\w-.#]+)*(?:\.mdx?)?(?:#[\w-]*)?)\)/g, (match, rawLink) => { | |
| // Split anchor | |
| let anchor = ''; | |
| let linkPath = rawLink; | |
| const hashIdx = linkPath.indexOf('#'); | |
| if (hashIdx !== -1) { | |
| anchor = linkPath.slice(hashIdx); | |
| linkPath = linkPath.slice(0, hashIdx); | |
| } | |
| // If the link doesn't have a / it's a same-directory link, leave it | |
| if (!linkPath.includes('/')) return match; | |
| // Resolve what the link is trying to point to (assuming it was meant as absolute from docs root) | |
| // Check if the resolved target exists | |
| const resolvedFromCurrent = path.join(docsRoot, fileDir, linkPath); | |
| if (fs.existsSync(resolvedFromCurrent) || fs.existsSync(resolvedFromCurrent.replace(/\.md$/, '.mdx'))) { | |
| // Link resolves correctly from current dir, no fix needed | |
| return match; | |
| } | |
| // Try resolving from docs root | |
| const resolvedFromRoot = path.join(docsRoot, linkPath); | |
| const resolvedFromRootMdx = resolvedFromRoot.replace(/\.md$/, '.mdx'); | |
| if (fs.existsSync(resolvedFromRoot) || fs.existsSync(resolvedFromRootMdx)) { | |
| // The link target exists at root level, compute relative path from current file | |
| const relativePath = path.relative(fileDir, linkPath); | |
| modified = true; | |
| return `](./${relativePath}${anchor})`; | |
| } | |
| // Try without extension | |
| const noExt = linkPath.replace(/\.mdx?$/, ''); | |
| const possibles = [ | |
| path.join(docsRoot, noExt + '.md'), | |
| path.join(docsRoot, noExt + '.mdx'), | |
| path.join(docsRoot, noExt, 'index.md'), | |
| path.join(docsRoot, noExt, 'index.mdx'), | |
| ]; | |
| for (const p of possibles) { | |
| if (fs.existsSync(p)) { | |
| const targetRel = path.relative(docsRoot, p); | |
| const targetDir = path.dirname(targetRel); | |
| const relativePath = path.relative(fileDir, targetRel); | |
| modified = true; | |
| return `](./${relativePath}${anchor})`; | |
| } | |
| } | |
| // Can't resolve, leave as-is | |
| return match; | |
| }); | |
| if (modified) { | |
| fs.writeFileSync(filePath, content); | |
| console.log(` Fixed: ${relFile}`); | |
| } | |
| } | |
| const files = findFiles(docsRoot); | |
| console.log(`Fixing links in ${files.length} files...`); | |
| for (const file of files) { | |
| fixAllLinks(file); | |
| } | |
| console.log('Done!'); |