Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 1 | /** |
| 2 | * A project's links: its homepage, its docs, any others, and production's | |
| 3 | * address when it is deployed elsewhere. Each is an http(s) address, | |
| 4 | * tidied as a repository's website is (crates/contracts/src/repos.rs | |
| 5 | * `clean_website`): `https://` added when no scheme is given, anything else | |
| 6 | * refused. A repository's own project follows its website as homepage | |
| 7 | * until the project is given one of its own, as its description does. | |
| 8 | */ | |
| 9 | ||
| 10 | import type { ProjectLink, ProjectLinks } from "@g1t/contracts"; | |
| 11 | ||
| 12 | // As contracts' MAX_PROJECT_LINKS, MAX_LINK_LABEL and MAX_LINK_URL; repeated | |
| 13 | // so this module's tests run without the package. | |
| 14 | const MAX_PROJECT_LINKS = 10; | |
| 15 | const MAX_LINK_LABEL = 40; | |
| 16 | const MAX_LINK_URL = 255; | |
| 17 | ||
| 18 | export type Cleaned<T> = { ok: true; value: T } | { ok: false; message: string }; | |
| 19 | ||
| 20 | /** An address as it is kept; null for blank. `what` names it in the refusal. */ | |
| 21 | export function cleanUrl(text: string | null | undefined, what = "A link"): Cleaned<string | null> { | |
| 22 | const trimmed = (text ?? "").trim(); | |
| 23 | if (!trimmed) return { ok: true, value: null }; | |
| 24 | let url: string; | |
| 25 | if (/^https?:\/\//i.test(trimmed)) url = trimmed.replace(/^https?/i, (scheme) => scheme.toLowerCase()); | |
| 26 | else if (trimmed.includes("://") || /^[a-z][a-z0-9+.-]*:/i.test(trimmed.split("/")[0]!.replace(/:\d+$/, ""))) { | |
| 27 | return { ok: false, message: `${what} is an http or https address.` }; | |
| 28 | } else url = `https://${trimmed}`; | |
| 29 | const host = url.split("://")[1]!.split(/[/?#]/)[0]!; | |
| 30 | const name = host.replace(/^[^@]*@/, "").replace(/:\d+$/, ""); | |
| 31 | if ( | |
| 32 | url.length > MAX_LINK_URL || | |
| 33 | /\s/.test(url) || | |
| 34 | !name.includes(".") || | |
| 35 | name.startsWith(".") || | |
| 36 | name.endsWith(".") || | |
| 37 | !/^[a-z0-9.-]+$/i.test(name) || | |
| 38 | host.includes("@") | |
| 39 | ) { | |
| 40 | return { ok: false, message: `${what} is not a web address, such as https://example.com.` }; | |
| 41 | } | |
| 42 | return { ok: true, value: url }; | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * The links to keep: each with a label and an address, blank rows left | |
| 47 | * out, at most `MAX_PROJECT_LINKS`. A label is the address's host when | |
| 48 | * none is given. | |
| 49 | */ | |
| 50 | export function cleanLinks(input: unknown): Cleaned<ProjectLink[]> { | |
| 51 | if (!Array.isArray(input)) return { ok: false, message: "Give links as a list of labels and addresses." }; | |
| 52 | const kept: ProjectLink[] = []; | |
| 53 | for (const item of input) { | |
| 54 | const entry = (item ?? {}) as { label?: unknown; url?: unknown }; | |
| 55 | const label = typeof entry.label === "string" ? entry.label.trim().replace(/\s+/g, " ") : ""; | |
| 56 | const raw = typeof entry.url === "string" ? entry.url : ""; | |
| 57 | if (!label && !raw.trim()) continue; | |
| 58 | const url = cleanUrl(raw, label ? `The address for ${label}` : "A link"); | |
| 59 | if (!url.ok) return url; | |
| 60 | if (!url.value) return { ok: false, message: `Give ${label} an address.` }; | |
| 61 | if (label.length > MAX_LINK_LABEL) return { ok: false, message: `A link's label is at most ${MAX_LINK_LABEL} characters.` }; | |
| 62 | kept.push({ label: label || url.value.split("://")[1]!.split(/[/?#]/)[0]!, url: url.value }); | |
| 63 | } | |
| 64 | if (kept.length > MAX_PROJECT_LINKS) return { ok: false, message: `A project keeps at most ${MAX_PROJECT_LINKS} links besides its homepage and docs.` }; | |
| 65 | return { ok: true, value: kept }; | |
| 66 | } | |
| 67 | ||
| 68 | /** Links as stored: a JSON list; anything unreadable is none. */ | |
| 69 | export function storedLinks(text: string | null | undefined): ProjectLink[] { | |
| 70 | if (!text) return []; | |
| 71 | try { | |
| 72 | const parsed = JSON.parse(text) as unknown; | |
| 73 | if (!Array.isArray(parsed)) return []; | |
| 74 | return parsed | |
| 75 | .filter((link): link is ProjectLink => typeof link?.label === "string" && typeof link?.url === "string") | |
| 76 | .slice(0, MAX_PROJECT_LINKS); | |
| 77 | } catch { | |
| 78 | return []; | |
| 79 | } | |
| 80 | } | |
| 81 | ||
| 82 | /** | |
| 83 | * The links a project shows, from its row. Only a repository's own | |
| 84 | * project (its primary one) follows the repository's website: another | |
| 85 | * project in it, from a directory of its own, is something else. | |
| 86 | */ | |
| 87 | export function projectLinks(row: { | |
| 88 | is_primary?: number; | |
| 89 | homepage?: string | null; | |
| 90 | repo_website?: string | null; | |
| 91 | docs_url?: string | null; | |
| 92 | links?: string | null; | |
| 93 | }): ProjectLinks { | |
| 94 | const own = row.homepage ?? null; | |
| 95 | const repo = row.is_primary ? (row.repo_website ?? null) : null; | |
| 96 | return { | |
| 97 | homepage: own ?? repo, | |
| 98 | homepageInherited: own == null && repo != null, | |
| 99 | docs: row.docs_url ?? null, | |
| 100 | custom: storedLinks(row.links), | |
| 101 | }; | |
| 102 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.