« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/util')
-rw-r--r--src/util/cli.js70
-rw-r--r--src/util/colors.js4
-rw-r--r--src/util/find.js62
-rw-r--r--src/util/html.js60
-rw-r--r--src/util/io.js10
-rw-r--r--src/util/link.js94
-rw-r--r--src/util/magic-constants.js8
-rw-r--r--src/util/node-utils.js10
-rw-r--r--src/util/replacer.js84
-rw-r--r--src/util/serialize.js20
-rw-r--r--src/util/sugar.js110
-rw-r--r--src/util/urls.js36
-rw-r--r--src/util/wiki-data.js84
13 files changed, 326 insertions, 326 deletions
diff --git a/src/util/cli.js b/src/util/cli.js
index 159d526b..d28ef40a 100644
--- a/src/util/cli.js
+++ b/src/util/cli.js
@@ -1,17 +1,17 @@
-// @format
-//
+/** @format */
+
 // Utility functions for CLI- and de8ugging-rel8ted stuff.
 //
 // A 8unch of these depend on process.stdout 8eing availa8le, so they won't
 // work within the 8rowser.
 
-const { process } = globalThis;
+const {process} = globalThis;
 
 export const ENABLE_COLOR =
   process &&
-  ((process.env.CLICOLOR_FORCE && process.env.CLICOLOR_FORCE === "1") ??
+  ((process.env.CLICOLOR_FORCE && process.env.CLICOLOR_FORCE === '1') ??
     (process.env.CLICOLOR &&
-      process.env.CLICOLOR === "1" &&
+      process.env.CLICOLOR === '1' &&
       process.stdout.hasColors &&
       process.stdout.hasColors()) ??
     (process.stdout.hasColors ? process.stdout.hasColors() : true));
@@ -20,17 +20,17 @@ const C = (n) =>
   ENABLE_COLOR ? (text) => `\x1b[${n}m${text}\x1b[0m` : (text) => text;
 
 export const color = {
-  bright: C("1"),
-  dim: C("2"),
-  normal: C("22"),
-  black: C("30"),
-  red: C("31"),
-  green: C("32"),
-  yellow: C("33"),
-  blue: C("34"),
-  magenta: C("35"),
-  cyan: C("36"),
-  white: C("37"),
+  bright: C('1'),
+  dim: C('2'),
+  normal: C('22'),
+  black: C('30'),
+  red: C('31'),
+  green: C('32'),
+  yellow: C('33'),
+  blue: C('34'),
+  magenta: C('35'),
+  cyan: C('36'),
+  white: C('37'),
 };
 
 const logColor =
@@ -51,7 +51,7 @@ const logColor =
       }
     }
     wc(`\x1b[0m`);
-    w("\n");
+    w('\n');
   };
 
 export const logInfo = logColor(2);
