import { attributesFor } from "./gitattributes"; import type { AttributeRule } from "./gitattributes"; /** * Language breakdown in the spirit of GitHub Linguist: files are classified by name and extension, only * programming and markup languages count (data and prose such as JSON, YAML and Markdown don't), and vendored, * generated and documentation paths are left out. `.gitattributes` `linguist-*` attributes override all of it. */ type Language = { color: string; extensions?: string[]; filenames?: string[] }; const LANGUAGES: Record = { TypeScript: { color: "#3178c6", extensions: ["ts", "tsx", "mts", "cts"] }, JavaScript: { color: "#f1e05a", extensions: ["js", "jsx", "mjs", "cjs"] }, Python: { color: "#3572A5", extensions: ["py", "pyi", "pyw"] }, Go: { color: "#00ADD8", extensions: ["go"] }, Rust: { color: "#dea584", extensions: ["rs"] }, Java: { color: "#b07219", extensions: ["java"] }, Kotlin: { color: "#A97BFF", extensions: ["kt", "kts"] }, Swift: { color: "#F05138", extensions: ["swift"] }, C: { color: "#555555", extensions: ["c", "h"] }, "C++": { color: "#f34b7d", extensions: ["cpp", "cc", "cxx", "c++", "hpp", "hh", "hxx", "h++", "ipp"] }, "C#": { color: "#178600", extensions: ["cs", "csx"] }, "Objective-C": { color: "#438eff", extensions: ["m"] }, "Objective-C++": { color: "#6866fb", extensions: ["mm"] }, Ruby: { color: "#701516", extensions: ["rb", "rake", "gemspec"], filenames: ["Gemfile", "Rakefile"] }, PHP: { color: "#4F5D95", extensions: ["php", "phtml"] }, Shell: { color: "#89e051", extensions: ["sh", "bash", "zsh", "ksh", "fish"] }, PowerShell: { color: "#012456", extensions: ["ps1", "psm1", "psd1"] }, Batchfile: { color: "#C1F12E", extensions: ["bat", "cmd"] }, HTML: { color: "#e34c26", extensions: ["html", "htm", "xhtml"] }, CSS: { color: "#663399", extensions: ["css"] }, SCSS: { color: "#c6538c", extensions: ["scss"] }, Sass: { color: "#a53b70", extensions: ["sass"] }, Less: { color: "#1d365d", extensions: ["less"] }, Vue: { color: "#41b883", extensions: ["vue"] }, Svelte: { color: "#ff3e00", extensions: ["svelte"] }, Astro: { color: "#ff5a03", extensions: ["astro"] }, MDX: { color: "#fcb32c", extensions: ["mdx"] }, Handlebars: { color: "#f7931e", extensions: ["hbs", "handlebars"] }, Dart: { color: "#00B4AB", extensions: ["dart"] }, Elixir: { color: "#6e4a7e", extensions: ["ex", "exs"] }, Erlang: { color: "#B83998", extensions: ["erl", "hrl"] }, Gleam: { color: "#ffaff3", extensions: ["gleam"] }, Haskell: { color: "#5e5086", extensions: ["hs", "lhs"] }, OCaml: { color: "#ef7a08", extensions: ["ml", "mli"] }, "F#": { color: "#b845fc", extensions: ["fs", "fsi", "fsx"] }, Scala: { color: "#c22d40", extensions: ["scala", "sc"] }, Clojure: { color: "#db5855", extensions: ["clj", "cljs", "cljc"] }, Groovy: { color: "#4298b8", extensions: ["groovy", "gradle"] }, Lua: { color: "#000080", extensions: ["lua"] }, Perl: { color: "#0298c3", extensions: ["pl", "pm"] }, R: { color: "#198CE7", extensions: ["r"] }, Julia: { color: "#a270ba", extensions: ["jl"] }, Zig: { color: "#ec915c", extensions: ["zig"] }, Nim: { color: "#ffc200", extensions: ["nim"] }, Crystal: { color: "#000100", extensions: ["cr"] }, Elm: { color: "#60B5CC", extensions: ["elm"] }, Nix: { color: "#7e7eff", extensions: ["nix"] }, HCL: { color: "#844FBA", extensions: ["tf", "hcl", "tfvars"] }, Dockerfile: { color: "#384d54", extensions: ["dockerfile"], filenames: ["Dockerfile", "Containerfile"] }, Makefile: { color: "#427819", extensions: ["mk", "mak"], filenames: ["Makefile", "makefile", "GNUmakefile"] }, CMake: { color: "#DA3434", extensions: ["cmake"], filenames: ["CMakeLists.txt"] }, Solidity: { color: "#AA6746", extensions: ["sol"] }, Assembly: { color: "#6E4C13", extensions: ["asm", "s", "nasm"] }, WebAssembly: { color: "#04133b", extensions: ["wat", "wast"] }, Cuda: { color: "#3A4E3A", extensions: ["cu", "cuh"] }, GLSL: { color: "#5686a5", extensions: ["glsl", "vert", "frag", "geom", "comp"] }, "Vim Script": { color: "#199f4b", extensions: ["vim"], filenames: [".vimrc"] }, "Emacs Lisp": { color: "#c065db", extensions: ["el"] }, "Common Lisp": { color: "#3fb68b", extensions: ["lisp", "lsp", "cl"] }, Scheme: { color: "#1e4aec", extensions: ["scm", "ss"] }, Racket: { color: "#3c5caa", extensions: ["rkt"] }, Fortran: { color: "#4d41b1", extensions: ["f", "f90", "f95", "f03", "for"] }, Pascal: { color: "#E3F171", extensions: ["pas", "pp"] }, D: { color: "#ba595e", extensions: ["d"] }, "Visual Basic .NET": { color: "#945db7", extensions: ["vb"] }, SystemVerilog: { color: "#DAE1C2", extensions: ["sv", "svh"] }, VHDL: { color: "#adb2cb", extensions: ["vhd", "vhdl"] }, QML: { color: "#44a51c", extensions: ["qml"] }, Tcl: { color: "#e4cc98", extensions: ["tcl"] }, TeX: { color: "#3D6117", extensions: ["tex", "sty", "cls"] }, "Jupyter Notebook": { color: "#DA5B0B", extensions: ["ipynb"] }, }; const OTHER_COLOR = "#8b8b8b"; /** Shares below this (0.1%) fold into "Other". */ const MIN_SHARE = 0.001; const BY_EXTENSION = new Map(); const BY_FILENAME = new Map(); const BY_LOWER_NAME = new Map(); for (const [name, language] of Object.entries(LANGUAGES)) { BY_LOWER_NAME.set(name.toLowerCase(), name); for (const extension of language.extensions ?? []) BY_EXTENSION.set(extension, name); for (const filename of language.filenames ?? []) BY_FILENAME.set(filename, name); } /** Linguist's default vendored, generated and documentation paths, trimmed to the common ones. */ const EXCLUDED = [ /(^|\/)(node_modules|bower_components|vendor|vendors|third[_-]party|Pods|Carthage|\.yarn|\.venv|venv|__pycache__)\//, /(^|\/)dist\//, /(^|\/)(docs?|[Dd]ocumentation|[Ee]xamples?)\//, /(^|\/)\.(git|github|vscode|idea)\//, /[.-]min\.(js|css)$/, /\.(pb\.go|pb\.cc|pb\.h|_pb2\.py|g\.dart|freezed\.dart)$/, /(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|Cargo\.lock|go\.sum)$/, ]; export function languageColor(name: string): string { return LANGUAGES[name]?.color ?? OTHER_COLOR; } function canonical(name: string): string { return BY_LOWER_NAME.get(name.toLowerCase()) ?? name; } /** The language `path` counts toward, or null when it doesn't count (unknown, data, vendored, ...). */ export function languageOf(path: string, rules: readonly AttributeRule[] = []): string | null { const attributes = rules.length ? attributesFor(rules, path) : null; const flag = (name: string) => attributes?.get(name); const detectable = flag("linguist-detectable"); if (detectable === false || detectable === "false") return null; for (const name of ["linguist-vendored", "linguist-generated", "linguist-documentation"]) { const value = flag(name); if (value === true || value === "true") return null; } const forced = ["linguist-vendored", "linguist-generated", "linguist-documentation"].some((name) => { const value = flag(name); return value === false || value === "false"; }); if (!forced && EXCLUDED.some((pattern) => pattern.test(path))) return null; const override = flag("linguist-language"); if (typeof override === "string" && override) return canonical(override.replaceAll("-", " ")); const basename = path.slice(path.lastIndexOf("/") + 1); const byName = BY_FILENAME.get(basename); if (byName) return byName; const dot = basename.lastIndexOf("."); if (dot <= 0) return null; return BY_EXTENSION.get(basename.slice(dot + 1).toLowerCase()) ?? null; } export type LanguageShare = { name: string; bytes: number }; /** Largest first; languages under 0.1% of the total fold into "Other", as the bar would not show them. */ export function summarizeLanguages(bytesByLanguage: ReadonlyMap, top = 12): LanguageShare[] { const sorted = [...bytesByLanguage.entries()] .filter(([, bytes]) => bytes > 0) .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])); const total = sorted.reduce((sum, [, bytes]) => sum + bytes, 0); const kept: LanguageShare[] = []; let other = 0; for (const [name, bytes] of sorted) { if (kept.length < top && bytes / total >= MIN_SHARE) kept.push({ name, bytes }); else other += bytes; } if (other > 0) kept.push({ name: "Other", bytes: other }); return kept; } /** The stored JSON column (written by summarizeLanguages), tolerating anything malformed. */ export function parseLanguages(stored: string | null | undefined): LanguageShare[] { if (!stored) return []; try { const parsed: unknown = JSON.parse(stored); if (!Array.isArray(parsed)) return []; return parsed.filter( (entry): entry is LanguageShare => typeof entry === "object" && entry !== null && typeof (entry as LanguageShare).name === "string" && typeof (entry as LanguageShare).bytes === "number", ); } catch { return []; } }