spb/spbgit Public MIT
SPB Git — the platform hosting itself
JavaScript 73.9%
CSS 11.7%
Nunjucks 11.6%
Shell 2.7%
1/**2 * ─────────────────────────────────────────────3 * SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : src/stats/languages.mjs8 * Purpose : Linguist-style language detection, byte percentages, colors9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213/**14 * Curated subset of GitHub Linguist languages.15 * type: 'programming' | 'markup' | 'data' | 'prose'16 * Only programming + markup count toward the language bar (Linguist behavior).17 */18export const LANGUAGES = Object.freeze({19 JavaScript: { color: '#f1e05a', type: 'programming', extensions: ['.js', '.mjs', '.cjs', '.jsx'] },20 TypeScript: { color: '#3178c6', type: 'programming', extensions: ['.ts', '.tsx', '.mts', '.cts'] },21 Python: { color: '#3572A5', type: 'programming', extensions: ['.py', '.pyw', '.pyi'] },22 Go: { color: '#00ADD8', type: 'programming', extensions: ['.go'] },23 Rust: { color: '#dea584', type: 'programming', extensions: ['.rs'] },24 C: { color: '#555555', type: 'programming', extensions: ['.c', '.h'] },25 'C++': { color: '#f34b7d', type: 'programming', extensions: ['.cpp', '.cc', '.cxx', '.hpp', '.hh', '.hxx'] },26 'C#': { color: '#178600', type: 'programming', extensions: ['.cs'] },27 Java: { color: '#b07219', type: 'programming', extensions: ['.java'] },28 Kotlin: { color: '#A97BFF', type: 'programming', extensions: ['.kt', '.kts'] },29 Swift: { color: '#F05138', type: 'programming', extensions: ['.swift'] },30 'Objective-C': { color: '#438eff', type: 'programming', extensions: ['.m', '.mm'] },31 Ruby: { color: '#701516', type: 'programming', extensions: ['.rb', '.rake', '.gemspec'] },32 PHP: { color: '#4F5D95', type: 'programming', extensions: ['.php'] },33 Shell: { color: '#89e051', type: 'programming', extensions: ['.sh', '.bash', '.zsh', '.fish'] },34 PowerShell: { color: '#012456', type: 'programming', extensions: ['.ps1', '.psm1'] },35 Perl: { color: '#0298c3', type: 'programming', extensions: ['.pl', '.pm'] },36 Lua: { color: '#000080', type: 'programming', extensions: ['.lua'] },37 R: { color: '#198CE7', type: 'programming', extensions: ['.r', '.R'] },38 Julia: { color: '#a270ba', type: 'programming', extensions: ['.jl'] },39 Scala: { color: '#c22d40', type: 'programming', extensions: ['.scala', '.sc'] },40 Haskell: { color: '#5e5086', type: 'programming', extensions: ['.hs'] },41 Elixir: { color: '#6e4a7e', type: 'programming', extensions: ['.ex', '.exs'] },42 Erlang: { color: '#B83998', type: 'programming', extensions: ['.erl', '.hrl'] },43 Clojure: { color: '#db5855', type: 'programming', extensions: ['.clj', '.cljs', '.cljc'] },44 Dart: { color: '#00B4AB', type: 'programming', extensions: ['.dart'] },45 Zig: { color: '#ec915c', type: 'programming', extensions: ['.zig'] },46 Nim: { color: '#ffc200', type: 'programming', extensions: ['.nim'] },47 OCaml: { color: '#ef7a08', type: 'programming', extensions: ['.ml', '.mli'] },48 'F#': { color: '#b845fc', type: 'programming', extensions: ['.fs', '.fsx'] },49 Fortran: { color: '#4d41b1', type: 'programming', extensions: ['.f90', '.f95', '.f03', '.f'] },50 MATLAB: { color: '#e16737', type: 'programming', extensions: ['.mat'] },51 Solidity: { color: '#AA6746', type: 'programming', extensions: ['.sol'] },52 Assembly: { color: '#6E4C13', type: 'programming', extensions: ['.asm', '.s'] },53 Cuda: { color: '#3A4E3A', type: 'programming', extensions: ['.cu', '.cuh'] },54 Vue: { color: '#41b883', type: 'programming', extensions: ['.vue'] },55 Svelte: { color: '#ff3e00', type: 'programming', extensions: ['.svelte'] },56 HTML: { color: '#e34c26', type: 'markup', extensions: ['.html', '.htm', '.xhtml'] },57 CSS: { color: '#663399', type: 'markup', extensions: ['.css'] },58 SCSS: { color: '#c6538c', type: 'markup', extensions: ['.scss', '.sass'] },59 Less: { color: '#1d365d', type: 'markup', extensions: ['.less'] },60 XML: { color: '#0060ac', type: 'data', extensions: ['.xml', '.xsl', '.plist'] },61 SVG: { color: '#ff9900', type: 'data', extensions: ['.svg'] },62 JSON: { color: '#292929', type: 'data', extensions: ['.json', '.jsonc'] },63 YAML: { color: '#cb171e', type: 'data', extensions: ['.yml', '.yaml'] },64 TOML: { color: '#9c4221', type: 'data', extensions: ['.toml'] },65 SQL: { color: '#e38c00', type: 'programming', extensions: ['.sql'] },66 GraphQL: { color: '#e10098', type: 'data', extensions: ['.graphql', '.gql'] },67 Markdown: { color: '#083fa1', type: 'prose', extensions: ['.md', '.markdown', '.mdx'] },68 reStructuredText: { color: '#141414', type: 'prose', extensions: ['.rst'] },69 TeX: { color: '#3D6117', type: 'markup', extensions: ['.tex', '.sty', '.bib'] },70 'Jupyter Notebook': { color: '#DA5B0B', type: 'programming', extensions: ['.ipynb'] },71 Dockerfile: { color: '#384d54', type: 'programming', extensions: ['.dockerfile'], filenames: ['Dockerfile', 'Containerfile'] },72 Makefile: { color: '#427819', type: 'programming', extensions: ['.mk'], filenames: ['Makefile', 'GNUmakefile', 'makefile'] },73 CMake: { color: '#DA3434', type: 'programming', extensions: ['.cmake'], filenames: ['CMakeLists.txt'] },74 Nix: { color: '#7e7eff', type: 'programming', extensions: ['.nix'] },75 Terraform: { color: '#844FBA', type: 'programming', extensions: ['.tf', '.tfvars'] },76 Groovy: { color: '#4298b8', type: 'programming', extensions: ['.groovy', '.gradle'] },77 'Vim Script': { color: '#199f4b', type: 'programming', extensions: ['.vim'], filenames: ['.vimrc'] },78 Nunjucks: { color: '#3d8137', type: 'markup', extensions: ['.njk'] },79 EJS: { color: '#a91e50', type: 'markup', extensions: ['.ejs'] },80 Handlebars: { color: '#f7931e', type: 'markup', extensions: ['.hbs'] },81 Astro: { color: '#ff5a03', type: 'programming', extensions: ['.astro'] },82 WebAssembly: { color: '#04133b', type: 'programming', extensions: ['.wat', '.wasm'] },83 Protobuf: { color: '#4a575d', type: 'data', extensions: ['.proto'] },84});8586/** Path patterns Linguist treats as vendored — excluded from stats. */87const VENDORED_RE = [88 /(^|\/)node_modules\//,89 /(^|\/)vendor\//,90 /(^|\/)third[_-]party\//,91 /(^|\/)dist\//,92 /(^|\/)build\//,93 /(^|\/)\.git\//,94 /(^|\/)coverage\//,95 /\.min\.(js|css)$/,96 /(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|poetry\.lock|uv\.lock|composer\.lock|Gemfile\.lock)$/,97 /\.(woff2?|ttf|otf|eot|png|jpe?g|gif|ico|webp|mp4|mp3|zip|gz|zst|pdf|bin|model|pt|onnx|safetensors)$/i,98];99100const extIndex = new Map();101const filenameIndex = new Map();102for (const [lang, def] of Object.entries(LANGUAGES)) {103 for (const ext of def.extensions ?? []) {104 if (!extIndex.has(ext)) extIndex.set(ext, lang);105 }106 for (const fn of def.filenames ?? []) filenameIndex.set(fn, lang);107}108109/**110 * Detect the language of a single path.111 * @param {string} path112 * @returns {string|null}113 */114export function detectLanguage(path) {115 const base = path.split('/').pop() ?? path;116 if (filenameIndex.has(base)) return filenameIndex.get(base);117 const dot = base.lastIndexOf('.');118 if (dot <= 0) return null;119 const ext = base.slice(dot).toLowerCase();120 // .R is case-sensitive in the table; try raw first, then lowercase.121 return extIndex.get(base.slice(dot)) ?? extIndex.get(ext) ?? null;122}123124/**125 * Compute language byte totals + percentages for a file listing.126 * @param {Array<{path: string, size: number}>} files127 * @returns {{languages: Array<{name: string, bytes: number, percent: number, color: string}>, totalBytes: number}}128 */129export function computeLanguages(files) {130 const totals = new Map();131 for (const file of files) {132 if (VENDORED_RE.some((re) => re.test(file.path))) continue;133 const lang = detectLanguage(file.path);134 if (!lang) continue;135 const def = LANGUAGES[lang];136 if (def.type !== 'programming' && def.type !== 'markup') continue;137 totals.set(lang, (totals.get(lang) ?? 0) + (file.size || 0));138 }139 const totalBytes = [...totals.values()].reduce((a, b) => a + b, 0);140 const languages = [...totals.entries()]141 .filter(([, bytes]) => bytes > 0)142 .sort((a, b) => b[1] - a[1])143 .map(([name, bytes]) => ({144 name,145 bytes,146 percent: totalBytes === 0 ? 0 : Math.round((bytes / totalBytes) * 1000) / 10,147 color: LANGUAGES[name].color,148 }));149 return { languages, totalBytes };150}151152/**153 * @param {string} name154 * @returns {string} linguist color (fallback grey)155 */156export function languageColor(name) {157 return LANGUAGES[name]?.color ?? '#8b93a3';158}159