// Links in README.md, docs/*.md and the site resolve: relative links and images point at files that exist, #anchors // at headings (GitHub / Forgejo slugs) or element ids, and links into this repo on git.crt.fyi // (/raw/branch/main/, /src/branch/main/) at files in the checkout. Other external URLs are not fetched. import { describe, expect, test } from 'bun:test'; import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { dirname, join, relative } from 'node:path'; const ROOT = join(import.meta.dir, '..'); const REPO = /^https:\/\/git\.crt\.fyi\/CRT\/storebound\/(?:raw|src)\/branch\/main\/([^?#]+)(#.*)?$/; /** GitHub-style heading slug: lower case, punctuation dropped, spaces to hyphens. */ const slug = (h: string): string => h.trim().toLowerCase().replace(/<[^>]+>/g, '').replace(/[`*_]/g, '') .replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/\s/g, '-'); function anchorsOf(file: string): Set { const src = readFileSync(file, 'utf8'); if (file.endsWith('.html')) return new Set([...src.matchAll(/\sid="([^"]+)"/g)].map(m => m[1]!)); const out = new Set(); const seen = new Map(); let fence = false; for (const line of src.split('\n')) { if (/^```/.test(line)) fence = !fence; const m = !fence && /^#{1,6} (.+)$/.exec(line); if (!m) continue; const s = slug(m[1]!); const n = seen.get(s) ?? 0; seen.set(s, n + 1); out.add(n ? `${s}-${n}` : s); } return out; } function linksOf(file: string): string[] { let src = readFileSync(file, 'utf8'); if (file.endsWith('.md')) src = src.replace(/^```[\s\S]*?^```/gm, '').replace(/`[^`\n]*`/g, ''); const md = [...src.matchAll(/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)].map(m => m[1]!); const html = [...src.matchAll(/\s(?:href|src|srcset|poster)="([^"]+)"/g)].flatMap(m => m[1]!.split(',').map(s => s.trim().split(/\s+/)[0]!)); return [...md, ...html].filter(Boolean); } function problems(file: string): string[] { const out: string[] = []; const base = dirname(file); for (const link of linksOf(file)) { const repo = REPO.exec(link); let target: string, anchor: string | undefined; if (repo) { target = join(ROOT, decodeURIComponent(repo[1]!)); anchor = repo[2]?.slice(1); } else if (/^[a-z]+:/i.test(link) || link.startsWith('//')) continue; // served by the desktop release server, not the site (tools/vds/apache-storebound-desktop.conf) else if (/(^|\/)latest\.json$/.test(link)) continue; else { const [p, a] = link.split('#') as [string, string | undefined]; target = p ? join(base, decodeURIComponent(p.split('?')[0]!)) : file; anchor = a; } const rel = relative(ROOT, file); if (!existsSync(target)) { out.push(`${rel}: ${link} (no such file)`); continue; } if (anchor && statSync(target).isFile() && /\.(md|html)$/.test(target) && !anchorsOf(target).has(anchor)) out.push(`${rel}: ${link} (no #${anchor})`); } return out; } const docs = readdirSync(join(ROOT, 'docs')).filter(n => n.endsWith('.md')).map(n => join(ROOT, 'docs', n)); describe('links resolve', () => { test('README.md and docs/*.md', () => { expect([join(ROOT, 'README.md'), ...docs].flatMap(problems)).toEqual([]); }); test('site pages', () => { const pages = ['site/index.html', 'site/desktop/index.html'].map(p => join(ROOT, p)).filter(existsSync); expect(pages.flatMap(problems)).toEqual([]); }); test('the checker catches a missing file, a missing anchor and a stale repo link', () => { expect(slug('Install for your AI tool')).toBe('install-for-your-ai-tool'); expect(slug('Credentials (never in the repo)')).toBe('credentials-never-in-the-repo'); expect(anchorsOf(join(ROOT, 'docs/getting-started.md')).has('install-for-your-ai-tool')).toBe(true); expect(REPO.exec('https://git.crt.fyi/CRT/storebound/raw/branch/main/site/img/showcase.webp')?.[1]).toBe('site/img/showcase.webp'); }); });