chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// 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<string, string>} */
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<string, string>} */
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<string, string>} */
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<string, number>} */
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)}%`;