399 lines · 10.8 KB · TypeScript
RawDownload
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<Record<string, MarkdownFileIcon>> = {  ".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<Record<string, MarkdownFileIcon>> = {  "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,  };}