1export interface CodexFileCitationLink {
2 readonly path: string;
3 readonly href: string;
4 readonly label: string;
5 readonly lineRangeStart?: number;
6}
7
8export type CodexFileCitationAttributes = Readonly<Record<string, string | null | undefined>>;
9
10function positiveInteger(value: string | null | undefined): number | undefined {
11 if (value === null || value === undefined || value.trim().length === 0) return undefined;
12 const parsed = Number(value);
13 return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
14}
15
16function citationLabel(path: string): string {
17 const normalized = path.replaceAll("\\", "/").replace(/\/+$/, "");
18 return normalized.slice(normalized.lastIndexOf("/") + 1) || normalized || "File";
19}
20
21function markdownDestinationPath(path: string): string {
22 return path.replaceAll("%", "%25").replaceAll("#", "%23").replaceAll("?", "%3F");
23}
24
25export function resolveCodexFileCitationLink(
26 attributes: CodexFileCitationAttributes | null | undefined,
27): CodexFileCitationLink | null {
28 const path = attributes?.path?.trim();
29 if (!path) return null;
30
31 const lineRangeStart = positiveInteger(attributes?.line_range_start);
32 const destinationPath = markdownDestinationPath(path);
33 return {
34 path,
35 href: lineRangeStart === undefined ? destinationPath : `${destinationPath}#L${lineRangeStart}`,
36 label: citationLabel(path),
37 ...(lineRangeStart === undefined ? {} : { lineRangeStart }),
38 };
39}
40
41function markdownLabel(value: string): string {
42 return value.replace(/[\\[\]*_`<&]/g, "\\$&");
43}
44
45function markdownDestination(value: string): string {
46 return value
47 .replaceAll("\\", "%5C")
48 .replaceAll("<", "%3C")
49 .replaceAll(">", "%3E")
50 .replaceAll("\r", "%0D")
51 .replaceAll("\n", "%0A");
52}
53
54export function codexFileCitationMarkdown(citation: CodexFileCitationLink): string {
55 return `[${markdownLabel(citation.label)}](<${markdownDestination(citation.href)}>)`;
56}