SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
8.6 KB · 159 lines javascript
Raw Blame History
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