| 1 | export interface CodexFileCitationLink { |
| 2 | readonly path: string; |
| 3 | readonly href: string; |
| 4 | readonly label: string; |
| 5 | readonly lineRangeStart?: number; |
| 6 | } |
| 7 | |
| 8 | export type CodexFileCitationAttributes = Readonly<Record<string, string | null | undefined>>; |
| 9 | |
| 10 | function 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 | |
| 16 | function citationLabel(path: string): string { |
| 17 | const normalized = path.replaceAll("\\", "/").replace(/\/+$/, ""); |
| 18 | return normalized.slice(normalized.lastIndexOf("/") + 1) || normalized || "File"; |
| 19 | } |
| 20 | |
| 21 | function markdownDestinationPath(path: string): string { |
| 22 | return path.replaceAll("%", "%25").replaceAll("#", "%23").replaceAll("?", "%3F"); |
| 23 | } |
| 24 | |
| 25 | export 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 | |
| 41 | function markdownLabel(value: string): string { |
| 42 | return value.replace(/[\\[\]*_`<&]/g, "\\$&"); |
| 43 | } |
| 44 | |
| 45 | function 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 | |
| 54 | export function codexFileCitationMarkdown(citation: CodexFileCitationLink): string { |
| 55 | return `[${markdownLabel(citation.label)}](<${markdownDestination(citation.href)}>)`; |
| 56 | } |