import type { MARKDOWN_FILE_ICON_SOURCES } from "./markdownFileIcons.generated"; const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:[\\/]/; const WINDOWS_UNC_PATH_PATTERN = /^\\\\/; const RELATIVE_PATH_PREFIX_PATTERN = /^(~\/|\.{1,2}\/)/; const RELATIVE_FILE_PATH_PATTERN = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}$/; const RELATIVE_FILE_NAME_PATTERN = /^[A-Za-z0-9._-]+\.[A-Za-z0-9_-]+(?::\d+){0,2}$/; const POSITION_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; const POSIX_FILE_ROOT_PREFIXES = [ "/Users/", "/home/", "/tmp/", "/var/", "/etc/", "/opt/", "/mnt/", "/Volumes/", "/private/", "/root/", ] as const; export type MarkdownLinkPresentation = | { readonly kind: "external"; readonly href: string; readonly host: string; } | { readonly kind: "file"; readonly href: string; readonly icon: MarkdownFileIcon; readonly label: string; readonly path: string; readonly line?: number; readonly column?: number; } | { readonly kind: "link"; readonly href: string | null; }; export type MarkdownFileIcon = keyof typeof MARKDOWN_FILE_ICON_SOURCES; const FILE_ICON_BY_NAME: Readonly> = { ".babelrc": "babel", ".babelrc.json": "babel", ".bash_profile": "bash", ".bashrc": "bash", ".browserslistrc": "browserslist", ".dockerignore": "docker", ".eslintignore": "eslint", ".eslintrc": "eslint", ".eslintrc.cjs": "eslint", ".eslintrc.js": "eslint", ".eslintrc.json": "eslint", ".eslintrc.yaml": "eslint", ".eslintrc.yml": "eslint", ".gitattributes": "git", ".gitignore": "git", ".gitkeep": "git", ".gitmodules": "git", ".oxlintrc.json": "oxc", ".postcssrc": "postcss", ".postcssrc.json": "postcss", ".postcssrc.yaml": "postcss", ".postcssrc.yml": "postcss", ".prettierignore": "prettier", ".prettierrc": "prettier", ".prettierrc.json": "prettier", ".prettierrc.cjs": "prettier", ".prettierrc.js": "prettier", ".prettierrc.mjs": "prettier", ".prettierrc.toml": "prettier", ".prettierrc.yaml": "prettier", ".prettierrc.yml": "prettier", ".stylelintignore": "stylelint", ".stylelintrc": "stylelint", ".stylelintrc.cjs": "stylelint", ".stylelintrc.js": "stylelint", ".stylelintrc.json": "stylelint", ".stylelintrc.mjs": "stylelint", ".stylelintrc.yaml": "stylelint", ".stylelintrc.yml": "stylelint", ".terraform.lock.hcl": "terraform", ".zprofile": "bash", ".zshenv": "bash", ".zshrc": "bash", "agents.md": "agents", "babel.config.js": "babel", "babel.config.cjs": "babel", "babel.config.json": "babel", "babel.config.mjs": "babel", "biome.json": "biome", "biome.jsonc": "biome", "bun.lock": "bun", "bun.lockb": "bun", "bunfig.toml": "bun", "claude.md": "claude", "compose.yaml": "docker", "compose.yml": "docker", "docker-compose.yaml": "docker", "docker-compose.yml": "docker", "docker-compose.override.yml": "docker", dockerfile: "docker", "eslint.config.js": "eslint", "eslint.config.cjs": "eslint", "eslint.config.mjs": "eslint", "eslint.config.mts": "eslint", "eslint.config.ts": "eslint", gemfile: "ruby", "next.config.js": "nextjs", "next.config.mjs": "nextjs", "next.config.mts": "nextjs", "next.config.ts": "nextjs", "package.json": "package", "pnpm-lock.yaml": "pnpm", "pnpm-workspace.yaml": "pnpm", "postcss.config.js": "postcss", "postcss.config.cjs": "postcss", "postcss.config.mjs": "postcss", "postcss.config.ts": "postcss", "prettier.config.js": "prettier", "prettier.config.cjs": "prettier", "prettier.config.mjs": "prettier", rakefile: "ruby", "readme.md": "readme", "stylelint.config.js": "stylelint", "stylelint.config.cjs": "stylelint", "stylelint.config.mjs": "stylelint", "svgo.config.js": "svgo", "svgo.config.cjs": "svgo", "svgo.config.mjs": "svgo", "svgo.config.ts": "svgo", "tailwind.config.js": "tailwind", "tailwind.config.cjs": "tailwind", "tailwind.config.mjs": "tailwind", "tailwind.config.ts": "tailwind", "tsconfig.json": "tsconfig", "vite.config.js": "vite", "vite.config.mjs": "vite", "vite.config.mts": "vite", "vite.config.ts": "vite", "webpack.config.js": "webpack", "webpack.config.babel.js": "webpack", "webpack.config.cjs": "webpack", "webpack.config.mjs": "webpack", "webpack.config.ts": "webpack", }; const FILE_ICON_BY_EXTENSION: Readonly> = { "7z": "zip", astro: "astro", avif: "image", "code-workspace": "vscode", bash: "bash", bmp: "image", bz2: "zip", c: "c", cc: "cpp", cpp: "cpp", cxx: "cpp", css: "css", csv: "table", cts: "typescript", db: "database", env: "text", "env.development": "text", "env.local": "text", "env.production": "text", eot: "font", erb: "ruby", fish: "bash", gif: "image", go: "go", gql: "graphql", graphql: "graphql", gz: "zip", h: "c", hh: "cpp", hpp: "cpp", hxx: "cpp", htm: "html", html: "html", ico: "image", icns: "image", ini: "text", inl: "cpp", jar: "zip", jpeg: "image", jpg: "image", js: "javascript", jsx: "react", json: "json", jsonc: "json", less: "css", md: "markdown", mdx: "markdown", "mdx.tsx": "markdown", mjs: "javascript", mts: "typescript", png: "image", postcss: "css", py: "python", pyi: "python", pyw: "python", pyx: "python", rake: "ruby", rar: "zip", rb: "ruby", rs: "rust", sass: "sass", scss: "sass", sh: "bash", sql: "database", sqlite: "database", sqlite3: "database", svelte: "svelte", svg: "svg", swift: "swift", tar: "zip", tf: "terraform", tfstate: "terraform", tfvars: "terraform", tgz: "zip", ts: "typescript", tsv: "table", tsx: "react", txt: "text", woff: "font", woff2: "font", vue: "vue", wasm: "wasm", webp: "image", yml: "yml", yaml: "yml", zig: "zig", zip: "zip", zsh: "bash", }; function safeDecode(value: string): string { try { return decodeURIComponent(value); } catch { return value; } } function normalizeDestination(value: string): string { const trimmed = value.trim(); return trimmed.startsWith("<") && trimmed.endsWith(">") ? trimmed.slice(1, -1) : trimmed; } function fileUrlTarget(href: string): { readonly path: string; readonly hash: string } | null { try { const parsed = new URL(href); if (parsed.protocol.toLowerCase() !== "file:") { return null; } const path = /^\/[A-Za-z]:[\\/]/.test(parsed.pathname) ? parsed.pathname.slice(1) : parsed.pathname; return { path, hash: parsed.hash }; } catch { return null; } } function stripSearchAndHash(value: string): { readonly path: string; readonly hash: string } { const hashIndex = value.indexOf("#"); const pathWithSearch = hashIndex >= 0 ? value.slice(0, hashIndex) : value; const hash = hashIndex >= 0 ? value.slice(hashIndex) : ""; const queryIndex = pathWithSearch.indexOf("?"); return { path: queryIndex >= 0 ? pathWithSearch.slice(0, queryIndex) : pathWithSearch, hash, }; } function splitFilePosition( path: string, hash: string, ): { readonly path: string; readonly line?: number; readonly column?: number } { const suffixMatch = path.match(/:(\d+)(?::(\d+))?$/); const hashMatch = suffixMatch ? null : hash.match(/^#L(\d+)(?:C(\d+))?$/i); const match = suffixMatch ?? hashMatch; if (!match?.[1]) { return { path }; } const line = Number.parseInt(match[1], 10); const column = match[2] ? Number.parseInt(match[2], 10) : undefined; const pathWithoutPosition = suffixMatch ? path.slice(0, -suffixMatch[0].length) : path; return { path: pathWithoutPosition, ...(line > 0 ? { line } : {}), ...(column !== undefined && column > 0 ? { column } : {}), }; } function looksLikePosixFilesystemPath(path: string): boolean { if (!path.startsWith("/")) { return false; } if (POSIX_FILE_ROOT_PREFIXES.some((prefix) => path.startsWith(prefix))) { return true; } if (POSITION_SUFFIX_PATTERN.test(path)) { return true; } const basename = path.slice(path.lastIndexOf("/") + 1); return /\.[A-Za-z0-9_-]+$/.test(basename); } function looksLikeFilePath(value: string): boolean { if (WINDOWS_DRIVE_PATH_PATTERN.test(value) || WINDOWS_UNC_PATH_PATTERN.test(value)) { return true; } if (RELATIVE_PATH_PREFIX_PATTERN.test(value)) { return true; } if (value.startsWith("/")) { return looksLikePosixFilesystemPath(value); } if (FILE_ICON_BY_NAME[value.replace(POSITION_SUFFIX_PATTERN, "").toLowerCase()]) { return true; } return RELATIVE_FILE_PATH_PATTERN.test(value) || RELATIVE_FILE_NAME_PATTERN.test(value); } function fileLabel(value: string): string { const normalized = value.replaceAll("\\", "/"); const basename = normalized.slice(normalized.lastIndexOf("/") + 1); return basename || normalized; } export function resolveMarkdownFileIcon(value: string): MarkdownFileIcon { const basename = fileLabel(value).replace(POSITION_SUFFIX_PATTERN, "").toLowerCase(); const exactIcon = FILE_ICON_BY_NAME[basename]; if (exactIcon) return exactIcon; if (basename.startsWith("tsconfig.") && basename.endsWith(".json")) { return "tsconfig"; } const segments = basename.split("."); for (let index = 1; index < segments.length; index += 1) { const icon = FILE_ICON_BY_EXTENSION[segments.slice(index).join(".")]; if (icon) return icon; } return "default"; } export function resolveMarkdownLinkPresentation(href: string): MarkdownLinkPresentation { const normalized = normalizeDestination(href); try { const parsed = new URL(normalized); if (parsed.protocol === "http:" || parsed.protocol === "https:") { return { kind: "external", href: parsed.toString(), host: parsed.hostname, }; } } catch { // Relative paths and non-URL link destinations are handled below. } const source = normalized.toLowerCase().startsWith("file:") ? fileUrlTarget(normalized) : stripSearchAndHash(normalized); const decodedSource = source ? { path: safeDecode(source.path.trim()), hash: safeDecode(source.hash.trim()) } : null; const fileTarget = decodedSource ? splitFilePosition(decodedSource.path, decodedSource.hash) : null; const targetWithPosition = fileTarget ? `${fileTarget.path}${ fileTarget.line ? `:${fileTarget.line}${fileTarget.column ? `:${fileTarget.column}` : ""}` : "" }` : null; if (fileTarget && targetWithPosition && looksLikeFilePath(targetWithPosition)) { return { kind: "file", href: normalized, icon: resolveMarkdownFileIcon(fileTarget.path), label: fileLabel(targetWithPosition), path: fileTarget.path, ...(fileTarget.line ? { line: fileTarget.line } : {}), ...(fileTarget.column ? { column: fileTarget.column } : {}), }; } return { kind: "link", href: /^(?:mailto|tel):/i.test(normalized) ? normalized : null, }; }