308 lines · 8.3 KB · TypeScript
RawDownload
import {  fileBasename,  formatFilePathPosition,  inlineCodeFilePathCandidate,  normalizeMarkdownLinkDestination,  parseMarkdownFileLink,} from "@t3tools/client-runtime/markdown-links";import { videoMimeType } from "@t3tools/shared/video"; import type { MARKDOWN_FILE_ICON_SOURCES } from "./markdownFileIcons.generated"; const POSITION_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; 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; export type MarkdownLinkIcon = "github"; /** * Sites whose brand mark replaces the generic external-link glyph. The marks * are monochrome and tinted with the link color, so they follow the theme. */export function resolveMarkdownLinkIcon(host: string): MarkdownLinkIcon | null {  const hostname = host.toLowerCase();  if (hostname === "github.com" || hostname.endsWith(".github.com")) return "github";  return null;} 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",}; /** Native link and media APIs have no document scheme to inherit from protocol-relative URLs. */export function normalizeNativeMarkdownUrl(value: string): string {  return value.startsWith("//") ? `https:${value}` : value;} export function resolveMarkdownFileIcon(value: string): MarkdownFileIcon {  const basename = fileBasename(value).replace(POSITION_SUFFIX_PATTERN, "").toLowerCase();  if (videoMimeType({ name: basename, mimeType: "" }) !== null) return "video";  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 = normalizeMarkdownLinkDestination(href);  try {    const parsed = new URL(normalizeNativeMarkdownUrl(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 target = parseMarkdownFileLink(normalized);  if (target) {    return {      kind: "file",      href: normalized,      icon: resolveMarkdownFileIcon(target.path),      label: fileBasename(formatFilePathPosition(target)),      path: target.path,      ...(target.line ? { line: target.line } : {}),      ...(target.column ? { column: target.column } : {}),    };  }   return {    kind: "link",    href: /^(?:mailto|tel):/i.test(normalized) ? normalized : null,  };} /** Backticks become file references only when the shared path heuristic recognizes the whole span. */export function resolveMarkdownInlineCodePresentation(  content: string,): Extract<MarkdownLinkPresentation, { readonly kind: "file" }> | null {  const candidate = inlineCodeFilePathCandidate(content);  if (candidate === null) return null;  const presentation = resolveMarkdownLinkPresentation(candidate);  return presentation.kind === "file" ? presentation : null;}