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.
| Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store | 1 | /** |
| 2 | * Code a page cites, and whether a change touched it. Pure. | |
| 3 | * | |
| 4 | * A citation names a repository (`owner/name`) and a path in it: a file | |
| 5 | * (`src/export.ts`), a folder (`src/export`, everything under it) or a | |
| 6 | * glob (`src/**\/*.sql`, `*` within a folder, `**` across folders, `?` one | |
| 7 | * character). Its kind says what at that path the page describes: the | |
| 8 | * path itself, a symbol, an endpoint or an environment variable, named by | |
| 9 | * its label. A page's citations come from its text (the editor's citation | |
| 10 | * chips, and links to files in a repository) and from its header. | |
| 11 | */ | |
| 12 | import type { DocCitation, DocCitationKind, DocDescribes } from "@g1t/contracts"; | |
| 13 | ||
| 14 | import { projectRef } from "./search.ts"; | |
| 15 | ||
| 16 | const KINDS = new Set<DocCitationKind>(["path", "symbol", "endpoint", "env"]); | |
| 17 | const MAX_PATH = 400; | |
| 18 | const MAX_LABEL = 200; | |
| 19 | /** Citations a page keeps from its text, and in its header. */ | |
| 20 | export const MAX_CITATIONS = 100; | |
| 21 | export const MAX_DESCRIBES = 20; | |
| 22 | ||
| 23 | /** A path as kept: no leading or trailing slash, no `.`/`..` parts, no doubled slashes; null when empty or unsafe. */ | |
| 24 | export function cleanPath(path: unknown): string | null { | |
| 25 | const parts = String(path ?? "") | |
| 26 | .trim() | |
| 27 | .replace(/\\/g, "/") | |
| 28 | .split("/") | |
| 29 | .filter((p) => p && p !== "."); | |
| 30 | if (!parts.length || parts.some((p) => p === ".." || /[\u0000-\u001f]/.test(p))) return null; | |
| 31 | const out = parts.join("/"); | |
| 32 | return out.length <= MAX_PATH ? out : null; | |
| 33 | } | |
| 34 | ||
| 35 | function isGlob(path: string): boolean { | |
| 36 | return /[*?]/.test(path); | |
| 37 | } | |
| 38 | ||
| 39 | /** A citation as stored, or null when it names no repository or path. */ | |
| 40 | export function cleanCitation(raw: Partial<DocCitation> | null | undefined, source: DocCitation["source"]): DocCitation | null { | |
| 41 | if (!raw || typeof raw !== "object") return null; | |
| 42 | const repo = projectRef(String(raw.repo ?? "")); | |
| 43 | const path = cleanPath(raw.path); | |
| 44 | if (!repo || !path) return null; | |
| 45 | const kind = KINDS.has(raw.kind as DocCitationKind) ? (raw.kind as DocCitationKind) : "path"; | |
| 46 | const label = kind === "path" ? null : String(raw.label ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_LABEL) || null; | |
| 47 | const ref = /^[0-9a-f]{7,64}$/i.test(String(raw.ref ?? "")) ? String(raw.ref).toLowerCase() : /^[A-Za-z0-9._/-]{1,100}$/.test(String(raw.ref ?? "")) ? String(raw.ref) : null; | |
| 48 | return { repo, path, kind, label, ref, source }; | |
| 49 | } | |
| 50 | ||
| 51 | /** The header's "Describes" list as stored: valid entries, once each, at most MAX_DESCRIBES. */ | |
| 52 | export function cleanDescribes(list: unknown): DocDescribes[] { | |
| 53 | if (!Array.isArray(list)) return []; | |
| 54 | const seen = new Set<string>(); | |
| 55 | const out: DocDescribes[] = []; | |
| 56 | for (const item of list) { | |
| 57 | const c = cleanCitation({ ...(item as object), kind: "path" }, "header"); | |
| 58 | if (!c || seen.has(`${c.repo}\n${c.path}`)) continue; | |
| 59 | seen.add(`${c.repo}\n${c.path}`); | |
| 60 | out.push({ repo: c.repo, path: c.path }); | |
| 61 | if (out.length >= MAX_DESCRIBES) break; | |
| 62 | } | |
| 63 | return out; | |
| 64 | } | |
| 65 | ||
| 66 | /** The folder a glob starts from: its parts before the first with a wildcard. */ | |
| 67 | export function globRoot(path: string): string { | |
| 68 | const parts = path.split("/"); | |
| 69 | const i = parts.findIndex((p) => isGlob(p)); | |
| 70 | return (i < 0 ? parts : parts.slice(0, i)).join("/"); | |
| 71 | } | |
| 72 | ||
| 73 | /** | |
| 74 | * Where a citation links: the file (or folder) in Code at the commit it | |
| 75 | * was cited at, or the default branch (`HEAD`) when none is known. A | |
| 76 | * glob links to the folder it starts from. | |
| 77 | */ | |
| 78 | export function citationHref(c: Pick<DocCitation, "repo" | "path" | "ref">): string { | |
| 79 | const ref = encodeURIComponent(c.ref || "HEAD"); | |
| 80 | const glob = isGlob(c.path); | |
| 81 | const path = (glob ? globRoot(c.path) : c.path) | |
| 82 | .split("/") | |
| 83 | .filter(Boolean) | |
| 84 | .map(encodeURIComponent) | |
| 85 | .join("/"); | |
| 86 | const kind = glob || !/\.[A-Za-z0-9]{1,10}$/.test(c.path) ? "tree" : "blob"; | |
| 87 | return `/${c.repo}/${kind}/${ref}${path ? `/${path}` : ""}`; | |
| 88 | } | |
| 89 | ||
| 90 | /** A citation's text in a page's Markdown: a link to the code. */ | |
| 91 | export function citationMarkdown(c: Pick<DocCitation, "repo" | "path" | "ref" | "kind" | "label">): string { | |
| 92 | const shown = c.kind !== "path" && c.label ? c.label : c.path; | |
| 93 | const ticks = shown.includes("`") ? "``" : "`"; | |
| 94 | return `[${ticks}${shown}${ticks}](${citationHref(c)})`; | |
| 95 | } | |
| 96 | ||
| 97 | /** | |
| 98 | * Links in Markdown to files and folders in a repository on this site: | |
| 99 | * `/<owner>/<repo>/blob|tree/<ref>/<path>`, absolute or on this origin. | |
| 100 | * The ref is one segment (a commit or a branch without a slash). | |
| 101 | */ | |
| 102 | export function citationsFromMarkdown(markdown: string): DocCitation[] { | |
| 103 | const out: DocCitation[] = []; | |
| 104 | const seen = new Set<string>(); | |
| 105 | const link = /\]\(\s*<?((?:https?:\/\/[^/\s)]+)?\/([A-Za-z0-9][A-Za-z0-9._-]*)\/([A-Za-z0-9._-]+)\/(?:blob|tree)\/([^/\s)#?]+)\/([^\s)#?>]+))[^)]*\)/g; | |
| 106 | for (const m of String(markdown ?? "").matchAll(link)) { | |
| 107 | if (m[1]!.startsWith("http") && !/^https?:\/\/([a-z0-9-]+\.)*g1t\.(sh|dev)(:\d+)?\//i.test(m[1]!)) continue; | |
| 108 | let path: string; | |
| 109 | let ref: string; | |
| 110 | try { | |
| 111 | path = m[5]! | |
| 112 | .split("/") | |
| 113 | .map((p) => decodeURIComponent(p)) | |
| 114 | .join("/"); | |
| 115 | ref = decodeURIComponent(m[4]!); | |
| 116 | } catch { | |
| 117 | continue; | |
| 118 | } | |
| 119 | const c = cleanCitation({ repo: `${m[2]}/${m[3]}`, path, ref: ref === "HEAD" ? null : ref, kind: "path" }, "body"); | |
| 120 | if (!c || seen.has(`${c.repo}\n${c.path}`)) continue; | |
| 121 | seen.add(`${c.repo}\n${c.path}`); | |
| 122 | out.push(c); | |
| 123 | } | |
| 124 | return out; | |
| 125 | } | |
| 126 | ||
| 127 | /** | |
| 128 | * A page's citations from its text: the citation chips (`nodes`), then | |
| 129 | * links to code not already a chip's own. At most MAX_CITATIONS. | |
| 130 | */ | |
| 131 | export function bodyCitations(nodes: Partial<DocCitation>[], markdown: string): DocCitation[] { | |
| 132 | const out: DocCitation[] = []; | |
| 133 | const seen = new Set<string>(); | |
| 134 | const key = (c: DocCitation) => `${c.repo}\n${c.path}\n${c.kind}\n${c.label ?? ""}`; | |
| 135 | const chips = new Set<string>(); | |
| 136 | for (const n of nodes) { | |
| 137 | const c = cleanCitation(n, "body"); | |
| 138 | if (!c || seen.has(key(c))) continue; | |
| 139 | seen.add(key(c)); | |
| 140 | chips.add(citationHref(c)); | |
| 141 | out.push(c); | |
| 142 | } | |
| 143 | for (const c of citationsFromMarkdown(markdown)) { | |
| 144 | // A chip's own link is the chip. | |
| 145 | if (chips.has(citationHref(c)) || out.some((o) => o.repo === c.repo && o.path === c.path)) continue; | |
| 146 | if (seen.has(key(c))) continue; | |
| 147 | seen.add(key(c)); | |
| 148 | out.push(c); | |
| 149 | } | |
| 150 | return out.slice(0, MAX_CITATIONS); | |
| 151 | } | |
| 152 | ||
| 153 | /** A glob as a regular expression over a whole path. */ | |
| 154 | function globExpression(glob: string): RegExp { | |
| 155 | let out = ""; | |
| 156 | for (let i = 0; i < glob.length; i++) { | |
| 157 | const ch = glob[i]!; | |
| 158 | if (ch === "*") { | |
| 159 | if (glob[i + 1] === "*") { | |
| 160 | // `**/` is any folders, or none; a trailing `**` anything below. | |
| 161 | if (glob[i + 2] === "/") { | |
| 162 | out += "(?:.*/)?"; | |
| 163 | i += 2; | |
| 164 | } else { | |
| 165 | out += ".*"; | |
| 166 | i += 1; | |
| 167 | } | |
| 168 | } else out += "[^/]*"; | |
| 169 | } else if (ch === "?") out += "[^/]"; | |
| 170 | else out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&"); | |
| 171 | } | |
| 172 | return new RegExp(`^${out}$`); | |
| 173 | } | |
| 174 | ||
| 175 | /** | |
| 176 | * Whether a change to `changed` touches what `cited` names: the same file, | |
| 177 | * anything under a cited folder, or a path a glob matches. | |
| 178 | */ | |
| 179 | export function touches(cited: string, changed: string): boolean { | |
| 180 | if (isGlob(cited)) return globExpression(cited).test(changed); | |
| 181 | return changed === cited || changed.startsWith(`${cited}/`); | |
| 182 | } | |
| 183 | ||
| 184 | /** Of `changed` paths, those any of the citations names; at most `max`. */ | |
| 185 | export function touchedPaths(citations: Pick<DocCitation, "path">[], changed: string[], max = 20): string[] { | |
| 186 | const out: string[] = []; | |
| 187 | for (const path of changed) { | |
| 188 | if (citations.some((c) => touches(c.path, path))) out.push(path); | |
| 189 | if (out.length >= max) break; | |
| 190 | } | |
| 191 | return out; | |
| 192 | } |