/** * ───────────────────────────────────────────── * SPB Git — Personal Git Platform * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : src/stats/languages.mjs * Purpose : Linguist-style language detection, byte percentages, colors * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ /** * Curated subset of GitHub Linguist languages. * type: 'programming' | 'markup' | 'data' | 'prose' * Only programming + markup count toward the language bar (Linguist behavior). */ export const LANGUAGES = Object.freeze({ JavaScript: { color: '#f1e05a', type: 'programming', extensions: ['.js', '.mjs', '.cjs', '.jsx'] }, TypeScript: { color: '#3178c6', type: 'programming', extensions: ['.ts', '.tsx', '.mts', '.cts'] }, Python: { color: '#3572A5', type: 'programming', extensions: ['.py', '.pyw', '.pyi'] }, Go: { color: '#00ADD8', type: 'programming', extensions: ['.go'] }, Rust: { color: '#dea584', type: 'programming', extensions: ['.rs'] }, C: { color: '#555555', type: 'programming', extensions: ['.c', '.h'] }, 'C++': { color: '#f34b7d', type: 'programming', extensions: ['.cpp', '.cc', '.cxx', '.hpp', '.hh', '.hxx'] }, 'C#': { color: '#178600', type: 'programming', extensions: ['.cs'] }, Java: { color: '#b07219', type: 'programming', extensions: ['.java'] }, Kotlin: { color: '#A97BFF', type: 'programming', extensions: ['.kt', '.kts'] }, Swift: { color: '#F05138', type: 'programming', extensions: ['.swift'] }, 'Objective-C': { color: '#438eff', type: 'programming', extensions: ['.m', '.mm'] }, Ruby: { color: '#701516', type: 'programming', extensions: ['.rb', '.rake', '.gemspec'] }, PHP: { color: '#4F5D95', type: 'programming', extensions: ['.php'] }, Shell: { color: '#89e051', type: 'programming', extensions: ['.sh', '.bash', '.zsh', '.fish'] }, PowerShell: { color: '#012456', type: 'programming', extensions: ['.ps1', '.psm1'] }, Perl: { color: '#0298c3', type: 'programming', extensions: ['.pl', '.pm'] }, Lua: { color: '#000080', type: 'programming', extensions: ['.lua'] }, R: { color: '#198CE7', type: 'programming', extensions: ['.r', '.R'] }, Julia: { color: '#a270ba', type: 'programming', extensions: ['.jl'] }, Scala: { color: '#c22d40', type: 'programming', extensions: ['.scala', '.sc'] }, Haskell: { color: '#5e5086', type: 'programming', extensions: ['.hs'] }, Elixir: { color: '#6e4a7e', type: 'programming', extensions: ['.ex', '.exs'] }, Erlang: { color: '#B83998', type: 'programming', extensions: ['.erl', '.hrl'] }, Clojure: { color: '#db5855', type: 'programming', extensions: ['.clj', '.cljs', '.cljc'] }, Dart: { color: '#00B4AB', type: 'programming', extensions: ['.dart'] }, Zig: { color: '#ec915c', type: 'programming', extensions: ['.zig'] }, Nim: { color: '#ffc200', type: 'programming', extensions: ['.nim'] }, OCaml: { color: '#ef7a08', type: 'programming', extensions: ['.ml', '.mli'] }, 'F#': { color: '#b845fc', type: 'programming', extensions: ['.fs', '.fsx'] }, Fortran: { color: '#4d41b1', type: 'programming', extensions: ['.f90', '.f95', '.f03', '.f'] }, MATLAB: { color: '#e16737', type: 'programming', extensions: ['.mat'] }, Solidity: { color: '#AA6746', type: 'programming', extensions: ['.sol'] }, Assembly: { color: '#6E4C13', type: 'programming', extensions: ['.asm', '.s'] }, Cuda: { color: '#3A4E3A', type: 'programming', extensions: ['.cu', '.cuh'] }, Vue: { color: '#41b883', type: 'programming', extensions: ['.vue'] }, Svelte: { color: '#ff3e00', type: 'programming', extensions: ['.svelte'] }, HTML: { color: '#e34c26', type: 'markup', extensions: ['.html', '.htm', '.xhtml'] }, CSS: { color: '#663399', type: 'markup', extensions: ['.css'] }, SCSS: { color: '#c6538c', type: 'markup', extensions: ['.scss', '.sass'] }, Less: { color: '#1d365d', type: 'markup', extensions: ['.less'] }, XML: { color: '#0060ac', type: 'data', extensions: ['.xml', '.xsl', '.plist'] }, SVG: { color: '#ff9900', type: 'data', extensions: ['.svg'] }, JSON: { color: '#292929', type: 'data', extensions: ['.json', '.jsonc'] }, YAML: { color: '#cb171e', type: 'data', extensions: ['.yml', '.yaml'] }, TOML: { color: '#9c4221', type: 'data', extensions: ['.toml'] }, SQL: { color: '#e38c00', type: 'programming', extensions: ['.sql'] }, GraphQL: { color: '#e10098', type: 'data', extensions: ['.graphql', '.gql'] }, Markdown: { color: '#083fa1', type: 'prose', extensions: ['.md', '.markdown', '.mdx'] }, reStructuredText: { color: '#141414', type: 'prose', extensions: ['.rst'] }, TeX: { color: '#3D6117', type: 'markup', extensions: ['.tex', '.sty', '.bib'] }, 'Jupyter Notebook': { color: '#DA5B0B', type: 'programming', extensions: ['.ipynb'] }, Dockerfile: { color: '#384d54', type: 'programming', extensions: ['.dockerfile'], filenames: ['Dockerfile', 'Containerfile'] }, Makefile: { color: '#427819', type: 'programming', extensions: ['.mk'], filenames: ['Makefile', 'GNUmakefile', 'makefile'] }, CMake: { color: '#DA3434', type: 'programming', extensions: ['.cmake'], filenames: ['CMakeLists.txt'] }, Nix: { color: '#7e7eff', type: 'programming', extensions: ['.nix'] }, Terraform: { color: '#844FBA', type: 'programming', extensions: ['.tf', '.tfvars'] }, Groovy: { color: '#4298b8', type: 'programming', extensions: ['.groovy', '.gradle'] }, 'Vim Script': { color: '#199f4b', type: 'programming', extensions: ['.vim'], filenames: ['.vimrc'] }, Nunjucks: { color: '#3d8137', type: 'markup', extensions: ['.njk'] }, EJS: { color: '#a91e50', type: 'markup', extensions: ['.ejs'] }, Handlebars: { color: '#f7931e', type: 'markup', extensions: ['.hbs'] }, Astro: { color: '#ff5a03', type: 'programming', extensions: ['.astro'] }, WebAssembly: { color: '#04133b', type: 'programming', extensions: ['.wat', '.wasm'] }, Protobuf: { color: '#4a575d', type: 'data', extensions: ['.proto'] }, }); /** Path patterns Linguist treats as vendored — excluded from stats. */ const VENDORED_RE = [ /(^|\/)node_modules\//, /(^|\/)vendor\//, /(^|\/)third[_-]party\//, /(^|\/)dist\//, /(^|\/)build\//, /(^|\/)\.git\//, /(^|\/)coverage\//, /\.min\.(js|css)$/, /(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|poetry\.lock|uv\.lock|composer\.lock|Gemfile\.lock)$/, /\.(woff2?|ttf|otf|eot|png|jpe?g|gif|ico|webp|mp4|mp3|zip|gz|zst|pdf|bin|model|pt|onnx|safetensors)$/i, ]; const extIndex = new Map(); const filenameIndex = new Map(); for (const [lang, def] of Object.entries(LANGUAGES)) { for (const ext of def.extensions ?? []) { if (!extIndex.has(ext)) extIndex.set(ext, lang); } for (const fn of def.filenames ?? []) filenameIndex.set(fn, lang); } /** * Detect the language of a single path. * @param {string} path * @returns {string|null} */ export function detectLanguage(path) { const base = path.split('/').pop() ?? path; if (filenameIndex.has(base)) return filenameIndex.get(base); const dot = base.lastIndexOf('.'); if (dot <= 0) return null; const ext = base.slice(dot).toLowerCase(); // .R is case-sensitive in the table; try raw first, then lowercase. return extIndex.get(base.slice(dot)) ?? extIndex.get(ext) ?? null; } /** * Compute language byte totals + percentages for a file listing. * @param {Array<{path: string, size: number}>} files * @returns {{languages: Array<{name: string, bytes: number, percent: number, color: string}>, totalBytes: number}} */ export function computeLanguages(files) { const totals = new Map(); for (const file of files) { if (VENDORED_RE.some((re) => re.test(file.path))) continue; const lang = detectLanguage(file.path); if (!lang) continue; const def = LANGUAGES[lang]; if (def.type !== 'programming' && def.type !== 'markup') continue; totals.set(lang, (totals.get(lang) ?? 0) + (file.size || 0)); } const totalBytes = [...totals.values()].reduce((a, b) => a + b, 0); const languages = [...totals.entries()] .filter(([, bytes]) => bytes > 0) .sort((a, b) => b[1] - a[1]) .map(([name, bytes]) => ({ name, bytes, percent: totalBytes === 0 ? 0 : Math.round((bytes / totalBytes) * 1000) / 10, color: LANGUAGES[name].color, })); return { languages, totalBytes }; } /** * @param {string} name * @returns {string} linguist color (fallback grey) */ export function languageColor(name) { return LANGUAGES[name]?.color ?? '#8b93a3'; }