~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
269 lines · 6.3 KB Raw History
// 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)}%`;