« 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/aggregate.js729
-rw-r--r--src/util/cli.js473
-rw-r--r--src/util/colors.js44
-rw-r--r--src/util/external-links.js1024
-rw-r--r--src/util/html.js2017
-rw-r--r--src/util/node-utils.js102
-rw-r--r--src/util/replacer.js852
-rw-r--r--src/util/search-spec.js259
-rw-r--r--src/util/serialize.js77
-rw-r--r--src/util/sort.js438
-rw-r--r--src/util/sugar.js849
-rw-r--r--src/util/urls.js251
-rw-r--r--src/util/wiki-data.js475
13 files changed, 0 insertions, 7590 deletions
diff --git a/src/util/aggregate.js b/src/util/aggregate.js
deleted file mode 100644
index e8f45f3b..00000000
--- a/src/util/aggregate.js
+++ /dev/null
@@ -1,729 +0,0 @@
-import {colors} from './cli.js';
-import {empty, typeAppearance} from './sugar.js';
-
-// Utility function for providing useful interfaces to the JS AggregateError
-// class.
-//
-// Generally, this works by returning a set of interfaces which operate on
-// functions: wrap() takes a function and returns a new function which passes
-// its arguments through and appends any resulting error to the internal error
-// list; call() simplifies this process by wrapping the provided function and
-// then calling it immediately. Once the process for which errors should be
-// aggregated is complete, close() constructs and throws an AggregateError
-// object containing all caught errors (or doesn't throw anything if there were
-// no errors).
-export function openAggregate({
-  // Constructor to use, defaulting to the builtin AggregateError class.
-  // Anything passed here should probably extend from that! May be used for
-  // letting callers programatically distinguish between multiple aggregate
-  // errors.
-  //
-  // This should be provided using the aggregateThrows utility function.
-  [openAggregate.errorClassSymbol]: errorClass = AggregateError,
-
-  // Optional human-readable message to describe the aggregate error, if
-  // constructed.
-  message = '',
-
-  // Optional flag to indicate that this layer of the aggregate error isn't
-  // generally useful outside of developer debugging purposes - it will be
-  // skipped by default when using showAggregate, showing contained errors
-  // inline with other children of this aggregate's parent.
-  //
-  // If set to 'single', it'll be hidden only if there's a single error in the
-  // aggregate (so it's not grouping multiple errors together).
-  translucent = false,
-
-  // 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.
-  // (This is primarily useful when wrapping a function and then providing it
-  // to another utility, e.g. array.map().)
-  returnOnFail = null,
-} = {}) {
-  const errors = [];
-
-  const aggregate = {};
-
-  aggregate.wrap =
-    (fn) =>
-    (...args) => {
-      try {
-        return fn(...args);
-      } catch (error) {
-        errors.push(error);
-        return typeof returnOnFail === 'function'
-          ? returnOnFail(...args)
-          : returnOnFail;
-      }
-    };
-
-  aggregate.wrapAsync =
-    (fn) =>
-    (...args) => {
-      return fn(...args).then(
-        (value) => value,
-        (error) => {
-          errors.push(error);
-          return typeof returnOnFail === 'function'
-            ? returnOnFail(...args)
-            : returnOnFail;
-        }
-      );
-    };
-
-  aggregate.push = (error) => {
-    errors.push(error);
-  };
-
-  aggregate.call = (fn, ...args) => {
-    return aggregate.wrap(fn)(...args);
-  };
-
-  aggregate.callAsync = (fn, ...args) => {
-    return aggregate.wrapAsync(fn)(...args);
-  };
-
-  aggregate.nest = (...args) => {
-    return aggregate.call(() => withAggregate(...args));
-  };
-
-  aggregate.nestAsync = (...args) => {
-    return aggregate.callAsync(() => withAggregateAsync(...args));
-  };
-
-  aggregate.receive = (results) => {
-    if (!Array.isArray(results)) {
-      if (typeof results === 'object' && results.aggregate) {
-        const {aggregate, result} = results;
-
-        try {
-          aggregate.close();
-        } catch (error) {
-          errors.push(error);
-        }
-
-        return result;
-      }
-
-      throw new Error(`Expected an array or {aggregate, result} object`);
-    }
-
-    return results.map(({aggregate, result}) => {
-      if (!aggregate) {
-        console.log('nope:', results);
-        throw new Error(`Expected an array of {aggregate, result} objects`);
-      }
-
-      try {
-        aggregate.close();
-      } catch (error) {
-        errors.push(error);
-      }
-
-      return result;
-    });
-  };
-
-  aggregate.contain = (results) => {
-    return {
-      aggregate,
-      result: aggregate.receive(results),
-    };
-  };
-
-  aggregate.map = (...args) => {
-    const parent = aggregate;
-    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);
-    parent.call(child.close);
-    return result;
-  };
-
-  aggregate.filter = (...args) => {
-    const parent = aggregate;
-    const {result, aggregate: child} = filterAggregate(...args);
-    parent.call(child.close);
-    return result;
-  };
-
-  aggregate.throws = aggregateThrows;
-
-  aggregate.close = () => {
-    if (errors.length) {
-      const error = Reflect.construct(errorClass, [errors, message]);
-
-      if (translucent) {
-        error[Symbol.for('hsmusic.aggregate.translucent')] = translucent;
-      }
-
-      throw error;
-    }
-  };
-
-  return aggregate;
-}
-
-openAggregate.errorClassSymbol = Symbol('error class');
-
-// Utility function for providing {errorClass} parameter to aggregate functions.
-export function aggregateThrows(errorClass) {
-  return {[openAggregate.errorClassSymbol]: errorClass};
-}
-
-// Helper function for allowing both (fn, opts) and (opts, fn) in aggregate
-// utilities (or other shapes besides functions).
-function _reorganizeAggregateArguments(arg1, arg2, desire = v => typeof v === 'function') {
-  if (desire(arg1)) {
-    return [arg1, arg2 ?? {}];
-  } else if (desire(arg2)) {
-    return [arg2, arg1];
-  } else {
-    return [undefined, undefined];
-  }
-}
-
-// Takes a list of {aggregate, result} objects, puts all the aggregates into
-// a new aggregate, and puts all the results into an array, returning both on
-// a new {aggregate, result} object. This is essentailly the generalized
-// composable version of functions like mapAggregate or filterAggregate.
-export function receiveAggregate(arg1, arg2) {
-  const [array, opts] = _reorganizeAggregateArguments(arg1, arg2, Array.isArray);
-  if (!array) {
-    throw new Error(`Expected an array`);
-  }
-
-  const aggregate = openAggregate(opts);
-  const result = aggregate.receive(array);
-  return {aggregate, result};
-}
-
-// Performs an ordinary array map with the given function, collating into a
-// results array (with errored inputs filtered out) and an error aggregate.
-//
-// Optionally, override returnOnFail to disable filtering and map errored inputs
-// to a particular output.
-//
-// Note the aggregate property is the result of openAggregate(), still unclosed;
-// use aggregate.close() to throw the error. (This aggregate may be passed to a
-// parent aggregate: `parent.call(aggregate.close)`!)
-export function mapAggregate(array, arg1, arg2) {
-  const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2);
-  if (!fn) {
-    throw new Error(`Expected a function`);
-  }
-
-  return _mapAggregate('sync', null, array, fn, opts);
-}
-
-export function mapAggregateAsync(array, arg1, arg2) {
-  const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2);
-  if (!fn) {
-    throw new Error(`Expected a function`);
-  }
-
-  const {promiseAll = Promise.all.bind(Promise), ...remainingOpts} = opts;
-  return _mapAggregate('async', promiseAll, array, fn, remainingOpts);
-}
-
-// Helper function for mapAggregate which holds code common between sync and
-// async versions.
-export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) {
-  const failureSymbol = Symbol();
-
-  const aggregate = openAggregate({
-    returnOnFail: failureSymbol,
-    ...aggregateOpts,
-  });
-
-  if (mode === 'sync') {
-    const result = array
-      .map(aggregate.wrap(fn))
-      .filter((value) => value !== failureSymbol);
-    return {result, aggregate};
-  } else {
-    return promiseAll(array.map(aggregate.wrapAsync(fn)))
-      .then((values) => {
-        const result = values.filter((value) => value !== failureSymbol);
-        return {result, aggregate};
-      });
-  }
-}
-
-// Performs an ordinary array filter with the given function, collating into a
-// results array (with errored inputs filtered out) and an error aggregate.
-//
-// Optionally, override returnOnFail to disable filtering errors and map errored
-// inputs to a particular output.
-//
-// As with mapAggregate, the returned aggregate property is not yet closed.
-export function filterAggregate(array, arg1, arg2) {
-  const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2);
-  if (!fn) {
-    throw new Error(`Expected a function`);
-  }
-
-  return _filterAggregate('sync', null, array, fn, opts);
-}
-
-export async function filterAggregateAsync(array, arg1, arg2) {
-  const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2);
-  if (!fn) {
-    throw new Error(`Expected a function`);
-  }
-
-  const {promiseAll = Promise.all.bind(Promise), ...remainingOpts} = opts;
-  return _filterAggregate('async', promiseAll, array, fn, remainingOpts);
-}
-
-// Helper function for filterAggregate which holds code common between sync and
-// async versions.
-function _filterAggregate(mode, promiseAll, array, fn, aggregateOpts) {
-  const failureSymbol = Symbol();
-
-  const aggregate = openAggregate({
-    returnOnFail: failureSymbol,
-    ...aggregateOpts,
-  });
-
-  function filterFunction(value) {
-    // Filter out results which match the failureSymbol, i.e. errored
-    // inputs.
-    if (value === failureSymbol) return false;
-
-    // Always keep results which match the overridden returnOnFail
-    // value, if provided.
-    if (value === aggregateOpts.returnOnFail) return true;
-
-    // Otherwise, filter according to the returned value of the wrapped
-    // function.
-    return value.output;
-  }
-
-  function mapFunction(value) {
-    // Then turn the results back into their corresponding input, or, if
-    // provided, the overridden returnOnFail value.
-    return value === aggregateOpts.returnOnFail ? value : value.input;
-  }
-
-  if (mode === 'sync') {
-    const result = array
-      .map(aggregate.wrap((input, index, array) => {
-        const output = fn(input, index, array);
-        return {input, output};
-      }))
-      .filter(filterFunction)
-      .map(mapFunction);
-
-    return {result, aggregate};
-  } else {
-    return promiseAll(
-      array.map(aggregate.wrapAsync(async (input, index, array) => {
-        const output = await fn(input, index, array);
-        return {input, output};
-      }))
-    ).then((values) => {
-      const result = values.filter(filterFunction).map(mapFunction);
-
-      return {result, aggregate};
-    });
-  }
-}
-
-// Totally sugar function for opening an aggregate, running the provided
-// function with it, then closing the function and returning the result (if
-// there's no throw).
-export function withAggregate(arg1, arg2) {
-  const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2);
-  if (!fn) {
-    throw new Error(`Expected a function`);
-  }
-
-  return _withAggregate('sync', opts, fn);
-}
-
-export function withAggregateAsync(arg1, arg2) {
-  const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2);
-  if (!fn) {
-    throw new Error(`Expected a function`);
-  }
-
-  return _withAggregate('async', opts, fn);
-}
-
-export function _withAggregate(mode, aggregateOpts, fn) {
-  const aggregate = openAggregate(aggregateOpts);
-
-  if (mode === 'sync') {
-    const result = fn(aggregate);
-    aggregate.close();
-    return result;
-  } else {
-    return fn(aggregate).then((result) => {
-      aggregate.close();
-      return result;
-    });
-  }
-}
-
-export const unhelpfulTraceLines = [
-  /sugar/,
-  /sort/,
-  /aggregate/,
-  /composite/,
-  /cacheable-object/,
-  /node:/,
-  /<anonymous>/,
-];
-
-export function getUsefulTraceLine(trace, {helpful, unhelpful}) {
-  if (!trace) return '';
-
-  for (const traceLine of trace.split('\n')) {
-    if (!traceLine.trim().startsWith('at')) {
-      continue;
-    }
-
-    if (!empty(unhelpful)) {
-      if (unhelpful.some(regex => regex.test(traceLine))) {
-        continue;
-      }
-    }
-
-    if (!empty(helpful)) {
-      for (const regex of helpful) {
-        const match = traceLine.match(regex);
-
-        if (match) {
-          return match[1] ?? traceLine;
-        }
-      }
-
-      continue;
-    }
-
-    return traceLine;
-  }
-
-  return '';
-}
-
-export function showAggregate(topError, {
-  pathToFileURL = f => f,
-  showTraces = true,
-  showTranslucent = showTraces,
-  print = true,
-} = {}) {
-  const getTranslucency = error =>
-    error[Symbol.for('hsmusic.aggregate.translucent')] ?? false;
-
-  const determineCauseHelper = cause => {
-    if (!cause) {
-      return null;
-    }
-
-    const translucency = getTranslucency(cause);
-
-    if (!translucency) {
-      return cause;
-    }
-
-    if (translucency === 'single') {
-      if (cause.errors?.length === 1) {
-        return determineCauseHelper(cause.errors[0]);
-      } else {
-        return cause;
-      }
-    }
-
-    return determineCauseHelper(cause.cause);
-  };
-
-  const determineCause = error =>
-    (showTranslucent
-      ? error.cause ?? null
-      : determineCauseHelper(error.cause));
-
-  const determineErrorsHelper = error => {
-    const translucency = getTranslucency(error);
-
-    if (!translucency) {
-      return [error];
-    }
-
-    if (translucency === 'single' && error.errors?.length >= 2) {
-      return [error];
-    }
-
-    const errors = [];
-
-    if (error.cause) {
-      errors.push(...determineErrorsHelper(error.cause));
-    }
-
-    if (error.errors) {
-      errors.push(...error.errors.flatMap(determineErrorsHelper));
-    }
-
-    return errors;
-  };
-
-  const determineErrors = error =>
-    (showTranslucent
-      ? error.errors ?? null
-      : error.errors?.flatMap(determineErrorsHelper) ?? null);
-
-  const flattenErrorStructure = (error, level = 0) => {
-    const cause = determineCause(error);
-    const errors = determineErrors(error);
-
-    return {
-      level,
-
-      kind: error.constructor.name,
-      message: error.message,
-
-      trace:
-        (error[Symbol.for(`hsmusic.aggregate.traceFrom`)]
-          ? error[Symbol.for(`hsmusic.aggregate.traceFrom`)].stack
-          : error.stack),
-
-      cause:
-        (cause
-          ? flattenErrorStructure(cause, level + 1)
-          : null),
-
-      errors:
-        (errors
-          ? errors.map(error => flattenErrorStructure(error, level + 1))
-          : null),
-
-      options: {
-        alwaysTrace:
-          error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)],
-
-        helpfulTraceLines:
-          error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)],
-
-        unhelpfulTraceLines:
-          error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)],
-      }
-    };
-  };
-
-  const recursive = ({
-    level,
-    kind,
-    message,
-    trace,
-    cause,
-    errors,
-    options: {
-      alwaysTrace,
-      helpfulTraceLines: ownHelpfulTraceLines,
-      unhelpfulTraceLines: ownUnhelpfulTraceLines,
-    },
-  }, index, apparentSiblings) => {
-    const subApparentSiblings =
-      (cause && errors
-        ? [cause, ...errors]
-     : cause
-        ? [cause]
-     : errors
-        ? errors
-        : []);
-
-    const anythingHasErrorsThisLayer =
-      apparentSiblings.some(({errors}) => !empty(errors));
-
-    const messagePart =
-      message || `(no message)`;
-
-    const kindPart =
-      kind || `unnamed kind`;
-
-    let headerPart =
-      (showTraces
-        ? `[${kindPart}] ${messagePart}`
-     : errors
-        ? `[${messagePart}]`
-     : anythingHasErrorsThisLayer
-        ? ` ${messagePart}`
-        : messagePart);
-
-    if (showTraces || alwaysTrace) {
-      const traceLine =
-        getUsefulTraceLine(trace, {
-          unhelpful:
-            (ownUnhelpfulTraceLines
-              ? unhelpfulTraceLines.concat(ownUnhelpfulTraceLines)
-              : unhelpfulTraceLines),
-
-          helpful:
-            (ownHelpfulTraceLines
-              ? ownHelpfulTraceLines
-              : null),
-        });
-
-      const tracePart =
-        (traceLine
-          ? '- ' +
-            traceLine
-              .trim()
-              .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match))
-          : '(no stack trace)');
-
-      headerPart += ` ${colors.dim(tracePart)}`;
-    }
-
-    const head1 = level % 2 === 0 ? '\u21aa' : colors.dim('\u21aa');
-    const bar1 = ' ';
-
-    const causePart =
-      (cause
-        ? recursive(cause, 0, subApparentSiblings)
-            .split('\n')
-            .map((line, i) => i === 0 ? ` ${head1} ${line}` : ` ${bar1} ${line}`)
-            .join('\n')
-        : '');
-
-    const head2 = level % 2 === 0 ? '\u257f' : colors.dim('\u257f');
-    const bar2 = level % 2 === 0 ? '\u2502' : colors.dim('\u254e');
-
-    const errorsPart =
-      (errors
-        ? errors
-            .map((error, index) => recursive(error, index + 1, subApparentSiblings))
-            .flatMap(str => str.split('\n'))
-            .map((line, i) => i === 0 ? ` ${head2} ${line}` : ` ${bar2} ${line}`)
-            .join('\n')
-        : '');
-
-    return [headerPart, errorsPart, causePart].filter(Boolean).join('\n');
-  };
-
-  const structure = flattenErrorStructure(topError);
-  const message = recursive(structure, 0, [structure]);
-
-  if (print) {
-    console.error(message);
-  } else {
-    return message;
-  }
-}
-
-export function annotateError(error, ...callbacks) {
-  for (const callback of callbacks) {
-    error = callback(error) ?? error;
-  }
-
-  return error;
-}
-
-export function annotateErrorWithIndex(error, index) {
-  return Object.assign(error, {
-    [Symbol.for('hsmusic.annotateError.indexInSourceArray')]:
-      index,
-
-    message:
-      `(${colors.yellow(`#${index + 1}`)}) ` +
-      error.message,
-  });
-}
-
-export function annotateErrorWithFile(error, file) {
-  return Object.assign(error, {
-    [Symbol.for('hsmusic.annotateError.file')]:
-      file,
-
-    message:
-      error.message +
-      (error.message.includes('\n') ? '\n' : ' ') +
-      `(file: ${colors.bright(colors.blue(file))})`,
-  });
-}
-
-export function asyncAdaptiveDecorateError(fn, callback) {
-  if (typeof callback !== 'function') {
-    throw new Error(`Expected callback to be a function, got ${typeAppearance(callback)}`);
-  }
-
-  const syncDecorated = function (...args) {
-    try {
-      return fn(...args);
-    } catch (caughtError) {
-      throw callback(caughtError, ...args);
-    }
-  };
-
-  const asyncDecorated = async function(...args) {
-    try {
-      return await fn(...args);
-    } catch (caughtError) {
-      throw callback(caughtError, ...args);
-    }
-  };
-
-  syncDecorated.async = asyncDecorated;
-
-  return syncDecorated;
-}
-
-export function decorateError(fn, callback) {
-  return asyncAdaptiveDecorateError(fn, callback);
-}
-
-export function asyncDecorateError(fn, callback) {
-  return asyncAdaptiveDecorateError(fn, callback).async;
-}
-
-export function decorateErrorWithAnnotation(fn, ...annotationCallbacks) {
-  return asyncAdaptiveDecorateError(fn,
-    (caughtError, ...args) =>
-      annotateError(caughtError,
-        ...annotationCallbacks
-          .map(callback => error => callback(error, ...args))));
-}
-
-export function decorateErrorWithIndex(fn) {
-  return decorateErrorWithAnnotation(fn,
-    (caughtError, _value, index) =>
-      annotateErrorWithIndex(caughtError, index));
-}
-
-export function decorateErrorWithCause(fn, cause) {
-  return asyncAdaptiveDecorateError(fn,
-    (caughtError) =>
-      Object.assign(caughtError, {cause}));
-}
-
-export function asyncDecorateErrorWithAnnotation(fn, ...annotationCallbacks) {
-  return decorateErrorWithAnnotation(fn, ...annotationCallbacks).async;
-}
-
-export function asyncDecorateErrorWithIndex(fn) {
-  return decorateErrorWithIndex(fn).async;
-}
-
-export function asyncDecorateErrorWithCause(fn, cause) {
-  return decorateErrorWithCause(fn, cause).async;
-}
-
-export function conditionallySuppressError(conditionFn, callbackFn) {
-  return (...args) => {
-    try {
-      return callbackFn(...args);
-    } catch (error) {
-      if (conditionFn(error, ...args) === true) {
-        return;
-      }
-
-      throw error;
-    }
-  };
-}
diff --git a/src/util/cli.js b/src/util/cli.js
deleted file mode 100644
index 72979d3f..00000000
--- a/src/util/cli.js
+++ /dev/null
@@ -1,473 +0,0 @@
-// 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;
-
-export const ENABLE_COLOR =
-  process &&
-  ((process.env.CLICOLOR_FORCE && process.env.CLICOLOR_FORCE === '1') ??
-    (process.env.CLICOLOR &&
-      process.env.CLICOLOR === '1' &&
-      process.stdout.hasColors &&
-      process.stdout.hasColors()) ??
-    (process.stdout.hasColors ? process.stdout.hasColors() : true));
-
-const C = (n) =>
-  ENABLE_COLOR ? (text) => `\x1b[${n}m${text}\x1b[0m` : (text) => text;
-
-export const colors = {
-  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 =
-  (color) =>
-  (literals, ...values) => {
-    const w = (s) => process.stdout.write(s);
-    const wc = (text) => {
-      if (ENABLE_COLOR) w(text);
-    };
-
-    wc(`\x1b[${color}m`);
-    for (let i = 0; i < literals.length; i++) {
-      w(literals[i]);
-      if (values[i] !== undefined) {
-        wc(`\x1b[1m`);
-        w(String(values[i]));
-        wc(`\x1b[0;${color}m`);
-      }
-    }
-    wc(`\x1b[0m`);
-    w('\n');
-  };
-
-export const logInfo = logColor(2);
-export const logWarn = logColor(33);
-export const logError = logColor(31);
-
-// Stolen as #@CK from mtui!
-export async function parseOptions(options, optionDescriptorMap) {
-  // This function is sorely lacking in comments, but the basic usage is
-  // as such:
-  //
-  // options is the array of options you want to process;
-  // optionDescriptorMap is a mapping of option names to objects that describe
-  // the expected value for their corresponding options.
-  //
-  // Returned is...
-  // - a mapping of any specified option names to their values
-  // - a process.exit(1) and error message if there were any issues
-  //
-  // Here are examples of optionDescriptorMap to cover all the things you can
-  // do with it:
-  //
-  // optionDescriptorMap: {
-  //   'telnet-server': {type: 'flag'},
-  //   't': {alias: 'telnet-server'}
-  // }
-  //
-  // options: ['t'] -> result: {'telnet-server': true}
-  //
-  // optionDescriptorMap: {
-  //   'directory': {
-  //     type: 'value',
-  //     validate(name) {
-  //       // const whitelistedDirectories = ['apple', 'banana']
-  //       if (whitelistedDirectories.includes(name)) {
-  //         return true
-  //       } else {
-  //         return 'a whitelisted directory'
-  //       }
-  //     }
-  //   },
-  //   'files': {type: 'series'}
-  // }
-  //
-  // ['--directory', 'apple'] -> {'directory': 'apple'}
-  // ['--directory', 'artichoke'] -> (error)
-  // ['--files', 'a', 'b', 'c', ';'] -> {'files': ['a', 'b', 'c']}
-
-  const handleDashless = optionDescriptorMap[parseOptions.handleDashless];
-  const handleUnknown = optionDescriptorMap[parseOptions.handleUnknown];
-
-  const result = Object.create(null);
-  for (let i = 0; i < options.length; i++) {
-    const option = options[i];
-    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 descriptor = optionDescriptorMap[name];
-
-      if (!descriptor) {
-        if (handleUnknown) {
-          handleUnknown(option);
-        } else {
-          console.error(`Unknown option name: ${name}`);
-          process.exit(1);
-        }
-        continue;
-      }
-
-      if (descriptor.alias) {
-        name = descriptor.alias;
-        descriptor = optionDescriptorMap[name];
-      }
-
-      switch (descriptor.type) {
-        case 'flag': {
-          result[name] = true;
-          break;
-        }
-
-        case 'value': {
-          let value = option.slice(2).split('=')[1];
-          if (!value) {
-            value = options[++i];
-            if (!value || value.startsWith('-')) {
-              value = null;
-            }
-          }
-
-          if (!value) {
-            console.error(`Expected a value for --${name}`);
-            process.exit(1);
-          }
-
-          result[name] = value;
-          break;
-        }
-
-        case '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(';');
-          result[name] = options.slice(i + 1, endIndex);
-          i = endIndex;
-          break;
-        }
-      }
-
-      if (descriptor.validate) {
-        const validation = await descriptor.validate(result[name]);
-        if (validation !== true) {
-          console.error(`Expected ${validation} for --${name}`);
-          process.exit(1);
-        }
-      }
-    } 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);
-      let descriptor = optionDescriptorMap[name];
-      if (!descriptor) {
-        if (handleUnknown) {
-          handleUnknown(option);
-        } else {
-          console.error(`Unknown option name: ${name}`);
-          process.exit(1);
-        }
-        continue;
-      }
-
-      if (descriptor.alias) {
-        name = descriptor.alias;
-        descriptor = optionDescriptorMap[name];
-      }
-
-      if (descriptor.type === 'flag') {
-        result[name] = true;
-      } else {
-        console.error(`Use --${name} (value) to specify ${name}`);
-        process.exit(1);
-      }
-    } else if (handleDashless) {
-      handleDashless(option);
-    }
-  }
-  return result;
-}
-
-// Takes precisely the same sort of structure as `parseOptions` above,
-// and displays associated help messages. Radical!
-//
-// 'indentWrap' should be the function from '#sugar', with its wrap option
-//   already bound.
-//
-// 'sort' should take care of sorting a list of {name, descriptor} entries.
-export function showHelpForOptions({
-  heading,
-  options,
-  indentWrap,
-  sort = entries => entries,
-}) {
-  if (heading) {
-    console.log(colors.bright(heading));
-  }
-
-  const sortedOptions =
-    sort(
-      Object.entries(options)
-        .map(([name, descriptor]) => ({name, descriptor})));
-
-  if (!sortedOptions.length) {
-    console.log(`(No options available)`)
-  }
-
-  let justInsertedPaddingLine = false;
-
-  for (const {name, descriptor} of sortedOptions) {
-    if (descriptor.alias) {
-      continue;
-    }
-
-    const aliases =
-      Object.entries(options)
-        .filter(([_name, {alias}]) => alias === name)
-        .map(([name]) => name);
-
-    let wrappedHelp, wrappedHelpLines = 0;
-    if (descriptor.help) {
-      wrappedHelp = indentWrap(descriptor.help, {spaces: 4});
-      wrappedHelpLines = wrappedHelp.split('\n').length;
-    }
-
-    if (wrappedHelpLines > 0 && !justInsertedPaddingLine) {
-      console.log('');
-    }
-
-    console.log(colors.bright(` --` + name) +
-      (aliases.length
-        ? ` (or: ${aliases.map(alias => colors.bright(`--` + alias)).join(', ')})`
-        : '') +
-      (descriptor.help
-        ? ''
-        : colors.dim('  (no help provided)')));
-
-    if (wrappedHelp) {
-      console.log(wrappedHelp);
-    }
-
-    if (wrappedHelpLines > 1) {
-      console.log('');
-      justInsertedPaddingLine = true;
-    } else {
-      justInsertedPaddingLine = false;
-    }
-  }
-
-  if (!justInsertedPaddingLine) {
-    console.log(``);
-  }
-}
-
-export const handleDashless = Symbol();
-export const handleUnknown = Symbol();
-
-export function decorateTime(arg1, arg2) {
-  const [id, functionToBeWrapped] =
-    typeof arg1 === 'string' || typeof arg1 === 'symbol'
-      ? [arg1, arg2]
-      : [Symbol(arg1.name), arg1];
-
-  const meta = decorateTime.idMetaMap[id] ?? {
-    wrappedName: functionToBeWrapped.name,
-    timeSpent: 0,
-    timesCalled: 0,
-    displayTime() {
-      const align1 = 48;
-      const align2 = 22;
-
-      const averageTime = (meta.timeSpent / meta.timesCalled).toExponential(1);
-      const idPart = typeof id === 'symbol' ? id.description : id;
-      const timePart = `${meta.timeSpent} ms / ${meta.timesCalled} calls`;
-      const avgPart = `(avg: ${averageTime} ms)`;
-
-      const alignPart1 =
-        (idPart.length >= align1
-          ? ' '
-          : ' ' + '.'.repeat(Math.max(0, align1 - 2 - idPart.length)) + ' ');
-
-      const alignPart2 =
-        (timePart.length >= align2
-          ? ' '
-          : ' '.repeat(Math.max(0, align2 - timePart.length)));
-
-      console.log(
-        colors.bright(idPart) +
-        alignPart1 +
-        timePart +
-        alignPart2 +
-        colors.dim(avgPart));
-    },
-  };
-
-  decorateTime.idMetaMap[id] = meta;
-
-  const fn = function (...args) {
-    const start = Date.now();
-    const ret = functionToBeWrapped.apply(this, args);
-    const end = Date.now();
-    meta.timeSpent += end - start;
-    meta.timesCalled++;
-    return ret;
-  };
-
-  fn.displayTime = meta.displayTime;
-
-  return fn;
-}
-
-decorateTime.idMetaMap = Object.create(null);
-
-decorateTime.displayTime = function () {
-  const map = decorateTime.idMetaMap;
-
-  const keys = [
-    ...Object.getOwnPropertySymbols(map),
-    ...Object.getOwnPropertyNames(map),
-  ];
-
-  if (!keys.length) {
-    return;
-  }
-
-  console.log(`\x1b[1mdecorateTime results: ` + '-'.repeat(40) + '\x1b[0m');
-
-  const metas =
-    keys
-      .map(key => map[key])
-      .filter(meta => meta.timeSpent >= 1)  // Milliseconds!
-      .sort((a, b) => a.timeSpent - b.timeSpent);
-
-  for (const meta of metas) {
-    meta.displayTime();
-  }
-};
-
-export function progressPromiseAll(msgOrMsgFn, array) {
-  if (!array.length) {
-    return Promise.resolve([]);
-  }
-
-  const msgFn =
-    typeof msgOrMsgFn === 'function' ? msgOrMsgFn : () => msgOrMsgFn;
-
-  let done = 0,
-    total = array.length;
-  process.stdout.write(`\r${msgFn()} [0/${total}]`);
-  const start = Date.now();
-  return Promise.all(
-    array.map((promise) =>
-      Promise.resolve(promise).then((val) => {
-        done++;
-        // const pc = `${done}/${total}`;
-        const pc = (Math.round((done / total) * 1000) / 10 + '%').padEnd(
-          '99.9%'.length,
-          ' '
-        );
-        if (done === total) {
-          const time = Date.now() - start;
-          process.stdout.write(
-            `\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n`
-          );
-        } else {
-          process.stdout.write(`\r${msgFn()} [${pc}] `);
-        }
-        return val;
-      })
-    )
-  );
-}
-
-export function progressCallAll(msgOrMsgFn, array) {
-  if (!array.length) {
-    return [];
-  }
-
-  const msgFn =
-    typeof msgOrMsgFn === 'function' ? msgOrMsgFn : () => msgOrMsgFn;
-
-  const updateInterval = 1000 / 60;
-
-  let done = 0,
-    total = array.length;
-  process.stdout.write(`\r${msgFn()} [0/${total}]`);
-  const start = Date.now();
-  const vals = [];
-  let lastTime = 0;
-
-  for (const fn of array) {
-    const val = fn();
-    done++;
-
-    if (done === total) {
-      const pc = '100%'.padEnd('99.9%'.length, ' ');
-      const time = Date.now() - start;
-      process.stdout.write(
-        `\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n`
-      );
-    } else if (Date.now() - lastTime >= updateInterval) {
-      const pc = (Math.round((done / total) * 1000) / 10 + '%').padEnd('99.9%'.length, ' ');
-      process.stdout.write(`\r${msgFn()} [${pc}] `);
-      lastTime = Date.now();
-    }
-    vals.push(val);
-  }
-
-  return vals;
-}
-
-export function fileIssue({
-  topMessage = `This shouldn't happen.`,
-} = {}) {
-  if (topMessage) {
-    console.error(colors.red(`${topMessage} Please let the HSMusic developers know:`));
-  }
-  console.error(colors.red(`- https://hsmusic.wiki/feedback/`));
-  console.error(colors.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`));
-}
-
-export async function logicalCWD() {
-  if (process.env.PWD) {
-    return process.env.PWD;
-  }
-
-  const {exec} = await import('node:child_process');
-  const {stat} = await import('node:fs/promises');
-
-  try {
-    await stat('/bin/sh');
-  } catch (error) {
-    // Not logical, so sad.
-    return process.cwd();
-  }
-
-  const proc = exec('/bin/pwd -L');
-
-  let output = '';
-  proc.stdout.on('data', buf => { output += buf; });
-
-  await new Promise(resolve => proc.on('exit', resolve));
-
-  return output.trim();
-}
-
-export async function logicalPathTo(target) {
-  const {relative} = await import('node:path');
-  const cwd = await logicalCWD();
-  return relative(cwd, target);
-}
diff --git a/src/util/colors.js b/src/util/colors.js
deleted file mode 100644
index 7298c46a..00000000
--- a/src/util/colors.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// Color and theming utility functions! Handy.
-
-export function getColors(themeColor, {
-  // chroma.js external dependency (https://gka.github.io/chroma.js/)
-  chroma,
-} = {}) {
-  if (!chroma) {
-    throw new Error('chroma.js library must be passed or bound for color manipulation');
-  }
-
-  const primary = chroma(themeColor);
-
-  const dark = primary.luminance(0.02);
-  const dim = primary.desaturate(2).darken(1.5);
-  const deep = primary.saturate(1.2).luminance(0.035);
-  const deepGhost = deep.alpha(0.8);
-  const light = chroma.average(['#ffffff', primary], 'rgb', [4, 1]);
-  const lightGhost = primary.luminance(0.8).saturate(4).alpha(0.08);
-
-  const bg = primary.luminance(0.008).desaturate(3.5).alpha(0.8);
-  const bgBlack = primary.saturate(1).luminance(0.0025).alpha(0.8);
-  const shadow = primary.desaturate(4).set('hsl.l', 0.05).alpha(0.8);
-
-  const hsl = primary.hsl();
-  if (isNaN(hsl[0])) hsl[0] = 0;
-
-  return {
-    primary: primary.hex(),
-
-    dark: dark.hex(),
-    dim: dim.hex(),
-    deep: deep.hex(),
-    deepGhost: deepGhost.hex(),
-    light: light.hex(),
-    lightGhost: lightGhost.hex(),
-
-    bg: bg.hex(),
-    bgBlack: bgBlack.hex(),
-    shadow: shadow.hex(),
-
-    rgb: primary.rgb(),
-    hsl,
-  };
-}
diff --git a/src/util/external-links.js b/src/util/external-links.js
deleted file mode 100644
index 43c09265..00000000
--- a/src/util/external-links.js
+++ /dev/null
@@ -1,1024 +0,0 @@
-import {empty, stitchArrays, withEntries} from '#sugar';
-
-import {
-  anyOf,
-  is,
-  isBoolean,
-  isObject,
-  isStringNonEmpty,
-  looseArrayOf,
-  optional,
-  validateAllPropertyValues,
-  validateArrayItems,
-  validateInstanceOf,
-  validateProperties,
-} from '#validators';
-
-export const externalLinkStyles = [
-  'platform',
-  'handle',
-  'icon-id',
-];
-
-export const isExternalLinkStyle = is(...externalLinkStyles);
-
-export const externalLinkContexts = [
-  'album',
-  'albumOneTrack',
-  'albumMultipleTracks',
-  'albumNoTracks',
-  'artist',
-  'flash',
-  'generic',
-  'group',
-  'track',
-];
-
-export const isExternalLinkContext =
-  anyOf(
-    is(...externalLinkContexts),
-    looseArrayOf(is(...externalLinkContexts)));
-
-// This might need to be adjusted for YAML importing...
-const isRegExp =
-  validateInstanceOf(RegExp);
-
-export const isExternalLinkTransformCommand =
-  is(...[
-    'decode-uri',
-    'find-replace',
-  ]);
-
-export const isExternalLinkTransformSpec =
-  anyOf(
-    isExternalLinkTransformCommand,
-    validateProperties({
-      [validateProperties.allowOtherKeys]: true,
-      command: isExternalLinkTransformCommand,
-    }));
-
-export const isExternalLinkExtractSpec =
-  validateProperties({
-    prefix: optional(isStringNonEmpty),
-    transform: optional(validateArrayItems(isExternalLinkTransformSpec)),
-    url: optional(isRegExp),
-    domain: optional(isRegExp),
-    pathname: optional(isRegExp),
-    query: optional(isRegExp),
-  });
-
-export const isExternalLinkSpec =
-  validateArrayItems(
-    validateProperties({
-      match: validateProperties({
-        // TODO: Don't allow providing both of these, and require providing one
-        domain: optional(isStringNonEmpty),
-        domains: optional(validateArrayItems(isStringNonEmpty)),
-
-        // TODO: Don't allow providing both of these
-        pathname: optional(isRegExp),
-        pathnames: optional(validateArrayItems(isRegExp)),
-
-        // TODO: Don't allow providing both of these
-        query: optional(isRegExp),
-        queries: optional(validateArrayItems(isRegExp)),
-
-        context: optional(isExternalLinkContext),
-      }),
-
-      platform: isStringNonEmpty,
-
-      handle: optional(isExternalLinkExtractSpec),
-
-      detail:
-        optional(anyOf(
-          isStringNonEmpty,
-          validateProperties({
-            [validateProperties.validateOtherKeys]:
-              isExternalLinkExtractSpec,
-
-            substring: isStringNonEmpty,
-          }))),
-
-      unusualDomain: optional(isBoolean),
-
-      icon: optional(isStringNonEmpty),
-    }));
-
-export const fallbackDescriptor = {
-  platform: 'external',
-  icon: 'globe',
-};
-
-// TODO: Define all this stuff in data as YAML!
-export const externalLinkSpec = [
-  // Special handling for album links
-
-  {
-    match: {
-      context: 'album',
-      domain: 'youtube.com',
-      pathname: /^playlist/,
-    },
-
-    platform: 'youtube',
-    detail: 'playlist',
-
-    icon: 'youtube',
-  },
-
-  {
-    match: {
-      context: 'albumMultipleTracks',
-      domain: 'youtube.com',
-      pathname: /^watch/,
-    },
-
-    platform: 'youtube',
-    detail: 'fullAlbum',
-
-    icon: 'youtube',
-  },
-
-  {
-    match: {
-      context: 'albumMultipleTracks',
-      domain: 'youtu.be',
-    },
-
-    platform: 'youtube',
-    detail: 'fullAlbum',
-
-    icon: 'youtube',
-  },
-
-  // Special handling for flash links
-
-  {
-    match: {
-      context: 'flash',
-      domain: 'bgreco.net',
-    },
-
-    platform: 'bgreco',
-    detail: 'flash',
-
-    icon: 'globe',
-  },
-
-  // This takes precedence over the secretPage match below.
-  {
-    match: {
-      context: 'flash',
-      domain: 'homestuck.com',
-    },
-
-    platform: 'homestuck',
-
-    detail: {
-      substring: 'page',
-      page: {pathname: /^story\/([0-9]+)\/?$/,},
-    },
-
-    icon: 'globe',
-  },
-
-  {
-    match: {
-      context: 'flash',
-      domain: 'homestuck.com',
-      pathname: /^story\/.+\/?$/,
-    },
-
-    platform: 'homestuck',
-    detail: 'secretPage',
-
-    icon: 'globe',
-  },
-
-  {
-    match: {
-      context: 'flash',
-      domains: ['youtube.com', 'youtu.be'],
-    },
-
-    platform: 'youtube',
-    detail: 'flash',
-
-    icon: 'youtube',
-  },
-
-  // Generic domains, sorted alphabetically (by string)
-
-  {
-    match: {
-      domains: [
-        'music.amazon.co.jp',
-        'music.amazon.com',
-      ],
-    },
-
-    platform: 'amazonMusic',
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: 'music.apple.com'},
-    platform: 'appleMusic',
-    icon: 'appleMusic',
-  },
-
-  {
-    match: {domain: 'artstation.com'},
-
-    platform: 'artstation',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'artstation',
-  },
-
-  {
-    match: {domain: '.artstation.com'},
-
-    platform: 'artstation',
-    handle: {domain: /^[^.]+/},
-
-    icon: 'artstation',
-  },
-
-  {
-    match: {domains: ['bc.s3m.us', 'music.solatrus.com']},
-
-    platform: 'bandcamp',
-    handle: {domain: /.+/},
-    unusualDomain: true,
-
-    icon: 'bandcamp',
-  },
-
-  {
-    match: {domain: '.bandcamp.com'},
-
-    platform: 'bandcamp',
-    handle: {domain: /^[^.]+/},
-
-    icon: 'bandcamp',
-  },
-
-  {
-    match: {domain: 'bsky.app'},
-
-    platform: 'bluesky',
-    handle: {pathname: /^profile\/([^/]+?)(?:\.bsky\.social)?\/?$/},
-
-    icon: 'bluesky',
-  },
-
-  {
-    match: {domain: '.carrd.co'},
-
-    platform: 'carrd',
-    handle: {domain: /^[^.]+/},
-
-    icon: 'carrd',
-  },
-
-  {
-    match: {domain: 'cohost.org'},
-
-    platform: 'cohost',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'cohost',
-  },
-
-  {
-    match: {domain: 'music.deconreconstruction.com'},
-    platform: 'deconreconstruction.music',
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: 'deconreconstruction.com'},
-    platform: 'deconreconstruction',
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: '.deviantart.com'},
-
-    platform: 'deviantart',
-    handle: {domain: /^[^.]+/},
-
-    icon: 'deviantart',
-  },
-
-  {
-    match: {domain: 'deviantart.com'},
-
-    platform: 'deviantart',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'deviantart',
-  },
-
-  {
-    match: {domain: 'deviantart.com'},
-    platform: 'deviantart',
-    icon: 'deviantart',
-  },
-
-  {
-    match: {domain: 'facebook.com'},
-
-    platform: 'facebook',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'facebook',
-  },
-
-  {
-    match: {domain: 'facebook.com'},
-
-    platform: 'facebook',
-    handle: {pathname: /^(?:pages|people)\/([^/]+)\/[0-9]+\/?$/},
-
-    icon: 'facebook',
-  },
-
-  {
-    match: {domain: 'facebook.com'},
-    platform: 'facebook',
-    icon: 'facebook',
-  },
-
-  {
-    match: {domain: 'm.nintendo.com'},
-
-    platform: 'nintendoMusic',
-
-    icon: 'nintendoMusic',
-  },
-
-  {
-    match: {domain: 'mspaintadventures.fandom.com'},
-
-    platform: 'fandom.mspaintadventures',
-
-    detail: {
-      substring: 'page',
-      page: {
-        pathname: /^wiki\/(.+)\/?$/,
-        transform: [
-          {command: 'decode-uri'},
-          {command: 'find-replace', find: /_/g, replace: ' '},
-        ],
-      },
-    },
-
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: 'mspaintadventures.fandom.com'},
-
-    platform: 'fandom.mspaintadventures',
-
-    icon: 'globe',
-  },
-
-  {
-    match: {domains: ['fandom.com', '.fandom.com']},
-    platform: 'fandom',
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: 'gamebanana.com'},
-    platform: 'gamebanana',
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: 'homestuck.com'},
-    platform: 'homestuck',
-    icon: 'globe',
-  },
-
-  {
-    match: {
-      domain: 'hsmusic.wiki',
-      pathname: /^media\/misc\/archive/,
-    },
-
-    platform: 'hsmusic.archive',
-
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: 'hsmusic.wiki'},
-    platform: 'hsmusic',
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: 'instagram.com'},
-
-    platform: 'instagram',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'instagram',
-  },
-
-  {
-    match: {domain: 'instagram.com'},
-    platform: 'instagram',
-    icon: 'instagram',
-  },
-
-  // The Wayback Machine is a separate entry.
-  {
-    match: {domain: 'archive.org'},
-    platform: 'internetArchive',
-    icon: 'internetArchive',
-  },
-
-  {
-    match: {domain: '.itch.io'},
-
-    platform: 'itch',
-    handle: {domain: /^[^.]+/},
-
-    icon: 'itch',
-  },
-
-  {
-    match: {domain: 'itch.io'},
-
-    platform: 'itch',
-    handle: {pathname: /^profile\/([^/]+)\/?$/},
-
-    icon: 'itch',
-  },
-
-  {
-    match: {domain: 'ko-fi.com'},
-
-    platform: 'kofi',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'kofi',
-  },
-
-  {
-    match: {domain: 'linktr.ee'},
-
-    platform: 'linktree',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'linktree',
-  },
-
-  {
-    match: {domains: [
-      'mastodon.social',
-      'shrike.club',
-      'types.pl',
-    ]},
-
-    platform: 'mastodon',
-    handle: {domain: /.+/},
-    unusualDomain: true,
-
-    icon: 'mastodon',
-  },
-
-  {
-    match: {domains: ['mspfa.com', '.mspfa.com']},
-    platform: 'mspfa',
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: '.neocities.org'},
-
-    platform: 'neocities',
-    handle: {domain: /.+/},
-
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: '.newgrounds.com'},
-
-    platform: 'newgrounds',
-    handle: {domain: /^[^.]+/},
-
-    icon: 'newgrounds',
-  },
-
-  {
-    match: {domain: 'newgrounds.com'},
-    platform: 'newgrounds',
-    icon: 'newgrounds',
-  },
-
-  {
-    match: {domain: 'patreon.com'},
-
-    platform: 'patreon',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'patreon',
-  },
-
-  {
-    match: {domain: 'patreon.com'},
-    platform: 'patreon',
-    icon: 'patreon',
-  },
-
-  {
-    match: {domain: 'poetryfoundation.org'},
-    platform: 'poetryFoundation',
-    icon: 'globe',
-  },
-
-  {
-    match: {domain: 'soundcloud.com'},
-
-    platform: 'soundcloud',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'soundcloud',
-  },
-
-  {
-    match: {domain: 'soundcloud.com'},
-    platform: 'soundcloud',
-    icon: 'soundcloud',
-  },
-
-  {
-    match: {domains: ['spotify.com', 'open.spotify.com']},
-    platform: 'spotify',
-    icon: 'spotify',
-  },
-
-  {
-    match: {domains: ['store.steampowered.com', 'steamcommunity.com']},
-    platform: 'steam',
-    icon: 'steam',
-  },
-
-  {
-    match: {domain: 'tiktok.com'},
-
-    platform: 'tiktok',
-    handle: {pathname: /^@?([^/]+)\/?$/},
-
-    icon: 'tiktok',
-  },
-
-  {
-    match: {domain: 'toyhou.se'},
-
-    platform: 'toyhouse',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'toyhouse',
-  },
-
-  {
-    match: {domain: '.tumblr.com'},
-
-    platform: 'tumblr',
-    handle: {domain: /^[^.]+/},
-
-    icon: 'tumblr',
-  },
-
-  {
-    match: {domain: 'tumblr.com'},
-
-    platform: 'tumblr',
-    handle: {pathname: /^([^/]+)\/?$/},
-
-    icon: 'tumblr',
-  },
-
-  {
-    match: {domain: 'tumblr.com'},
-    platform: 'tumblr',
-    icon: 'tumblr',
-  },
-
-  {
-    match: {domain: 'twitch.tv'},
-
-    platform: 'twitch',
-    handle: {pathname: /^(.+)\/?/},
-
-    icon: 'twitch',
-  },
-
-  {
-    match: {domain: 'twitter.com'},
-
-    platform: 'twitter',
-    handle: {pathname: /^@?([^/]+)\/?$/},
-
-    icon: 'twitter',
-  },
-
-  {
-    match: {domain: 'twitter.com'},
-    platform: 'twitter',
-    icon: 'twitter',
-  },
-
-  {
-    match: {domain: 'web.archive.org'},
-    platform: 'waybackMachine',
-    icon: 'internetArchive',
-  },
-
-  {
-    match: {domains: ['wikipedia.org', '.wikipedia.org']},
-    platform: 'wikipedia',
-    icon: 'misc',
-  },
-
-  {
-    match: {domain: 'youtube.com'},
-
-    platform: 'youtube',
-    handle: {pathname: /^@([^/]+)\/?$/},
-
-    icon: 'youtube',
-  },
-
-  {
-    match: {domains: ['youtube.com', 'youtu.be']},
-    platform: 'youtube',
-    icon: 'youtube',
-  },
-];
-
-function urlParts(url) {
-  const {
-    hostname: domain,
-    pathname,
-    search: query,
-  } = new URL(url);
-
-  return {domain, pathname, query};
-}
-
-function createEmptyResults() {
-  return Object.fromEntries(externalLinkStyles.map(style => [style, null]));
-}
-
-export function getMatchingDescriptorsForExternalLink(url, descriptors, {
-  context = 'generic',
-} = {}) {
-  const {domain, pathname, query} = urlParts(url);
-
-  const compareDomain = string => {
-    // A dot at the start of the descriptor's domain indicates
-    // we're looking to match a subdomain.
-    if (string.startsWith('.')) matchSubdomain: {
-      // "www" is never an acceptable subdomain for this purpose.
-      // Sorry to people whose usernames are www!!
-      if (domain.startsWith('www.')) {
-        return false;
-      }
-
-      return domain.endsWith(string);
-    }
-
-    // No dot means we're looking for an exact/full domain match.
-    // But let "www" pass here too, implicitly.
-    return domain === string || domain === 'www.' + string;
-  };
-
-  const comparePathname = regex => regex.test(pathname.slice(1));
-  const compareQuery = regex => regex.test(query.slice(1));
-
-  const compareExtractSpec = extract =>
-    extractPartFromExternalLink(url, extract, {mode: 'test'});
-
-  const contextArray =
-    (Array.isArray(context)
-      ? context
-      : [context]).filter(Boolean);
-
-  const matchingDescriptors =
-    descriptors
-      .filter(({match}) =>
-        (match.domain
-          ? compareDomain(match.domain)
-       : match.domains
-          ? match.domains.some(compareDomain)
-          : false))
-
-      .filter(({match}) =>
-        (Array.isArray(match.context)
-          ? match.context.some(c => contextArray.includes(c))
-       : match.context
-          ? contextArray.includes(match.context)
-          : true))
-
-      .filter(({match}) =>
-        (match.pathname
-          ? comparePathname(match.pathname)
-       : match.pathnames
-          ? match.pathnames.some(comparePathname)
-          : true))
-
-      .filter(({match}) =>
-        (match.query
-          ? compareQuery(match.query)
-       : match.queries
-          ? match.quieries.some(compareQuery)
-          : true))
-
-      .filter(({handle}) =>
-        (handle
-          ? compareExtractSpec(handle)
-          : true))
-
-      .filter(({detail}) =>
-        (typeof detail === 'object'
-          ? Object.entries(detail)
-              .filter(([key]) => key !== 'substring')
-              .map(([_key, value]) => value)
-              .every(compareExtractSpec)
-          : true));
-
-  return [...matchingDescriptors, fallbackDescriptor];
-}
-
-export function extractPartFromExternalLink(url, extract, {
-  // Set to 'test' to just see if this would extract anything.
-  // This disables running custom transformations.
-  mode = 'extract',
-} = {}) {
-  const {domain, pathname, query} = urlParts(url);
-
-  let regexen = [];
-  let tests = [];
-  let transform = [];
-  let prefix = '';
-
-  if (extract instanceof RegExp) {
-    regexen.push(extract);
-    tests.push(url);
-  } else {
-    for (const [key, value] of Object.entries(extract)) {
-      switch (key) {
-        case 'prefix':
-          prefix = value;
-          continue;
-
-        case 'transform':
-          for (const entry of value) {
-            const command =
-              (typeof entry === 'string'
-                ? command
-                : entry.command);
-
-            const options =
-              (typeof entry === 'string'
-                ? {}
-                : entry);
-
-            switch (command) {
-              case 'decode-uri':
-                transform.push(value =>
-                  decodeURIComponent(value));
-                break;
-
-              case 'find-replace':
-                transform.push(value =>
-                  value.replace(options.find, options.replace));
-                break;
-            }
-          }
-          continue;
-
-        case 'url':
-          tests.push(url);
-          break;
-
-        case 'domain':
-          tests.push(domain);
-          break;
-
-        case 'pathname':
-          tests.push(pathname.slice(1));
-          break;
-
-        case 'query':
-          tests.push(query.slice(1));
-          break;
-
-        default:
-          tests.push('');
-          break;
-      }
-
-      regexen.push(value);
-    }
-  }
-
-  let value;
-  for (const {regex, test} of stitchArrays({
-    regex: regexen,
-    test: tests,
-  })) {
-    const match = test.match(regex);
-    if (match) {
-      value = match[1] ?? match[0];
-      break;
-    }
-  }
-
-  if (mode === 'test') {
-    return !!value;
-  }
-
-  if (!value) {
-    return null;
-  }
-
-  if (prefix) {
-    value = prefix + value;
-  }
-
-  for (const fn of transform) {
-    value = fn(value);
-  }
-
-  return value;
-}
-
-export function extractAllCustomPartsFromExternalLink(url, custom) {
-  const customParts = {};
-
-  // All or nothing: if one part doesn't match, all results are scrapped.
-  for (const [key, value] of Object.entries(custom)) {
-    customParts[key] = extractPartFromExternalLink(url, value);
-    if (!customParts[key]) return null;
-  }
-
-  return customParts;
-}
-
-export function getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}) {
-  const prefix = 'misc.external';
-
-  function getDetail() {
-    if (!descriptor.detail) {
-      return null;
-    }
-
-    if (typeof descriptor.detail === 'string') {
-      return language.$(prefix, descriptor.platform, descriptor.detail);
-    } else {
-      const {substring, ...rest} = descriptor.detail;
-
-      const opts =
-        withEntries(rest, entries => entries
-          .map(([key, value]) => [
-            key,
-            extractPartFromExternalLink(url, value),
-          ]));
-
-      return language.$(prefix, descriptor.platform, substring, opts);
-    }
-  }
-
-  switch (style) {
-    case 'platform': {
-      const platform = language.$(prefix, descriptor.platform);
-      const domain = urlParts(url).domain;
-
-      if (descriptor === fallbackDescriptor) {
-        // The fallback descriptor has a "platform" which is just
-        // the word "External". This isn't really useful when you're
-        // looking for platform info!
-        if (domain) {
-          return language.sanitize(domain.replace(/^www\./, ''));
-        } else {
-          return platform;
-        }
-      } else if (descriptor.detail) {
-        return getDetail();
-      } else if (descriptor.unusualDomain && domain) {
-        return language.$(prefix, 'withDomain', {platform, domain});
-      } else {
-        return platform;
-      }
-    }
-
-    case 'handle': {
-      if (descriptor.handle) {
-        return extractPartFromExternalLink(url, descriptor.handle);
-      } else {
-        return null;
-      }
-    }
-
-    case 'icon-id': {
-      if (descriptor.icon) {
-        return descriptor.icon;
-      } else {
-        return null;
-      }
-    }
-  }
-}
-
-export function couldDescriptorSupportStyle(descriptor, style) {
-  if (style === 'platform') {
-    return true;
-  }
-
-  if (style === 'handle') {
-    return !!descriptor.handle;
-  }
-
-  if (style === 'icon-id') {
-    return !!descriptor.icon;
-  }
-}
-
-export function getExternalLinkStringOfStyleFromDescriptors(url, style, descriptors, {
-  language,
-  context = 'generic',
-}) {
-  const matchingDescriptors =
-    getMatchingDescriptorsForExternalLink(url, descriptors, {context});
-
-  const styleFilteredDescriptors =
-    matchingDescriptors.filter(descriptor =>
-      couldDescriptorSupportStyle(descriptor, style));
-
-  for (const descriptor of styleFilteredDescriptors) {
-    const descriptorResult =
-      getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language});
-
-    if (descriptorResult) {
-      return descriptorResult;
-    }
-  }
-
-  return null;
-}
-
-export function getExternalLinkStringsFromDescriptor(url, descriptor, {language}) {
-  return (
-    Object.fromEntries(
-      externalLinkStyles.map(style =>
-        getExternalLinkStringOfStyleFromDescriptor(
-          url,
-          style,
-          descriptor, {language}))));
-}
-
-export function getExternalLinkStringsFromDescriptors(url, descriptors, {
-  language,
-  context = 'generic',
-}) {
-  const results = createEmptyResults();
-  const remainingKeys = new Set(Object.keys(results));
-
-  const matchingDescriptors =
-    getMatchingDescriptorsForExternalLink(url, descriptors, {context});
-
-  for (const descriptor of matchingDescriptors) {
-    const descriptorResults =
-      getExternalLinkStringsFromDescriptor(url, descriptor, {language});
-
-    const descriptorKeys =
-      new Set(
-        Object.entries(descriptorResults)
-          .filter(entry => entry[1])
-          .map(entry => entry[0]));
-
-    for (const key of remainingKeys) {
-      if (descriptorKeys.has(key)) {
-        results[key] = descriptorResults[key];
-        remainingKeys.delete(key);
-      }
-    }
-
-    if (empty(remainingKeys)) {
-      return results;
-    }
-  }
-
-  return results;
-}
diff --git a/src/util/html.js b/src/util/html.js
deleted file mode 100644
index 0fe424df..00000000
--- a/src/util/html.js
+++ /dev/null
@@ -1,2017 +0,0 @@
-// Some really, really simple functions for formatting HTML content.
-
-import {inspect} from 'node:util';
-
-import {withAggregate} from '#aggregate';
-import {colors} from '#cli';
-import {empty, typeAppearance, unique} from '#sugar';
-import * as commonValidators from '#validators';
-
-const {
-  anyOf,
-  is,
-  isArray,
-  isBoolean,
-  isNumber,
-  isString,
-  isSymbol,
-  looseArrayOf,
-  validateAllPropertyValues,
-  validateArrayItems,
-  validateInstanceOf,
-} = commonValidators;
-
-// 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',
-];
-
-// Not so comprehensive!!
-export const attributeSpec = {
-  'class': {
-    arraylike: true,
-    join: ' ',
-    unique: true,
-  },
-
-  'style': {
-    arraylike: true,
-    join: '; ',
-  },
-};
-
-// Pass to tag() as an attributes key to make tag() return a 8lank tag if the
-// provided content is empty. Useful for when you'll only 8e showing an element
-// according to the presence of content that would 8elong there.
-export const onlyIfContent = Symbol();
-
-// Pass to tag() as an attributes key to make tag() return a blank tag if
-// this tag doesn't get shown beside any siblings! (I.e, siblings who don't
-// also have the [html.onlyIfSiblings] attribute.) Since they'd just be blank,
-// tags with [html.onlyIfSiblings] never make the difference in counting as
-// content for [html.onlyIfContent]. Useful for <summary> and such.
-export const onlyIfSiblings = Symbol();
-
-// Pass to tag() as an attributes key to make children be joined together by the
-// provided string. This is handy, for example, for joining lines by <br> tags,
-// or putting some other divider between each child. Note this will only have an
-// effect if the tag content is passed as an array of children and not a single
-// string.
-export const joinChildren = Symbol();
-
-// Pass to tag() as an attributes key to prevent additional whitespace from
-// being added to the inner start and end of the tag's content - basically,
-// ensuring that the start of the content begins immediately after the ">"
-// ending the opening tag, and ends immediately before the "<" at the start of
-// the closing tag. This has effect when a single child spans multiple lines,
-// or when there are multiple children.
-export const noEdgeWhitespace = Symbol();
-
-// Pass as a value on an object-shaped set of attributes to indicate that it's
-// always, absolutely, no matter what, a valid attribute addition. It will be
-// completely exempt from validation, which may provide a significant speed
-// boost IF THIS OPERATION IS REPEATED MANY TENS OF THOUSANDS OF TIMES.
-// Basically, don't use this unless you're 1) providing a constant set of
-// attributes, and 2) writing a very basic building block which loads of other
-// content will build off of!
-export const blessAttributes = Symbol();
-
-// Don't pass this directly, use html.metatag('blockwrap') instead.
-// Causes *following* content (past the metatag) to be placed inside a span
-// which is styled 'inline-block', which ensures that the words inside the
-// metatag all stay together, line-breaking only if needed, and following
-// text is displayed immediately after the last character of the last line of
-// the metatag (provided there's room on that line for the following word or
-// character).
-export const blockwrap = Symbol();
-
-// Don't pass this directly, use html.metatag('chunkwrap') instead.
-// Causes *contained* content to be split by the metatag's "split" attribute,
-// and each chunk to be considered its own unit for word wrapping. All these
-// units are *not* wrapped in any containing element, so only the chunks are
-// considered wrappable units, not the entire element!
-export const chunkwrap = Symbol();
-
-// Don't pass this directly, use html.metatag('imaginary-sibling') instead.
-// A tag without any content, which is completely ignored when serializing,
-// but makes siblings with [onlyIfSiblings] feel less shy and show up on
-// their own, even without a non-blank (and non-onlyIfSiblings) sibling.
-export const imaginarySibling = Symbol();
-
-// Recursive helper function for isBlank, which basically flattens an array
-// and returns as soon as it finds any content - a non-blank case - and doesn't
-// traverse templates of its own accord. If it doesn't find directly non-blank
-// content nor any templates, it returns true; if it saw templates, but no
-// other content, then those templates are returned in a flat array, to be
-// traversed externally.
-function isBlankArrayHelper(content) {
-  // First look for string items. These are the easiest to
-  // test blankness.
-
-  const nonStringContent = [];
-
-  for (const item of content) {
-    if (typeof item === 'string') {
-      if (item.length > 0) {
-        return false;
-      }
-    } else {
-      nonStringContent.push(item);
-    }
-  }
-
-  // Analyze the content more closely. Put arrays (and
-  // content of tags marked onlyIfContent) into one array,
-  // and templates into another. And if there's anything
-  // else, that's a non-blank condition we'll detect now.
-  // We'll flat-out skip items marked onlyIfSiblings,
-  // since they could never count as content alone
-  // (some other item will have to count).
-
-  const arrayContent = [];
-  const templateContent = [];
-
-  for (const item of nonStringContent) {
-    if (item instanceof Tag) {
-      if (item.onlyIfSiblings) {
-        continue;
-      } else if (item.onlyIfContent || item.contentOnly) {
-        arrayContent.push(item.content);
-      } else {
-        return false;
-      }
-    } else if (Array.isArray(item)) {
-      arrayContent.push(item);
-    } else if (item instanceof Template) {
-      templateContent.push(item);
-    } else {
-      return false;
-    }
-  }
-
-  // Iterate over arrays and tag content recursively.
-  // The result will always be true/false (blank or not),
-  // or an array of templates. Defer accessing templates
-  // until later - we'll check on them from the outside
-  // end only if nothing else matches.
-
-  for (const item of arrayContent) {
-    const result = isBlankArrayHelper(item);
-    if (result === false) {
-      return false;
-    } else if (Array.isArray(result)) {
-      templateContent.push(...result);
-    }
-  }
-
-  // Return templates, if there are any. We don't actually
-  // handle the base case of evaluating these templates
-  // inside this recursive function - the topmost caller
-  // will handle that.
-
-  if (!empty(templateContent)) {
-    return templateContent;
-  }
-
-  // If there weren't any templates found (as direct or
-  // indirect descendants), then we're good to go!
-  // This content is definitely blank.
-
-  return true;
-}
-
-// Checks if the content provided would be represented as nothing if included
-// on a page. This can be used on its own, and is the underlying "interface"
-// layer for specific classes' `blank` getters, so its definition and usage
-// tend to be recursive.
-//
-// Note that this shouldn't be used to infer anything about non-content values
-// (e.g. attributes) - it's only suited for actual page content.
-export function isBlank(content) {
-  if (typeof content === 'string') {
-    return content.length === 0;
-  }
-
-  if (content instanceof Tag || content instanceof Template) {
-    return content.blank;
-  }
-
-  if (Array.isArray(content)) {
-    const result = isBlankArrayHelper(content);
-
-    // If the result is true or false, the helper came to
-    // a conclusive decision on its own.
-    if (typeof result === 'boolean') {
-      return result;
-    }
-
-    // Otherwise, it couldn't immediately find any content,
-    // but did come across templates that prospectively
-    // could include content. These need to be checked too.
-    // Check each of the templates one at a time.
-    for (const template of result) {
-      const content = template.content;
-
-      if (content instanceof Tag && content.onlyIfSiblings) {
-        continue;
-      }
-
-      if (isBlank(content)) {
-        continue;
-      }
-
-      return false;
-    }
-
-    // If none of the templates included content either,
-    // then there really isn't any content to find in this
-    // tree at all. It's blank!
-    return true;
-  }
-
-  return false;
-}
-
-export const validators = {
-  isBlank(value) {
-    if (!isBlank(value)) {
-      throw new TypeError(`Expected blank content`);
-    }
-
-    return true;
-  },
-
-  isTag(value) {
-    return isTag(value);
-  },
-
-  isTemplate(value) {
-    return isTemplate(value);
-  },
-
-  isHTML(value) {
-    return isHTML(value);
-  },
-
-  isAttributes(value) {
-    return isAttributesAdditionSinglet(value);
-  },
-};
-
-export function blank() {
-  return [];
-}
-
-export function blankAttributes() {
-  return new Attributes();
-}
-
-export function tag(tagName, ...args) {
-  const lastArg = args.at(-1);
-
-  const lastArgIsAttributes =
-    typeof lastArg === 'object' && lastArg !== null &&
-    !Array.isArray(lastArg) &&
-    !(lastArg instanceof Tag) &&
-    !(lastArg instanceof Template);
-
-  const content =
-    (lastArgIsAttributes
-      ? null
-      : args.at(-1));
-
-  const attributes =
-    (lastArgIsAttributes
-      ? args
-      : args.slice(0, -1));
-
-  return new Tag(tagName, attributes, content);
-}
-
-export function tags(content, ...attributes) {
-  return new Tag(null, attributes, content);
-}
-
-export function metatag(identifier, ...args) {
-  let content;
-  let opts = {};
-
-  if (
-    typeof args[0] === 'object' &&
-    !(Array.isArray(args[0]) ||
-      args[0] instanceof Tag ||
-      args[0] instanceof Template)
-  ) {
-    opts = args[0];
-    content = args[1];
-  } else {
-    content = args[0];
-  }
-
-  switch (identifier) {
-    case 'blockwrap':
-      return new Tag(null, {[blockwrap]: true}, content);
-
-    case 'chunkwrap':
-      return new Tag(null, {[chunkwrap]: true, ...opts}, content);
-
-    case 'imaginary-sibling':
-      return new Tag(null, {[imaginarySibling]: true}, content);
-
-    default:
-      throw new Error(`Unknown metatag "${identifier}"`);
-  }
-}
-
-export function normalize(content) {
-  return Tag.normalize(content);
-}
-
-export class Tag {
-  #tagName = '';
-  #content = null;
-  #attributes = null;
-
-  #traceError = null;
-
-  constructor(tagName, attributes, content) {
-    this.tagName = tagName;
-    this.attributes = attributes;
-    this.content = content;
-
-    this.#traceError = new Error();
-  }
-
-  clone() {
-    return Reflect.construct(this.constructor, [
-      this.tagName,
-      this.attributes,
-      this.content,
-    ]);
-  }
-
-  set tagName(value) {
-    if (value === undefined || value === null) {
-      this.tagName = '';
-      return;
-    }
-
-    if (typeof value !== 'string') {
-      throw new Error(`Expected tagName to be a string`);
-    }
-
-    if (selfClosingTags.includes(value) && this.content.length) {
-      throw new Error(`Tag <${value}> is self-closing but this tag has content`);
-    }
-
-    this.#tagName = value;
-  }
-
-  get tagName() {
-    return this.#tagName;
-  }
-
-  set attributes(attributes) {
-    if (attributes instanceof Attributes) {
-      this.#attributes = attributes;
-    } else {
-      this.#attributes = new Attributes(attributes);
-    }
-  }
-
-  get attributes() {
-    if (this.#attributes === null) {
-      this.attributes = {};
-    }
-
-    return this.#attributes;
-  }
-
-  set content(value) {
-    const contentful =
-      value !== null &&
-      value !== undefined &&
-      value &&
-      (Array.isArray(value)
-        ? !empty(value.filter(Boolean))
-        : true);
-
-    if (this.selfClosing && contentful) {
-      throw new Error(`Tag <${this.tagName}> is self-closing but got content`);
-    }
-
-    if (this.imaginarySibling && contentful) {
-      throw new Error(`html.metatag('imaginary-sibling') can't have content`);
-    }
-
-    const contentArray =
-      (Array.isArray(value)
-        ? value.flat(Infinity).filter(Boolean)
-     : value
-        ? [value]
-        : []);
-
-    if (this.chunkwrap) {
-      if (contentArray.some(content => content?.blockwrap)) {
-        throw new Error(`No support for blockwrap as a direct descendant of chunkwrap`);
-      }
-    }
-
-    this.#content = contentArray;
-    this.#content.toString = () => this.#stringifyContent();
-  }
-
-  get content() {
-    if (this.#content === null) {
-      this.#content = [];
-    }
-
-    return this.#content;
-  }
-
-  get selfClosing() {
-    if (this.tagName) {
-      return selfClosingTags.includes(this.tagName);
-    } else {
-      return false;
-    }
-  }
-
-  get blank() {
-    // Tags don't have a reference to their parent, so this only evinces
-    // something about this tag's own content or attributes. It does *not*
-    // account for [html.onlyIfSiblings]!
-
-    if (this.imaginarySibling) {
-      return true;
-    }
-
-    if (this.onlyIfContent && isBlank(this.content)) {
-      return true;
-    }
-
-    if (this.contentOnly && isBlank(this.content)) {
-      return true;
-    }
-
-    return false;
-  }
-
-  get contentOnly() {
-    if (this.tagName !== '') return false;
-    if (this.chunkwrap) return true;
-    if (!this.attributes.blank) return false;
-    if (this.blockwrap) return false;
-    return true;
-  }
-
-  #setAttributeFlag(attribute, value) {
-    if (value) {
-      this.attributes.set(attribute, true);
-    } else {
-      this.attributes.remove(attribute);
-    }
-  }
-
-  #getAttributeFlag(attribute) {
-    return !!this.attributes.get(attribute);
-  }
-
-  #setAttributeString(attribute, value) {
-    // Note: This function accepts and records the empty string ('')
-    // distinctly from null/undefined.
-
-    if (value === undefined || value === null) {
-      this.attributes.remove(attribute);
-      return undefined;
-    } else {
-      this.attributes.set(attribute, String(value));
-    }
-  }
-
-  #getAttributeString(attribute) {
-    const value = this.attributes.get(attribute);
-
-    if (value === undefined || value === null) {
-      return undefined;
-    } else {
-      return String(value);
-    }
-  }
-
-  set onlyIfContent(value) {
-    this.#setAttributeFlag(onlyIfContent, value);
-  }
-
-  get onlyIfContent() {
-    return this.#getAttributeFlag(onlyIfContent);
-  }
-
-  set onlyIfSiblings(value) {
-    this.#setAttributeFlag(onlyIfSiblings, value);
-  }
-
-  get onlyIfSiblings() {
-    return this.#getAttributeFlag(onlyIfSiblings);
-  }
-
-  set joinChildren(value) {
-    this.#setAttributeString(joinChildren, value);
-  }
-
-  get joinChildren() {
-    // A chunkwrap - which serves as the top layer of a smush() when
-    // stringifying that chunkwrap - is only meant to be an invisible
-    // layer, so its own children are never specially joined.
-    if (this.chunkwrap) {
-      return '';
-    }
-
-    return this.#getAttributeString(joinChildren);
-  }
-
-  set noEdgeWhitespace(value) {
-    this.#setAttributeFlag(noEdgeWhitespace, value);
-  }
-
-  get noEdgeWhitespace() {
-    return this.#getAttributeFlag(noEdgeWhitespace);
-  }
-
-  set blockwrap(value) {
-    this.#setAttributeFlag(blockwrap, value);
-  }
-
-  get blockwrap() {
-    return this.#getAttributeFlag(blockwrap);
-  }
-
-  set chunkwrap(value) {
-    this.#setAttributeFlag(chunkwrap, value);
-
-    try {
-      this.content = this.content;
-    } catch (error) {
-      this.#setAttributeFlag(chunkwrap, false);
-      throw error;
-    }
-  }
-
-  get chunkwrap() {
-    return this.#getAttributeFlag(chunkwrap);
-  }
-
-  set imaginarySibling(value) {
-    this.#setAttributeFlag(imaginarySibling, value);
-
-    try {
-      this.content = this.content;
-    } catch (error) {
-      this.#setAttributeFlag(imaginarySibling, false);
-    }
-  }
-
-  get imaginarySibling() {
-    return this.#getAttributeFlag(imaginarySibling);
-  }
-
-  toString() {
-    if (this.onlyIfContent && isBlank(this.content)) {
-      return '';
-    }
-
-    const attributesString = this.attributes.toString();
-    const contentString = this.content.toString();
-
-    if (!this.tagName) {
-      return contentString;
-    }
-
-    const openTag = (attributesString
-      ? `<${this.tagName} ${attributesString}>`
-      : `<${this.tagName}>`);
-
-    if (this.selfClosing) {
-      return openTag;
-    }
-
-    const closeTag = `</${this.tagName}>`;
-
-    if (!this.content.length) {
-      return openTag + closeTag;
-    }
-
-    if (!contentString.includes('\n')) {
-      return openTag + contentString + closeTag;
-    }
-
-    const parts = [
-      openTag,
-      contentString
-        .split('\n')
-        .map((line, i) =>
-          (i === 0 && this.noEdgeWhitespace
-            ? line
-            : '    ' + line))
-        .join('\n'),
-      closeTag,
-    ];
-
-    return parts.join(
-      (this.noEdgeWhitespace
-        ? ''
-        : '\n'));
-  }
-
-  #getContentJoiner() {
-    if (this.joinChildren === undefined) {
-      return '\n';
-    }
-
-    if (this.joinChildren === '') {
-      return '';
-    }
-
-    return `\n${this.joinChildren}\n`;
-  }
-
-  #stringifyContent() {
-    if (this.selfClosing) {
-      return '';
-    }
-
-    const joiner = this.#getContentJoiner();
-
-    let content = '';
-    let blockwrapClosers = '';
-
-    let seenSiblingIndependentContent = false;
-
-    const chunkwrapSplitter =
-      (this.chunkwrap
-        ? this.#getAttributeString('split')
-        : null);
-
-    let seenChunkwrapSplitter =
-      (this.chunkwrap
-        ? false
-        : null);
-
-    let contentItems;
-
-    determineContentItems: {
-      if (this.chunkwrap) {
-        contentItems = smush(this).content;
-        break determineContentItems;
-      }
-
-      contentItems = this.content;
-    }
-
-    for (const [index, item] of contentItems.entries()) {
-      const nonTemplateItem =
-        Template.resolve(item);
-
-      if (nonTemplateItem instanceof Tag && nonTemplateItem.imaginarySibling) {
-        seenSiblingIndependentContent = true;
-        continue;
-      }
-
-      let itemContent;
-      try {
-        itemContent = nonTemplateItem.toString();
-      } catch (caughtError) {
-        const indexPart = colors.yellow(`child #${index + 1}`);
-
-        const error =
-          new Error(
-            `Error in ${indexPart} ` +
-            `of ${inspect(this, {compact: true})}`,
-            {cause: caughtError});
-
-        error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)] = true;
-        error[Symbol.for(`hsmusic.aggregate.traceFrom`)] = this.#traceError;
-
-        error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)] = [
-          /content-function\.js/,
-          /util\/html\.js/,
-        ];
-
-        error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)] = [
-          /content\/dependencies\/(.*\.js:.*(?=\)))/,
-        ];
-
-        throw error;
-      }
-
-      if (!itemContent) {
-        continue;
-      }
-
-      if (!(nonTemplateItem instanceof Tag) || !nonTemplateItem.onlyIfSiblings) {
-        seenSiblingIndependentContent = true;
-      }
-
-      const chunkwrapChunks =
-        (typeof nonTemplateItem === 'string' && chunkwrapSplitter
-          ? itemContent.split(chunkwrapSplitter)
-          : null);
-
-      const itemIncludesChunkwrapSplit =
-        (chunkwrapChunks
-          ? chunkwrapChunks.length > 1
-          : null);
-
-      if (content) {
-        if (itemIncludesChunkwrapSplit && !seenChunkwrapSplitter) {
-          // The first time we see a chunkwrap splitter, backtrack and wrap
-          // the content *so far* in a chunk. This will be treated just like
-          // any other open chunkwrap, and closed after the first chunk of
-          // this item! (That means the existing content is part of the same
-          // chunk as the first chunk included in this content, which makes
-          // sense, because that first chink is really just more text that
-          // precedes the first split.)
-          content = `<span class="chunkwrap">` + content;
-        }
-
-        content += joiner;
-      } else if (itemIncludesChunkwrapSplit) {
-        // We've encountered a chunkwrap split before any other content.
-        // This means there's no content to wrap, no existing chunkwrap
-        // to close, and no reason to add a joiner, but we *do* need to
-        // enter a chunkwrap wrapper *now*, so the first chunk of this
-        // item will be properly wrapped.
-        content = `<span class="chunkwrap">`;
-      }
-
-      if (itemIncludesChunkwrapSplit) {
-        seenChunkwrapSplitter = true;
-      }
-
-      // Blockwraps only apply if they actually contain some content whose
-      // words should be kept together, so it's okay to put them beneath the
-      // itemContent check. They also never apply at the very start of content,
-      // because at that point there aren't any preceding words from which the
-      // blockwrap would differentiate its content.
-      if (nonTemplateItem instanceof Tag && nonTemplateItem.blockwrap && content) {
-        content += `<span class="blockwrap">`;
-        blockwrapClosers += `</span>`;
-      }
-
-      appendItemContent: {
-        if (itemIncludesChunkwrapSplit) {
-          for (const [index, chunk] of chunkwrapChunks.entries()) {
-            if (index === 0) {
-              // The first chunk isn't actually a chunk all on its own, it's
-              // text that should be appended to the previous chunk. We will
-              // close this chunk as the first appended content as we process
-              // the next chunk.
-              content += chunk;
-            } else {
-              const whitespace = chunk.match(/^\s+/) ?? '';
-              content += chunkwrapSplitter;
-              content += '</span>';
-              content += whitespace;
-              content += '<span class="chunkwrap">';
-              content += chunk.slice(whitespace.length);
-            }
-          }
-
-          break appendItemContent;
-        }
-
-        content += itemContent;
-      }
-    }
-
-    // If we've only seen sibling-dependent content (or just no content),
-    // then the content in total is blank.
-    if (!seenSiblingIndependentContent) {
-      return '';
-    }
-
-    if (chunkwrapSplitter) {
-      if (seenChunkwrapSplitter) {
-        content += '</span>';
-      } else {
-        // Since chunkwraps take responsibility for wrapping *away* from the
-        // parent element, we generally always want there to be at least one
-        // chunk that gets wrapped as a single unit. So if no chunkwrap has
-        // been seen at all, just wrap everything in one now.
-        content = `<span class="chunkwrap">${content}</span>`;
-      }
-    }
-
-    content += blockwrapClosers;
-
-    return content;
-  }
-
-  static normalize(content) {
-    // Normalizes contents that are valid from an `isHTML` perspective so
-    // that it's always a pure, single Tag object.
-
-    if (content instanceof Template) {
-      return Tag.normalize(Template.resolve(content));
-    }
-
-    if (content instanceof Tag) {
-      return content;
-    }
-
-    return new Tag(null, null, content);
-  }
-
-  smush() {
-    if (!this.contentOnly) {
-      return tags([this]);
-    }
-
-    const joiner = this.#getContentJoiner();
-
-    const result = [];
-    const attributes = {};
-
-    // Don't use built-in item joining, since we'll be handling it here -
-    // we need to account for descendants having custom joiners too, and
-    // simply using *this* tag's joiner would overwrite those descendants'
-    // differing joiners.
-    attributes[joinChildren] = '';
-
-    let workingText = '';
-
-    for (const item of this.content) {
-      const smushed = smush(item);
-      const smushedItems = smushed.content.slice();
-
-      if (empty(smushedItems)) {
-        continue;
-      }
-
-      if (typeof smushedItems[0] === 'string') {
-        if (workingText) {
-          workingText += joiner;
-        }
-
-        workingText += smushedItems.shift();
-      }
-
-      if (empty(smushedItems)) {
-        continue;
-      }
-
-      if (workingText) {
-        result.push(workingText + joiner);
-      } else if (!empty(result)) {
-        result.push(joiner);
-      }
-
-      if (typeof smushedItems.at(-1) === 'string') {
-        // The last smushed item already had its joiner processed from its own
-        // parent - this isn't an appropriate place for us to insert our own
-        // joiner.
-        workingText = smushedItems.pop();
-      } else {
-        workingText = '';
-      }
-
-      result.push(...smushedItems);
-    }
-
-    if (workingText) {
-      result.push(workingText);
-    }
-
-    return new Tag(null, attributes, result);
-  }
-
-  [inspect.custom](depth, opts) {
-    const lines = [];
-
-    const niceAttributes = ['id', 'class'];
-    const attributes = blankAttributes();
-
-    for (const attribute of niceAttributes) {
-      if (this.attributes.has(attribute)) {
-        const value = this.attributes.get(attribute);
-
-        if (!value) continue;
-        if (Array.isArray(value) && empty(value)) continue;
-
-        let string;
-        let suffix = '';
-
-        if (Array.isArray(value)) {
-          string = value[0].toString();
-          if (value.length > 1) {
-            suffix = ` (+${value.length - 1})`;
-          }
-        } else {
-          string = value.toString();
-        }
-
-        const trim =
-          (string.length > 15
-            ? `${string.slice(0, 12)}...`
-            : string);
-
-        attributes.set(attribute, trim + suffix);
-      }
-    }
-
-    const attributesPart =
-      (attributes.blank
-        ? ``
-        : ` ${attributes.toString({color: true})}`);
-
-    const tagNamePart =
-      (this.tagName
-        ? colors.bright(colors.blue(this.tagName))
-        : ``);
-
-    const tagPart =
-      (this.tagName
-        ? [
-            `<`,
-            tagNamePart,
-            attributesPart,
-            (empty(this.content) ? ` />` : `>`),
-          ].join(``)
-        : ``);
-
-    const accentText =
-      (this.tagName
-        ? (empty(this.content)
-            ? ``
-            : `(${this.content.length} items)`)
-        : (empty(this.content)
-            ? `(no name)`
-            : `(no name, ${this.content.length} items)`));
-
-    const accentPart =
-      (accentText
-        ? `${colors.dim(accentText)}`
-        : ``);
-
-    const headingParts = [
-      `Tag`,
-      tagPart,
-      accentPart,
-    ];
-
-    const heading = headingParts.filter(Boolean).join(` `);
-
-    lines.push(heading);
-
-    if (!opts.compact && (depth === null || depth >= 0)) {
-      const nextDepth =
-        (depth === null
-          ? null
-          : depth - 1);
-
-      for (const child of this.content) {
-        const childLines = [];
-
-        if (typeof child === 'string') {
-          const childFlat = child.replace(/\n/g, String.raw`\n`);
-          const childTrim =
-            (childFlat.length >= 40
-              ? childFlat.slice(0, 37) + '...'
-              : childFlat);
-
-          childLines.push(
-            `  Text: ${opts.stylize(`"${childTrim}"`, 'string')}`);
-        } else {
-          childLines.push(...
-            inspect(child, {depth: nextDepth})
-              .split('\n')
-              .map(line => `  ${line}`));
-        }
-
-        lines.push(...childLines);
-      }
-    }
-
-    return lines.join('\n');
-  }
-}
-
-export function attributes(attributes) {
-  return new Attributes(attributes);
-}
-
-export function parseAttributes(string) {
-  return Attributes.parse(string);
-}
-
-export class Attributes {
-  #attributes = Object.create(null);
-
-  constructor(attributes) {
-    this.attributes = attributes;
-  }
-
-  clone() {
-    return new Attributes(this);
-  }
-
-  set attributes(value) {
-    this.#attributes = Object.create(null);
-
-    if (value === undefined || value === null) {
-      return;
-    }
-
-    this.add(value);
-  }
-
-  get attributes() {
-    return this.#attributes;
-  }
-
-  get blank() {
-    const keepAnyAttributes =
-      Object.entries(this.attributes).some(([attribute, value]) =>
-        this.#keepAttributeValue(attribute, value));
-
-    return !keepAnyAttributes;
-  }
-
-  set(attribute, value) {
-    if (value instanceof Template) {
-      value = Template.resolve(value);
-    }
-
-    if (Array.isArray(value)) {
-      value = value.flat(Infinity);
-    }
-
-    if (value === null || value === undefined) {
-      this.remove(attribute);
-    } else {
-      this.#attributes[attribute] = value;
-    }
-
-    return value;
-  }
-
-  add(...args) {
-    switch (args.length) {
-      case 1:
-        isAttributesAdditionSinglet(args[0]);
-        return this.#addMultipleAttributes(args[0]);
-
-      case 2:
-        isAttributesAdditionPair(args);
-        return this.#addOneAttribute(args[0], args[1]);
-
-      default:
-        throw new Error(
-          `Expected array or object, or attribute and value`);
-    }
-  }
-
-  with(...args) {
-    const clone = this.clone();
-    clone.add(...args);
-    return clone;
-  }
-
-  #addMultipleAttributes(attributes) {
-    const flatInputAttributes =
-      [attributes].flat(Infinity).filter(Boolean);
-
-    const attributeSets =
-      flatInputAttributes.map(attributes => this.#getAttributeSet(attributes));
-
-    const resultList = [];
-
-    for (const set of attributeSets) {
-      const setResults = {};
-
-      for (const key of Reflect.ownKeys(set)) {
-        if (key === blessAttributes) continue;
-
-        const value = set[key];
-        setResults[key] = this.#addOneAttribute(key, value);
-      }
-
-      resultList.push(setResults);
-    }
-
-    return resultList;
-  }
-
-  #getAttributeSet(attributes) {
-    if (attributes instanceof Attributes) {
-      return attributes.attributes;
-    }
-
-    if (attributes instanceof Template) {
-      const resolved = Template.resolve(attributes);
-      isAttributesAdditionSinglet(resolved);
-      return resolved;
-    }
-
-    if (typeof attributes === 'object') {
-      return attributes;
-    }
-
-    throw new Error(
-      `Expected Attributes, Template, or object, ` +
-      `got ${typeAppearance(attributes)}`);
-  }
-
-  #addOneAttribute(attribute, value) {
-    if (value === null || value === undefined) {
-      return;
-    }
-
-    if (value instanceof Template) {
-      return this.#addOneAttribute(attribute, Template.resolve(value));
-    }
-
-    if (Array.isArray(value)) {
-      value = value.flat(Infinity);
-    }
-
-    if (!this.has(attribute)) {
-      return this.set(attribute, value);
-    }
-
-    const descriptor = attributeSpec[attribute];
-    const existingValue = this.get(attribute);
-
-    let newValue = value;
-
-    if (descriptor?.arraylike) {
-      const valueArray =
-        (Array.isArray(value)
-          ? value
-          : [value]);
-
-      const existingValueArray =
-        (Array.isArray(existingValue)
-          ? existingValue
-          : [existingValue]);
-
-      newValue = existingValueArray.concat(valueArray);
-
-      if (descriptor.unique) {
-        newValue = unique(newValue);
-      }
-
-      if (newValue.length === 1) {
-        newValue = newValue[0];
-      }
-    }
-
-    return this.set(attribute, newValue);
-  }
-
-  get(attribute) {
-    return this.#attributes[attribute];
-  }
-
-  has(attribute, pattern) {
-    if (typeof pattern === 'undefined') {
-      return attribute in this.#attributes;
-    } else if (this.has(attribute)) {
-      const value = this.get(attribute);
-      if (Array.isArray(value)) {
-        return value.includes(pattern);
-      } else {
-        return value === pattern;
-      }
-    }
-  }
-
-  remove(attribute) {
-    return delete this.#attributes[attribute];
-  }
-
-  push(attribute, ...values) {
-    const oldValue = this.get(attribute);
-    const newValue =
-      (Array.isArray(oldValue)
-        ? oldValue.concat(values)
-     : oldValue
-        ? [oldValue, ...values]
-        : values);
-    this.set(attribute, newValue);
-    return newValue;
-  }
-
-  toString({color = false} = {}) {
-    const attributeKeyValues =
-      Object.entries(this.attributes)
-        .map(([key, value]) =>
-          (this.#keepAttributeValue(key, value)
-            ? [key, this.#transformAttributeValue(key, value), true]
-            : [key, undefined, false]))
-        .filter(([_key, _value, keep]) => keep)
-        .map(([key, value]) => [key, value]);
-
-    const attributeParts =
-      attributeKeyValues
-        .map(([key, value]) => {
-          const keyPart = key;
-          const escapedValue = this.#escapeAttributeValue(value);
-          const valuePart =
-            (color
-              ? colors.green(`"${escapedValue}"`)
-              : `"${escapedValue}"`);
-
-          return (
-            (typeof value === 'boolean'
-              ? `${keyPart}`
-              : `${keyPart}=${valuePart}`));
-        });
-
-    return attributeParts.join(' ');
-  }
-
-  #keepAttributeValue(attribute, value) {
-    switch (typeof value) {
-      case 'undefined':
-        return false;
-
-      case 'object':
-        if (Array.isArray(value)) {
-          return value.some(Boolean);
-        } else if (value === null) {
-          return false;
-        } else {
-          // Other objects are an error.
-          break;
-        }
-
-      case 'boolean':
-        return value;
-
-      case 'string':
-      case 'number':
-        return true;
-
-      case 'array':
-        return value.some(Boolean);
-    }
-
-    throw new Error(
-      `Value for attribute "${attribute}" should be primitive or array, ` +
-      `got ${typeAppearance(value)}: ${inspect(value)}`);
-  }
-
-  #transformAttributeValue(attribute, value) {
-    const descriptor = attributeSpec[attribute];
-
-    switch (typeof value) {
-      case 'boolean':
-        return value;
-
-      case 'number':
-        return value.toString();
-
-      // If it's a kept object, it's an array.
-      case 'object': {
-        const joiner =
-          (descriptor?.arraylike && descriptor?.join)
-            ?? ' ';
-
-        return value.filter(Boolean).join(joiner);
-      }
-
-      default:
-        return value;
-    }
-  }
-
-  #escapeAttributeValue(value) {
-    return value
-      .toString()
-      .replaceAll('"', '&quot;')
-      .replaceAll("'", '&apos;');
-  }
-
-  static parse(string) {
-    const attributes = Object.create(null);
-
-    const skipWhitespace = i => {
-      if (!/\s/.test(string[i])) {
-        return i;
-      }
-
-      const match = string.slice(i).match(/[^\s]/);
-      if (match) {
-        return i + match.index;
-      }
-
-      return string.length;
-    };
-
-    for (let i = 0; i < string.length; ) {
-      i = skipWhitespace(i);
-      const aStart = i;
-      const aEnd = i + string.slice(i).match(/[\s=]|$/).index;
-      const attribute = string.slice(aStart, aEnd);
-      i = skipWhitespace(aEnd);
-      if (string[i] === '=') {
-        i = skipWhitespace(i + 1);
-        let end, endOffset;
-        if (string[i] === '"' || string[i] === "'") {
-          end = string[i];
-          endOffset = 1;
-          i++;
-        } else {
-          end = '\\s';
-          endOffset = 0;
-        }
-        const vStart = i;
-        const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index;
-        const value = string.slice(vStart, vEnd);
-        i = vEnd + endOffset;
-        attributes[attribute] = value;
-      } else {
-        attributes[attribute] = attribute;
-      }
-    }
-
-    return (
-      Reflect.construct(this, [
-        Object.fromEntries(
-          Object.entries(attributes)
-            .map(([key, val]) => [
-              key,
-              (val === 'true'
-                ? true
-             : val === 'false'
-                ? false
-             : val === key
-                ? true
-                : val),
-            ])),
-      ]));
-  }
-
-  [inspect.custom]() {
-    const visiblePart = this.toString({color: true});
-
-    const numSymbols = Object.getOwnPropertySymbols(this.#attributes).length;
-    const numSymbolsPart =
-      (numSymbols >= 2
-        ? `${numSymbols} symbol`
-     : numSymbols === 1
-        ? `1 symbol`
-        : ``);
-
-    const symbolPart =
-      (visiblePart && numSymbolsPart
-        ? `(+${numSymbolsPart})`
-     : numSymbols
-        ? `(${numSymbolsPart})`
-        : ``);
-
-    const contentPart =
-      (visiblePart && symbolPart
-        ? `<${visiblePart} ${symbolPart}>`
-     : visiblePart || symbolPart
-        ? `<${visiblePart || symbolPart}>`
-        : `<no attributes>`);
-
-    return `Attributes ${contentPart}`;
-  }
-}
-
-export function resolve(tagOrTemplate, {
-  normalize = null,
-  slots = null,
-} = {}) {
-  if (slots) {
-    return Template.resolveForSlots(tagOrTemplate, slots);
-  } else if (normalize === 'tag') {
-    return Tag.normalize(tagOrTemplate);
-  } else if (normalize === 'string') {
-    return Tag.normalize(tagOrTemplate).toString();
-  } else if (normalize) {
-    throw new TypeError(`Expected normalize to be 'tag', 'string', or null`);
-  } else {
-    return Template.resolve(tagOrTemplate);
-  }
-}
-
-export function smush(smushee) {
-  if (
-    typeof smushee === 'string' ||
-    typeof smushee === 'number'
-  ) {
-    return tags([smushee.toString()]);
-  }
-
-  if (smushee instanceof Template) {
-    // Smushing is only really useful if the contents are resolved, because
-    // otherwise we can't actually inspect the boundaries. However, as usual
-    // for smushing, we don't care at all about the contents of tags (which
-    // aren't contentOnly) *within* the content we're smushing, so this won't
-    // for example smush a template nested within a *tag* within the contents
-    // of this template.
-    return smush(Template.resolve(smushee));
-  }
-
-  if (smushee instanceof Tag) {
-    return smushee.smush();
-  }
-
-  return smush(Tag.normalize(smushee));
-}
-
-// Much gentler version of smush - this only flattens nested html.tags(), and
-// guarantees the result is itself an html.tags(). It doesn't manipulate text
-// content, and it doesn't resolve templates.
-export function smooth(smoothie) {
-  // Helper function to avoid intermediate html.tags() calls.
-  function helper(tag) {
-    if (tag instanceof Tag && tag.contentOnly) {
-      return tag.content.flatMap(helper);
-    } else {
-      return tag;
-    }
-  }
-
-  return tags(helper(smoothie));
-}
-
-export function template(description) {
-  return new Template(description);
-}
-
-export class Template {
-  #description = {};
-  #slotValues = {};
-
-  constructor(description) {
-    if (!description[Stationery.validated]) {
-      Template.validateDescription(description);
-    }
-
-    this.#description = description;
-  }
-
-  clone() {
-    const clone = Reflect.construct(this.constructor, [
-      this.#description,
-    ]);
-
-    // getSlotValue(), called via #getReadySlotValues(), is responsible for
-    // preparing slot values for consumption, which includes cloning mutable
-    // html/attributes. We reuse that behavior here, in a recursive manner,
-    // so that clone() is effectively "deep" - slots that may be mutated are
-    // cloned, so that this template and its clones will never mutate the same
-    // identities.
-    clone.setSlots(this.#getReadySlotValues());
-
-    return clone;
-  }
-
-  static validateDescription(description) {
-    if (typeof description !== 'object') {
-      throw new TypeError(`Expected object, got ${typeAppearance(description)}`);
-    }
-
-    if (description === null) {
-      throw new TypeError(`Expected object, got null`);
-    }
-
-    const topErrors = [];
-
-    if (!('content' in description)) {
-      topErrors.push(new TypeError(`Expected description.content`));
-    } else if (typeof description.content !== 'function') {
-      topErrors.push(new TypeError(`Expected description.content to be function`));
-    }
-
-    if ('annotation' in description) {
-      if (typeof description.annotation !== 'string') {
-        topErrors.push(new TypeError(`Expected annotation to be string`));
-      }
-    }
-
-    if ('slots' in description) validateSlots: {
-      if (typeof description.slots !== 'object') {
-        topErrors.push(new TypeError(`Expected description.slots to be object`));
-        break validateSlots;
-      }
-
-      try {
-        this.validateSlotsDescription(description.slots);
-      } catch (slotError) {
-        topErrors.push(slotError);
-      }
-    }
-
-    if (!empty(topErrors)) {
-      throw new AggregateError(topErrors,
-        (typeof description.annotation === 'string'
-          ? `Errors validating template "${description.annotation}" description`
-          : `Errors validating template description`));
-    }
-
-    return true;
-  }
-
-  static validateSlotsDescription(slots) {
-    const slotErrors = [];
-
-    for (const [slotName, slotDescription] of Object.entries(slots)) {
-      if (typeof slotDescription !== 'object' || slotDescription === null) {
-        slotErrors.push(new TypeError(`(${slotName}) Expected slot description to be object`));
-        continue;
-      }
-
-      if ('default' in slotDescription) validateDefault: {
-        if (
-          slotDescription.default === undefined ||
-          slotDescription.default === null
-        ) {
-          slotErrors.push(new TypeError(`(${slotName}) Leave slot default unspecified instead of undefined or null`));
-          break validateDefault;
-        }
-
-        try {
-          Template.validateSlotValueAgainstDescription(slotDescription.default, slotDescription);
-        } catch (error) {
-          error.message = `(${slotName}) Error validating slot default value: ${error.message}`;
-          slotErrors.push(error);
-        }
-      }
-
-      if ('validate' in slotDescription && 'type' in slotDescription) {
-        slotErrors.push(new TypeError(`(${slotName}) Don't specify both slot validate and type`));
-      } else if (!('validate' in slotDescription || 'type' in slotDescription)) {
-        slotErrors.push(new TypeError(`(${slotName}) Expected either slot validate or type`));
-      } else if ('validate' in slotDescription) {
-        if (typeof slotDescription.validate !== 'function') {
-          slotErrors.push(new TypeError(`(${slotName}) Expected slot validate to be function`));
-        }
-      } else if ('type' in slotDescription) {
-        const acceptableSlotTypes = [
-          'string',
-          'number',
-          'bigint',
-          'boolean',
-          'symbol',
-          'html',
-          'attributes',
-        ];
-
-        if (slotDescription.type === 'function') {
-          slotErrors.push(new TypeError(`(${slotName}) Functions shouldn't be provided to slots`));
-        } else if (slotDescription.type === 'object') {
-          slotErrors.push(new TypeError(`(${slotName}) Provide validate function instead of type: object`));
-        } else if (
-          (slotDescription.type === 'html' || slotDescription.type === 'attributes') &&
-          !('mutable' in slotDescription)
-        ) {
-          slotErrors.push(new TypeError(`(${slotName}) Specify mutable: true/false alongside type: ${slotDescription.type}`));
-        } else if (!acceptableSlotTypes.includes(slotDescription.type)) {
-          slotErrors.push(new TypeError(`(${slotName}) Expected slot type to be one of ${acceptableSlotTypes.join(', ')}`));
-        }
-      }
-
-      if ('mutable' in slotDescription) {
-        if (slotDescription.type !== 'html' && slotDescription.type !== 'attributes') {
-          slotErrors.push(new TypeError(`(${slotName}) Only specify mutable alongside type: html or attributes`));
-        }
-
-        if (typeof slotDescription.mutable !== 'boolean') {
-          slotErrors.push(new TypeError(`(${slotName}) Expected slot mutable to be boolean`));
-        }
-      }
-    }
-
-    if (!empty(slotErrors)) {
-      throw new AggregateError(slotErrors, `Errors in slot descriptions`);
-    }
-
-    return true;
-  }
-
-  slot(slotName, value) {
-    this.setSlot(slotName, value);
-    return this;
-  }
-
-  slots(slotNamesToValues) {
-    this.setSlots(slotNamesToValues);
-    return this;
-  }
-
-  setSlot(slotName, value) {
-    const description = this.#getSlotDescriptionOrError(slotName);
-
-    try {
-      Template.validateSlotValueAgainstDescription(value, description);
-    } catch (error) {
-      error.message =
-        (this.description.annotation
-          ? `Error validating template "${this.description.annotation}" slot "${slotName}" value: ${error.message}`
-          : `Error validating template slot "${slotName}" value: ${error.message}`);
-      throw error;
-    }
-
-    this.#slotValues[slotName] = value;
-  }
-
-  setSlots(slotNamesToValues) {
-    if (
-      typeof slotNamesToValues !== 'object' ||
-      Array.isArray(slotNamesToValues) ||
-      slotNamesToValues === null
-    ) {
-      throw new TypeError(`Expected object mapping of slot names to values`);
-    }
-
-    const slotErrors = [];
-
-    for (const [slotName, value] of Object.entries(slotNamesToValues)) {
-      const description = this.#getSlotDescriptionNoError(slotName);
-      if (!description) {
-        slotErrors.push(new TypeError(`(${slotName}) Template doesn't have a "${slotName}" slot`));
-        continue;
-      }
-
-      try {
-        Template.validateSlotValueAgainstDescription(value, description);
-      } catch (error) {
-        error.message = `(${slotName}) ${error.message}`;
-        slotErrors.push(error);
-      }
-    }
-
-    if (!empty(slotErrors)) {
-      throw new AggregateError(slotErrors,
-        (this.description.annotation
-          ? `Error validating template "${this.description.annotation}" slots`
-          : `Error validating template slots`));
-    }
-
-    Object.assign(this.#slotValues, slotNamesToValues);
-  }
-
-  static validateSlotValueAgainstDescription(value, description) {
-    if (value === undefined) {
-      throw new TypeError(`Specify value as null or don't specify at all`);
-    }
-
-    // Null is always an acceptable slot value.
-    if (value === null) {
-      return true;
-    }
-
-    if (Object.hasOwn(description, 'validate')) {
-      description.validate({
-        ...commonValidators,
-        ...validators,
-      })(value);
-
-      return true;
-    }
-
-    if (Object.hasOwn(description, 'type')) {
-      switch (description.type) {
-        case 'html': {
-          return isHTML(value);
-        }
-
-        case 'attributes': {
-          return isAttributesAdditionSinglet(value);
-        }
-
-        case 'string': {
-          if (typeof value === 'string')
-            return true;
-
-          // Tags and templates are valid in string arguments - they'll be
-          // stringified when exposed to the description's .content() function.
-          if (value instanceof Tag || value instanceof Template)
-            return true;
-
-          return true;
-        }
-
-        default: {
-          if (typeof value !== description.type)
-            throw new TypeError(`Slot expects ${description.type}, got ${typeof value}`);
-
-          return true;
-        }
-      }
-    }
-
-    return true;
-  }
-
-  getSlotValue(slotName) {
-    const description = this.#getSlotDescriptionOrError(slotName);
-    const providedValue = this.#slotValues[slotName] ?? null;
-
-    if (description.type === 'html') {
-      if (!providedValue) {
-        return blank();
-      }
-
-      if (
-        (providedValue instanceof Tag || providedValue instanceof Template) &&
-        description.mutable
-      ) {
-        return providedValue.clone();
-      }
-
-      return providedValue;
-    }
-
-    if (description.type === 'attributes') {
-      if (!providedValue) {
-        return blankAttributes();
-      }
-
-      if (providedValue instanceof Attributes) {
-        if (description.mutable) {
-          return providedValue.clone();
-        } else {
-          return providedValue;
-        }
-      }
-
-      return new Attributes(providedValue);
-    }
-
-    if (description.type === 'string') {
-      if (providedValue instanceof Tag || providedValue instanceof Template) {
-        return providedValue.toString();
-      }
-
-      if (isBlank(providedValue)) {
-        return null;
-      }
-    }
-
-    if (providedValue !== null) {
-      return providedValue;
-    }
-
-    if ('default' in description) {
-      return description.default;
-    }
-
-    return null;
-  }
-
-  getSlotDescription(slotName) {
-    return this.#getSlotDescriptionOrError(slotName);
-  }
-
-  #getSlotDescriptionNoError(slotName) {
-    if (this.#description.slots) {
-      if (Object.hasOwn(this.#description.slots, slotName)) {
-        return this.#description.slots[slotName];
-      }
-    }
-
-    return null;
-  }
-
-  #getSlotDescriptionOrError(slotName) {
-    const description = this.#getSlotDescriptionNoError(slotName);
-
-    if (!description) {
-      throw new TypeError(
-        (this.description.annotation
-          ? `Template "${this.description.annotation}" doesn't have a "${slotName}" slot`
-          : `Template doesn't have a "${slotName}" slot`));
-    }
-
-    return description;
-  }
-
-  #getReadySlotValues() {
-    const slots = {};
-
-    for (const slotName of Object.keys(this.description.slots ?? {})) {
-      slots[slotName] = this.getSlotValue(slotName);
-    }
-
-    return slots;
-  }
-
-  set content(_value) {
-    throw new Error(`Template content can't be changed after constructed`);
-  }
-
-  get content() {
-    const slots = this.#getReadySlotValues();
-
-    try {
-      return this.description.content(slots);
-    } catch (caughtError) {
-      throw new Error(
-        `Error in content of ${inspect(this, {compact: true})}`,
-        {cause: caughtError});
-    }
-  }
-
-  set description(_value) {
-    throw new Error(`Template description can't be changed after constructed`);
-  }
-
-  get description() {
-    return this.#description;
-  }
-
-  get blank() {
-    return isBlank(this.content);
-  }
-
-  toString() {
-    return this.content.toString();
-  }
-
-  static resolve(tagOrTemplate) {
-    // Flattens contents of a template, recursively "resolving" until a
-    // non-template is ready (or just returns a provided non-template
-    // argument as-is).
-
-    if (!(tagOrTemplate instanceof Template)) {
-      return tagOrTemplate;
-    }
-
-    let {content} = tagOrTemplate;
-
-    while (content instanceof Template) {
-      content = content.content;
-    }
-
-    return content;
-  }
-
-  static resolveForSlots(tagOrTemplate, slots) {
-    if (!slots || typeof slots !== 'object') {
-      throw new Error(
-        `Expected slots to be an object or array, ` +
-        `got ${typeAppearance(slots)}`);
-    }
-
-    if (!Array.isArray(slots)) {
-      return Template.resolveForSlots(tagOrTemplate, Object.keys(slots)).slots(slots);
-    }
-
-    while (tagOrTemplate && tagOrTemplate instanceof Template) {
-      try {
-        for (const slot of slots) {
-          tagOrTemplate.getSlotDescription(slot);
-        }
-
-        return tagOrTemplate;
-      } catch {
-        tagOrTemplate = tagOrTemplate.content;
-      }
-    }
-
-    throw new Error(
-      `Didn't find slots ${inspect(slots, {compact: true})} ` +
-      `resolving ${inspect(tagOrTemplate, {compact: true})}`);
-  }
-
-  [inspect.custom]() {
-    const {annotation} = this.description;
-
-    return (
-      (annotation
-        ? `Template ${colors.bright(colors.blue(`"${annotation}"`))}`
-        : `Template ${colors.dim(`(no annotation)`)}`));
-  }
-}
-
-export function stationery(description) {
-  return new Stationery(description);
-}
-
-export class Stationery {
-  #templateDescription = null;
-
-  static validated = Symbol('Stationery.validated');
-
-  constructor(templateDescription) {
-    Template.validateDescription(templateDescription);
-    templateDescription[Stationery.validated] = true;
-    this.#templateDescription = templateDescription;
-  }
-
-  template() {
-    return new Template(this.#templateDescription);
-  }
-
-  [inspect.custom]() {
-    const {annotation} = this.description;
-
-    return (
-      (annotation
-        ? `Stationery ${colors.bright(colors.blue(`"${annotation}"`))}`
-        : `Stationery ${colors.dim(`(no annotation)`)}`));
-  }
-}
-
-export const isTag =
-  validateInstanceOf(Tag);
-
-export const isTemplate =
-  validateInstanceOf(Template);
-
-export const isArrayOfHTML =
-  validateArrayItems(value => isHTML(value));
-
-export const isHTML =
-  anyOf(
-    is(null, undefined, false),
-    isString,
-    isTag,
-    isTemplate,
-
-    value => {
-      isArray(value);
-      return value.length === 0;
-    },
-
-    isArrayOfHTML);
-
-export const isAttributeKey =
-  anyOf(isString, isSymbol);
-
-export const isAttributeValue =
-  anyOf(
-    isString, isNumber, isBoolean, isArray,
-    isTag, isTemplate,
-    validateArrayItems(item => isAttributeValue(item)));
-
-export const isAttributesAdditionPair = pair => {
-  isArray(pair);
-
-  if (pair.length !== 2) {
-    throw new TypeError(`Expected attributes pair to have two items`);
-  }
-
-  withAggregate({message: `Error validating attributes pair`}, ({push}) => {
-    try {
-      isAttributeKey(pair[0]);
-    } catch (caughtError) {
-      push(new Error(`Error validating key`, {cause: caughtError}));
-    }
-
-    try {
-      isAttributeValue(pair[1]);
-    } catch (caughtError) {
-      push(new Error(`Error validating value`, {cause: caughtError}));
-    }
-  });
-
-  return true;
-};
-
-const isAttributesAdditionSingletHelper =
-  anyOf(
-    validateInstanceOf(Template),
-    validateInstanceOf(Attributes),
-    validateAllPropertyValues(isAttributeValue),
-    looseArrayOf(value => isAttributesAdditionSinglet(value)));
-
-export const isAttributesAdditionSinglet = (value) => {
-  if (typeof value === 'object' && value !== null) {
-    if (Object.hasOwn(value, blessAttributes)) {
-      return true;
-    }
-
-    if (
-      Array.isArray(value) &&
-      value.length === 1 &&
-      typeof value[0] === 'object' &&
-      value[0] !== null &&
-      Object.hasOwn(value[0], blessAttributes)
-    ) {
-      return true;
-    }
-  }
-
-  return isAttributesAdditionSingletHelper(value);
-};
diff --git a/src/util/node-utils.js b/src/util/node-utils.js
deleted file mode 100644
index 345d10aa..00000000
--- a/src/util/node-utils.js
+++ /dev/null
@@ -1,102 +0,0 @@
-// Utility functions which are only relevant to particular Node.js constructs.
-
-import {readdir, stat} from 'node:fs/promises';
-import * as path from 'node:path';
-import {fileURLToPath} from 'node:url';
-
-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!
-// Here's a straightforward workaround.
-export function commandExists(command) {
-  return _commandExists(command).then(
-    () => true,
-    () => false
-  );
-}
-
-// Very cool function origin8ting in... http-music pro8a8ly!
-// Sorry if we happen to 8e violating past-us's copyright, lmao.
-export function promisifyProcess(proc, showLogging = true) {
-  // Takes a process (from the child_process module) and returns a promise
-  // that resolves when the process exits (or rejects, if the exit code is
-  // non-zero).
-  //
-  // Ayy look, no alpha8etical second letter! Couldn't tell this was written
-  // like three years ago 8efore I was me. 8888)
-
-  return new Promise((resolve, reject) => {
-    if (showLogging) {
-      proc.stdout.pipe(process.stdout);
-      proc.stderr.pipe(process.stderr);
-    }
-
-    proc.on('exit', (code) => {
-      if (code === 0) {
-        resolve();
-      } else {
-        reject(code);
-      }
-    });
-  });
-}
-
-// Handy-dandy utility function for detecting whether the passed URL is the
-// running JavaScript file. This takes `import.meta.url` from ES6 modules, which
-// is great 'cuz (module === require.main) doesn't work without CommonJS
-// modules.
-export function isMain(importMetaURL) {
-  const metaPath = fileURLToPath(importMetaURL);
-  const relative = path.relative(process.argv[1], metaPath);
-  const isIndexJS = path.basename(metaPath) === 'index.js';
-  return [
-    '',
-    isIndexJS && 'index.js'
-  ].includes(relative);
-}
-
-// Like readdir... but it's recursive! This returns a flat list of file paths.
-// By default, the paths include the provided top/root path, but this can be
-// changed with prefixPath to prefix some other path, or to just return paths
-// relative to the root. Change pathStyle to specify posix or win32, or leave
-// it as the default device-correct style. Provide a filterDir function to
-// control which directory names are traversed at all, and filterFile to
-// select which filenames are included in the final list.
-export async function traverse(rootPath, {
-  pathStyle = 'device',
-  filterFile = () => true,
-  filterDir = () => true,
-  prefixPath = rootPath,
-} = {}) {
-  const pathJoinDevice = path.join;
-  const pathJoinStyle = {
-    'device': path.join,
-    'posix': path.posix.join,
-    'win32': path.win32.join,
-  }[pathStyle];
-
-  if (!pathJoinStyle) {
-    throw new Error(`Expected pathStyle to be device, posix, or win32`);
-  }
-
-  const recursive = (names, ...subdirectories) =>
-    Promise.all(names.map(async name => {
-      const devicePath = pathJoinDevice(rootPath, ...subdirectories, name);
-      const stats = await stat(devicePath);
-
-      if (stats.isDirectory() && !filterDir(name)) return [];
-      else if (stats.isFile() && !filterFile(name)) return [];
-      else if (!stats.isDirectory() && !stats.isFile()) return [];
-
-      if (stats.isDirectory()) {
-        return recursive(await readdir(devicePath), ...subdirectories, name);
-      } else {
-        return pathJoinStyle(prefixPath, ...subdirectories, name);
-      }
-    }));
-
-  const names = await readdir(rootPath);
-  const results = await recursive(names);
-  return results.flat(Infinity);
-}
diff --git a/src/util/replacer.js b/src/util/replacer.js
deleted file mode 100644
index e3f5623e..00000000
--- a/src/util/replacer.js
+++ /dev/null
@@ -1,852 +0,0 @@
-// Regex-based forward parser for wiki content, breaking up text input into
-// text and (possibly nested) tag nodes.
-//
-// The behavior here is quite tied into the `transformContent` content
-// function, which converts nodes parsed here into actual HTML, links, etc
-// for embedding in a wiki webpage.
-
-import * as marked from 'marked';
-
-import * as html from '#html';
-import {escapeRegex, typeAppearance} from '#sugar';
-
-export const replacerSpec = {
-  'album': {
-    find: 'album',
-    link: 'linkAlbumDynamically',
-  },
-
-  'album-commentary': {
-    find: 'album',
-    link: 'linkAlbumCommentary',
-  },
-
-  'album-gallery': {
-    find: 'album',
-    link: 'linkAlbumGallery',
-  },
-
-  'artist': {
-    find: 'artist',
-    link: 'linkArtist',
-  },
-
-  'artist-gallery': {
-    find: 'artist',
-    link: 'linkArtistGallery',
-  },
-
-  'commentary-index': {
-    find: null,
-    link: 'linkCommentaryIndex',
-  },
-
-  'date': {
-    find: null,
-    value: (ref) => new Date(ref),
-    html: (date, {html, language}) =>
-      html.tag('time',
-        {datetime: date.toUTCString()},
-        language.formatDate(date)),
-  },
-
-  'flash-index': {
-    find: null,
-    link: 'linkFlashIndex',
-  },
-
-  'flash': {
-    find: 'flash',
-    link: 'linkFlash',
-    transformName(name, node, input) {
-      const nextCharacter = input[node.iEnd];
-      const lastCharacter = name[name.length - 1];
-      if (![' ', '\n', '<'].includes(nextCharacter) && lastCharacter === '.') {
-        return name.slice(0, -1);
-      } else {
-        return name;
-      }
-    },
-  },
-
-  'flash-act': {
-    find: 'flashAct',
-    link: 'linkFlashAct',
-  },
-
-  'group': {
-    find: 'group',
-    link: 'linkGroup',
-  },
-
-  'group-gallery': {
-    find: 'group',
-    link: 'linkGroupGallery',
-  },
-
-  'home': {
-    find: null,
-    link: 'linkWikiHome',
-  },
-
-  'listing-index': {
-    find: null,
-    link: 'linkListingIndex',
-  },
-
-  'listing': {
-    find: 'listing',
-    link: 'linkListing',
-  },
-
-  'media': {
-    find: null,
-    link: 'linkPathFromMedia',
-  },
-
-  'news-index': {
-    find: null,
-    link: 'linkNewsIndex',
-  },
-
-  'news-entry': {
-    find: 'newsEntry',
-    link: 'linkNewsEntry',
-  },
-
-  'root': {
-    find: null,
-    link: 'linkPathFromRoot',
-  },
-
-  'site': {
-    find: null,
-    link: 'linkPathFromSite',
-  },
-
-  'static': {
-    find: 'staticPage',
-    link: 'linkStaticPage',
-  },
-
-  'string': {
-    find: null,
-    value: (ref) => ref,
-    html: (ref, {language, args}) => language.$(ref, args),
-  },
-
-  'tag': {
-    find: 'artTag',
-    link: 'linkArtTag',
-  },
-
-  'track': {
-    find: 'track',
-    link: 'linkTrackDynamically',
-  },
-};
-
-// Syntax literals.
-const tagBeginning = '[[';
-const tagEnding = ']]';
-const tagReplacerValue = ':';
-const tagHash = '#';
-const tagArgument = '*';
-const tagArgumentValue = '=';
-const tagLabel = '|';
-
-const noPrecedingWhitespace = '(?<!\\s)';
-
-const R_tagBeginning = escapeRegex(tagBeginning);
-
-const R_tagEnding = escapeRegex(tagEnding);
-
-const R_tagReplacerValue =
-  noPrecedingWhitespace + escapeRegex(tagReplacerValue);
-
-const R_tagHash = noPrecedingWhitespace + escapeRegex(tagHash);
-
-const R_tagArgument = escapeRegex(tagArgument);
-
-const R_tagArgumentValue = escapeRegex(tagArgumentValue);
-
-const R_tagLabel = escapeRegex(tagLabel);
-
-const regexpCache = {};
-
-const makeError = (i, message) => ({i, type: 'error', data: {message}});
-const endOfInput = (i, comment) =>
-  makeError(i, `Unexpected end of input (${comment}).`);
-
-// These are 8asically stored on the glo8al scope, which might seem odd
-// for a recursive function, 8ut the values are only ever used immediately
-// after they're set.
-let stopped, stop_iParse, stop_literal;
-
-function parseOneTextNode(input, i, stopAt) {
-  return parseNodes(input, i, stopAt, true)[0];
-}
-
-function parseNodes(input, i, stopAt, textOnly) {
-  let nodes = [];
-  let string = '';
-  let iString = 0;
-
-  stopped = false;
-
-  const pushTextNode = (isLast) => {
-    string = input.slice(iString, i);
-
-    // If this is the last text node 8efore stopping (at a stopAt match
-    // or the end of the input), trim off whitespace at the end.
-    if (isLast) {
-      string = string.trimEnd();
-    }
-
-    string = cleanRawText(string);
-
-    if (string.length) {
-      nodes.push({i: iString, iEnd: i, type: 'text', data: string});
-      string = '';
-    }
-  };
-
-  const literalsToMatch = stopAt
-    ? stopAt.concat([R_tagBeginning])
-    : [R_tagBeginning];
-
-  // The 8ackslash stuff here is to only match an even (or zero) num8er
-  // of sequential 'slashes. Even amounts always cancel out! Odd amounts
-  // don't, which would mean the following literal is 8eing escaped and
-  // should 8e counted only as part of the current string/text.
-  //
-  // Inspired 8y this: https://stackoverflow.com/a/41470813
-  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
-  // and cache them for reuse instead.
-  let regexp;
-  if (Object.hasOwn(regexpCache, regexpSource)) {
-    regexp = regexpCache[regexpSource];
-  } else {
-    regexp = new RegExp(regexpSource);
-    regexpCache[regexpSource] = regexp;
-  }
-
-  // Skip whitespace at the start of parsing. This is run every time
-  // parseNodes is called (and thus parseOneTextNode too), so spaces
-  // at the start of syntax elements will always 8e skipped. We don't
-  // skip whitespace that shows up inside content (i.e. once we start
-  // parsing below), though!
-  const whitespaceOffset = input.slice(i).search(/[^\s]/);
-
-  // If the string is all whitespace, that's just zero content, so
-  // return the empty nodes array.
-  if (whitespaceOffset === -1) {
-    return nodes;
-  }
-
-  i += whitespaceOffset;
-
-  while (i < input.length) {
-    const match = input.slice(i).match(regexp);
-
-    if (!match) {
-      iString = i;
-      i = input.length;
-      pushTextNode(true);
-      break;
-    }
-
-    const closestMatch = match[0];
-    const closestMatchIndex = i + match.index;
-
-    if (textOnly && closestMatch === tagBeginning)
-      throw makeError(i, `Unexpected [[tag]] - expected only text here.`);
-
-    const stopHere = closestMatch !== tagBeginning;
-
-    iString = i;
-    i = closestMatchIndex;
-    pushTextNode(stopHere);
-
-    i += closestMatch.length;
-
-    if (stopHere) {
-      stopped = true;
-      stop_iParse = i;
-      stop_literal = closestMatch;
-      break;
-    }
-
-    if (closestMatch === tagBeginning) {
-      const iTag = closestMatchIndex;
-
-      let N;
-
-      // Replacer key (or value)
-
-      N = parseOneTextNode(input, i, [
-        R_tagReplacerValue,
-        R_tagHash,
-        R_tagArgument,
-        R_tagLabel,
-        R_tagEnding,
-      ]);
-
-      if (!stopped) throw endOfInput(i, `reading replacer key`);
-
-      if (!N) {
-        switch (stop_literal) {
-          case tagReplacerValue:
-          case tagArgument:
-            throw makeError(i, `Expected text (replacer key).`);
-          case tagLabel:
-          case tagHash:
-          case tagEnding:
-            throw makeError(i, `Expected text (replacer key/value).`);
-        }
-      }
-
-      const replacerFirst = N;
-      i = stop_iParse;
-
-      // Replacer value (if explicit)
-
-      let replacerSecond;
-
-      if (stop_literal === tagReplacerValue) {
-        N = parseNodes(input, i, [
-          R_tagHash,
-          R_tagArgument,
-          R_tagLabel,
-          R_tagEnding,
-        ]);
-
-        if (!stopped) throw endOfInput(i, `reading replacer value`);
-        if (!N.length) throw makeError(i, `Expected content (replacer value).`);
-
-        replacerSecond = N;
-        i = stop_iParse;
-      }
-
-      // Assign first & second to replacer key/value
-
-      let replacerKey, replacerValue;
-
-      // Value is an array of nodes, 8ut key is just one (or null).
-      // So if we use replacerFirst as the value, we need to stick
-      // it in an array (on its own).
-      if (replacerSecond) {
-        replacerKey = replacerFirst;
-        replacerValue = replacerSecond;
-      } else {
-        replacerKey = null;
-        replacerValue = [replacerFirst];
-      }
-
-      // Hash
-
-      let hash;
-
-      if (stop_literal === tagHash) {
-        N = parseOneTextNode(input, i, [R_tagArgument, R_tagLabel, R_tagEnding]);
-
-        if (!stopped) throw endOfInput(i, `reading hash`);
-        if (!N) throw makeError(i, `Expected text (hash).`);
-
-        hash = N;
-        i = stop_iParse;
-      }
-
-      // Arguments
-
-      const args = [];
-
-      while (stop_literal === tagArgument) {
-        N = parseOneTextNode(input, i, [
-          R_tagArgumentValue,
-          R_tagArgument,
-          R_tagLabel,
-          R_tagEnding,
-        ]);
-
-        if (!stopped) throw endOfInput(i, `reading argument key`);
-
-        if (stop_literal !== tagArgumentValue)
-          throw makeError(
-            i,
-            `Expected ${tagArgumentValue.literal} (tag argument).`
-          );
-
-        if (!N) throw makeError(i, `Expected text (argument key).`);
-
-        const key = N;
-        i = stop_iParse;
-
-        N = parseNodes(input, i, [R_tagArgument, R_tagLabel, R_tagEnding]);
-
-        if (!stopped) throw endOfInput(i, `reading argument value`);
-        if (!N.length) throw makeError(i, `Expected content (argument value).`);
-
-        const value = N;
-        i = stop_iParse;
-
-        args.push({key, value});
-      }
-
-      let label;
-
-      if (stop_literal === tagLabel) {
-        N = parseOneTextNode(input, i, [R_tagEnding]);
-
-        if (!stopped) throw endOfInput(i, `reading label`);
-        if (!N) throw makeError(i, `Expected text (label).`);
-
-        label = N;
-        i = stop_iParse;
-      }
-
-      nodes.push({
-        i: iTag,
-        iEnd: i,
-        type: 'tag',
-        data: {replacerKey, replacerValue, hash, args, label},
-      });
-
-      continue;
-    }
-  }
-
-  return nodes;
-}
-
-export function squashBackslashes(text) {
-  // Squash backslashes which aren't themselves escaped into
-  // the following character, unless that character is one of
-  // a set of characters where the backslash carries meaning
-  // into later formatting (i.e. markdown). Note that we do
-  // NOT compress double backslashes into single backslashes.
-  return text.replace(/([^\\](?:\\{2})*)\\(?![\\*_~>-])/g, '$1');
-}
-
-export function restoreRawHTMLTags(text) {
-  // Replace stuff like <html:a> with <a>; these signal that
-  // the tag shouldn't be processed by the replacer system,
-  // and should just be embedded into the content as raw HTML.
-  return text.replace(/<html:(.*?)(?=[ >])/g, '<$1');
-}
-
-export function cleanRawText(text) {
-  text = squashBackslashes(text);
-  text = restoreRawHTMLTags(text);
-  return text;
-}
-
-export function postprocessComments(inputNodes) {
-  const outputNodes = [];
-
-  for (const node of inputNodes) {
-    if (node.type !== 'text') {
-      outputNodes.push(node);
-      continue;
-    }
-
-    const commentRegexp =
-      new RegExp(
-        (// Remove comments which occupy entire lines, trimming the line break
-         // leading into them. These comments never include the ending of a
-         // comment which does not end a line, which is a regex way of saying
-         // "please fail early if we hit a --> that doesn't happen at the end
-         // of the line".
-         String.raw`\n<!--(?:(?!-->(?!$))[\s\S])*?-->(?=$)`
-       + '|' +
-
-         // Remove comments which appear at the start of a line, and any
-         // following spaces.
-         String.raw`^<!--[\s\S]*?--> *` +
-       + '|' +
-
-         // Remove comments which appear anywhere else, including in the
-         // middle of a line or at the end of a line, and any leading spaces.
-         String.raw` *<!--[\s\S]*?-->`),
-
-        'gm');
-
-    outputNodes.push({
-      type: 'text',
-
-      data:
-        node.data.replace(commentRegexp, ''),
-
-      i: node.i,
-      iEnd: node.iEnd,
-    });
-  }
-
-  return outputNodes;
-}
-
-export function postprocessImages(inputNodes) {
-  const outputNodes = [];
-
-  let atStartOfLine = true;
-
-  const lastNode = inputNodes.at(-1);
-
-  for (const node of inputNodes) {
-    if (node.type === 'tag') {
-      atStartOfLine = false;
-    }
-
-    if (node.type === 'text') {
-      const imageRegexp = /<img (.*?)>/g;
-
-      let match = null, parseFrom = 0;
-      while (match = imageRegexp.exec(node.data)) {
-        const previousText = node.data.slice(parseFrom, match.index);
-
-        outputNodes.push({
-          type: 'text',
-          data: previousText,
-          i: node.i + parseFrom,
-          iEnd: node.i + parseFrom + match.index,
-        });
-
-        parseFrom = match.index + match[0].length;
-
-        const imageNode = {type: 'image'};
-        const attributes = html.parseAttributes(match[1]);
-
-        imageNode.src = attributes.get('src');
-
-        if (previousText.endsWith('\n')) {
-          atStartOfLine = true;
-        } else if (previousText.length) {
-          atStartOfLine = false;
-        }
-
-        imageNode.inline = (() => {
-          // Images can force themselves to be rendered inline using a custom
-          // attribute - this style just works better for certain embeds,
-          // usually jokes or small images.
-          if (attributes.get('inline')) return true;
-
-          // If we've already determined we're in the middle of a line,
-          // we're inline. (Of course!)
-          if (!atStartOfLine) {
-            return true;
-          }
-
-          // If there's more text to go in this text node, and what's
-          // remaining doesn't start with a line break, we're inline.
-          if (
-            parseFrom !== node.data.length &&
-            node.data[parseFrom] !== '\n'
-          ) {
-            return true;
-          }
-
-          // If we're at the end of this text node, but this text node
-          // isn't the last node overall, we're inline.
-          if (
-            parseFrom === node.data.length &&
-            node !== lastNode
-          ) {
-            return true;
-          }
-
-          // If no other condition matches, this image is on its own line.
-          return false;
-        })();
-
-        if (attributes.get('link')) imageNode.link = attributes.get('link');
-        if (attributes.get('style')) imageNode.style = attributes.get('style');
-        if (attributes.get('width')) imageNode.width = parseInt(attributes.get('width'));
-        if (attributes.get('height')) imageNode.height = parseInt(attributes.get('height'));
-        if (attributes.get('align')) imageNode.align = attributes.get('align');
-        if (attributes.get('pixelate')) imageNode.pixelate = true;
-
-        if (attributes.get('warning')) {
-          imageNode.warnings =
-            attributes.get('warning').split(', ');
-        }
-
-        outputNodes.push(imageNode);
-
-        // No longer at the start of a line after an image - there will at
-        // least be a text node with only '\n' before the next image that's
-        // on its own line.
-        atStartOfLine = false;
-      }
-
-      if (parseFrom !== node.data.length) {
-        outputNodes.push({
-          type: 'text',
-          data: node.data.slice(parseFrom),
-          i: node.i + parseFrom,
-          iEnd: node.iEnd,
-        });
-      }
-
-      continue;
-    }
-
-    outputNodes.push(node);
-  }
-
-  return outputNodes;
-}
-
-export function postprocessVideos(inputNodes) {
-  const outputNodes = [];
-
-  for (const node of inputNodes) {
-    if (node.type !== 'text') {
-      outputNodes.push(node);
-      continue;
-    }
-
-    const videoRegexp = /<video (.*?)>(<\/video>)?/g;
-
-    let match = null, parseFrom = 0;
-    while (match = videoRegexp.exec(node.data)) {
-      const previousText = node.data.slice(parseFrom, match.index);
-
-      outputNodes.push({
-        type: 'text',
-        data: previousText,
-        i: node.i + parseFrom,
-        iEnd: node.i + parseFrom + match.index,
-      });
-
-      parseFrom = match.index + match[0].length;
-
-      const videoNode = {type: 'video'};
-      const attributes = html.parseAttributes(match[1]);
-
-      videoNode.src = attributes.get('src');
-
-      if (attributes.get('width')) videoNode.width = parseInt(attributes.get('width'));
-      if (attributes.get('height')) videoNode.height = parseInt(attributes.get('height'));
-      if (attributes.get('align')) videoNode.align = attributes.get('align');
-      if (attributes.get('pixelate')) videoNode.pixelate = true;
-
-      outputNodes.push(videoNode);
-    }
-
-    if (parseFrom !== node.data.length) {
-      outputNodes.push({
-        type: 'text',
-        data: node.data.slice(parseFrom),
-        i: node.i + parseFrom,
-        iEnd: node.iEnd,
-      });
-    }
-  }
-
-  return outputNodes;
-}
-
-export function postprocessHeadings(inputNodes) {
-  const outputNodes = [];
-
-  for (const node of inputNodes) {
-    if (node.type !== 'text') {
-      outputNodes.push(node);
-      continue;
-    }
-
-    const headingRegexp = /<h2 (.*?)>/g;
-
-    let textContent = '';
-
-    let match = null, parseFrom = 0;
-    while (match = headingRegexp.exec(node.data)) {
-      textContent += node.data.slice(parseFrom, match.index);
-      parseFrom = match.index + match[0].length;
-
-      const attributes = html.parseAttributes(match[1]);
-      attributes.push('class', 'content-heading');
-
-      // We're only modifying the opening tag here. The remaining content,
-      // including the closing tag, will be pushed as-is.
-      textContent += `<h2 ${attributes}>`;
-    }
-
-    if (parseFrom !== node.data.length) {
-      textContent += node.data.slice(parseFrom);
-    }
-
-    outputNodes.push({
-      type: 'text',
-      data: textContent,
-      i: node.i,
-      iEnd: node.iEnd,
-    });
-  }
-
-  return outputNodes;
-}
-
-export function postprocessSummaries(inputNodes) {
-  const outputNodes = [];
-
-  for (const node of inputNodes) {
-    if (node.type !== 'text') {
-      outputNodes.push(node);
-      continue;
-    }
-
-    const summaryRegexp = /<summary>(.*)<\/summary>/g;
-
-    let textContent = '';
-
-    let match = null, parseFrom = 0;
-    while (match = summaryRegexp.exec(node.data)) {
-      textContent += node.data.slice(parseFrom, match.index);
-      parseFrom = match.index + match[0].length;
-
-      const colorizeWholeSummary = !match[1].includes('<b>');
-
-      // We're wrapping the contents of the <summary> with a <span>, and
-      // possibly with a <b>, too. This means we have to add the closing tags
-      // where the summary ends.
-      textContent += `<summary><span>`;
-      textContent += (colorizeWholeSummary ? `<b>` : ``);
-      textContent += match[1];
-      textContent += (colorizeWholeSummary ? `</b>` : ``);
-      textContent += `</span></summary>`;
-    }
-
-    if (parseFrom !== node.data.length) {
-      textContent += node.data.slice(parseFrom);
-    }
-
-    outputNodes.push({
-      type: 'text',
-      data: textContent,
-      i: node.i,
-      iEnd: node.iEnd,
-    });
-  }
-
-  return outputNodes;
-}
-
-export function postprocessExternalLinks(inputNodes) {
-  const outputNodes = [];
-
-  for (const node of inputNodes) {
-    if (node.type !== 'text') {
-      outputNodes.push(node);
-      continue;
-    }
-
-    const plausibleLinkRegexp = /\[.*?\)/g;
-
-    let textContent = '';
-
-    let plausibleMatch = null, parseFrom = 0;
-    while (plausibleMatch = plausibleLinkRegexp.exec(node.data)) {
-      textContent += node.data.slice(parseFrom, plausibleMatch.index);
-
-      // Pedantic rules use more particular parentheses detection in link
-      // destinations - they allow one level of balanced parentheses, and
-      // otherwise, parentheses must be escaped. This allows for entire links
-      // to be wrapped in parentheses, e.g below:
-      //
-      //   This is so cool. ([You know??](https://example.com))
-      //
-      const definiteMatch =
-        marked.Lexer.rules.inline.pedantic.link
-          .exec(node.data.slice(plausibleMatch.index));
-
-      if (definiteMatch) {
-        const {1: label, 2: href} = definiteMatch;
-
-        // Split the containing text node into two - the second of these will
-        // be added after iterating over matches, or by the next match.
-        if (textContent.length) {
-          outputNodes.push({type: 'text', data: textContent});
-          textContent = '';
-        }
-
-        const offset = plausibleMatch.index + definiteMatch.index;
-        const length = definiteMatch[0].length;
-
-        outputNodes.push({
-          i: node.i + offset,
-          iEnd: node.i + offset + length,
-          type: 'external-link',
-          data: {label, href},
-        });
-
-        parseFrom = offset + length;
-      } else {
-        parseFrom = plausibleMatch.index;
-      }
-    }
-
-    if (parseFrom !== node.data.length) {
-      textContent += node.data.slice(parseFrom);
-    }
-
-    if (textContent.length) {
-      outputNodes.push({type: 'text', data: textContent});
-    }
-  }
-
-  return outputNodes;
-}
-
-export function parseInput(input) {
-  if (typeof input !== 'string') {
-    throw new TypeError(`Expected input to be string, got ${typeAppearance(input)}`);
-  }
-
-  try {
-    let output = parseNodes(input, 0);
-    output = postprocessComments(output);
-    output = postprocessImages(output);
-    output = postprocessVideos(output);
-    output = postprocessHeadings(output);
-    output = postprocessSummaries(output);
-    output = postprocessExternalLinks(output);
-    return output;
-  } catch (errorNode) {
-    if (errorNode.type !== 'error') {
-      throw errorNode;
-    }
-
-    const {
-      i,
-      data: {message},
-    } = errorNode;
-
-    let lineStart = input.slice(0, i).lastIndexOf('\n');
-    if (lineStart >= 0) {
-      lineStart += 1;
-    } else {
-      lineStart = 0;
-    }
-
-    let lineEnd = input.slice(i).indexOf('\n');
-    if (lineEnd >= 0) {
-      lineEnd += i;
-    } else {
-      lineEnd = input.length;
-    }
-
-    const line = input.slice(lineStart, lineEnd);
-
-    const cursor = i - lineStart;
-
-    throw new SyntaxError([
-      `Parse error (at pos ${i}): ${message}`,
-      line,
-      '-'.repeat(cursor) + '^',
-    ].join('\n'));
-  }
-}
diff --git a/src/util/search-spec.js b/src/util/search-spec.js
deleted file mode 100644
index bc24e1a1..00000000
--- a/src/util/search-spec.js
+++ /dev/null
@@ -1,259 +0,0 @@
-// Index structures shared by client and server, and relevant interfaces.
-
-function getArtworkPath(thing) {
-  switch (thing.constructor[Symbol.for('Thing.referenceType')]) {
-    case 'album': {
-      return [
-        'media.albumCover',
-        thing.directory,
-        thing.coverArtFileExtension,
-      ];
-    }
-
-    case 'flash': {
-      return [
-        'media.flashArt',
-        thing.directory,
-        thing.coverArtFileExtension,
-      ];
-    }
-
-    case 'track': {
-      if (thing.hasUniqueCoverArt) {
-        return [
-          'media.trackCover',
-          thing.album.directory,
-          thing.directory,
-          thing.coverArtFileExtension,
-        ];
-      } else if (thing.album.hasCoverArt) {
-        return [
-          'media.albumCover',
-          thing.album.directory,
-          thing.album.coverArtFileExtension,
-        ];
-      } else {
-        return null;
-      }
-    }
-
-    default:
-      return null;
-  }
-}
-
-function prepareArtwork(thing, {
-  checkIfImagePathHasCachedThumbnails,
-  getThumbnailEqualOrSmaller,
-  urls,
-}) {
-  const hasWarnings =
-    thing.artTags?.some(artTag => artTag.isContentWarning);
-
-  const artworkPath =
-    getArtworkPath(thing);
-
-  if (!artworkPath) {
-    return undefined;
-  }
-
-  const mediaSrc =
-    urls
-      .from('media.root')
-      .to(...artworkPath);
-
-  if (!checkIfImagePathHasCachedThumbnails(mediaSrc)) {
-    return undefined;
-  }
-
-  const selectedSize =
-    getThumbnailEqualOrSmaller(
-      (hasWarnings ? 'mini' : 'adorb'),
-      mediaSrc);
-
-  const mediaSrcJpeg =
-    mediaSrc.replace(/\.(png|jpg)$/, `.${selectedSize}.jpg`);
-
-  const displaySrc =
-    urls
-      .from('thumb.root')
-      .to('thumb.path', mediaSrcJpeg);
-
-  const serializeSrc =
-    displaySrc.replace(thing.directory, '<>');
-
-  return serializeSrc;
-}
-
-export const searchSpec = {
-  generic: {
-    query: ({
-      albumData,
-      artTagData,
-      artistData,
-      flashData,
-      groupData,
-      trackData,
-    }) => [
-      albumData,
-
-      artTagData,
-
-      artistData
-        .filter(artist => !artist.isAlias),
-
-      flashData,
-
-      groupData,
-
-      trackData
-        // Exclude rereleases - there's no reasonable way to differentiate
-        // them from the main release as part of this query.
-        .filter(track => !track.originalReleaseTrack),
-    ].flat(),
-
-    process(thing, opts) {
-      const fields = {};
-
-      fields.primaryName =
-        thing.name;
-
-      const kind =
-        thing.constructor[Symbol.for('Thing.referenceType')];
-
-      fields.parentName =
-        (kind === 'track'
-          ? thing.album.name
-       : kind === 'group'
-          ? thing.category.name
-       : kind === 'flash'
-          ? thing.act.name
-          : null);
-
-      fields.color =
-        thing.color;
-
-      fields.artTags =
-        (Object.hasOwn(thing, 'artTags')
-          ? thing.artTags.map(artTag => artTag.nameShort)
-          : []);
-
-      fields.additionalNames =
-        (Object.hasOwn(thing, 'additionalNames')
-          ? thing.additionalNames.map(entry => entry.name)
-       : Object.hasOwn(thing, 'aliasNames')
-          ? thing.aliasNames
-          : []);
-
-      const contribKeys = [
-        'artistContribs',
-        'bannerArtistContribs',
-        'contributorContribs',
-        'coverArtistContribs',
-        'wallpaperArtistContribs',
-      ];
-
-      const contributions =
-        contribKeys
-          .filter(key => Object.hasOwn(thing, key))
-          .flatMap(key => thing[key]);
-
-      fields.contributors =
-        contributions
-          .flatMap(({artist}) => [
-            artist.name,
-            ...artist.aliasNames,
-          ]);
-
-      const groups =
-         (Object.hasOwn(thing, 'groups')
-           ? thing.groups
-        : Object.hasOwn(thing, 'album')
-           ? thing.album.groups
-           : []);
-
-      const mainContributorNames =
-        contributions
-          .map(({artist}) => artist.name);
-
-      fields.groups =
-        groups
-          .filter(group => !mainContributorNames.includes(group.name))
-          .map(group => group.name);
-
-      fields.artwork =
-        prepareArtwork(thing, opts);
-
-      return fields;
-    },
-
-    index: [
-      'primaryName',
-      'parentName',
-      'artTags',
-      'additionalNames',
-      'contributors',
-      'groups',
-    ],
-
-    store: [
-      'primaryName',
-      'artwork',
-      'color',
-    ],
-  },
-};
-
-export function makeSearchIndex(descriptor, {FlexSearch}) {
-  return new FlexSearch.Document({
-    id: 'reference',
-    index: descriptor.index,
-    store: descriptor.store,
-  });
-}
-
-// TODO: This function basically mirrors bind-utilities.js, which isn't
-// exactly robust, but... binding might need some more thought across the
-// codebase in *general.*
-function bindSearchUtilities({
-  checkIfImagePathHasCachedThumbnails,
-  getThumbnailEqualOrSmaller,
-  thumbsCache,
-  urls,
-}) {
-  const bound = {
-    urls,
-  };
-
-  bound.checkIfImagePathHasCachedThumbnails =
-    (imagePath) =>
-      checkIfImagePathHasCachedThumbnails(imagePath, thumbsCache);
-
-  bound.getThumbnailEqualOrSmaller =
-    (preferred, imagePath) =>
-      getThumbnailEqualOrSmaller(preferred, imagePath, thumbsCache);
-
-  return bound;
-}
-
-export function populateSearchIndex(index, descriptor, opts) {
-  const {wikiData} = opts;
-  const bound = bindSearchUtilities(opts);
-
-  const collection = descriptor.query(wikiData);
-
-  for (const thing of collection) {
-    const reference = thing.constructor.getReference(thing);
-
-    let processed;
-    try {
-      processed = descriptor.process(thing, bound);
-    } catch (caughtError) {
-      throw new Error(
-        `Failed to process searchable thing ${reference}`,
-        {cause: caughtError});
-    }
-
-    index.add({reference, ...processed});
-  }
-}
diff --git a/src/util/serialize.js b/src/util/serialize.js
deleted file mode 100644
index eb18a759..00000000
--- a/src/util/serialize.js
+++ /dev/null
@@ -1,77 +0,0 @@
-// Utils used when per-wiki-object data files.
-// Retained for reference and/or later reorganization.
-//
-// Not to be confused with data/serialize.js, which provides a generic
-// interface for serializing any Thing object.
-
-/*
-export function serializeLink(thing) {
-  const ret = {};
-  ret.name = thing.name;
-  ret.directory = thing.directory;
-  if (thing.color) ret.color = thing.color;
-  return ret;
-}
-
-export function serializeContribs(contribs) {
-  return contribs.map(({artist, annotation}) => {
-    const ret = {};
-    ret.artist = serializeLink(artist);
-    if (annotation) ret.contribution = annotation;
-    return ret;
-  });
-}
-
-export function serializeImagePaths(original, {thumb}) {
-  return {
-    original,
-    medium: thumb.medium(original),
-    small: thumb.small(original),
-  };
-}
-
-export function serializeCover(thing, pathFunction, {
-  serializeImagePaths,
-  urls,
-}) {
-  const coverPath = pathFunction(thing, {
-    to: urls.from('media.root').to,
-  });
-
-  const {artTags} = thing;
-
-  const cwTags = artTags.filter((tag) => tag.isContentWarning);
-  const linkTags = artTags.filter((tag) => !tag.isContentWarning);
-
-  return {
-    paths: serializeImagePaths(coverPath),
-    tags: linkTags.map(serializeLink),
-    warnings: cwTags.map((tag) => tag.name),
-  };
-}
-
-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};
-    })
-    .map(({group, index, next, previous}) => ({
-      link: serializeLink(group),
-      descriptionShort: group.descriptionShort,
-      albumIndex: index,
-      nextAlbum: next && serializeLink(next),
-      previousAlbum: previous && serializeLink(previous),
-      urls: group.urls,
-    }));
-}
-
-export function serializeGroupsForTrack(track, {serializeLink}) {
-  return track.album.groups.map((group) => ({
-    link: serializeLink(group),
-    urls: group.urls,
-  }));
-}
-*/
diff --git a/src/util/sort.js b/src/util/sort.js
deleted file mode 100644
index ea1e024a..00000000
--- a/src/util/sort.js
+++ /dev/null
@@ -1,438 +0,0 @@
-// Sorting functions - all utils here are mutating, so make sure to initially
-// slice/filter/somehow generate a new array from input data if retaining the
-// initial sort matters! (Spoilers: If what you're doing involves any kind of
-// parallelization, it definitely matters.)
-
-import {empty, sortMultipleArrays, unique}
-  from './sugar.js';
-
-// General sorting utilities! These don't do any sorting on their own but are
-// handy in the sorting functions below (or if you're making your own sort).
-
-export function compareCaseLessSensitive(a, b) {
-  // Compare two strings without considering capitalization... unless they
-  // happen to be the same that way.
-
-  const al = a.toLowerCase();
-  const bl = b.toLowerCase();
-
-  return al === bl
-    ? a.localeCompare(b, undefined, {numeric: true})
-    : al.localeCompare(bl, undefined, {numeric: true});
-}
-
-// Subtract common prefixes and other characters which some people don't like
-// to have considered while sorting. The words part of this is English-only for
-// now, which is totally evil.
-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');
-
-  // 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, '').trim();
-
-  // Remove common English (only, for now) prefixes.
-  s = s.replace(/^(?:an?|the) /i, '');
-
-  return s;
-}
-
-// Component sort functions - these sort by one particular property, applying
-// unique particulars where appropriate. Usually you don't want to use these
-// directly, but if you're making a custom sort they can come in handy.
-
-// Universal method for sorting things into a predictable order, as directory
-// is taken to be unique. There are two exceptions where this function (and
-// thus any of the composite functions that start with it) *can't* be taken as
-// deterministic:
-//
-//  1) Mixed data of two different Things, as directories are only taken as
-//     unique within one given class of Things. For example, this function
-//     won't be deterministic if its array contains both <album:ithaca> and
-//     <track:ithaca>.
-//
-//  2) Duplicate directories, or multiple instances of the "same" Thing.
-//     This function doesn't differentiate between two objects of the same
-//     directory, regardless of any other properties or the overall "identity"
-//     of the object.
-//
-// These exceptions are unavoidable except for not providing that kind of data
-// in the first place, but you can still ensure the overall program output is
-// deterministic by ensuring the input is arbitrarily sorted according to some
-// other criteria - ex, although sortByDirectory itself isn't determinstic when
-// given mixed track and album data, the final output (what goes on the site)
-// will always be the same if you're doing sortByDirectory([...albumData,
-// ...trackData]), because the initial sort places albums before tracks - and
-// sortByDirectory will handle the rest, given all directories are unique
-// except when album and track directories overlap with each other.
-export function sortByDirectory(data, {
-  getDirectory = object => object.directory,
-} = {}) {
-  const directories = data.map(getDirectory);
-
-  sortMultipleArrays(data, directories,
-    (a, b, directoryA, directoryB) =>
-      compareCaseLessSensitive(directoryA, directoryB));
-
-  return data;
-}
-
-export function sortByName(data, {
-  getName = object => object.name,
-} = {}) {
-  const names = data.map(getName);
-  const normalizedNames = names.map(normalizeName);
-
-  sortMultipleArrays(data, normalizedNames, names,
-    (
-      a, b,
-      normalizedA, normalizedB,
-      nonNormalizedA, nonNormalizedB,
-    ) =>
-      compareNormalizedNames(
-        normalizedA, normalizedB,
-        nonNormalizedA, nonNormalizedB,
-      ));
-
-  return data;
-}
-
-export function compareNormalizedNames(
-  normalizedA, normalizedB,
-  nonNormalizedA, nonNormalizedB,
-) {
-  const comparison = compareCaseLessSensitive(normalizedA, normalizedB);
-  return (
-    (comparison === 0
-      ? compareCaseLessSensitive(nonNormalizedA, nonNormalizedB)
-      : comparison));
-}
-
-export function sortByDate(data, {
-  getDate = object => object.date,
-  latestFirst = false,
-} = {}) {
-  const dates = data.map(getDate);
-
-  sortMultipleArrays(data, dates,
-    (a, b, dateA, dateB) =>
-      compareDates(dateA, dateB, {latestFirst}));
-
-  return data;
-}
-
-export function compareDates(a, b, {
-  latestFirst = false,
-} = {}) {
-  if (a && b) {
-    return (latestFirst ? b - a : a - b);
-  }
-
-  // It's possible for objects with and without dates to be mixed
-  // together in the same array. If that's the case, we put all items
-  // without dates at the end.
-  if (a) return -1;
-  if (b) return 1;
-
-  // If neither of the items being compared have a date, don't move
-  // them relative to each other. This is basically the same as
-  // filtering out all non-date items and then pushing them at the
-  // end after sorting the rest.
-  return 0;
-}
-
-export function getLatestDate(dates) {
-  const filtered = dates.filter(Boolean);
-  if (empty(filtered)) return null;
-
-  return filtered
-    .reduce(
-      (accumulator, date) =>
-        date > accumulator ? date : accumulator,
-      -Infinity);
-}
-
-export function getEarliestDate(dates) {
-  const filtered = dates.filter(Boolean);
-  if (empty(filtered)) return null;
-
-  return filtered
-    .reduce(
-      (accumulator, date) =>
-        date < accumulator ? date : accumulator,
-      Infinity);
-}
-
-// Funky sort which takes a data set and a corresponding list of "counts",
-// which are really arbitrary numbers representing some property of each data
-// object defined by the caller. It sorts and mutates *both* of these, so the
-// sorted data will still correspond to the same indexed count.
-export function sortByCount(data, counts, {
-  greatestFirst = false,
-} = {}) {
-  sortMultipleArrays(data, counts, (data1, data2, count1, count2) =>
-    (greatestFirst
-      ? count2 - count1
-      : count1 - count2));
-
-  return data;
-}
-
-export function sortByPositionInParent(data, {
-  getParent,
-  getChildren,
-}) {
-  return data.sort((a, b) => {
-    const parentA = getParent(a);
-    const parentB = getParent(b);
-
-    // Don't change the sort when the two items are from separate parents.
-    // This function doesn't change the order of parents or try to "merge"
-    // two separated chunks of items from the same parent together.
-    if (parentA !== parentB) {
-      return 0;
-    }
-
-    // Don't change the sort when either (or both) of the items doesn't
-    // even have a parent (e.g. it's the passed data is a mixed array of
-    // children and parents).
-    if (!parentA || !parentB) {
-      return 0;
-    }
-
-    const indexA = getChildren(parentA).indexOf(a);
-    const indexB = getChildren(parentB).indexOf(b);
-
-    // If the getParent/getChildren relationship doesn't go both ways for
-    // some reason, don't change the sort.
-    if (indexA === -1 || indexB === -1) {
-      return 0;
-    }
-
-    return indexA - indexB;
-  });
-}
-
-export function sortByPositionInAlbum(data) {
-  return sortByPositionInParent(data, {
-    getParent: track => track.album,
-    getChildren: album => album.tracks,
-  });
-}
-
-export function sortByPositionInFlashAct(data) {
-  return sortByPositionInParent(data, {
-    getParent: flash => flash.act,
-    getChildren: act => act.flashes,
-  });
-}
-
-// Sorts data so that items are grouped together according to whichever of a
-// set of arbitrary given conditions is true first. If no conditions are met
-// for a given item, it's moved over to the end!
-export function sortByConditions(data, conditions) {
-  return data.sort((a, b) => {
-    const ai = conditions.findIndex((f) => f(a));
-    const bi = conditions.findIndex((f) => f(b));
-
-    if (ai >= 0 && bi >= 0) {
-      return ai - bi;
-    } else if (ai >= 0) {
-      return -1;
-    } else if (bi >= 0) {
-      return 1;
-    } else {
-      return 0;
-    }
-  });
-}
-
-// Composite sorting functions - these consider multiple properties, generally
-// always returning the same output regardless of how the input was originally
-// sorted (or left unsorted). If you're working with arbitrarily sorted inputs
-// (typically wiki data, either in full or unsorted filter), these make sure
-// what gets put on the actual website (or wherever) is deterministic. Also
-// they're just handy sorting utilities.
-//
-// Note that because these are each comprised of multiple component sorting
-// functions, they expect more than just one property to be present for full
-// sorting (listed above each function). If you're mapping thing objects to
-// another representation, try to include all of these listed properties.
-
-// Expects thing properties:
-//  * directory (or override getDirectory)
-//  * name (or override getName)
-export function sortAlphabetically(data, {
-  getDirectory,
-  getName,
-} = {}) {
-  sortByDirectory(data, {getDirectory});
-  sortByName(data, {getName});
-  return data;
-}
-
-// Expects thing properties:
-//  * directory (or override getDirectory)
-//  * name (or override getName)
-//  * date (or override getDate)
-export function sortChronologically(data, {
-  latestFirst = false,
-  getDirectory,
-  getName,
-  getDate,
-} = {}) {
-  sortAlphabetically(data, {getDirectory, getName});
-  sortByDate(data, {latestFirst, getDate});
-  return data;
-}
-
-// This one's a little odd! Sorts an array of {entry, thing} pairs using
-// the provided sortFunction, which will operate on each item's `thing`, not
-// its entry (or the item as a whole). If multiple entries are associated
-// with the same thing, they'll end up bunched together in the output,
-// retaining their original relative positioning.
-export function sortEntryThingPairs(data, sortFunction) {
-  const things = unique(data.map(item => item.thing));
-  sortFunction(things);
-
-  const outputArrays = [];
-  const thingToOutputArray = new Map();
-
-  for (const thing of things) {
-    const array = [];
-    thingToOutputArray.set(thing, array);
-    outputArrays.push(array);
-  }
-
-  for (const item of data) {
-    thingToOutputArray.get(item.thing).push(item);
-  }
-
-  data.splice(0, data.length, ...outputArrays.flat());
-
-  return data;
-}
-
-/*
-// Alternate draft version of sortEntryThingPairs.
-// See: https://github.com/hsmusic/hsmusic-wiki/issues/90#issuecomment-1607412168
-
-// Maps the provided "preparation" function across a list of arbitrary values,
-// building up a list of sortable values; sorts these with the provided sorting
-// function; and reorders the sources to match their corresponding prepared
-// values. As usual, if multiple source items correspond to the same sorting
-// data, this retains the source relative positioning.
-export function prepareAndSort(sources, prepareForSort, sortFunction) {
-  const prepared = [];
-  const preparedToSource = new Map();
-
-  for (const original of originals) {
-    const prep = prepareForSort(source);
-    prepared.push(prep);
-    preparedToSource.set(prep, source);
-  }
-
-  sortFunction(prepared);
-
-  sources.splice(0, ...sources.length, prepared.map(prep => preparedToSource.get(prep)));
-
-  return sources;
-}
-*/
-
-// Highly contextual sort functions - these are only for very specific types
-// of Things, and have appropriately hard-coded behavior.
-
-// Sorts so that tracks from the same album are generally grouped together in
-// their original (album track list) order, while prioritizing date (by default
-// release date but can be overridden) above all else.
-//
-// This function also works for data lists which contain only tracks.
-export function sortAlbumsTracksChronologically(data, {
-  latestFirst = false,
-  getDate,
-} = {}) {
-  // Sort albums before tracks...
-  sortByConditions(data, [(t) => t.album === undefined]);
-
-  // Group tracks by album...
-  sortByDirectory(data, {
-    getDirectory: (t) => (t.album ? t.album.directory : t.directory),
-  });
-
-  // Sort tracks by position in album...
-  sortByPositionInAlbum(data);
-
-  // ...and finally sort by date. If tracks from more than one album were
-  // 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, {latestFirst, getDate});
-
-  return data;
-}
-
-export function sortFlashesChronologically(data, {
-  latestFirst = false,
-  getDate,
-} = {}) {
-  // Group flashes by act...
-  sortAlphabetically(data, {
-    getName: flash => flash.act.name,
-    getDirectory: flash => flash.act.directory,
-  });
-
-  // Sort flashes by position in act...
-  sortByPositionInFlashAct(data);
-
-  // ...and finally sort by date. If flashes from more than one act were
-  // released on the same date, they'll still be grouped together by act,
-  // and flashes within an act will retain their relative positioning (i.e.
-  // stay in the same order as the act's flash listing).
-  sortByDate(data, {latestFirst, getDate});
-
-  return data;
-}
-
-export function sortContributionsChronologically(data, sortThings, {
-  latestFirst = false,
-} = {}) {
-  // Contributions only have one date property (which is provided when
-  // the contribution is created). They're sorted by this most primarily,
-  // but otherwise use the same sort as is provided.
-
-  const entries =
-    data.map(contrib => ({
-      entry: contrib,
-      thing: contrib.thing,
-    }));
-
-  sortEntryThingPairs(
-    entries,
-    things =>
-      sortThings(things, {latestFirst}));
-
-  const contribs =
-    entries
-      .map(({entry: contrib}) => contrib);
-
-  sortByDate(contribs, {latestFirst});
-
-  // We're not actually operating on the original data array at any point,
-  // so since this is meant to be a mutating function like any other, splice
-  // the sorted contribs into the original array.
-  data.splice(0, data.length, ...contribs);
-
-  return data;
-}
diff --git a/src/util/sugar.js b/src/util/sugar.js
deleted file mode 100644
index 7dd173a0..00000000
--- a/src/util/sugar.js
+++ /dev/null
@@ -1,849 +0,0 @@
-// Syntactic sugar! (Mostly.)
-// Generic functions - these are useful just a8out everywhere.
-//
-// Friendly(!) disclaimer: these utility functions haven't 8een tested all that
-// much. Do not assume it will do exactly what you want it to do in all cases.
-// 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 {colors} 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
-// though we don't really make use of the 8enefits of generators any time we
-// actually use this. 8ut it's still awesome, 8ecause I say so.
-export function* splitArray(array, fn) {
-  let lastIndex = 0;
-  while (lastIndex < array.length) {
-    let nextIndex = array.findIndex((item, index) => index >= lastIndex && fn(item));
-    if (nextIndex === -1) {
-      nextIndex = array.length;
-    }
-    yield array.slice(lastIndex, nextIndex);
-    // Plus one because we don't want to include the dividing line in the
-    // next array we yield.
-    lastIndex = nextIndex + 1;
-  }
-}
-
-// Null-accepting function to check if an array or set is empty. Accepts null
-// (which is treated as empty) as a shorthand for "hey, check if this property
-// is an array with/without stuff in it" for objects where properties that are
-// PRESENT but don't currently have a VALUE are null (rather than undefined).
-export function empty(value) {
-  if (value === null) {
-    return true;
-  }
-
-  if (Array.isArray(value)) {
-    return value.length === 0;
-  }
-
-  if (value instanceof Set) {
-    return value.size === 0;
-  }
-
-  throw new Error(`Expected array, set, or null`);
-}
-
-// Repeats all the items of an array a number of times.
-export function repeat(times, array) {
-  if (times === 0) return [];
-  if (array === null || array === undefined) return [];
-  if (Array.isArray(array) && empty(array)) return [];
-
-  const out = [];
-
-  for (let n = 1; n <= times; n++) {
-    const value =
-      (typeof array === 'function'
-        ? array()
-        : array);
-
-    if (Array.isArray(value)) out.push(...value);
-    else out.push(value);
-  }
-
-  return out;
-}
-
-// Gets a random item from an array.
-export function pick(array) {
-  return array[Math.floor(Math.random() * array.length)];
-}
-
-// Gets the item at an index relative to another index.
-export function atOffset(array, index, offset, {
-  wrap = false,
-  valuePastEdge = null,
-} = {}) {
-  if (index === -1) {
-    return valuePastEdge;
-  }
-
-  if (offset === 0) {
-    return array[index];
-  }
-
-  if (wrap) {
-    return array[(index + offset) % array.length];
-  }
-
-  if (offset > 0 && index + offset > array.length - 1) {
-    return valuePastEdge;
-  }
-
-  if (offset < 0 && index + offset < 0) {
-    return valuePastEdge;
-  }
-
-  return array[index + offset];
-}
-
-// Gets the index of the first item that satisfies the provided function,
-// or, if none does, returns the length of the array (the index just past the
-// final item).
-export function findIndexOrEnd(array, fn) {
-  const index = array.findIndex(fn);
-  if (index >= 0) {
-    return index;
-  } else {
-    return array.length;
-  }
-}
-
-// Sums the values in an array, optionally taking a function which maps each
-// item to a number (handy for accessing a certain property on an array of like
-// objects). This also coalesces null values to zero, so if the mapping function
-// returns null (or values in the array are nullish), they'll just be skipped in
-// the sum.
-export function accumulateSum(array, fn = x => x) {
-  return array.reduce(
-    (accumulator, value, index, array) =>
-      accumulator +
-        fn(value, index, array) ?? 0,
-    0);
-}
-
-// Stitches together the items of separate arrays into one array of objects
-// whose keys are the corresponding items from each array at that index.
-// This is mostly useful for iterating over multiple arrays at once!
-export function stitchArrays(keyToArray) {
-  const errors = [];
-
-  for (const [key, value] of Object.entries(keyToArray)) {
-    if (value === null) continue;
-    if (Array.isArray(value)) continue;
-    errors.push(new TypeError(`(${key}) Expected array or null, got ${typeAppearance(value)}`));
-  }
-
-  if (!empty(errors)) {
-    throw new AggregateError(errors, `Expected arrays or null`);
-  }
-
-  const keys = Object.keys(keyToArray);
-  const arrays = Object.values(keyToArray).filter(val => Array.isArray(val));
-  const length = Math.max(...arrays.map(({length}) => length));
-  const results = [];
-
-  for (let i = 0; i < length; i++) {
-    const object = {};
-    for (const key of keys) {
-      object[key] =
-        (Array.isArray(keyToArray[key])
-          ? keyToArray[key][i]
-          : null);
-    }
-    results.push(object);
-  }
-
-  return results;
-}
-
-// Like Map.groupBy! Collects the items of an unsorted array into buckets
-// according to a per-item computed value.
-export function groupArray(items, fn) {
-  const buckets = new Map();
-
-  for (const [index, item] of Array.prototype.entries.call(items)) {
-    const key = fn(item, index);
-    if (buckets.has(key)) {
-      buckets.get(key).push(item);
-    } else {
-      buckets.set(key, [item]);
-    }
-  }
-
-  return buckets;
-}
-
-// Turns this:
-//
-//   [
-//     [123, 'orange', null],
-//     [456, 'apple', true],
-//     [789, 'banana', false],
-//     [1000, 'pear', undefined],
-//   ]
-//
-// Into this:
-//
-//   [
-//     [123, 456, 789, 1000],
-//     ['orange', 'apple', 'banana', 'pear'],
-//     [null, true, false, undefined],
-//   ]
-//
-// And back again, if you call it again on its results.
-export function transposeArrays(arrays) {
-  if (empty(arrays)) {
-    return [];
-  }
-
-  const length = arrays[0].length;
-  const results = new Array(length).fill(null).map(() => []);
-
-  for (const array of arrays) {
-    for (let i = 0; i < length; i++) {
-      results[i].push(array[i]);
-    }
-  }
-
-  return results;
-}
-
-export const mapInPlace = (array, fn) =>
-  array.splice(0, array.length, ...array.map(fn));
-
-export const unique = (arr) => Array.from(new Set(arr));
-
-export const compareArrays = (arr1, arr2, {checkOrder = true} = {}) =>
-  arr1.length === arr2.length &&
-  (checkOrder
-    ? arr1.every((x, i) => arr2[i] === x)
-    : arr1.every((x) => arr2.includes(x)));
-
-export function compareObjects(obj1, obj2, {
-  checkOrder = false,
-  checkSymbols = true,
-} = {}) {
-  const keys1 = Object.keys(obj1);
-  const keys2 = Object.keys(obj2);
-  if (!compareArrays(keys1, keys2, {checkOrder})) return false;
-
-  let syms1, syms2;
-  if (checkSymbols) {
-    syms1 = Object.getOwnPropertySymbols(obj1);
-    syms2 = Object.getOwnPropertySymbols(obj2);
-    if (!compareArrays(syms1, syms2, {checkOrder})) return false;
-  }
-
-  for (const key of keys1) {
-    if (obj2[key] !== obj1[key]) return false;
-  }
-
-  if (checkSymbols) {
-    for (const sym of syms1) {
-      if (obj2[sym] !== obj1[sym]) return false;
-    }
-  }
-
-  return true;
-}
-
-// Stolen from jq! Which pro8a8ly stole the concept from other places. Nice.
-export const withEntries = (obj, fn) => {
-  const result = fn(Object.entries(obj));
-  if (result instanceof Promise) {
-    return result.then(entries => Object.fromEntries(entries));
-  } else {
-    return Object.fromEntries(result);
-  }
-}
-
-export function setIntersection(set1, set2) {
-  const intersection = new Set();
-  for (const item of set1) {
-    if (set2.has(item)) {
-      intersection.add(item);
-    }
-  }
-  return intersection;
-}
-
-export function filterProperties(object, properties, {
-  preserveOriginalOrder = false,
-} = {}) {
-  if (typeof object !== 'object' || object === null) {
-    throw new TypeError(`Expected object to be an object, got ${typeAppearance(object)}`);
-  }
-
-  if (!Array.isArray(properties)) {
-    throw new TypeError(`Expected properties to be an array, got ${typeAppearance(properties)}`);
-  }
-
-  const filteredObject = {};
-
-  if (preserveOriginalOrder) {
-    for (const property of Object.keys(object)) {
-      if (properties.includes(property)) {
-        filteredObject[property] = object[property];
-      }
-    }
-  } else {
-    for (const property of properties) {
-      if (Object.hasOwn(object, property)) {
-        filteredObject[property] = object[property];
-      }
-    }
-  }
-
-  return filteredObject;
-}
-
-export function queue(array, max = 50) {
-  if (max === 0) {
-    return array.map((fn) => fn());
-  }
-
-  const begin = [];
-  let current = 0;
-  const ret = array.map(
-    (fn) =>
-      new Promise((resolve, reject) => {
-        begin.push(() => {
-          current++;
-          Promise.resolve(fn()).then((value) => {
-            current--;
-            if (current < max && begin.length) {
-              begin.shift()();
-            }
-            resolve(value);
-          }, reject);
-        });
-      })
-  );
-
-  for (let i = 0; i < max && begin.length; i++) {
-    begin.shift()();
-  }
-
-  return ret;
-}
-
-export function delay(ms) {
-  return new Promise((res) => setTimeout(res, ms));
-}
-
-export function promiseWithResolvers() {
-  let obj = {};
-
-  obj.promise =
-    new Promise((...opts) =>
-      ([obj.resolve, obj.reject] = opts));
-
-  return obj;
-}
-
-// Stolen from here: https://stackoverflow.com/a/3561711
-//
-// There's a proposal for a native JS function like this, 8ut it's not even
-// past stage ~~1~~ 2 yet: https://github.com/tc39/proposal-regex-escaping
-export function escapeRegex(string) {
-  return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
-}
-
-export function splitKeys(key) {
-  return key.split(/(?<=(?<!\\)(?:\\\\)*)\./);
-}
-
-// Follows a key path like 'foo.bar.baz' to get an item nested deeply inside
-// an object.
-export function getNestedProp(obj, key) {
-  const recursive = (o, k) =>
-    (k.length === 1
-      ? o[k[0]]
-      : recursive(o[k[0]], k.slice(1)));
-
-  return recursive(obj, splitKeys(key));
-}
-
-// Gets the "look" of some arbitrary value. It's like typeof, but smarter.
-// Don't use this for actually validating types - it's only suitable for
-// inclusion in error messages.
-export function typeAppearance(value) {
-  if (value === null) return 'null';
-  if (value === undefined) return 'undefined';
-  if (Array.isArray(value)) return 'array';
-  return typeof value;
-}
-
-// Limits a string to the desired length, filling in an ellipsis at the end
-// if it cuts any text off.
-export function cut(text, length = 40) {
-  if (text.length >= length) {
-    const index = Math.max(1, length - 3);
-    return text.slice(0, index) + '...';
-  } else {
-    return text;
-  }
-}
-
-// Limits a string to the desired length, filling in an ellipsis at the start
-// if it cuts any text off.
-export function cutStart(text, length = 40) {
-  if (text.length >= length) {
-    const index = Math.min(text.length - 1, text.length - length + 3);
-    return '...' + text.slice(index);
-  } else {
-    return text;
-  }
-}
-
-// Wrapper function around wrap(), ha, ha - this requires the Node module
-// 'node-wrap'.
-export function indentWrap(str, {
-  wrap,
-  spaces = 0,
-  width = 60,
-  bullet = false,
-}) {
-  const wrapped =
-    wrap(str, {
-      width: width - spaces,
-      indent: ' '.repeat(spaces),
-    });
-
-  if (bullet) {
-    return wrapped.trimStart();
-  } else {
-    return wrapped;
-  }
-}
-
-// Annotates {index, length} results from another iterator with contextual
-// details, including:
-//
-// * its line and column numbers;
-// * if `formatWhere` is true (the default), a pretty-formatted,
-//   human-readable indication of the match's placement in the string;
-// * if `getContainingLine` is true, the entire line (or multiple lines)
-//   of text containing the match.
-//
-export function* iterateMultiline(content, iterator, {
-  formatWhere = true,
-  getContainingLine = false,
-} = {}) {
-  const lineRegexp = /\n/g;
-  const isMultiline = content.includes('\n');
-
-  let lineNumber = 0;
-  let startOfLine = 0;
-  let previousIndex = 0;
-
-  const countLineBreaks = (index, length) => {
-    const range = content.slice(index, index + length);
-    const lineBreaks = Array.from(range.matchAll(lineRegexp));
-    if (!empty(lineBreaks)) {
-      lineNumber += lineBreaks.length;
-      startOfLine = index + lineBreaks.at(-1).index + 1;
-    }
-  };
-
-  for (const result of iterator) {
-    const {index, length} = result;
-
-    countLineBreaks(previousIndex, index - previousIndex);
-
-    const matchStartOfLine = startOfLine;
-
-    previousIndex = index + length;
-
-    const columnNumber = index - startOfLine;
-
-    let where = null;
-    if (formatWhere) {
-      where =
-        colors.yellow(
-          (isMultiline
-            ? `line: ${lineNumber + 1}, col: ${columnNumber + 1}`
-            : `pos: ${index + 1}`));
-    }
-
-    countLineBreaks(index, length);
-
-    let containingLine = null;
-    if (getContainingLine) {
-      const nextLineResult =
-        content
-          .slice(previousIndex)
-          .matchAll(lineRegexp)
-          .next();
-
-      const nextStartOfLine =
-        (nextLineResult.done
-          ? content.length
-          : previousIndex + nextLineResult.value.index);
-
-      containingLine =
-        content.slice(matchStartOfLine, nextStartOfLine);
-    }
-
-    yield {
-      ...result,
-      lineNumber,
-      columnNumber,
-      where,
-      containingLine,
-    };
-  }
-}
-
-// Iterates over regular expression matches within a single- or multiline
-// string, yielding each match as well as contextual details; this accepts
-// the same options (and provides the same context) as iterateMultiline.
-export function* matchMultiline(content, matchRegexp, options) {
-  const matchAllIterator =
-    content.matchAll(matchRegexp);
-
-  const cleanMatchAllIterator =
-    (function*() {
-      for (const match of matchAllIterator) {
-        yield {
-          index: match.index,
-          length: match[0].length,
-          match,
-        };
-      }
-    })();
-
-  const multilineIterator =
-    iterateMultiline(content, cleanMatchAllIterator, options);
-
-  yield* multilineIterator;
-}
-
-// Binds default values for arguments in a {key: value} type function argument
-// (typically the second argument, but may be overridden by providing a
-// [bindOpts.bindIndex] argument). Typically useful for preparing a function for
-// reuse within one or multiple other contexts, which may not be aware of
-// required or relevant values provided in the initial context.
-//
-// This function also passes the identity of `this` through (the returned value
-// is not an arrow function), though note it's not a true bound function either
-// (since Function.prototype.bind only supports positional arguments, not
-// "options" specified via key/value).
-//
-export function bindOpts(fn, bind) {
-  const bindIndex = bind[bindOpts.bindIndex] ?? 1;
-
-  const bound = function (...args) {
-    const opts = args[bindIndex] ?? {};
-    return Reflect.apply(fn, this, [
-      ...args.slice(0, bindIndex),
-      {...bind, ...opts}
-    ]);
-  };
-
-  annotateFunction(bound, {
-    name: fn,
-    trait: 'options-bound',
-  });
-
-  for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(fn))) {
-    if (key === 'length') continue;
-    if (key === 'name') continue;
-    if (key === 'arguments') continue;
-    if (key === 'caller') continue;
-    if (key === 'prototype') continue;
-    Object.defineProperty(bound, key, descriptor);
-  }
-
-  return bound;
-}
-
-bindOpts.bindIndex = Symbol();
-
-// Sorts multiple arrays by an arbitrary function (which is the last argument).
-// Paired values from each array are provided to the callback sequentially:
-//
-//   (a_fromFirstArray, b_fromFirstArray,
-//    a_fromSecondArray, b_fromSecondArray,
-//    a_fromThirdArray, b_fromThirdArray) =>
-//     relative positioning (negative, positive, or zero)
-//
-// Like native single-array sort, this is a mutating function.
-export function sortMultipleArrays(...args) {
-  const arrays = args.slice(0, -1);
-  const fn = args.at(-1);
-
-  const length = arrays[0].length;
-  const symbols = new Array(length).fill(null).map(() => Symbol());
-  const indexes = Object.fromEntries(symbols.map((symbol, index) => [symbol, index]));
-
-  symbols.sort((a, b) => {
-    const indexA = indexes[a];
-    const indexB = indexes[b];
-
-    const args = [];
-    for (let i = 0; i < arrays.length; i++) {
-      args.push(arrays[i][indexA]);
-      args.push(arrays[i][indexB]);
-    }
-
-    return fn(...args);
-  });
-
-  for (const array of arrays) {
-    // Note: We're mutating this array pulling values from itself, but only all
-    // at once after all those values have been pulled.
-    array.splice(0, array.length, ...symbols.map(symbol => array[indexes[symbol]]));
-  }
-
-  return arrays;
-}
-
-// Filters multiple arrays by an arbitrary function (which is the last argument).
-// Values from each array are provided to the callback sequentially:
-//
-//   (value_fromFirstArray,
-//    value_fromSecondArray,
-//    value_fromThirdArray,
-//    index,
-//    [firstArray, secondArray, thirdArray]) =>
-//      true or false
-//
-// Please be aware that this is a mutating function, unlike native single-array
-// filter. The mutated arrays are returned. Also attached under `.removed` are
-// corresponding arrays of items filtered out.
-export function filterMultipleArrays(...args) {
-  const arrays = args.slice(0, -1);
-  const fn = args.at(-1);
-
-  const removed = new Array(arrays.length).fill(null).map(() => []);
-
-  for (let i = arrays[0].length - 1; i >= 0; i--) {
-    const args = arrays.map(array => array[i]);
-    args.push(i, arrays);
-
-    if (!fn(...args)) {
-      for (let j = 0; j < arrays.length; j++) {
-        const item = arrays[j][i];
-        arrays[j].splice(i, 1);
-        removed[j].unshift(item);
-      }
-    }
-  }
-
-  Object.assign(arrays, {removed});
-  return arrays;
-}
-
-// Corresponding filter function for sortByCount. By default, items whose
-// corresponding count is zero will be removed.
-export function filterByCount(data, counts, {
-  min = 1,
-  max = Infinity,
-} = {}) {
-  filterMultipleArrays(data, counts, (data, count) =>
-    count >= min && count <= max);
-}
-
-// Reduces multiple arrays with an arbitrary function (which is the last
-// argument). Note that this reduces into multiple accumulators, one for
-// each input array, not just a single value. That's reflected in both the
-// callback parameters:
-//
-//   (accumulator1,
-//    accumulator2,
-//    value_fromFirstArray,
-//    value_fromSecondArray,
-//    index,
-//    [firstArray, secondArray]) =>
-//      [newAccumulator1, newAccumulator2]
-//
-// As well as the final return value of reduceMultipleArrays:
-//
-//   [finalAccumulator1, finalAccumulator2]
-//
-// This is not a mutating function.
-export function reduceMultipleArrays(...args) {
-  const [arrays, fn, initialAccumulators] =
-    (typeof args.at(-1) === 'function'
-      ? [args.slice(0, -1), args.at(-1), null]
-      : [args.slice(0, -2), args.at(-2), args.at(-1)]);
-
-  if (empty(arrays[0])) {
-    throw new TypeError(`Reduce of empty arrays with no initial value`);
-  }
-
-  let [accumulators, i] =
-    (initialAccumulators
-      ? [initialAccumulators, 0]
-      : [arrays.map(array => array[0]), 1]);
-
-  for (; i < arrays[0].length; i++) {
-    const args = [...accumulators, ...arrays.map(array => array[i])];
-    args.push(i, arrays);
-    accumulators = fn(...args);
-  }
-
-  return accumulators;
-}
-
-export function chunkByConditions(array, conditions) {
-  if (empty(array)) {
-    return [];
-  }
-
-  if (empty(conditions)) {
-    return [array];
-  }
-
-  const out = [];
-  let cur = [array[0]];
-  for (let i = 1; i < array.length; i++) {
-    const item = array[i];
-    const prev = array[i - 1];
-    let chunk = false;
-    for (const condition of conditions) {
-      if (condition(item, prev)) {
-        chunk = true;
-        break;
-      }
-    }
-    if (chunk) {
-      out.push(cur);
-      cur = [item];
-    } else {
-      cur.push(item);
-    }
-  }
-  out.push(cur);
-  return out;
-}
-
-export function chunkByProperties(array, properties) {
-  return chunkByConditions(
-    array,
-    properties.map((p) => (a, b) => {
-      if (a[p] instanceof Date && b[p] instanceof Date) return +a[p] !== +b[p];
-
-      if (a[p] !== b[p]) return true;
-
-      // Not sure if this line is still necessary with the specific check for
-      // d8tes a8ove, 8ut, uh, keeping it anyway, just in case....?
-      if (a[p] != b[p]) return true;
-
-      return false;
-    })
-  ).map((chunk) => ({
-    ...Object.fromEntries(properties.map((p) => [p, chunk[0][p]])),
-    chunk,
-  }));
-}
-
-export function chunkMultipleArrays(...args) {
-  const arrays = args.slice(0, -1);
-  const fn = args.at(-1);
-
-  if (arrays[0].length === 0) {
-    return [];
-  }
-
-  const newChunk = index => arrays.map(array => [array[index]]);
-  const results = [newChunk(0)];
-
-  for (let i = 1; i < arrays[0].length; i++) {
-    const current = results.at(-1);
-
-    const args = [];
-    for (let j = 0; j < arrays.length; j++) {
-      const item = arrays[j][i];
-      const previous = current[j].at(-1);
-      args.push(item, previous);
-    }
-
-    if (fn(...args)) {
-      results.push(newChunk(i));
-      continue;
-    }
-
-    for (let j = 0; j < arrays.length; j++) {
-      current[j].push(arrays[j][i]);
-    }
-  }
-
-  return results;
-}
-
-// Delicious function annotations, such as:
-//
-//   (*bound) soWeAreBackInTheMine
-//   (data *unfulfilled) generateShrekTwo
-//
-export function annotateFunction(fn, {
-  name: nameOrFunction = null,
-  description: newDescription,
-  trait: newTrait,
-}) {
-  let name;
-
-  if (typeof nameOrFunction === 'function') {
-    name = nameOrFunction.name;
-  } else if (typeof nameOrFunction === 'string') {
-    name = nameOrFunction;
-  }
-
-  name ??= fn.name ?? 'anonymous';
-
-  const match = name.match(/^ *(?<prefix>.*?) *\((?<description>.*)( #(?<trait>.*))?\) *(?<suffix>.*) *$/);
-
-  let prefix, suffix, description, trait;
-  if (match) {
-    ({prefix, suffix, description, trait} = match.groups);
-  }
-
-  prefix ??= '';
-  suffix ??= name;
-  description ??= '';
-  trait ??= '';
-
-  if (newDescription) {
-    if (description) {
-      description += '; ' + newDescription;
-    } else {
-      description = newDescription;
-    }
-  }
-
-  if (newTrait) {
-    if (trait) {
-      trait += ' #' + newTrait;
-    } else {
-      trait = '#' + newTrait;
-    }
-  }
-
-  let parenthesesPart;
-
-  if (description && trait) {
-    parenthesesPart = `${description} ${trait}`;
-  } else if (description || trait) {
-    parenthesesPart = description || trait;
-  } else {
-    parenthesesPart = '';
-  }
-
-  let finalName;
-
-  if (prefix && parenthesesPart) {
-    finalName = `${prefix} (${parenthesesPart}) ${suffix}`;
-  } else if (parenthesesPart) {
-    finalName = `(${parenthesesPart}) ${suffix}`;
-  } else {
-    finalName = suffix;
-  }
-
-  Object.defineProperty(fn, 'name', {value: finalName});
-}
diff --git a/src/util/urls.js b/src/util/urls.js
deleted file mode 100644
index 11b9b8b0..00000000
--- a/src/util/urls.js
+++ /dev/null
@@ -1,251 +0,0 @@
-// 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
-// which can really quickly take su8stitute parameters to link from any one
-// place to another; 8ut there are also a few other utilities, too.
-
-import * as path from 'node:path';
-
-import {withEntries} from '#sugar';
-
-// This export is only provided for convenience, i.e. to enable the following:
-//
-//   import {urlSpec} from '#urls';
-//
-// It's not actually defined in this module's variable scope, and functions
-// exported here require a urlSpec (whether this default one or another) to be
-// passed directly.
-//
-export {default as urlSpec} from '../url-spec.js';
-
-export function generateURLs(urlSpec) {
-  const getValueForFullKey = (obj, fullKey) => {
-    const [groupKey, subKey] = fullKey.split('.');
-    if (!groupKey || !subKey) {
-      throw new Error(`Expected group key and subkey (got ${fullKey})`);
-    }
-
-    if (!Object.hasOwn(obj, groupKey)) {
-      throw new Error(`Expected valid group key (got ${groupKey})`);
-    }
-
-    const group = obj[groupKey];
-
-    if (!Object.hasOwn(group, subKey)) {
-      throw new Error(`Expected valid subkey (got ${subKey} for group ${groupKey})`);
-    }
-
-    return {
-      value: group[subKey],
-      group,
-    };
-  };
-
-  // 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 generateTo = (fromPath, fromGroup) => {
-    const A = trimLeadingSlash(fromPath);
-
-    const rebasePrefix = '../'
-      .repeat((fromGroup.prefix || '').split('/').filter(Boolean).length);
-
-    const pathHelper = (toPath, toGroup) => {
-      let B = trimLeadingSlash(toPath);
-
-      let argIndex = 0;
-      B = B.replaceAll('<>', () => `<${argIndex++}>`);
-
-      if (toGroup.prefix !== fromGroup.prefix) {
-        // TODO: Handle differing domains in prefixes.
-        B = rebasePrefix + (toGroup.prefix || '') + B;
-      }
-
-      const suffix = toPath.endsWith('/') ? '/' : '';
-
-      return {
-        posix: path.posix.relative(A, B) + suffix,
-        device: path.relative(A, B) + suffix,
-      };
-    };
-
-    const groupSymbol = Symbol();
-
-    const groupHelper = (urlGroup) => ({
-      [groupSymbol]: urlGroup,
-      ...withEntries(urlGroup.paths, (entries) =>
-        entries.map(([key, path]) => [key, pathHelper(path, urlGroup)])
-      ),
-    });
-
-    const relative = withEntries(urlSpec, (entries) =>
-      entries.map(([key, urlGroup]) => [key, groupHelper(urlGroup)])
-    );
-
-    const toHelper =
-      ({device}) =>
-      (key, ...args) => {
-        const {
-          value: {
-            [device ? 'device' : 'posix']: template,
-          },
-        } = getValueForFullKey(relative, key);
-
-        let missing = 0;
-        let result = template.replaceAll(/<([0-9]+)>/g, (match, n) => {
-          if (n < args.length) {
-            const value = args[n];
-            if (device) {
-              return value;
-            } else {
-              let encoded = encodeURIComponent(value);
-              encoded = encoded.replaceAll('%2F', '/');
-              return encoded;
-            }
-          } else {
-            missing++;
-          }
-        });
-
-        if (missing) {
-          throw new Error(
-            `Expected ${missing + args.length} arguments, got ${
-              args.length
-            } (key ${key}, args [${args}])`
-          );
-        }
-
-        return result;
-      };
-
-    return {
-      to: toHelper({device: false}),
-      toDevice: toHelper({device: true}),
-    };
-  };
-
-  const generateFrom = () => {
-    const map = withEntries(
-      urlSpec,
-      (entries) => entries.map(([key, group]) => [
-        key,
-        withEntries(group.paths, (entries) =>
-          entries.map(([key, path]) => [key, generateTo(path, group)])
-        ),
-      ]));
-
-    const from = (key) => getValueForFullKey(map, key).value;
-
-    return {from, map};
-  };
-
-  return generateFrom();
-}
-
-const thumbnailHelper = (name) => (file) =>
-  file.replace(/\.(jpg|png)$/, name + '.jpg');
-
-export const thumb = {
-  large: thumbnailHelper('.large'),
-  medium: thumbnailHelper('.medium'),
-  small: thumbnailHelper('.small'),
-};
-
-// Makes the generally-used and wiki-specialized "to" page utility.
-// "to" returns a relative path from the current page to the target.
-export function getURLsFrom({
-  baseDirectory,
-  pagePath,
-  urls,
-}) {
-  const pageSubKey = pagePath[0];
-  const subdirectoryPrefix = getPageSubdirectoryPrefix({pagePath});
-
-  return (targetFullKey, ...args) => {
-    const [groupKey, subKey] = targetFullKey.split('.');
-    let from, to;
-
-    // When linking to *outside* the localized area of the site, we need to
-    // make sure the result is correctly relative to the 8ase directory.
-    if (
-      groupKey !== 'localized' &&
-      groupKey !== 'localizedDefaultLanguage' &&
-      baseDirectory
-    ) {
-      from = 'localizedWithBaseDirectory.' + pageSubKey;
-      to = targetFullKey;
-    } else if (groupKey === 'localizedDefaultLanguage' && baseDirectory) {
-      // Special case for specifically linking *from* a page with base
-      // directory *to* a page without! Used for the language switcher and
-      // hopefully nothing else oh god.
-      from = 'localizedWithBaseDirectory.' + pageSubKey;
-      to = 'localized.' + subKey;
-    } else if (groupKey === 'localizedDefaultLanguage') {
-      // Linking to the default, except surprise, we're already IN the default
-      // (no baseDirectory set).
-      from = 'localized.' + pageSubKey;
-      to = 'localized.' + subKey;
-    } else {
-      // If we're linking inside the localized area (or there just is no
-      // 8ase directory), the 8ase directory doesn't matter.
-      from = 'localized.' + pageSubKey;
-      to = targetFullKey;
-    }
-
-    return (
-      subdirectoryPrefix +
-      urls.from(from).to(to, ...args));
-  };
-}
-
-// Makes the generally-used and wiki-specialized "absoluteTo" page utility.
-// "absoluteTo" returns an absolute path, starting at site root (/) leading
-// to the target.
-export function getURLsFromRoot({
-  baseDirectory,
-  urls,
-}) {
-  const {to} = urls.from('shared.root');
-
-  return (targetFullKey, ...args) => {
-    const [groupKey, subKey] = targetFullKey.split('.');
-    return (
-      '/' +
-      (groupKey === 'localized' && baseDirectory
-        ? to(
-            'localizedWithBaseDirectory.' + subKey,
-            baseDirectory,
-            ...args
-          )
-        : to(targetFullKey, ...args))
-    );
-  };
-}
-
-export function getPagePathname({
-  baseDirectory,
-  device = false,
-  pagePath,
-  urls,
-}) {
-  const {[device ? 'toDevice' : 'to']: to} = urls.from('shared.root');
-
-  return (baseDirectory
-    ? to('localizedWithBaseDirectory.' + pagePath[0], baseDirectory, ...pagePath.slice(1))
-    : to('localized.' + pagePath[0], ...pagePath.slice(1)));
-}
-
-// Needed for the rare path arguments which themselves contains one or more
-// slashes, e.g. for listings, with arguments like 'albums/by-name'.
-export function getPageSubdirectoryPrefix({
-  pagePath,
-}) {
-  const timesNestedDeeply = (pagePath
-    .slice(1) // skip URL key, only check arguments
-    .join('/')
-    .split('/')
-    .length - 1);
-  return '../'.repeat(timesNestedDeeply);
-}
diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js
deleted file mode 100644
index f97ecd63..00000000
--- a/src/util/wiki-data.js
+++ /dev/null
@@ -1,475 +0,0 @@
-// Utility functions for interacting with wiki data.
-
-import {accumulateSum, empty, unique} from './sugar.js';
-import {sortByDate} from './sort.js';
-
-// This is a duplicate binding of filterMultipleArrays that's included purely
-// to leave wiki-data.js compatible with the release build of HSMusic.
-// Sorry! This is really ridiculous!! If the next update after 10/25/2023 has
-// released, this binding is no longer needed!
-export {filterMultipleArrays} from './sugar.js';
-
-// Generic value operations
-
-export function getKebabCase(name) {
-  return name
-
-    // Spaces to dashes
-    .split(' ')
-    .join('-')
-
-    // Punctuation as words
-    .replace(/&/g, '-and-')
-    .replace(/\+/g, '-plus-')
-    .replace(/%/g, '-percent-')
-
-    // Punctuation which only divides words, not single characters
-    .replace(/(\b[^\s-.]{2,})\./g, '$1-')
-    .replace(/\.([^\s-.]{2,})\b/g, '-$1')
-
-    // Punctuation which doesn't divide a number following a non-number
-    .replace(/(?<=[0-9])\^/g, '-')
-    .replace(/\^(?![0-9])/g, '-')
-
-    // General punctuation which always separates surrounding words
-    .replace(/[/@#$%*()_=,[\]{}|\\;:<>?`~]/g, '-')
-
-    // Accented characters
-    .replace(/[áâäàå]/gi, 'a')
-    .replace(/[çč]/gi, 'c')
-    .replace(/[éêëè]/gi, 'e')
-    .replace(/[íîïì]/gi, 'i')
-    .replace(/[óôöò]/gi, 'o')
-    .replace(/[úûüù]/gi, 'u')
-
-    // Strip other characters
-    .replace(/[^a-z0-9-]/gi, '')
-
-    // Combine consecutive dashes
-    .replace(/-{2,}/g, '-')
-
-    // Trim dashes on boundaries
-    .replace(/^-+|-+$/g, '')
-
-    // Always lowercase
-    .toLowerCase();
-}
-
-// Specific data utilities
-
-// Matches heading details from commentary data in roughly the formats:
-//
-//    <i>artistReference:</i> (annotation, date)
-//    <i>artistReference|artistDisplayText:</i> (annotation, date)
-//
-// where capturing group "annotation" can be any text at all, except that the
-// last entry (past a comma or the only content within parentheses), if parsed
-// as a date, is the capturing group "date". "Parsing as a date" means matching
-// one of these formats:
-//
-//   * "25 December 2019" - one or two number digits, followed by any text,
-//     followed by four number digits
-//   * "December 25, 2019" - one all-letters word, a space, one or two number
-//     digits, a comma, and four number digits
-//   * "12/25/2019" etc - three sets of one to four number digits, separated
-//     by slashes or dashes (only valid orders are MM/DD/YYYY and YYYY/MM/DD)
-//
-// Note that the annotation and date are always wrapped by one opening and one
-// closing parentheses. The whole heading does NOT need to match the entire
-// line it occupies (though it does always start at the first position on that
-// line), and if there is more than one closing parenthesis on the line, the
-// annotation will always cut off only at the last parenthesis, or a comma
-// preceding a date and then the last parenthesis. This is to ensure that
-// parentheses can be part of the actual annotation content.
-//
-// Capturing group "artistReference" is all the characters between <i> and </i>
-// (apart from the pipe and "artistDisplayText" text, if present), and is either
-// the name of an artist or an "artist:directory"-style reference.
-//
-// This regular expression *doesn't* match bodies, which will need to be parsed
-// out of the original string based on the indices matched using this.
-//
-
-const dateRegex = groupName =>
-  String.raw`(?<${groupName}>[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,4}[-/][0-9]{1,4}[-/][0-9]{1,4})`;
-
-const commentaryRegexRaw =
-  String.raw`^<i>(?<artistReferences>.+?)(?:\|(?<artistDisplayText>.+))?:<\/i>(?: \((?<annotation>(?:.*?(?=,|\)[^)]*$))*?)(?:,? ?(?:(?<dateKind>sometime|throughout|around) )?${dateRegex('date')}(?: ?- ?${dateRegex('secondDate')})?(?: (?<accessKind>captured|accessed) ${dateRegex('accessDate')})?)?\))?`;
-export const commentaryRegexCaseInsensitive =
-  new RegExp(commentaryRegexRaw, 'gmi');
-export const commentaryRegexCaseSensitive =
-  new RegExp(commentaryRegexRaw, 'gm');
-export const commentaryRegexCaseSensitiveOneShot =
-  new RegExp(commentaryRegexRaw);
-
-export function filterAlbumsByCommentary(albums) {
-  return albums
-    .filter((album) => [album, ...album.tracks].some((x) => x.commentary));
-}
-
-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);
-  } else {
-    return null;
-  }
-}
-
-export function getAlbumListTag(album) {
-  return album.hasTrackNumbers ? 'ol' : 'ul';
-}
-
-// This gets all the track o8jects defined in every al8um, and sorts them 8y
-// date released. Generally, albumData will pro8a8ly already 8e sorted 8efore
-// you pass it to this function, 8ut individual tracks can have their own
-// original release d8, distinct from the al8um's d8. I allowed that 8ecause
-// in Homestuck, the first four Vol.'s were com8ined into one al8um really
-// early in the history of the 8andcamp, and I still want to use that as the
-// al8um listing (not the original four al8um listings), 8ut if I only did
-// that, all the tracks would 8e sorted as though they were released at the
-// same time as the compilation al8um - i.e, after some other al8ums (including
-// Vol.'s 5 and 6!) were released. That would mess with chronological listings
-// including tracks from multiple al8ums, like artist pages. So, to fix that,
-// I gave tracks an Original Date field, defaulting to the release date of the
-// al8um if not specified. Pretty reasona8le, I think! Oh, and this feature can
-// 8e used for other projects too, like if you wanted to have an al8um listing
-// compiling a 8unch of songs with radically different & interspersed release
-// d8s, 8ut still keep the al8um listing in a specific order, since that isn't
-// sorted 8y date.
-export function getAllTracks(albumData) {
-  return sortByDate(albumData.flatMap((album) => album.tracks));
-}
-
-export function getArtistNumContributions(artist) {
-  return accumulateSum(
-    [
-      unique(
-        ([
-          artist.trackArtistContributions,
-          artist.trackContributorContributions,
-          artist.trackCoverArtistContributions,
-        ]).flat()
-          .map(({thing}) => thing)),
-
-      artist.albumCoverArtistContributions,
-      artist.flashContributorContributions,
-    ],
-    ({length}) => length);
-}
-
-export function getFlashCover(flash, {to}) {
-  return to('media.flashArt', flash.directory, flash.coverArtFileExtension);
-}
-
-export function getFlashLink(flash) {
-  return `https://homestuck.com/story/${flash.page}`;
-}
-
-export function getTotalDuration(tracks, {
-  originalReleasesOnly = false,
-} = {}) {
-  if (originalReleasesOnly) {
-    tracks = tracks.filter(t => !t.originalReleaseTrack);
-  }
-
-  return accumulateSum(tracks, track => track.duration);
-}
-
-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.hasUniqueCoverArt) {
-    return getAlbumCover(track.album, {to});
-  } else {
-    return to('media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension);
-  }
-}
-
-export function getArtistAvatar(artist, {to}) {
-  return to('media.artistAvatar', artist.directory, artist.avatarFileExtension);
-}
-
-// Big-ass homepage row functions
-
-export function getNewAdditions(numAlbums, {albumData}) {
-  const sortedAlbums = albumData
-    .filter((album) => album.isListedOnHomepage)
-    .sort((a, b) => {
-      if (a.dateAddedToWiki > b.dateAddedToWiki) return -1;
-      if (a.dateAddedToWiki < b.dateAddedToWiki) return 1;
-      if (a.date > b.date) return -1;
-      if (a.date < b.date) return 1;
-      return 0;
-    });
-
-  // When multiple al8ums are added to the wiki at a time, we want to show
-  // all of them 8efore pulling al8ums from the next (earlier) date. We also
-  // want to show a diverse selection of al8ums - with limited space, we'd
-  // rather not show only the latest al8ums, if those happen to all 8e
-  // closely rel8ted!
-  //
-  // Specifically, we're concerned with avoiding too much overlap amongst
-  // the primary (first/top-most) group. We do this 8y collecting every
-  // primary group present amongst the al8ums for a given d8 into one
-  // (ordered) array, initially sorted (inherently) 8y latest al8um from
-  // the group. Then we cycle over the array, adding one al8um from each
-  // group until all the al8ums from that release d8 have 8een added (or
-  // we've met the total target num8er of al8ums). Once we've added all the
-  // al8ums for a given group, it's struck from the array (so the groups
-  // with the most additions on one d8 will have their oldest releases
-  // collected more towards the end of the list).
-
-  const albums = [];
-
-  let i = 0;
-  outerLoop: while (i < sortedAlbums.length) {
-    // 8uild up a list of groups and their al8ums 8y order of decending
-    // release, iter8ting until we're on a different d8. (We use a map for
-    // indexing so we don't have to iter8te through the entire array each
-    // time we access one of its entries. This is 8asically unnecessary
-    // since this will never 8e an expensive enough task for that to
-    // matter.... 8ut it's nicer code. BBBB) )
-    const currentDate = sortedAlbums[i].dateAddedToWiki;
-    const groupMap = new Map();
-    const groupArray = [];
-    for (let album; (album = sortedAlbums[i]) && +album.dateAddedToWiki === +currentDate; i++) {
-      const primaryGroup = album.groups[0];
-      if (groupMap.has(primaryGroup)) {
-        groupMap.get(primaryGroup).push(album);
-      } else {
-        const entry = [album];
-        groupMap.set(primaryGroup, entry);
-        groupArray.push(entry);
-      }
-    }
-
-    // Then cycle over that sorted array, adding one al8um from each to
-    // the main array until we've run out or have met the target num8er
-    // of al8ums.
-    while (!empty(groupArray)) {
-      let j = 0;
-      while (j < groupArray.length) {
-        const entry = groupArray[j];
-        const album = entry.shift();
-        albums.push(album);
-
-        // This is the only time we ever add anything to the main al8um
-        // list, so it's also the only place we need to check if we've
-        // met the target length.
-        if (albums.length === numAlbums) {
-          // If we've met it, 8r8k out of the outer loop - we're done
-          // here!
-          break outerLoop;
-        }
-
-        if (empty(entry)) {
-          groupArray.splice(j, 1);
-        } else {
-          j++;
-        }
-      }
-    }
-  }
-
-  return albums;
-}
-
-export function getNewReleases(numReleases, {albumData}) {
-  return albumData
-    .filter((album) => album.isListedOnHomepage)
-    .reverse()
-    .slice(0, numReleases);
-}
-
-// Carousel layout and utilities
-
-// Layout constants:
-//
-// Carousels support fitting 4-18 items, with a few "dead" zones to watch out
-// for, namely when a multiple of 6, 5, or 4 columns would drop the last tiles.
-//
-// Carousels are limited to 1-3 rows and 4-6 columns.
-// Lower edge case: 1-3 items are treated as 4 items (with blank space).
-// Upper edge case: all items past 18 are dropped (treated as 18 items).
-//
-// This is all done through JS instead of CSS because it's just... ANNOYING...
-// to write a mapping like this in CSS lol.
-const carouselLayoutMap = [
-  // 0-3
-  null, null, null, null,
-
-  // 4-6
-  {rows: 1, columns: 4}, //  4: 1x4, drop 0
-  {rows: 1, columns: 5}, //  5: 1x5, drop 0
-  {rows: 1, columns: 6}, //  6: 1x6, drop 0
-
-  // 7-12
-  {rows: 1, columns: 6}, //  7: 1x6, drop 1
-  {rows: 2, columns: 4}, //  8: 2x4, drop 0
-  {rows: 2, columns: 4}, //  9: 2x4, drop 1
-  {rows: 2, columns: 5}, // 10: 2x5, drop 0
-  {rows: 2, columns: 5}, // 11: 2x5, drop 1
-  {rows: 2, columns: 6}, // 12: 2x6, drop 0
-
-  // 13-18
-  {rows: 2, columns: 6}, // 13: 2x6, drop 1
-  {rows: 2, columns: 6}, // 14: 2x6, drop 2
-  {rows: 3, columns: 5}, // 15: 3x5, drop 0
-  {rows: 3, columns: 5}, // 16: 3x5, drop 1
-  {rows: 3, columns: 5}, // 17: 3x5, drop 2
-  {rows: 3, columns: 6}, // 18: 3x6, drop 0
-];
-
-const minCarouselLayoutItems = carouselLayoutMap.findIndex(x => x !== null);
-const maxCarouselLayoutItems = carouselLayoutMap.length - 1;
-const shortestCarouselLayout = carouselLayoutMap[minCarouselLayoutItems];
-const longestCarouselLayout = carouselLayoutMap[maxCarouselLayoutItems];
-
-export function getCarouselLayoutForNumberOfItems(numItems) {
-  return (
-    numItems < minCarouselLayoutItems ? shortestCarouselLayout :
-    numItems > maxCarouselLayoutItems ? longestCarouselLayout :
-    carouselLayoutMap[numItems]);
-}
-
-export function filterItemsForCarousel(items) {
-  if (empty(items)) {
-    return [];
-  }
-
-  return items
-    .filter(item => item.hasCoverArt)
-    .filter(item => item.artTags.every(tag => !tag.isContentWarning))
-    .slice(0, maxCarouselLayoutItems + 1);
-}
-
-// Ridiculous caching support nonsense
-
-export class TupleMap {
-  static maxNestedTupleLength = 25;
-
-  #store = [undefined, null, null, null];
-
-  #lifetime(value) {
-    if (Array.isArray(value) && value.length <= TupleMap.maxNestedTupleLength) {
-      return 'tuple';
-    } else if (
-      typeof value === 'object' && value !== null ||
-      typeof value === 'function'
-    ) {
-      return 'weak';
-    } else {
-      return 'strong';
-    }
-  }
-
-  #getSubstoreShallow(value, store) {
-    const lifetime = this.#lifetime(value);
-    const mapIndex = {weak: 1, strong: 2, tuple: 3}[lifetime];
-
-    let map = store[mapIndex];
-    if (map === null) {
-      map = store[mapIndex] =
-        (lifetime === 'weak' ? new WeakMap()
-       : lifetime === 'strong' ? new Map()
-       : lifetime === 'tuple' ? new TupleMap()
-       : null);
-    }
-
-    if (map.has(value)) {
-      return map.get(value);
-    } else {
-      const substore = [undefined, null, null, null];
-      map.set(value, substore);
-      return substore;
-    }
-  }
-
-  #getSubstoreDeep(tuple, store = this.#store) {
-    if (tuple.length === 0) {
-      return store;
-    } else {
-      const [first, ...rest] = tuple;
-      return this.#getSubstoreDeep(rest, this.#getSubstoreShallow(first, store));
-    }
-  }
-
-  get(tuple) {
-    const store = this.#getSubstoreDeep(tuple);
-    return store[0];
-  }
-
-  has(tuple) {
-    const store = this.#getSubstoreDeep(tuple);
-    return store[0] !== undefined;
-  }
-
-  set(tuple, value) {
-    const store = this.#getSubstoreDeep(tuple);
-    store[0] = value;
-    return value;
-  }
-}
-
-export class TupleMapForBabies {
-  #here = new WeakMap();
-  #next = new WeakMap();
-
-  set(...args) {
-    const first = args.at(0);
-    const last = args.at(-1);
-    const rest = args.slice(1, -1);
-
-    if (empty(rest)) {
-      this.#here.set(first, last);
-    } else if (this.#next.has(first)) {
-      this.#next.get(first).set(...rest, last);
-    } else {
-      const tupleMap = new TupleMapForBabies();
-      this.#next.set(first, tupleMap);
-      tupleMap.set(...rest, last);
-    }
-  }
-
-  get(...args) {
-    const first = args.at(0);
-    const rest = args.slice(1);
-
-    if (empty(rest)) {
-      return this.#here.get(first);
-    } else if (this.#next.has(first)) {
-      return this.#next.get(first).get(...rest);
-    } else {
-      return undefined;
-    }
-  }
-
-  has(...args) {
-    const first = args.at(0);
-    const rest = args.slice(1);
-
-    if (empty(rest)) {
-      return this.#here.has(first);
-    } else if (this.#next.has(first)) {
-      return this.#next.get(first).has(...rest);
-    } else {
-      return false;
-    }
-  }
-}
-
-const combinedWikiDataTupleMap = new TupleMapForBabies();
-
-export function combineWikiDataArrays(arrays) {
-  const map = combinedWikiDataTupleMap;
-  if (map.has(...arrays)) {
-    return map.get(...arrays);
-  } else {
-    const combined = arrays.flat();
-    map.set(...arrays, combined);
-    return combined;
-  }
-}