// What a repository is written in, weighed by the bytes of its files. // // The rules are the ones GitHub's linguist applies, so a reader arriving from // a repository page finds the same answer. Only programming and markup files // count: a lockfile, a README and a CI config carry no language, which is // what keeps a repository from reading as mostly JSON. Vendored and generated // paths carry none either. The colours are linguist's own. /** Language name by lowercase extension. */ /** @type {Record} */ const BY_EXTENSION = { astro: 'Astro', bash: 'Shell', bat: 'Batchfile', c: 'C', 'c++': 'C++', cc: 'C++', cjs: 'JavaScript', clj: 'Clojure', cljs: 'Clojure', cmake: 'CMake', cmd: 'Batchfile', coffee: 'CoffeeScript', cpp: 'C++', cr: 'Crystal', cs: 'C#', css: 'CSS', cts: 'TypeScript', cxx: 'C++', dart: 'Dart', dockerfile: 'Dockerfile', ejs: 'EJS', el: 'Emacs Lisp', elm: 'Elm', erl: 'Erlang', ex: 'Elixir', exs: 'Elixir', fs: 'F#', fsx: 'F#', gleam: 'Gleam', go: 'Go', gql: 'GraphQL', graphql: 'GraphQL', groovy: 'Groovy', h: 'C', handlebars: 'Handlebars', hbs: 'Handlebars', hcl: 'HCL', hpp: 'C++', hrl: 'Erlang', hs: 'Haskell', htm: 'HTML', html: 'HTML', ipynb: 'Jupyter Notebook', java: 'Java', jl: 'Julia', js: 'JavaScript', jsx: 'JavaScript', just: 'Just', kt: 'Kotlin', kts: 'Kotlin', less: 'Less', lisp: 'Common Lisp', lua: 'Lua', m: 'Objective-C', mdx: 'MDX', mjs: 'JavaScript', ml: 'OCaml', mli: 'OCaml', mts: 'TypeScript', nim: 'Nim', nix: 'Nix', php: 'PHP', pl: 'Perl', pm: 'Perl', ps1: 'PowerShell', psm1: 'PowerShell', pug: 'Pug', py: 'Python', pyi: 'Python', r: 'R', rb: 'Ruby', rkt: 'Racket', rs: 'Rust', s: 'Assembly', sass: 'Sass', sbt: 'Scala', scala: 'Scala', scm: 'Scheme', scss: 'SCSS', sh: 'Shell', sol: 'Solidity', styl: 'Stylus', svelte: 'Svelte', swift: 'Swift', tex: 'TeX', tf: 'HCL', tfvars: 'HCL', ts: 'TypeScript', tsx: 'TypeScript', twig: 'Twig', vue: 'Vue', zig: 'Zig', zsh: 'Shell', }; /** Language name by lowercase filename, for the files with no extension. */ /** @type {Record} */ const BY_NAME = { cmakelists: 'CMake', containerfile: 'Dockerfile', dockerfile: 'Dockerfile', gnumakefile: 'Makefile', justfile: 'Just', makefile: 'Makefile', rakefile: 'Ruby', }; /** Each language's colour, as linguist publishes it. */ /** @type {Record} */ const COLORS = { Assembly: '#6e4c13', Astro: '#ff5a03', Batchfile: '#c1f12e', C: '#555555', 'C#': '#7355dd', 'C++': '#f34b7d', CMake: '#da3434', Clojure: '#db5855', CoffeeScript: '#244776', 'Common Lisp': '#3fb68b', Crystal: '#000100', CSS: '#663399', Dart: '#00b4ab', Dockerfile: '#384d54', EJS: '#a91e50', Elixir: '#6e4a7e', Elm: '#60b5cc', 'Emacs Lisp': '#c065db', Erlang: '#b83998', 'F#': '#b845fc', Gleam: '#ffaff3', Go: '#00add8', GraphQL: '#e10098', Groovy: '#4298b8', Handlebars: '#f7931e', Haskell: '#5e5086', HCL: '#844fba', HTML: '#e34c26', Java: '#b07219', JavaScript: '#f1e05a', Julia: '#a270ba', 'Jupyter Notebook': '#da5b0b', Just: '#384d54', Kotlin: '#a97bff', Less: '#1d365d', Lua: '#000080', Makefile: '#427819', MDX: '#fcb32c', Nim: '#ffc200', Nix: '#7e7eff', OCaml: '#ef7a08', 'Objective-C': '#438eff', Perl: '#0298c3', PHP: '#4f5d95', PowerShell: '#012456', Pug: '#a86454', Python: '#3572a5', R: '#198ce7', Racket: '#3c5caa', Ruby: '#701516', Rust: '#dea584', Sass: '#a53b70', Scala: '#c22d40', Scheme: '#1e4aec', SCSS: '#c6538c', Shell: '#89e051', Solidity: '#aa6746', Stylus: '#ff6347', Svelte: '#ff3e00', Swift: '#f05138', TeX: '#3d6117', Twig: '#c1d026', TypeScript: '#3178c6', Vue: '#41b883', Zig: '#ec915c', }; /** The name for everything past the languages the bar names one by one. */ const OTHER = 'Other'; /** Grey, the way linguist paints a language it has no colour for. */ const OTHER_COLOR = '#8b949e'; /** Paths whose bytes belong to somebody else, or to a build. */ const IGNORED_PATH = /(^|\/)(node_modules|bower_components|vendors?|third_party|dist|\.yarn|\.pnpm)\/|\.min\.(js|css)$/; /** How many languages the bar names before the rest become Other. */ const NAMED_LIMIT = 6; /** * The language a path is written in, or '' where it carries none. * @param {string} path * @returns {string} */ export function languageOf(path) { if (IGNORED_PATH.test(path)) return ''; const name = path.slice(path.lastIndexOf('/') + 1).toLowerCase(); const dot = name.lastIndexOf('.'); if (dot > 0) { const language = BY_EXTENSION[name.slice(dot + 1)]; if (language) return language; } // A name like Makefile.am reads by its first segment, which is what names // the language for the whole family. return BY_NAME[name] ?? BY_NAME[name.split('.')[0]] ?? ''; } /** * @typedef {Object} LanguageShare * @property {string} name * @property {string} color * @property {number} bytes * @property {number} share - percent of the languaged bytes, 0 to 100 */ /** * The languages of a file list, largest first. Everything past the sixth is * added together as Other, the way the repository page shows a long tail. * @param {Array<{path: string, size: number}>} files * @returns {LanguageShare[]} */ export function languageShares(files) { /** @type {Map} */ const totals = new Map(); let counted = 0; for (const file of files) { const name = languageOf(file.path); if (!name) continue; totals.set(name, (totals.get(name) ?? 0) + file.size); counted += file.size; } if (counted === 0) return []; const ranked = [...totals].sort((a, b) => b[1] - a[1]); const named = ranked.slice(0, NAMED_LIMIT); const rest = ranked .slice(NAMED_LIMIT) .reduce((sum, [, size]) => sum + size, 0); if (rest > 0) named.push([OTHER, rest]); return named.map(([name, size]) => ({ name, color: COLORS[name] ?? OTHER_COLOR, bytes: size, share: (size / counted) * 100, })); } /** * A share as the bar prints it. A language too small to round to a tenth * still reads as the smallest share the bar shows, because a segment is * drawn for it. * @param {number} share * @returns {string} */ export const percent = (share) => `${share < 0.05 ? '<0.1' : share.toFixed(1)}%`;