@@ -105,9 +105,9 @@ export async function parseOptions(options, optionDescriptorMap) {
   const result = Object.create(null);
   for (let i = 0; i < options.length; i++) {
     const option = options[i];
-    if (option.startsWith("--")) {
+    if (option.startsWith('--')) {
       // --x can be a flag or expect a value or series of values
-      let name = option.slice(2).split("=")[0]; // '--x'.split('=') = ['--x']
+      let name = option.slice(2).split('=')[0]; // '--x'.split('=') = ['--x']
       let descriptor = optionDescriptorMap[name];
       if (!descriptor) {
         if (handleUnknown) {
@@ -122,13 +122,13 @@ export async function parseOptions(options, optionDescriptorMap) {
         name = descriptor.alias;
         descriptor = optionDescriptorMap[name];
       }
-      if (descriptor.type === "flag") {
+      if (descriptor.type === 'flag') {
         result[name] = true;
-      } else if (descriptor.type === "value") {
-        let value = option.slice(2).split("=")[1];
+      } else if (descriptor.type === 'value') {
+        let value = option.slice(2).split('=')[1];
         if (!value) {
           value = options[++i];
-          if (!value || value.startsWith("-")) {
+          if (!value || value.startsWith('-')) {
             value = null;
           }
         }
@@ -137,14 +137,14 @@ export async function parseOptions(options, optionDescriptorMap) {
           process.exit(1);
         }
         result[name] = value;
-      } else if (descriptor.type === "series") {
-        if (!options.slice(i).includes(";")) {
+      } else if (descriptor.type === 'series') {
+        if (!options.slice(i).includes(';')) {
           console.error(
             `Expected a series of values concluding with ; (\\;) for --${name}`
           );
           process.exit(1);
         }
-        const endIndex = i + options.slice(i).indexOf(";");
+        const endIndex = i + options.slice(i).indexOf(';');
         result[name] = options.slice(i + 1, endIndex);
         i = endIndex;
       }
@@ -155,7 +155,7 @@ export async function parseOptions(options, optionDescriptorMap) {
           process.exit(1);
         }
       }
-    } else if (option.startsWith("-")) {
+    } else if (option.startsWith('-')) {
       // mtui doesn't use any -x=y or -x y format optionuments
       // -x will always just be a flag
       let name = option.slice(1);
@@ -173,7 +173,7 @@ export async function parseOptions(options, optionDescriptorMap) {
         name = descriptor.alias;
         descriptor = optionDescriptorMap[name];
       }
-      if (descriptor.type === "flag") {
+      if (descriptor.type === 'flag') {
         result[name] = true;
       } else {
         console.error(`Use --${name} (value) to specify ${name}`);
@@ -191,7 +191,7 @@ export const handleUnknown = Symbol();
 
 export function decorateTime(arg1, arg2) {
   const [id, functionToBeWrapped] =
-    typeof arg1 === "string" || typeof arg1 === "symbol"
+    typeof arg1 === 'string' || typeof arg1 === 'symbol'
       ? [arg1, arg2]
       : [Symbol(arg1.name), arg1];
 
@@ -202,7 +202,7 @@ export function decorateTime(arg1, arg2) {
     displayTime() {
       const averageTime = meta.timeSpent / meta.timesCalled;
       console.log(
-        `\x1b[1m${typeof id === "symbol" ? id.description : id}(...):\x1b[0m ${
+        `\x1b[1m${typeof id === 'symbol' ? id.description : id}(...):\x1b[0m ${
           meta.timeSpent
         } ms / ${meta.timesCalled} calls \x1b[2m(avg: ${averageTime} ms)\x1b[0m`
       );
@@ -236,7 +236,7 @@ decorateTime.displayTime = function () {
   ];
 
   if (keys.length) {
-    console.log(`\x1b[1mdecorateTime results: ` + "-".repeat(40) + "\x1b[0m");
+    console.log(`\x1b[1mdecorateTime results: ` + '-'.repeat(40) + '\x1b[0m');
     for (const key of keys) {
       map[key].displayTime();
     }
@@ -249,7 +249,7 @@ export function progressPromiseAll(msgOrMsgFn, array) {
   }
 
   const msgFn =
-    typeof msgOrMsgFn === "function" ? msgOrMsgFn : () => msgOrMsgFn;
+    typeof msgOrMsgFn === 'function' ? msgOrMsgFn : () => msgOrMsgFn;
 
   let done = 0,
     total = array.length;
@@ -260,9 +260,9 @@ export function progressPromiseAll(msgOrMsgFn, array) {
       Promise.resolve(promise).then((val) => {
         done++;
         // const pc = `${done}/${total}`;
-        const pc = (Math.round((done / total) * 1000) / 10 + "%").padEnd(
-          "99.9%".length,
-          " "
+        const pc = (Math.round((done / total) * 1000) / 10 + '%').padEnd(
+          '99.9%'.length,
+          ' '
         );
         if (done === total) {
           const time = Date.now() - start;
diff --git a/src/util/colors.js b/src/util/colors.js
index 8d6b24df..5848a820 100644
--- a/src/util/colors.js
+++ b/src/util/colors.js
@@ -1,5 +1,5 @@
-// @format
-//
+/** @format */
+
 // Color and theming utility functions! Handy.
 
 // Graciously stolen from https://stackoverflow.com/a/54071699! ::::)
diff --git a/src/util/find.js b/src/util/find.js
index 6ee0583c..460a4fad 100644
--- a/src/util/find.js
+++ b/src/util/find.js
@@ -1,14 +1,14 @@
-// @format
+/** @format */
 
-import { color, logError, logWarn } from "./cli.js";
+import {color, logError, logWarn} from './cli.js';
 
-import { inspect } from "util";
+import {inspect} from 'util';
 
 function warnOrThrow(mode, message) {
   switch (mode) {
-    case "error":
+    case 'error':
       throw new Error(message);
-    case "warn":
+    case 'warn':
       logWarn(message);
     default:
       return null;
@@ -26,16 +26,16 @@ function findHelper(keys, findFns = {}) {
   const byName = findFns.byName || matchName;
 
   const keyRefRegex = new RegExp(
-    String.raw`^(?:(${keys.join("|")}):(?=\S))?(.*)$`
+    String.raw`^(?:(${keys.join('|')}):(?=\S))?(.*)$`
   );
 
   // The mode argument here may be 'warn', 'error', or 'quiet'. 'error' throws
   // errors for null matches (with details about the error), while 'warn' and
   // 'quiet' both return null, with 'warn' logging details directly to the
   // console.
-  return (fullRef, data, { mode = "warn" } = {}) => {
+  return (fullRef, data, {mode = 'warn'} = {}) => {
     if (!fullRef) return null;
-    if (typeof fullRef !== "string") {
+    if (typeof fullRef !== 'string') {
       throw new Error(
         `Got a reference that is ${typeof fullRef}, not string: ${fullRef}`
       );
@@ -81,19 +81,19 @@ function findHelper(keys, findFns = {}) {
 }
 
 function matchDirectory(ref, data, mode) {
-  return data.find(({ directory }) => directory === ref);
+  return data.find(({directory}) => directory === ref);
 }
 
 function matchName(ref, data, mode) {
   const matches = data.filter(
-    ({ name }) => name.toLowerCase() === ref.toLowerCase()
+    ({name}) => name.toLowerCase() === ref.toLowerCase()
   );
 
   if (matches.length > 1) {
     return warnOrThrow(
       mode,
       `Multiple matches for reference "${ref}". Please resolve:\n` +
-        matches.map((match) => `- ${inspect(match)}\n`).join("") +
+        matches.map((match) => `- ${inspect(match)}\n`).join('') +
         `Returning null for this reference.`
     );
   }
@@ -115,19 +115,19 @@ function matchName(ref, data, mode) {
 }
 
 function matchTagName(ref, data, quiet) {
-  return matchName(ref.startsWith("cw: ") ? ref.slice(4) : ref, data, quiet);
+  return matchName(ref.startsWith('cw: ') ? ref.slice(4) : ref, data, quiet);
 }
 
 const find = {
-  album: findHelper(["album", "album-commentary"]),
-  artist: findHelper(["artist", "artist-gallery"]),
-  artTag: findHelper(["tag"], { byName: matchTagName }),
-  flash: findHelper(["flash"]),
-  group: findHelper(["group", "group-gallery"]),
-  listing: findHelper(["listing"]),
-  newsEntry: findHelper(["news-entry"]),
-  staticPage: findHelper(["static"]),
-  track: findHelper(["track"]),
+  album: findHelper(['album', 'album-commentary']),
+  artist: findHelper(['artist', 'artist-gallery']),
+  artTag: findHelper(['tag'], {byName: matchTagName}),
+  flash: findHelper(['flash']),
+  group: findHelper(['group', 'group-gallery']),
+  listing: findHelper(['listing']),
+  newsEntry: findHelper(['news-entry']),
+  staticPage: findHelper(['static']),
+  track: findHelper(['track']),
 };
 
 export default find;
@@ -140,15 +140,15 @@ export default find;
 export function bindFind(wikiData, opts1) {
   return Object.fromEntries(
     Object.entries({
-      album: "albumData",
-      artist: "artistData",
-      artTag: "artTagData",
-      flash: "flashData",
-      group: "groupData",
-      listing: "listingSpec",
-      newsEntry: "newsData",
-      staticPage: "staticPageData",
-      track: "trackData",
+      album: 'albumData',
+      artist: 'artistData',
+      artTag: 'artTagData',
+      flash: 'flashData',
+      group: 'groupData',
+      listing: 'listingSpec',
+      newsEntry: 'newsData',
+      staticPage: 'staticPageData',
+      track: 'trackData',
     }).map(([key, value]) => {
       const findFn = find[key];
       const thingData = wikiData[value];
@@ -157,7 +157,7 @@ export function bindFind(wikiData, opts1) {
         opts1
           ? (ref, opts2) =>
               opts2
-                ? findFn(ref, thingData, { ...opts1, ...opts2 })
+                ? findFn(ref, thingData, {...opts1, ...opts2})
                 : findFn(ref, thingData, opts1)
           : (ref, opts2) =>
               opts2 ? findFn(ref, thingData, opts2) : findFn(ref, thingData),
diff --git a/src/util/html.js b/src/util/html.js
index 913dc7b2..f5b7bdcc 100644
--- a/src/util/html.js
+++ b/src/util/html.js
@@ -1,23 +1,23 @@
-// @format
-//
+/** @format */
+
 // Some really simple functions for formatting HTML content.
 
 // COMPREHENSIVE!
 // https://html.spec.whatwg.org/multipage/syntax.html#void-elements
 export const selfClosingTags = [
-  "area",
-  "base",
-  "br",
-  "col",
-  "embed",
-  "hr",
-  "img",
-  "input",
-  "link",
-  "meta",
-  "source",
-  "track",
-  "wbr",
+  'area',
+  'base',
+  'br',
+  'col',
+  'embed',
+  'hr',
+  'img',
+  'input',
+  'link',
+  'meta',
+  'source',
+  'track',
+  'wbr',
 ];
 
 // Pass to tag() as an attri8utes key to make tag() return a 8lank string
@@ -32,7 +32,7 @@ export function tag(tagName, ...args) {
   let content;
   let attrs;
 
-  if (typeof args[0] === "object" && !Array.isArray(args[0])) {
+  if (typeof args[0] === 'object' && !Array.isArray(args[0])) {
     attrs = args[0];
     content = args[1];
   } else {
@@ -44,7 +44,7 @@ export function tag(tagName, ...args) {
   }
 
   if (attrs?.[onlyIfContent] && !content) {
-    return "";
+    return '';
   }
 
   if (attrs) {
@@ -59,17 +59,17 @@ export function tag(tagName, ...args) {
   }
 
   if (Array.isArray(content)) {
-    content = content.filter(Boolean).join("\n");
+    content = content.filter(Boolean).join('\n');
   }
 
   if (content) {
-    if (content.includes("\n")) {
+    if (content.includes('\n')) {
       return (
         `<${openTag}>\n` +
         content
-          .split("\n")
-          .map((line) => "    " + line + "\n")
-          .join("") +
+          .split('\n')
+          .map((line) => '    ' + line + '\n')
+          .join('') +
         `</${tagName}>`
       );
     } else {
@@ -85,18 +85,18 @@ export function tag(tagName, ...args) {
 }
 
 export function escapeAttributeValue(value) {
-  return value.replaceAll('"', "&quot;").replaceAll("'", "&apos;");
+  return value.replaceAll('"', '&quot;').replaceAll("'", '&apos;');
 }
 
 export function attributes(attribs) {
   return Object.entries(attribs)
     .map(([key, val]) => {
-      if (typeof val === "undefined" || val === null) return [key, val, false];
-      else if (typeof val === "string") return [key, val, true];
-      else if (typeof val === "boolean") return [key, val, val];
-      else if (typeof val === "number") return [key, val.toString(), true];
+      if (typeof val === 'undefined' || val === null) return [key, val, false];
+      else if (typeof val === 'string') return [key, val, true];
+      else if (typeof val === 'boolean') return [key, val, val];
+      else if (typeof val === 'number') return [key, val.toString(), true];
       else if (Array.isArray(val))
-        return [key, val.filter(Boolean).join(" "), val.length > 0];
+        return [key, val.filter(Boolean).join(' '), val.length > 0];
       else
         throw new Error(
           `Attribute value for ${key} should be primitive or array, got ${typeof val}`
@@ -104,9 +104,9 @@ export function attributes(attribs) {
     })
     .filter(([key, val, keep]) => keep)
     .map(([key, val]) =>
-      typeof val === "boolean"
+      typeof val === 'boolean'
         ? `${key}`
         : `${key}="${escapeAttributeValue(val)}"`
     )
-    .join(" ");
+    .join(' ');
 }
diff --git a/src/util/io.js b/src/util/io.js
index 6ea1e221..5c1ab240 100644
--- a/src/util/io.js
+++ b/src/util/io.js
@@ -1,14 +1,14 @@
-// @format
-//
+/** @format */
+
 // Utility functions for interacting with files and other external data
 // interfacey constructs.
 
-import { readdir } from "fs/promises";
-import * as path from "path";
+import {readdir} from 'fs/promises';
+import * as path from 'path';
 
 export async function findFiles(
   dataPath,
-  { filter = (f) => true, joinParentDirectory = true } = {}
+  {filter = (f) => true, joinParentDirectory = true} = {}
 ) {
   return (await readdir(dataPath))
     .filter((file) => filter(file))
diff --git a/src/util/link.js b/src/util/link.js
index 4095b17d..ee3579d5 100644
--- a/src/util/link.js
+++ b/src/util/link.js
@@ -1,5 +1,5 @@
-// @format
-//
+/** @format */
+
 // This file is essentially one level of a8straction a8ove urls.js (and the
 // urlSpec it gets its paths from). It's a 8unch of utility functions which
 // take certain types of wiki data o8jects (colloquially known as "things")
@@ -11,74 +11,74 @@
 // options availa8le in all the functions, making a common interface for
 // gener8ting just a8out any link on the site.
 
-import * as html from "./html.js";
-import { getColors } from "./colors.js";
+import * as html from './html.js';
+import {getColors} from './colors.js';
 
 export function getLinkThemeString(color) {
-  if (!color) return "";
+  if (!color) return '';
 
-  const { primary, dim } = getColors(color);
+  const {primary, dim} = getColors(color);
   return `--primary-color: ${primary}; --dim-color: ${dim}`;
 }
 
 const appendIndexHTMLRegex = /^(?!https?:\/\/).+\/$/;
 
 const linkHelper =
-  (hrefFn, { color = true, attr = null } = {}) =>
+  (hrefFn, {color = true, attr = null} = {}) =>
   (
     thing,
     {
       to,
-      text = "",
+      text = '',
       attributes = null,
-      class: className = "",
+      class: className = '',
       color: color2 = true,
-      hash = "",
+      hash = '',
     }
   ) => {
-    let href = hrefFn(thing, { to });
+    let href = hrefFn(thing, {to});
 
     if (link.globalOptions.appendIndexHTML) {
       if (appendIndexHTMLRegex.test(href)) {
-        href += "index.html";
+        href += 'index.html';
       }
     }
 
     if (hash) {
-      href += (hash.startsWith("#") ? "" : "#") + hash;
+      href += (hash.startsWith('#') ? '' : '#') + hash;
     }
 
     return html.tag(
-      "a",
+      'a',
       {
         ...(attr ? attr(thing) : {}),
         ...(attributes ? attributes : {}),
         href,
         style:
-          typeof color2 === "string"
+          typeof color2 === 'string'
             ? getLinkThemeString(color2)
             : color2 && color
             ? getLinkThemeString(thing.color)
-            : "",
+            : '',
         class: className,
       },
       text || thing.name
     );
   };
 
-const linkDirectory = (key, { expose = null, attr = null, ...conf } = {}) =>
-  linkHelper((thing, { to }) => to("localized." + key, thing.directory), {
+const linkDirectory = (key, {expose = null, attr = null, ...conf} = {}) =>
+  linkHelper((thing, {to}) => to('localized.' + key, thing.directory), {
     attr: (thing) => ({
       ...(attr ? attr(thing) : {}),
-      ...(expose ? { [expose]: thing.directory } : {}),
+      ...(expose ? {[expose]: thing.directory} : {}),
     }),
     ...conf,
   });
 
 const linkPathname = (key, conf) =>
-  linkHelper(({ directory: pathname }, { to }) => to(key, pathname), conf);
+  linkHelper(({directory: pathname}, {to}) => to(key, pathname), conf);
 const linkIndex = (key, conf) =>
-  linkHelper((_, { to }) => to("localized." + key), conf);
+  linkHelper((_, {to}) => to('localized.' + key), conf);
 
 const link = {
   globalOptions: {
@@ -90,50 +90,50 @@ const link = {
     appendIndexHTML: false,
   },
 
-  album: linkDirectory("album"),
-  albumCommentary: linkDirectory("albumCommentary"),
-  artist: linkDirectory("artist", { color: false }),
-  artistGallery: linkDirectory("artistGallery", { color: false }),
-  commentaryIndex: linkIndex("commentaryIndex", { color: false }),
-  flashIndex: linkIndex("flashIndex", { color: false }),
-  flash: linkDirectory("flash"),
-  groupInfo: linkDirectory("groupInfo"),
-  groupGallery: linkDirectory("groupGallery"),
-  home: linkIndex("home", { color: false }),
-  listingIndex: linkIndex("listingIndex"),
-  listing: linkDirectory("listing"),
-  newsIndex: linkIndex("newsIndex", { color: false }),
-  newsEntry: linkDirectory("newsEntry", { color: false }),
-  staticPage: linkDirectory("staticPage", { color: false }),
-  tag: linkDirectory("tag"),
-  track: linkDirectory("track", { expose: "data-track" }),
+  album: linkDirectory('album'),
+  albumCommentary: linkDirectory('albumCommentary'),
+  artist: linkDirectory('artist', {color: false}),
+  artistGallery: linkDirectory('artistGallery', {color: false}),
+  commentaryIndex: linkIndex('commentaryIndex', {color: false}),
+  flashIndex: linkIndex('flashIndex', {color: false}),
+  flash: linkDirectory('flash'),
+  groupInfo: linkDirectory('groupInfo'),
+  groupGallery: linkDirectory('groupGallery'),
+  home: linkIndex('home', {color: false}),
+  listingIndex: linkIndex('listingIndex'),
+  listing: linkDirectory('listing'),
+  newsIndex: linkIndex('newsIndex', {color: false}),
+  newsEntry: linkDirectory('newsEntry', {color: false}),
+  staticPage: linkDirectory('staticPage', {color: false}),
+  tag: linkDirectory('tag'),
+  track: linkDirectory('track', {expose: 'data-track'}),
 
   // TODO: This is a bit hacky. Files are just strings (not objects), so we
   // have to manually provide the album alongside the file. They also don't
   // follow the usual {name: whatever} type shape, so we have to provide that
   // ourselves.
   _albumAdditionalFileHelper: linkHelper(
-    (fakeFileObject, { to }) =>
+    (fakeFileObject, {to}) =>
       to(
-        "media.albumAdditionalFile",
+        'media.albumAdditionalFile',
         fakeFileObject.album.directory,
         fakeFileObject.name
       ),
-    { color: false }
+    {color: false}
   ),
-  albumAdditionalFile: ({ file, album }, { to }) =>
+  albumAdditionalFile: ({file, album}, {to}) =>
     link._albumAdditionalFileHelper(
       {
         name: file,
         album,
       },
-      { to }
+      {to}
     ),
 
-  media: linkPathname("media.path", { color: false }),
-  root: linkPathname("shared.path", { color: false }),
-  data: linkPathname("data.path", { color: false }),
-  site: linkPathname("localized.path", { color: false }),
+  media: linkPathname('media.path', {color: false}),
+  root: linkPathname('shared.path', {color: false}),
+  data: linkPathname('data.path', {color: false}),
+  site: linkPathname('localized.path', {color: false}),
 };
 
 export default link;
diff --git a/src/util/magic-constants.js b/src/util/magic-constants.js
index a7b29332..dbdbcfda 100644
--- a/src/util/magic-constants.js
+++ b/src/util/magic-constants.js
@@ -1,5 +1,5 @@
-// @format
-//
+/** @format */
+
 // Magic constants only! These are hard-coded, and any use of them should be
 // considered a flaw in the codebase - areas where we use hard-coded behavior
 // to support one use of the wiki software (i.e. HSMusic, usually), rather than
@@ -8,5 +8,5 @@
 // All such uses should eventually be replaced with better code in due time
 // (TM).
 
-export const OFFICIAL_GROUP_DIRECTORY = "official";
-export const FANDOM_GROUP_DIRECTORY = "fandom";
+export const OFFICIAL_GROUP_DIRECTORY = 'official';
+export const FANDOM_GROUP_DIRECTORY = 'fandom';
diff --git a/src/util/node-utils.js b/src/util/node-utils.js
index f638e4ad..df446654 100644
--- a/src/util/node-utils.js
+++ b/src/util/node-utils.js
@@ -1,10 +1,10 @@
-// @format
-//
+/** @format */
+
 // Utility functions which are only relevant to particular Node.js constructs.
 
-import { fileURLToPath } from "url";
+import {fileURLToPath} from 'url';
 
-import _commandExists from "command-exists";
+import _commandExists from 'command-exists';
 
 // This package throws an error instead of returning false when the command
 // doesn't exist, for some reason. Yay for making logic more difficult!
@@ -32,7 +32,7 @@ export function promisifyProcess(proc, showLogging = true) {
       proc.stderr.pipe(process.stderr);
     }
 
-    proc.on("exit", (code) => {
+    proc.on('exit', (code) => {
       if (code === 0) {
         resolve();
       } else {
diff --git a/src/util/replacer.js b/src/util/replacer.js
index 10603b6c..6f4d0e9b 100644
--- a/src/util/replacer.js
+++ b/src/util/replacer.js
@@ -1,14 +1,14 @@
-// @format
+/** @format */
 
-import { logError, logWarn } from "./cli.js";
-import { escapeRegex } from "./sugar.js";
+import {logError, logWarn} from './cli.js';
+import {escapeRegex} from './sugar.js';
 
-export function validateReplacerSpec(replacerSpec, { find, link }) {
+export function validateReplacerSpec(replacerSpec, {find, link}) {
   let success = true;
 
   for (const [
     key,
-    { link: linkKey, find: findKey, value, html },
+    {link: linkKey, find: findKey, value, html},
   ] of Object.entries(replacerSpec)) {
     if (!html && !link[linkKey]) {
       logError`The replacer spec ${key} has invalid link key ${linkKey}! Specify it in link specs or fix typo.`;
@@ -24,15 +24,15 @@ export function validateReplacerSpec(replacerSpec, { find, link }) {
 }
 
 // Syntax literals.
-const tagBeginning = "[[";
-const tagEnding = "]]";
-const tagReplacerValue = ":";
-const tagHash = "#";
-const tagArgument = "*";
-const tagArgumentValue = "=";
-const tagLabel = "|";
+const tagBeginning = '[[';
+const tagEnding = ']]';
+const tagReplacerValue = ':';
+const tagHash = '#';
+const tagArgument = '*';
+const tagArgumentValue = '=';
+const tagLabel = '|';
 
-const noPrecedingWhitespace = "(?<!\\s)";
+const noPrecedingWhitespace = '(?<!\\s)';
 
 const R_tagBeginning = escapeRegex(tagBeginning);
 
@@ -51,7 +51,7 @@ const R_tagLabel = escapeRegex(tagLabel);
 
 const regexpCache = {};
 
-const makeError = (i, message) => ({ i, type: "error", data: { message } });
+const makeError = (i, message) => ({i, type: 'error', data: {message}});
 const endOfInput = (i, comment) =>
   makeError(i, `Unexpected end of input (${comment}).`);
 
@@ -67,7 +67,7 @@ function parseOneTextNode(input, i, stopAt) {
 function parseNodes(input, i, stopAt, textOnly) {
   let nodes = [];
   let escapeNext = false;
-  let string = "";
+  let string = '';
   let iString = 0;
 
   stopped = false;
@@ -82,8 +82,8 @@ function parseNodes(input, i, stopAt, textOnly) {
     }
 
     if (string.length) {
-      nodes.push({ i: iString, iEnd: i, type: "text", data: string });
-      string = "";
+      nodes.push({i: iString, iEnd: i, type: 'text', data: string});
+      string = '';
     }
   };
 
@@ -97,7 +97,7 @@ function parseNodes(input, i, stopAt, textOnly) {
   // should 8e counted only as part of the current string/text.
   //
   // Inspired 8y this: https://stackoverflow.com/a/41470813
-  const regexpSource = `(?<!\\\\)(?:\\\\{2})*(${literalsToMatch.join("|")})`;
+  const regexpSource = `(?<!\\\\)(?:\\\\{2})*(${literalsToMatch.join('|')})`;
 
   // There are 8asically only a few regular expressions we'll ever use,
   // 8ut it's a pain to hard-code them all, so we dynamically gener8te
@@ -271,7 +271,7 @@ function parseNodes(input, i, stopAt, textOnly) {
         const value = N;
         i = stop_iParse;
 
-        args.push({ key, value });
+        args.push({key, value});
       }
 
       let label;
@@ -289,8 +289,8 @@ function parseNodes(input, i, stopAt, textOnly) {
       nodes.push({
         i: iTag,
         iEnd: i,
-        type: "tag",
-        data: { replacerKey, replacerValue, hash, args, label },
+        type: 'tag',
+        data: {replacerKey, replacerValue, hash, args, label},
       });
 
       continue;
@@ -304,23 +304,23 @@ export function parseInput(input) {
   try {
     return parseNodes(input, 0);
   } catch (errorNode) {
-    if (errorNode.type !== "error") {
+    if (errorNode.type !== 'error') {
       throw errorNode;
     }
 
     const {
       i,
-      data: { message },
+      data: {message},
     } = errorNode;
 
-    let lineStart = input.slice(0, i).lastIndexOf("\n");
+    let lineStart = input.slice(0, i).lastIndexOf('\n');
     if (lineStart >= 0) {
       lineStart += 1;
     } else {
       lineStart = 0;
     }
 
-    let lineEnd = input.slice(i).indexOf("\n");
+    let lineEnd = input.slice(i).indexOf('\n');
     if (lineEnd >= 0) {
       lineEnd += i;
     } else {
@@ -334,18 +334,18 @@ export function parseInput(input) {
     throw new SyntaxError(fixWS`
             Parse error (at pos ${i}): ${message}
             ${line}
-            ${"-".repeat(cursor) + "^"}
+            ${'-'.repeat(cursor) + '^'}
         `);
   }
 }
 
 function evaluateTag(node, opts) {
-  const { find, input, language, link, replacerSpec, to, wikiData } = opts;
+  const {find, input, language, link, replacerSpec, to, wikiData} = opts;
 
   const source = input.slice(node.i, node.iEnd);
 
   const replacerKeyImplied = !node.data.replacerKey;
-  const replacerKey = replacerKeyImplied ? "track" : node.data.replacerKey.data;
+  const replacerKey = replacerKeyImplied ? 'track' : node.data.replacerKey.data;
 
   if (!replacerSpec[replacerKey]) {
     logWarn`The link ${source} has an invalid replacer key!`;
@@ -395,7 +395,7 @@ function evaluateTag(node, opts) {
   const args =
     node.data.args &&
     Object.fromEntries(
-      node.data.args.map(({ key, value }) => [
+      node.data.args.map(({key, value}) => [
         transformNode(key, opts),
         transformNodes(value, opts),
       ])
@@ -404,7 +404,7 @@ function evaluateTag(node, opts) {
   const fn = htmlFn ? htmlFn : link[linkKey];
 
   try {
-    return fn(value, { text: label, hash, args, language, to });
+    return fn(value, {text: label, hash, args, language, to});
   } catch (error) {
     logError`The link ${source} failed to be processed: ${error}`;
     return source;
@@ -413,17 +413,17 @@ function evaluateTag(node, opts) {
 
 function transformNode(node, opts) {
   if (!node) {
-    throw new Error("Expected a node!");
+    throw new Error('Expected a node!');
   }
 
   if (Array.isArray(node)) {
-    throw new Error("Got an array - use transformNodes here!");
+    throw new Error('Got an array - use transformNodes here!');
   }
 
   switch (node.type) {
-    case "text":
+    case 'text':
       return node.data;
-    case "tag":
+    case 'tag':
       return evaluateTag(node, opts);
     default:
       throw new Error(`Unknown node type ${node.type}`);
@@ -435,19 +435,19 @@ function transformNodes(nodes, opts) {
     throw new Error(`Expected an array of nodes! Got: ${nodes}`);
   }
 
-  return nodes.map((node) => transformNode(node, opts)).join("");
+  return nodes.map((node) => transformNode(node, opts)).join('');
 }
 
 export function transformInline(
   input,
-  { replacerSpec, find, link, language, to, wikiData }
+  {replacerSpec, find, link, language, to, wikiData}
 ) {
-  if (!replacerSpec) throw new Error("Expected replacerSpec");
-  if (!find) throw new Error("Expected find");
-  if (!link) throw new Error("Expected link");
-  if (!language) throw new Error("Expected language");
-  if (!to) throw new Error("Expected to");
-  if (!wikiData) throw new Error("Expected wikiData");
+  if (!replacerSpec) throw new Error('Expected replacerSpec');
+  if (!find) throw new Error('Expected find');
+  if (!link) throw new Error('Expected link');
+  if (!language) throw new Error('Expected language');
+  if (!to) throw new Error('Expected to');
+  if (!wikiData) throw new Error('Expected wikiData');
 
   const nodes = parseInput(input);
   return transformNodes(nodes, {
diff --git a/src/util/serialize.js b/src/util/serialize.js
index ab864836..9aa8b0c5 100644
--- a/src/util/serialize.js
+++ b/src/util/serialize.js
@@ -1,4 +1,4 @@
-// @format
+/** @format */
 
 export function serializeLink(thing) {
   const ret = {};
@@ -9,7 +9,7 @@ export function serializeLink(thing) {
 }
 
 export function serializeContribs(contribs) {
-  return contribs.map(({ who, what }) => {
+  return contribs.map(({who, what}) => {
     const ret = {};
     ret.artist = serializeLink(who);
     if (what) ret.contribution = what;
@@ -17,7 +17,7 @@ export function serializeContribs(contribs) {
   });
 }
 
-export function serializeImagePaths(original, { thumb }) {
+export function serializeImagePaths(original, {thumb}) {
   return {
     original,
     medium: thumb.medium(original),
@@ -28,13 +28,13 @@ export function serializeImagePaths(original, { thumb }) {
 export function serializeCover(
   thing,
   pathFunction,
-  { serializeImagePaths, urls }
+  {serializeImagePaths, urls}
 ) {
   const coverPath = pathFunction(thing, {
-    to: urls.from("media.root").to,
+    to: urls.from('media.root').to,
   });
 
-  const { artTags } = thing;
+  const {artTags} = thing;
 
   const cwTags = artTags.filter((tag) => tag.isContentWarning);
   const linkTags = artTags.filter((tag) => !tag.isContentWarning);
@@ -46,15 +46,15 @@ export function serializeCover(
   };
 }
 
-export function serializeGroupsForAlbum(album, { serializeLink }) {
+export function serializeGroupsForAlbum(album, {serializeLink}) {
   return album.groups
     .map((group) => {
       const index = group.albums.indexOf(album);
       const next = group.albums[index + 1] || null;
       const previous = group.albums[index - 1] || null;
-      return { group, index, next, previous };
+      return {group, index, next, previous};
     })
-    .map(({ group, index, next, previous }) => ({
+    .map(({group, index, next, previous}) => ({
       link: serializeLink(group),
       descriptionShort: group.descriptionShort,
       albumIndex: index,
@@ -64,7 +64,7 @@ export function serializeGroupsForAlbum(album, { serializeLink }) {
     }));
 }
 
-export function serializeGroupsForTrack(track, { serializeLink }) {
+export function serializeGroupsForTrack(track, {serializeLink}) {
   return track.album.groups.map((group) => ({
     link: serializeLink(group),
     urls: group.urls,
diff --git a/src/util/sugar.js b/src/util/sugar.js
index a4504daa..0a5de482 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -1,5 +1,5 @@
-// @format
-//
+/** @format */
+
 // Syntactic sugar! (Mostly.)
 // Generic functions - these are useful just a8out everywhere.
 //
@@ -8,7 +8,7 @@
 // It will likely only do exactly what I want it to, and only in the cases I
 // decided were relevant enough to 8other handling.
 
-import { color } from "./cli.js";
+import {color} from './cli.js';
 
 // Apparently JavaScript doesn't come with a function to split an array into
 // chunks! Weird. Anyway, this is an awesome place to use a generator, even
@@ -35,13 +35,13 @@ export const mapInPlace = (array, fn) =>
 
 export const filterEmptyLines = (string) =>
   string
-    .split("\n")
+    .split('\n')
     .filter((line) => line.trim())
-    .join("\n");
+    .join('\n');
 
 export const unique = (arr) => Array.from(new Set(arr));
 
-export const compareArrays = (arr1, arr2, { checkOrder = true } = {}) =>
+export const compareArrays = (arr1, arr2, {checkOrder = true} = {}) =>
   arr1.length === arr2.length &&
   (checkOrder
     ? arr1.every((x, i) => arr2[i] === x)
@@ -90,7 +90,7 @@ export function delay(ms) {
 // There's a proposal for a native JS function like this, 8ut it's not even
 // past stage 1 yet: https://github.com/tc39/proposal-regex-escaping
 export function escapeRegex(string) {
-  return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
+  return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
 }
 
 export function bindOpts(fn, bind) {
@@ -98,10 +98,10 @@ export function bindOpts(fn, bind) {
 
   const bound = function (...args) {
     const opts = args[bindIndex] ?? {};
-    return fn(...args.slice(0, bindIndex), { ...bind, ...opts });
+    return fn(...args.slice(0, bindIndex), {...bind, ...opts});
   };
 
-  Object.defineProperty(bound, "name", {
+  Object.defineProperty(bound, 'name', {
     value: fn.name ? `(options-bound) ${fn.name}` : `(options-bound)`,
   });
 
@@ -132,7 +132,7 @@ export function openAggregate({
 
   // Optional human-readable message to describe the aggregate error, if
   // constructed.
-  message = "",
+  message = '',
 
   // Value to return when a provided function throws an error. If this is a
   // function, it will be called with the arguments given to the function.
@@ -151,7 +151,7 @@ export function openAggregate({
         return fn(...args);
       } catch (error) {
         errors.push(error);
-        return typeof returnOnFail === "function"
+        return typeof returnOnFail === 'function'
           ? returnOnFail(...args)
           : returnOnFail;
       }
@@ -164,7 +164,7 @@ export function openAggregate({
         (value) => value,
         (error) => {
           errors.push(error);
-          return typeof returnOnFail === "function"
+          return typeof returnOnFail === 'function'
             ? returnOnFail(...args)
             : returnOnFail;
         }
@@ -189,21 +189,21 @@ export function openAggregate({
 
   aggregate.map = (...args) => {
     const parent = aggregate;
-    const { result, aggregate: child } = mapAggregate(...args);
+    const {result, aggregate: child} = mapAggregate(...args);
     parent.call(child.close);
     return result;
   };
 
   aggregate.mapAsync = async (...args) => {
     const parent = aggregate;
-    const { result, aggregate: child } = await mapAggregateAsync(...args);
+    const {result, aggregate: child} = await mapAggregateAsync(...args);
     parent.call(child.close);
     return result;
   };
 
   aggregate.filter = (...args) => {
     const parent = aggregate;
-    const { result, aggregate: child } = filterAggregate(...args);
+    const {result, aggregate: child} = filterAggregate(...args);
     parent.call(child.close);
     return result;
   };
@@ -219,11 +219,11 @@ export function openAggregate({
   return aggregate;
 }
 
-openAggregate.errorClassSymbol = Symbol("error class");
+openAggregate.errorClassSymbol = Symbol('error class');
 
 // Utility function for providing {errorClass} parameter to aggregate functions.
 export function aggregateThrows(errorClass) {
-  return { [openAggregate.errorClassSymbol]: errorClass };
+  return {[openAggregate.errorClassSymbol]: errorClass};
 }
 
 // Performs an ordinary array map with the given function, collating into a
@@ -236,15 +236,15 @@ export function aggregateThrows(errorClass) {
 // use aggregate.close() to throw the error. (This aggregate may be passed to a
 // parent aggregate: `parent.call(aggregate.close)`!)
 export function mapAggregate(array, fn, aggregateOpts) {
-  return _mapAggregate("sync", null, array, fn, aggregateOpts);
+  return _mapAggregate('sync', null, array, fn, aggregateOpts);
 }
 
 export function mapAggregateAsync(
   array,
   fn,
-  { promiseAll = Promise.all.bind(Promise), ...aggregateOpts } = {}
+  {promiseAll = Promise.all.bind(Promise), ...aggregateOpts} = {}
 ) {
-  return _mapAggregate("async", promiseAll, array, fn, aggregateOpts);
+  return _mapAggregate('async', promiseAll, array, fn, aggregateOpts);
 }
 
 // Helper function for mapAggregate which holds code common between sync and
@@ -257,15 +257,15 @@ export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) {
     ...aggregateOpts,
   });
 
-  if (mode === "sync") {
+  if (mode === 'sync') {
     const result = array
       .map(aggregate.wrap(fn))
       .filter((value) => value !== failureSymbol);
-    return { result, aggregate };
+    return {result, aggregate};
   } else {
     return promiseAll(array.map(aggregate.wrapAsync(fn))).then((values) => {
       const result = values.filter((value) => value !== failureSymbol);
-      return { result, aggregate };
+      return {result, aggregate};
     });
   }
 }
@@ -278,15 +278,15 @@ export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) {
 //
 // As with mapAggregate, the returned aggregate property is not yet closed.
 export function filterAggregate(array, fn, aggregateOpts) {
-  return _filterAggregate("sync", null, array, fn, aggregateOpts);
+  return _filterAggregate('sync', null, array, fn, aggregateOpts);
 }
 
 export async function filterAggregateAsync(
   array,
   fn,
-  { promiseAll = Promise.all.bind(Promise), ...aggregateOpts } = {}
+  {promiseAll = Promise.all.bind(Promise), ...aggregateOpts} = {}
 ) {
-  return _filterAggregate("async", promiseAll, array, fn, aggregateOpts);
+  return _filterAggregate('async', promiseAll, array, fn, aggregateOpts);
 }
 
 // Helper function for filterAggregate which holds code common between sync and
@@ -326,30 +326,30 @@ function _filterAggregate(mode, promiseAll, array, fn, aggregateOpts) {
     };
   }
 
-  if (mode === "sync") {
+  if (mode === 'sync') {
     const result = array
       .map(
         aggregate.wrap((input, index, array) => {
           const output = fn(input, index, array);
-          return { input, output };
+          return {input, output};
         })
       )
       .filter(filterFunction)
       .map(mapFunction);
 
-    return { result, aggregate };
+    return {result, aggregate};
   } else {
     return promiseAll(
       array.map(
         aggregate.wrapAsync(async (input, index, array) => {
           const output = await fn(input, index, array);
-          return { input, output };
+          return {input, output};
         })
       )
     ).then((values) => {
       const result = values.filter(filterFunction).map(mapFunction);
 
-      return { result, aggregate };
+      return {result, aggregate};
     });
   }
 }
@@ -358,22 +358,22 @@ function _filterAggregate(mode, promiseAll, array, fn, aggregateOpts) {
 // function with it, then closing the function and returning the result (if
 // there's no throw).
 export function withAggregate(aggregateOpts, fn) {
-  return _withAggregate("sync", aggregateOpts, fn);
+  return _withAggregate('sync', aggregateOpts, fn);
 }
 
 export function withAggregateAsync(aggregateOpts, fn) {
-  return _withAggregate("async", aggregateOpts, fn);
+  return _withAggregate('async', aggregateOpts, fn);
 }
 
 export function _withAggregate(mode, aggregateOpts, fn) {
-  if (typeof aggregateOpts === "function") {
+  if (typeof aggregateOpts === 'function') {
     fn = aggregateOpts;
     aggregateOpts = {};
   }
 
   const aggregate = openAggregate(aggregateOpts);
 
-  if (mode === "sync") {
+  if (mode === 'sync') {
     const result = fn(aggregate);
     aggregate.close();
     return result;
@@ -387,56 +387,56 @@ export function _withAggregate(mode, aggregateOpts, fn) {
 
 export function showAggregate(
   topError,
-  { pathToFile = (p) => p, showTraces = true } = {}
+  {pathToFile = (p) => p, showTraces = true} = {}
 ) {
-  const recursive = (error, { level }) => {
+  const recursive = (error, {level}) => {
     let header = showTraces
-      ? `[${error.constructor.name || "unnamed"}] ${
-          error.message || "(no message)"
+      ? `[${error.constructor.name || 'unnamed'}] ${
+          error.message || '(no message)'
         }`
       : error instanceof AggregateError
-      ? `[${error.message || "(no message)"}]`
-      : error.message || "(no message)";
+      ? `[${error.message || '(no message)'}]`
+      : error.message || '(no message)';
     if (showTraces) {
-      const stackLines = error.stack?.split("\n");
+      const stackLines = error.stack?.split('\n');
       const stackLine = stackLines?.find(
         (line) =>
-          line.trim().startsWith("at") &&
-          !line.includes("sugar") &&
-          !line.includes("node:") &&
-          !line.includes("<anonymous>")
+          line.trim().startsWith('at') &&
+          !line.includes('sugar') &&
+          !line.includes('node:') &&
+          !line.includes('<anonymous>')
       );
       const tracePart = stackLine
-        ? "- " +
+        ? '- ' +
           stackLine
             .trim()
             .replace(/file:\/\/(.*\.js)/, (match, pathname) =>
               pathToFile(pathname)
             )
-        : "(no stack trace)";
+        : '(no stack trace)';
       header += ` ${color.dim(tracePart)}`;
     }
-    const bar = level % 2 === 0 ? "\u2502" : color.dim("\u254e");
-    const head = level % 2 === 0 ? "\u257f" : color.dim("\u257f");
+    const bar = level % 2 === 0 ? '\u2502' : color.dim('\u254e');
+    const head = level % 2 === 0 ? '\u257f' : color.dim('\u257f');
 
     if (error instanceof AggregateError) {
       return (
         header +
-        "\n" +
+        '\n' +
         error.errors
-          .map((error) => recursive(error, { level: level + 1 }))
-          .flatMap((str) => str.split("\n"))
+          .map((error) => recursive(error, {level: level + 1}))
+          .flatMap((str) => str.split('\n'))
           .map((line, i, lines) =>
             i === 0 ? ` ${head} ${line}` : ` ${bar} ${line}`
           )
-          .join("\n")
+          .join('\n')
       );
     } else {
       return header;
     }
   };
 
-  console.error(recursive(topError, { level: 0 }));
+  console.error(recursive(topError, {level: 0}));
 }
 
 export function decorateErrorWithIndex(fn) {
diff --git a/src/util/urls.js b/src/util/urls.js
index 4e398082..ce747df2 100644
--- a/src/util/urls.js
+++ b/src/util/urls.js
@@ -1,5 +1,5 @@
-// @format
-//
+/** @format */
+
 // Code that deals with URLs (really the pathnames that get referenced all
 // throughout the gener8ted HTML). Most nota8ly here is generateURLs, which
 // is in charge of pre-gener8ting a complete network of template strings
@@ -10,12 +10,12 @@
 // actual path strings. More a8stract operations using wiki data o8jects is
 // the domain of link.js.
 
-import * as path from "path";
-import { withEntries } from "./sugar.js";
+import * as path from 'path';
+import {withEntries} from './sugar.js';
 
 export function generateURLs(urlSpec) {
   const getValueForFullKey = (obj, fullKey, prop = null) => {
-    const [groupKey, subKey] = fullKey.split(".");
+    const [groupKey, subKey] = fullKey.split('.');
     if (!groupKey || !subKey) {
       throw new Error(`Expected group key and subkey (got ${fullKey})`);
     }
@@ -41,27 +41,27 @@ export function generateURLs(urlSpec) {
   // This should be called on values which are going to be passed to
   // path.relative, because relative will resolve a leading slash as the root
   // directory of the working device, which we aren't looking for here.
-  const trimLeadingSlash = (P) => (P.startsWith("/") ? P.slice(1) : P);
+  const trimLeadingSlash = (P) => (P.startsWith('/') ? P.slice(1) : P);
 
   const generateTo = (fromPath, fromGroup) => {
     const A = trimLeadingSlash(fromPath);
 
-    const rebasePrefix = "../".repeat(
-      (fromGroup.prefix || "").split("/").filter(Boolean).length
+    const rebasePrefix = '../'.repeat(
+      (fromGroup.prefix || '').split('/').filter(Boolean).length
     );
 
     const pathHelper = (toPath, toGroup) => {
       let B = trimLeadingSlash(toPath);
 
       let argIndex = 0;
-      B = B.replaceAll("<>", () => `<${argIndex++}>`);
+      B = B.replaceAll('<>', () => `<${argIndex++}>`);
 
       if (toGroup.prefix !== fromGroup.prefix) {
         // TODO: Handle differing domains in prefixes.
-        B = rebasePrefix + (toGroup.prefix || "") + B;
+        B = rebasePrefix + (toGroup.prefix || '') + B;
       }
 
-      const suffix = toPath.endsWith("/") ? "/" : "";
+      const suffix = toPath.endsWith('/') ? '/' : '';
 
       return {
         posix: path.posix.relative(A, B) + suffix,
@@ -86,7 +86,7 @@ export function generateURLs(urlSpec) {
       (delimiterMode) =>
       (key, ...args) => {
         const {
-          value: { [delimiterMode]: template },
+          value: {[delimiterMode]: template},
         } = getValueForFullKey(relative, key);
 
         let missing = 0;
@@ -110,8 +110,8 @@ export function generateURLs(urlSpec) {
       };
 
     return {
-      to: toHelper("posix"),
-      toDevice: toHelper("device"),
+      to: toHelper('posix'),
+      toDevice: toHelper('device'),
     };
   };
 
@@ -127,16 +127,16 @@ export function generateURLs(urlSpec) {
 
     const from = (key) => getValueForFullKey(map, key).value;
 
-    return { from, map };
+    return {from, map};
   };
 
   return generateFrom();
 }
 
 const thumbnailHelper = (name) => (file) =>
-  file.replace(/\.(jpg|png)$/, name + ".jpg");
+  file.replace(/\.(jpg|png)$/, name + '.jpg');
 
 export const thumb = {
-  medium: thumbnailHelper(".medium"),
-  small: thumbnailHelper(".small"),
+  medium: thumbnailHelper('.medium'),
+  small: thumbnailHelper('.small'),
 };
diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js
index 7e16580d..65eb7d7c 100644
--- a/src/util/wiki-data.js
+++ b/src/util/wiki-data.js
@@ -1,17 +1,17 @@
-// @format
-//
+/** @format */
+
 // Utility functions for interacting with wiki data.
 
 // Generic value operations
 
 export function getKebabCase(name) {
   return name
-    .split(" ")
-    .join("-")
-    .replace(/&/g, "and")
-    .replace(/[^a-zA-Z0-9\-]/g, "")
-    .replace(/-{2,}/g, "-")
-    .replace(/^-+|-+$/g, "")
+    .split(' ')
+    .join('-')
+    .replace(/&/g, 'and')
+    .replace(/[^a-zA-Z0-9\-]/g, '')
+    .replace(/-{2,}/g, '-')
+    .replace(/^-+|-+$/g, '')
     .toLowerCase();
 }
 
@@ -81,8 +81,8 @@ export function compareCaseLessSensitive(a, b) {
   const bl = b.toLowerCase();
 
   return al === bl
-    ? a.localeCompare(b, undefined, { numeric: true })
-    : al.localeCompare(bl, undefined, { numeric: true });
+    ? a.localeCompare(b, undefined, {numeric: true})
+    : al.localeCompare(bl, undefined, {numeric: true});
 }
 
 // Subtract common prefixes and other characters which some people don't like
@@ -92,22 +92,22 @@ export function normalizeName(s) {
   // Turn (some) ligatures into expanded variant for cleaner sorting, e.g.
   // "ff" into "ff", in decompose mode, so that "ü" is represented as two
   // bytes ("u" + \u0308 combining diaeresis).
-  s = s.normalize("NFKD");
+  s = s.normalize('NFKD');
 
   // Replace one or more whitespace of any kind in a row, as well as certain
   // punctuation, with a single typical space, then trim the ends.
   s = s
     .replace(
       /[\p{Separator}\p{Dash_Punctuation}\p{Connector_Punctuation}]+/gu,
-      " "
+      ' '
     )
     .trim();
 
   // Discard anything that isn't a letter, number, or space.
-  s = s.replace(/[^\p{Letter}\p{Number} ]/gu, "");
+  s = s.replace(/[^\p{Letter}\p{Number} ]/gu, '');
 
   // Remove common English (only, for now) prefixes.
-  s = s.replace(/^(?:an?|the) /i, "");
+  s = s.replace(/^(?:an?|the) /i, '');
 
   return s;
 }
@@ -142,7 +142,7 @@ export function normalizeName(s) {
 // except when album and track directories overlap with each other.
 export function sortByDirectory(
   data,
-  { getDirectory = (o) => o.directory } = {}
+  {getDirectory = (o) => o.directory} = {}
 ) {
   return data.sort((a, b) => {
     const ad = getDirectory(a);
@@ -151,7 +151,7 @@ export function sortByDirectory(
   });
 }
 
-export function sortByName(data, { getName = (o) => o.name } = {}) {
+export function sortByName(data, {getName = (o) => o.name} = {}) {
   return data.sort((a, b) => {
     const an = getName(a);
     const bn = getName(b);
@@ -163,7 +163,7 @@ export function sortByName(data, { getName = (o) => o.name } = {}) {
   });
 }
 
-export function sortByDate(data, { getDate = (o) => o.date } = {}) {
+export function sortByDate(data, {getDate = (o) => o.date} = {}) {
   return data.sort((a, b) => {
     const ad = getDate(a);
     const bd = getDate(b);
@@ -254,9 +254,9 @@ export function sortByConditions(data, conditions) {
 // Expects thing properties:
 //  * directory (or override getDirectory)
 //  * name (or override getName)
-export function sortAlphabetically(data, { getDirectory, getName } = {}) {
-  sortByDirectory(data, { getDirectory });
-  sortByName(data, { getName });
+export function sortAlphabetically(data, {getDirectory, getName} = {}) {
+  sortByDirectory(data, {getDirectory});
+  sortByName(data, {getName});
   return data;
 }
 
@@ -266,10 +266,10 @@ export function sortAlphabetically(data, { getDirectory, getName } = {}) {
 //  * date (or override getDate)
 export function sortChronologically(
   data,
-  { getDirectory, getName, getDate } = {}
+  {getDirectory, getName, getDate} = {}
 ) {
-  sortAlphabetically(data, { getDirectory, getName });
-  sortByDate(data, { getDate });
+  sortAlphabetically(data, {getDirectory, getName});
+  sortByDate(data, {getDate});
   return data;
 }
 
@@ -281,7 +281,7 @@ export function sortChronologically(
 // release date but can be overridden) above all else.
 //
 // This function also works for data lists which contain only tracks.
-export function sortAlbumsTracksChronologically(data, { getDate } = {}) {
+export function sortAlbumsTracksChronologically(data, {getDate} = {}) {
   // Sort albums before tracks...
   sortByConditions(data, [(t) => t.album === undefined]);
 
@@ -297,7 +297,7 @@ export function sortAlbumsTracksChronologically(data, { getDate } = {}) {
   // released on the same date, they'll still be grouped together by album,
   // and tracks within an album will retain their relative positioning (i.e.
   // stay in the same order as part of the album's track listing).
-  sortByDate(data, { getDate });
+  sortByDate(data, {getDate});
 
   return data;
 }
@@ -310,17 +310,17 @@ export function filterAlbumsByCommentary(albums) {
   );
 }
 
-export function getAlbumCover(album, { to }) {
+export function getAlbumCover(album, {to}) {
   // Some albums don't have art! This function returns null in that case.
   if (album.hasCoverArt) {
-    return to("media.albumCover", album.directory, album.coverArtFileExtension);
+    return to('media.albumCover', album.directory, album.coverArtFileExtension);
   } else {
     return null;
   }
 }
 
 export function getAlbumListTag(album) {
-  return album.hasTrackNumbers ? "ol" : "ul";
+  return album.hasTrackNumbers ? 'ol' : 'ul';
 }
 
 // This gets all the track o8jects defined in every al8um, and sorts them 8y
@@ -352,8 +352,8 @@ export function getArtistNumContributions(artist) {
   );
 }
 
-export function getFlashCover(flash, { to }) {
-  return to("media.flashArt", flash.directory, flash.coverArtFileExtension);
+export function getFlashCover(flash, {to}) {
+  return to('media.flashArt', flash.directory, flash.coverArtFileExtension);
 }
 
 export function getFlashLink(flash) {
@@ -364,16 +364,16 @@ export function getTotalDuration(tracks) {
   return tracks.reduce((duration, track) => duration + track.duration, 0);
 }
 
-export function getTrackCover(track, { to }) {
+export function getTrackCover(track, {to}) {
   // Some albums don't have any track art at all, and in those, every track
   // just inherits the album's own cover art. Note that since cover art isn't
   // guaranteed on albums either, it's possible that this function returns
   // null!
   if (!track.hasCoverArt) {
-    return getAlbumCover(track.album, { to });
+    return getAlbumCover(track.album, {to});
   } else {
     return to(
-      "media.trackCover",
+      'media.trackCover',
       track.album.directory,
       track.directory,
       track.coverArtFileExtension
@@ -381,14 +381,14 @@ export function getTrackCover(track, { to }) {
   }
 }
 
-export function getArtistAvatar(artist, { to }) {
-  return to("media.artistAvatar", artist.directory, artist.avatarFileExtension);
+export function getArtistAvatar(artist, {to}) {
+  return to('media.artistAvatar', artist.directory, artist.avatarFileExtension);
 }
 
 // Big-ass homepage row functions
 
-export function getNewAdditions(numAlbums, { wikiData }) {
-  const { albumData } = wikiData;
+export function getNewAdditions(numAlbums, {wikiData}) {
+  const {albumData} = wikiData;
 
   // Sort al8ums, in descending order of priority, 8y...
   //
@@ -486,11 +486,11 @@ export function getNewAdditions(numAlbums, { wikiData }) {
   // Finally, do some quick mapping shenanigans to 8etter display the result
   // in a grid. (This should pro8a8ly 8e a separ8te, shared function, 8ut
   // whatevs.)
-  return albums.map((album) => ({ large: album.isMajorRelease, item: album }));
+  return albums.map((album) => ({large: album.isMajorRelease, item: album}));
 }
 
-export function getNewReleases(numReleases, { wikiData }) {
-  const { albumData } = wikiData;
+export function getNewReleases(numReleases, {wikiData}) {
+  const {albumData} = wikiData;
 
   const latestFirst = albumData
     .filter((album) => album.isListedOnHomepage)
@@ -503,7 +503,7 @@ export function getNewReleases(numReleases, { wikiData }) {
     .slice(0, numReleases - majorReleases.length);
 
   return [
-    ...majorReleases.map((album) => ({ large: true, item: album })),
-    ...otherReleases.map((album) => ({ large: false, item: album })),
+    ...majorReleases.map((album) => ({large: true, item: album})),
+    ...otherReleases.map((album) => ({large: false, item: album})),
   ];
 }