« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data
diff options
context:
space:
mode:
Diffstat (limited to 'src/data')
-rw-r--r--src/data/cacheable-object.js93
-rw-r--r--src/data/patches.js7
-rw-r--r--src/data/serialize.js15
-rw-r--r--src/data/things.js73
-rw-r--r--src/data/validators.js94
-rw-r--r--src/data/yaml.js620
6 files changed, 362 insertions, 540 deletions
diff --git a/src/data/cacheable-object.js b/src/data/cacheable-object.js
index 688d8a0f..04e029f0 100644
--- a/src/data/cacheable-object.js
+++ b/src/data/cacheable-object.js
@@ -1,7 +1,3 @@
-/**
- * @format
- */
-
 // Generally extendable class for caching properties and handling dependencies,
 // with a few key properties:
 //
@@ -112,9 +108,7 @@ export default class CacheableObject {
         get: (obj, key) => {
           if (!Object.hasOwn(obj, key)) {
             if (key !== 'constructor') {
-              CacheableObject._invalidAccesses.add(
-                `(${obj.constructor.name}).${key}`
-              );
+              CacheableObject._invalidAccesses.add(`(${obj.constructor.name}).${key}`);
             }
           }
           return obj[key];
@@ -124,9 +118,7 @@ export default class CacheableObject {
   }
 
   #initializeUpdatingPropertyValues() {
-    for (const [property, descriptor] of Object.entries(
-      this.constructor.propertyDescriptors
-    )) {
+    for (const [property, descriptor] of Object.entries(this.constructor.propertyDescriptors)) {
       const {flags, update} = descriptor;
 
       if (!flags.update) {
@@ -143,14 +135,10 @@ export default class CacheableObject {
 
   #defineProperties() {
     if (!this.constructor.propertyDescriptors) {
-      throw new Error(
-        `Expected constructor ${this.constructor.name} to define propertyDescriptors`
-      );
+      throw new Error(`Expected constructor ${this.constructor.name} to define propertyDescriptors`);
     }
 
-    for (const [property, descriptor] of Object.entries(
-      this.constructor.propertyDescriptors
-    )) {
+    for (const [property, descriptor] of Object.entries(this.constructor.propertyDescriptors)) {
       const {flags} = descriptor;
 
       const definition = {
@@ -159,13 +147,11 @@ export default class CacheableObject {
       };
 
       if (flags.update) {
-        definition.set =
-          this.#getUpdateObjectDefinitionSetterFunction(property);
+        definition.set = this.#getUpdateObjectDefinitionSetterFunction(property);
       }
 
       if (flags.expose) {
-        definition.get =
-          this.#getExposeObjectDefinitionGetterFunction(property);
+        definition.get = this.#getExposeObjectDefinitionGetterFunction(property);
       }
 
       Object.defineProperty(this, property, definition);
@@ -198,9 +184,11 @@ export default class CacheableObject {
             throw new TypeError(`Validation failed for value ${newValue}`);
           }
         } catch (error) {
-          error.message = `Property ${color.green(property)} (${inspect(
-            this[property]
-          )} -> ${inspect(newValue)}): ${error.message}`;
+          error.message = [
+            `Property ${color.green(property)}`,
+            `(${inspect(this[property])} -> ${inspect(newValue)}):`,
+            error.message
+          ].join(' ');
           throw error;
         }
       }
@@ -215,8 +203,12 @@ export default class CacheableObject {
   }
 
   #invalidateCachesDependentUpon(property) {
-    for (const invalidate of this.#propertyUpdateCacheInvalidators[property] ||
-      []) {
+    const invalidators = this.#propertyUpdateCacheInvalidators[property];
+    if (!invalidators) {
+      return;
+    }
+
+    for (const invalidate of invalidators) {
       invalidate();
     }
   }
@@ -236,9 +228,7 @@ export default class CacheableObject {
         }
       };
     } else if (!flags.update && !compute) {
-      throw new Error(
-        `Exposed property ${property} does not update and is missing compute function`
-      );
+      throw new Error(`Exposed property ${property} does not update and is missing compute function`);
     } else {
       return () => this.#propertyUpdateValues[property];
     }
@@ -253,30 +243,31 @@ export default class CacheableObject {
     if (flags.update && !transform) {
       return null;
     } else if (flags.update && compute) {
-      throw new Error(
-        `Updating property ${property} has compute function, should be formatted as transform`
-      );
+      throw new Error(`Updating property ${property} has compute function, should be formatted as transform`);
     } else if (!flags.update && !compute) {
-      throw new Error(
-        `Exposed property ${property} does not update and is missing compute function`
-      );
+      throw new Error(`Exposed property ${property} does not update and is missing compute function`);
     }
 
-    const dependencyKeys = expose.dependencies || [];
-    const dependencyGetters = dependencyKeys.map((key) => () => [
-      key,
-      this.#propertyUpdateValues[key],
-    ]);
-    const getAllDependencies = () =>
-      Object.fromEntries(
-        dependencyGetters
-          .map((f) => f())
-          .concat([[this.constructor.instance, this]])
-      );
+    let getAllDependencies;
+
+    const dependencyKeys = expose.dependencies;
+    if (dependencyKeys?.length > 0) {
+      const reflectionEntry = [this.constructor.instance, this];
+      const dependencyGetters = dependencyKeys
+        .map(key => () => [key, this.#propertyUpdateValues[key]]);
+
+      getAllDependencies = () =>
+        Object.fromEntries(dependencyGetters
+          .map(f => f())
+          .concat([reflectionEntry]));
+    } else {
+      const allDependencies = {[this.constructor.instance]: this};
+      Object.freeze(allDependencies);
+      getAllDependencies = () => allDependencies;
+    }
 
     if (flags.update) {
-      return () =>
-        transform(this.#propertyUpdateValues[property], getAllDependencies());
+      return () => transform(this.#propertyUpdateValues[property], getAllDependencies());
     } else {
       return () => compute(getAllDependencies());
     }
@@ -321,14 +312,14 @@ export default class CacheableObject {
       return;
     }
 
-    if (!obj.constructor.propertyDescriptors) {
+    const {propertyDescriptors} = obj.constructor;
+
+    if (!propertyDescriptors) {
       console.warn('Missing property descriptors:', obj);
       return;
     }
 
-    for (const [property, descriptor] of Object.entries(
-      obj.constructor.propertyDescriptors
-    )) {
+    for (const [property, descriptor] of Object.entries(propertyDescriptors)) {
       const {flags} = descriptor;
 
       if (!flags.expose) {
diff --git a/src/data/patches.js b/src/data/patches.js
index dc757fa9..feeaf39b 100644
--- a/src/data/patches.js
+++ b/src/data/patches.js
@@ -1,5 +1,3 @@
-/** @format */
-
 // --> Patch
 
 export class Patch {
@@ -137,6 +135,7 @@ export class PatchManager extends Patch {
     this.#externalInputPatch = new PatchManagerExternalInputPatch({
       manager: this,
     });
+
     this.#externalOutputPatch = new PatchManagerExternalOutputPatch({
       manager: this,
     });
@@ -184,9 +183,7 @@ export class PatchManager extends Patch {
 
   addManagedInput(patchWithInput, inputName, patchWithOutput, outputName) {
     if (patchWithInput.manager !== this || patchWithOutput.manager !== this) {
-      throw new Error(
-        `Input and output patches must belong to same manager (this)`
-      );
+      throw new Error(`Input and output patches must belong to same manager (this)`);
     }
 
     const input = patchWithInput.inputs[inputName];
diff --git a/src/data/serialize.js b/src/data/serialize.js
index a4206fd0..52aacb07 100644
--- a/src/data/serialize.js
+++ b/src/data/serialize.js
@@ -1,6 +1,4 @@
-/** @format */
-
-// serialize-util.js: simple interface and utility functions for converting
+// serialize.js: simple interface and utility functions for converting
 // Things into a directly serializeable format
 
 // Utility functions
@@ -27,17 +25,14 @@ export const serializeDescriptors = Symbol();
 
 export function serializeThing(thing) {
   const descriptors = thing.constructor[serializeDescriptors];
+
   if (!descriptors) {
-    throw new Error(
-      `Constructor ${thing.constructor.name} does not provide serialize descriptors`
-    );
+    throw new Error(`Constructor ${thing.constructor.name} does not provide serialize descriptors`);
   }
 
   return Object.fromEntries(
-    Object.entries(descriptors).map(([property, transform]) => [
-      property,
-      transform(thing[property]),
-    ])
+    Object.entries(descriptors)
+      .map(([property, transform]) => [property, transform(thing[property])])
   );
 }
 
diff --git a/src/data/things.js b/src/data/things.js
index 4aa684d4..33880460 100644
--- a/src/data/things.js
+++ b/src/data/things.js
@@ -1,5 +1,3 @@
-/** @format */
-
 // things.js: class definitions for various object types used across the wiki,
 // most of which correspond to an output page, such as Track, Album, Artist
 
@@ -236,9 +234,7 @@ Thing.common = {
   referenceList: (thingClass) => {
     const {[Thing.referenceType]: referenceType} = thingClass;
     if (!referenceType) {
-      throw new Error(
-        `The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`
-      );
+      throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`);
     }
 
     return {
@@ -251,9 +247,7 @@ Thing.common = {
   singleReference: (thingClass) => {
     const {[Thing.referenceType]: referenceType} = thingClass;
     if (!referenceType) {
-      throw new Error(
-        `The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`
-      );
+      throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`);
     }
 
     return {
@@ -441,15 +435,13 @@ Thing.common = {
 // constructor's [Thing.referenceType] as the prefix. This will throw an error
 // if the thing's directory isn't yet provided/computable.
 Thing.getReference = function (thing) {
-  if (!thing.constructor[Thing.referenceType])
-    throw TypeError(
-      `Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`
-    );
+  if (!thing.constructor[Thing.referenceType]) {
+    throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`);
+  }
 
-  if (!thing.directory)
-    throw TypeError(
-      `Passed ${thing.constructor.name} is missing its directory`
-    );
+  if (!thing.directory) {
+    throw TypeError(`Passed ${thing.constructor.name} is missing its directory`);
+  }
 
   return `${thing.constructor[Thing.referenceType]}:${thing.directory}`;
 };
@@ -735,10 +727,11 @@ Track.propertyDescriptors = {
 
     expose: {
       dependencies: ['albumData', 'coverArtistContribsByRef'],
-      transform: (
-        hasCoverArt,
-        {albumData, coverArtistContribsByRef, [Track.instance]: track}
-      ) =>
+      transform: (hasCoverArt, {
+        albumData,
+        coverArtistContribsByRef,
+        [Track.instance]: track,
+      }) =>
         Track.hasCoverArt(
           track,
           albumData,
@@ -755,15 +748,12 @@ Track.propertyDescriptors = {
 
     expose: {
       dependencies: ['albumData', 'coverArtistContribsByRef'],
-      transform: (
-        coverArtFileExtension,
-        {
-          albumData,
-          coverArtistContribsByRef,
-          hasCoverArt,
-          [Track.instance]: track,
-        }
-      ) =>
+      transform: (coverArtFileExtension, {
+        albumData,
+        coverArtistContribsByRef,
+        hasCoverArt,
+        [Track.instance]: track,
+      }) =>
         coverArtFileExtension ??
         (Track.hasCoverArt(
           track,
@@ -851,10 +841,11 @@ Track.propertyDescriptors = {
 
     expose: {
       dependencies: ['albumData', 'dateFirstReleased'],
-      transform: (
-        coverArtDate,
-        {albumData, dateFirstReleased, [Track.instance]: track}
-      ) =>
+      transform: (coverArtDate, {
+        albumData,
+        dateFirstReleased,
+        [Track.instance]: track,
+      }) =>
         coverArtDate ??
         dateFirstReleased ??
         Track.findAlbum(track, albumData)?.trackArtDate ??
@@ -1691,9 +1682,7 @@ Object.assign(Language.prototype, {
 
   formatString(key, args = {}) {
     if (this.strings && !this.strings_htmlEscaped) {
-      throw new Error(
-        `HTML-escaped strings unavailable - please ensure escapeHTML function is provided`
-      );
+      throw new Error(`HTML-escaped strings unavailable - please ensure escapeHTML function is provided`);
     }
 
     return this.formatStringHelper(this.strings_htmlEscaped, key, args);
@@ -1780,12 +1769,7 @@ Object.assign(Language.prototype, {
 
   formatIndex(value) {
     this.assertIntlAvailable('intl_pluralOrdinal');
-    return this.formatString(
-      'count.index.' + this.intl_pluralOrdinal.select(value),
-      {
-        index: value,
-      }
-    );
+    return this.formatString('count.index.' + this.intl_pluralOrdinal.select(value), {index: value});
   },
 
   formatNumber(value) {
@@ -1803,10 +1787,7 @@ Object.assign(Language.prototype, {
         ? this.formatString('count.words.thousand', {words: num})
         : this.formatString('count.words', {words: num});
 
-    return this.formatString(
-      'count.words.withUnit.' + this.getUnitForm(value),
-      {words}
-    );
+    return this.formatString('count.words.withUnit.' + this.getUnitForm(value), {words});
   },
 
   // Conjunction list: A, B, and C
diff --git a/src/data/validators.js b/src/data/validators.js
index 8d922399..5c357c83 100644
--- a/src/data/validators.js
+++ b/src/data/validators.js
@@ -1,5 +1,3 @@
-/** @format */
-
 import {withAggregate} from '../util/sugar.js';
 
 import {color, ENABLE_COLOR} from '../util/cli.js';
@@ -104,9 +102,7 @@ export function isInstance(value, constructor) {
   isObject(value);
 
   if (!(value instanceof constructor))
-    throw new TypeError(
-      `Expected ${constructor.name}, got ${value.constructor.name}`
-    );
+    throw new TypeError(`Expected ${constructor.name}, got ${value.constructor.name}`);
 
   return true;
 }
@@ -142,9 +138,7 @@ function validateArrayItemsHelper(itemValidator) {
         throw new Error(`Expected validator to return true`);
       }
     } catch (error) {
-      error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${
-        error.message
-      }`;
+      error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${error.message}`;
       throw error;
     }
   };
@@ -174,10 +168,8 @@ export function isColor(color) {
   isStringNonEmpty(color);
 
   if (color.startsWith('#')) {
-    if (![1 + 3, 1 + 4, 1 + 6, 1 + 8].includes(color.length))
-      throw new TypeError(
-        `Expected #rgb, #rgba, #rrggbb, or #rrggbbaa, got length ${color.length}`
-      );
+    if (![4, 5, 7, 9].includes(color.length))
+      throw new TypeError(`Expected #rgb, #rgba, #rrggbb, or #rrggbbaa, got length ${color.length}`);
 
     if (/[^0-9a-fA-F]/.test(color.slice(1)))
       throw new TypeError(`Expected hexadecimal digits`);
@@ -204,37 +196,26 @@ export function validateProperties(spec) {
     if (Array.isArray(object))
       throw new TypeError(`Expected an object, got array`);
 
-    withAggregate(
-      {message: `Errors validating object properties`},
-      ({call}) => {
-        for (const [specKey, specValidator] of specEntries) {
-          call(() => {
-            const value = object[specKey];
-            try {
-              specValidator(value);
-            } catch (error) {
-              error.message = `(key: ${color.green(specKey)}, value: ${inspect(
-                value
-              )}) ${error.message}`;
-              throw error;
-            }
-          });
-        }
+    withAggregate({message: `Errors validating object properties`}, ({call}) => {
+      for (const [specKey, specValidator] of specEntries) {
+        call(() => {
+          const value = object[specKey];
+          try {
+            specValidator(value);
+          } catch (error) {
+            error.message = `(key: ${color.green(specKey)}, value: ${inspect(value)}) ${error.message}`;
+            throw error;
+          }
+        });
+      }
 
-        const unknownKeys = Object.keys(object).filter(
-          (key) => !specKeys.includes(key)
-        );
-        if (unknownKeys.length > 0) {
-          call(() => {
-            throw new Error(
-              `Unknown keys present (${
-                unknownKeys.length
-              }): [${unknownKeys.join(', ')}]`
-            );
-          });
-        }
+      const unknownKeys = Object.keys(object).filter((key) => !specKeys.includes(key));
+      if (unknownKeys.length > 0) {
+        call(() => {
+          throw new Error(`Unknown keys present (${unknownKeys.length}): [${unknownKeys.join(', ')}]`);
+        });
       }
-    );
+    });
 
     return true;
   };
@@ -243,7 +224,9 @@ export function validateProperties(spec) {
 export const isContribution = validateProperties({
   who: isArtistRef,
   what: (value) =>
-    value === undefined || value === null || isStringNonEmpty(value),
+    value === undefined ||
+    value === null ||
+    isStringNonEmpty(value),
 });
 
 export const isContributionList = validateArrayItems(isContribution);
@@ -251,7 +234,9 @@ export const isContributionList = validateArrayItems(isContribution);
 export const isAdditionalFile = validateProperties({
   title: isString,
   description: (value) =>
-    value === undefined || value === null || isString(value),
+    value === undefined ||
+    value === null ||
+    isString(value),
   files: validateArrayItems(isString),
 });
 
@@ -274,9 +259,7 @@ export function isDirectory(directory) {
   isStringNonEmpty(directory);
 
   if (directory.match(/[^a-zA-Z0-9_-]/))
-    throw new TypeError(
-      `Expected only letters, numbers, dash, and underscore, got "${directory}"`
-    );
+    throw new TypeError(`Expected only letters, numbers, dash, and underscore, got "${directory}"`);
 
   return true;
 }
@@ -331,16 +314,14 @@ export function validateReference(type = 'track') {
 
     if (!match) throw new TypeError(`Malformed reference`);
 
-    const {
-      groups: {typePart, directoryPart},
-    } = match;
+    const {groups: {typePart, directoryPart}} = match;
 
-    if (typePart && typePart !== type)
-      throw new TypeError(
-        `Expected ref to begin with "${type}:", got "${typePart}:"`
-      );
+    if (typePart) {
+      if (typePart !== type)
+        throw new TypeError(`Expected ref to begin with "${type}:", got "${typePart}:"`);
 
-    if (typePart) isDirectory(directoryPart);
+      isDirectory(directoryPart);
+    }
 
     isName(ref);
 
@@ -381,9 +362,6 @@ export function oneOf(...checks) {
       error.check = check;
       errors.push(error);
     }
-    throw new AggregateError(
-      errors,
-      `Expected one of ${checks.length} possible checks, but none were true`
-    );
+    throw new AggregateError(errors, `Expected one of ${checks.length} possible checks, but none were true`);
   };
 }
diff --git a/src/data/yaml.js b/src/data/yaml.js
index e18b7334..2adce50b 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -1,5 +1,3 @@
-/** @format */
-
 // yaml.js - specification for HSMusic YAML data file format and utilities for
 // loading and processing YAML files and documents
 
@@ -112,11 +110,8 @@ function makeProcessDocument(
   // Invert the property-field mapping, since it'll come in handy for
   // assigning update() source values later.
   const fieldPropertyMapping = Object.fromEntries(
-    Object.entries(propertyFieldMapping).map(([property, field]) => [
-      field,
-      property,
-    ])
-  );
+    Object.entries(propertyFieldMapping)
+      .map(([property, field]) => [field, property]));
 
   const decorateErrorWithName = (fn) => {
     const nameField = propertyFieldMapping['name'];
@@ -136,9 +131,8 @@ function makeProcessDocument(
   };
 
   return decorateErrorWithName((document) => {
-    const documentEntries = Object.entries(document).filter(
-      ([field]) => !ignoredFields.includes(field)
-    );
+    const documentEntries = Object.entries(document)
+      .filter(([field]) => !ignoredFields.includes(field));
 
     const unknownFields = documentEntries
       .map(([field]) => field)
@@ -167,22 +161,17 @@ function makeProcessDocument(
 
     const thing = Reflect.construct(thingClass, []);
 
-    withAggregate(
-      {message: `Errors applying ${color.green(thingClass.name)} properties`},
-      ({call}) => {
-        for (const [property, value] of Object.entries(sourceProperties)) {
-          call(() => (thing[property] = value));
-        }
+    withAggregate({message: `Errors applying ${color.green(thingClass.name)} properties`}, ({call}) => {
+      for (const [property, value] of Object.entries(sourceProperties)) {
+        call(() => (thing[property] = value));
       }
-    );
+    });
 
     return thing;
   });
 }
 
-makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends (
-  Error
-) {
+makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends Error {
   constructor(fields) {
     super(`Unknown fields present: ${fields.join(', ')}`);
     this.fields = fields;
@@ -191,13 +180,13 @@ makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends (
 
 export const processAlbumDocument = makeProcessDocument(Album, {
   fieldTransformations: {
-    Artists: parseContributors,
+    'Artists': parseContributors,
     'Cover Artists': parseContributors,
     'Default Track Cover Artists': parseContributors,
     'Wallpaper Artists': parseContributors,
     'Banner Artists': parseContributors,
 
-    Date: (value) => new Date(value),
+    'Date': (value) => new Date(value),
     'Date Added': (value) => new Date(value),
     'Cover Art Date': (value) => new Date(value),
     'Default Track Cover Art Date': (value) => new Date(value),
@@ -263,13 +252,13 @@ export const processTrackGroupDocument = makeProcessDocument(TrackGroup, {
 
 export const processTrackDocument = makeProcessDocument(Track, {
   fieldTransformations: {
-    Duration: getDurationInSeconds,
+    'Duration': getDurationInSeconds,
 
     'Date First Released': (value) => new Date(value),
     'Cover Art Date': (value) => new Date(value),
 
-    Artists: parseContributors,
-    Contributors: parseContributors,
+    'Artists': parseContributors,
+    'Contributors': parseContributors,
     'Cover Artists': parseContributors,
 
     'Additional Files': parseAdditionalFiles,
@@ -323,9 +312,9 @@ export const processArtistDocument = makeProcessDocument(Artist, {
 
 export const processFlashDocument = makeProcessDocument(Flash, {
   fieldTransformations: {
-    Date: (value) => new Date(value),
+    'Date': (value) => new Date(value),
 
-    Contributors: parseContributors,
+    'Contributors': parseContributors,
   },
 
   propertyFieldMapping: {
@@ -354,7 +343,7 @@ export const processFlashActDocument = makeProcessDocument(FlashAct, {
 
 export const processNewsEntryDocument = makeProcessDocument(NewsEntry, {
   fieldTransformations: {
-    Date: (value) => new Date(value),
+    'Date': (value) => new Date(value),
   },
 
   propertyFieldMapping: {
@@ -421,16 +410,13 @@ export const processWikiInfoDocument = makeProcessDocument(WikiInfo, {
   },
 });
 
-export const processHomepageLayoutDocument = makeProcessDocument(
-  HomepageLayout,
-  {
-    propertyFieldMapping: {
-      sidebarContent: 'Sidebar Content',
-    },
+export const processHomepageLayoutDocument = makeProcessDocument(HomepageLayout, {
+  propertyFieldMapping: {
+    sidebarContent: 'Sidebar Content',
+  },
 
-    ignoredFields: ['Homepage'],
-  }
-);
+  ignoredFields: ['Homepage'],
+});
 
 export function makeProcessHomepageLayoutRowDocument(rowClass, spec) {
   return makeProcessDocument(rowClass, {
@@ -459,9 +445,8 @@ export const homepageLayoutRowTypeProcessMapping = {
 export function processHomepageLayoutRowDocument(document) {
   const type = document['Type'];
 
-  const match = Object.entries(homepageLayoutRowTypeProcessMapping).find(
-    ([key]) => key === type
-  );
+  const match = Object.entries(homepageLayoutRowTypeProcessMapping)
+    .find(([key]) => key === type);
 
   if (!match) {
     throw new TypeError(`No processDocument function for row type ${type}!`);
@@ -507,14 +492,9 @@ export function parseAdditionalFiles(array) {
 
 export function parseCommentary(text) {
   if (text) {
-    const lines = String(text).split('\n');
+    const lines = String(text.trim()).split('\n');
     if (!lines[0].replace(/<\/b>/g, '').includes(':</i>')) {
-      return {
-        error: `An entry is missing commentary citation: "${lines[0].slice(
-          0,
-          40
-        )}..."`,
-      };
+      throw new Error(`Missing commentary citation: "${lines[0].slice(0, 40)}..."`);
     }
     return text;
   } else {
@@ -547,9 +527,7 @@ export function parseContributors(contributors) {
 
   const badContributor = contributors.find((val) => typeof val === 'string');
   if (badContributor) {
-    return {
-      error: `An entry has an incorrectly formatted contributor, "${badContributor}".`,
-    };
+    throw new Error(`Incorrectly formatted contribution: "${badContributor}".`);
   }
 
   if (contributors.length === 1 && contributors[0].who === 'none') {
@@ -565,13 +543,17 @@ function parseDimensions(string) {
   }
 
   const parts = string.split(/[x,* ]+/g);
-  if (parts.length !== 2)
-    throw new Error(`Invalid dimensions: ${string} (expected width & height)`);
+
+  if (parts.length !== 2) {
+    throw new Error(`Invalid dimensions: ${string} (expected "width & height")`);
+  }
+
   const nums = parts.map((part) => Number(part.trim()));
-  if (nums.includes(NaN))
-    throw new Error(
-      `Invalid dimensions: ${string} (couldn't parse as numbers)`
-    );
+
+  if (nums.includes(NaN)) {
+    throw new Error(`Invalid dimensions: ${string} (couldn't parse as numbers)`);
+  }
+
   return nums;
 }
 
@@ -671,7 +653,7 @@ export const dataSteps = [
     files: async (dataPath) =>
       (
         await findFiles(path.join(dataPath, DATA_ALBUM_DIRECTORY), {
-          filter: f => path.extname(f) === '.yaml',
+          filter: (f) => path.extname(f) === '.yaml',
           joinParentDirectory: false,
         })
       ).map(file => path.join(DATA_ALBUM_DIRECTORY, file)),
@@ -759,16 +741,14 @@ export const dataSteps = [
 
       const artistAliasData = results.flatMap((artist) => {
         const origRef = Thing.getReference(artist);
-        return (
-          artist.aliasNames?.map((name) => {
-            const alias = new Artist();
-            alias.name = name;
-            alias.isAlias = true;
-            alias.aliasedArtistRef = origRef;
-            alias.artistData = artistData;
-            return alias;
-          }) ?? []
-        );
+        return artist.aliasNames?.map((name) => {
+          const alias = new Artist();
+          alias.name = name;
+          alias.isAlias = true;
+          alias.aliasedArtistRef = origRef;
+          alias.artistData = artistData;
+          return alias;
+        }) ?? [];
       });
 
       return {artistData, artistAliasData};
@@ -856,9 +836,7 @@ export const dataSteps = [
       }
 
       const groupData = results.filter((x) => x instanceof Group);
-      const groupCategoryData = results.filter(
-        (x) => x instanceof GroupCategory
-      );
+      const groupCategoryData = results.filter((x) => x instanceof GroupCategory);
 
       return {groupData, groupCategoryData};
     },
@@ -945,9 +923,7 @@ export async function loadAndProcessDataDocuments({dataPath}) {
       } catch (error) {
         error.message +=
           (error.message.includes('\n') ? '\n' : ' ') +
-          `(file: ${color.bright(
-            color.blue(path.relative(dataPath, x.file))
-          )})`;
+          `(file: ${color.bright(color.blue(path.relative(dataPath, x.file)))})`;
         throw error;
       }
     };
@@ -968,17 +944,14 @@ export async function loadAndProcessDataDocuments({dataPath}) {
           documentMode === documentModes.oneDocumentTotal
         ) {
           if (!dataStep.file) {
-            throw new Error(
-              `Expected 'file' property for ${documentMode.toString()}`
-            );
+            throw new Error(`Expected 'file' property for ${documentMode.toString()}`);
           }
 
           const file = path.join(
             dataPath,
             typeof dataStep.file === 'function'
               ? await callAsync(dataStep.file, dataPath)
-              : dataStep.file
-          );
+              : dataStep.file);
 
           const readResult = await callAsync(readFile, file, 'utf-8');
 
@@ -1005,8 +978,7 @@ export async function loadAndProcessDataDocuments({dataPath}) {
             const {result, aggregate} = mapAggregate(
               yamlResult,
               decorateErrorWithIndex(dataStep.processDocument),
-              {message: `Errors processing documents`}
-            );
+              {message: `Errors processing documents`});
             processResults = result;
             call(aggregate.close);
           }
@@ -1023,9 +995,7 @@ export async function loadAndProcessDataDocuments({dataPath}) {
         }
 
         if (!dataStep.files) {
-          throw new Error(
-            `Expected 'files' property for ${documentMode.toString()}`
-          );
+          throw new Error(`Expected 'files' property for ${documentMode.toString()}`);
         }
 
         let files = (
@@ -1042,8 +1012,7 @@ export async function loadAndProcessDataDocuments({dataPath}) {
 
         const readResults = await mapAsync(
           files,
-          (file) =>
-            readFile(file, 'utf-8').then((contents) => ({file, contents})),
+          (file) => readFile(file, 'utf-8').then((contents) => ({file, contents})),
           {message: `Errors reading data files`}
         );
 
@@ -1059,82 +1028,76 @@ export async function loadAndProcessDataDocuments({dataPath}) {
         let processResults;
 
         if (documentMode === documentModes.headerAndEntries) {
-          nest(
-            {message: `Errors processing data files as valid documents`},
-            ({call, map}) => {
-              processResults = [];
-
-              yamlResults.forEach(({file, documents}) => {
-                const [headerDocument, ...entryDocuments] = documents;
-
-                const header = call(
-                  decorateErrorWithFile(({document}) =>
-                    dataStep.processHeaderDocument(document)
-                  ),
-                  {file, document: headerDocument}
-                );
-
-                // Don't continue processing files whose header
-                // document is invalid - the entire file is excempt
-                // from data in this case.
-                if (!header) {
-                  return;
-                }
-
-                const entries = map(
-                  entryDocuments.map((document) => ({file, document})),
-                  decorateErrorWithFile(
-                    decorateErrorWithIndex(({document}) =>
-                      dataStep.processEntryDocument(document)
-                    )
-                  ),
-                  {message: `Errors processing entry documents`}
-                );
+          nest({message: `Errors processing data files as valid documents`}, ({call, map}) => {
+            processResults = [];
+
+            yamlResults.forEach(({file, documents}) => {
+              const [headerDocument, ...entryDocuments] = documents;
+
+              const header = call(
+                decorateErrorWithFile(({document}) =>
+                  dataStep.processHeaderDocument(document)
+                ),
+                {file, document: headerDocument}
+              );
+
+              // Don't continue processing files whose header
+              // document is invalid - the entire file is excempt
+              // from data in this case.
+              if (!header) {
+                return;
+              }
 
-                // Entries may be incomplete (i.e. any errored
-                // documents won't have a processed output
-                // represented here) - this is intentional! By
-                // principle, partial output is preferred over
-                // erroring an entire file.
-                processResults.push({header, entries});
-              });
-            }
-          );
+              const entries = map(
+                entryDocuments.map((document) => ({file, document})),
+                decorateErrorWithFile(
+                  decorateErrorWithIndex(({document}) =>
+                    dataStep.processEntryDocument(document)
+                  )
+                ),
+                {message: `Errors processing entry documents`}
+              );
+
+              // Entries may be incomplete (i.e. any errored
+              // documents won't have a processed output
+              // represented here) - this is intentional! By
+              // principle, partial output is preferred over
+              // erroring an entire file.
+              processResults.push({header, entries});
+            });
+          });
         }
 
         if (documentMode === documentModes.onePerFile) {
-          nest(
-            {message: `Errors processing data files as valid documents`},
-            ({call}) => {
-              processResults = [];
-
-              yamlResults.forEach(({file, documents}) => {
-                if (documents.length > 1) {
-                  call(
-                    decorateErrorWithFile(() => {
-                      throw new Error(
-                        `Only expected one document to be present per file`
-                      );
-                    })
-                  );
-                  return;
-                }
-
-                const result = call(
-                  decorateErrorWithFile(({document}) =>
-                    dataStep.processDocument(document)
-                  ),
-                  {file, document: documents[0]}
+          nest({message: `Errors processing data files as valid documents`}, ({call}) => {
+            processResults = [];
+
+            yamlResults.forEach(({file, documents}) => {
+              if (documents.length > 1) {
+                call(
+                  decorateErrorWithFile(() => {
+                    throw new Error(
+                      `Only expected one document to be present per file`
+                    );
+                  })
                 );
+                return;
+              }
 
-                if (!result) {
-                  return;
-                }
+              const result = call(
+                decorateErrorWithFile(({document}) =>
+                  dataStep.processDocument(document)
+                ),
+                {file, document: documents[0]}
+              );
 
-                processResults.push(result);
-              });
-            }
-          );
+              if (!result) {
+                return;
+              }
+
+              processResults.push(result);
+            });
+          });
         }
 
         const saveResult = call(dataStep.save, processResults);
@@ -1158,9 +1121,10 @@ export async function loadAndProcessDataDocuments({dataPath}) {
 export function linkWikiDataArrays(wikiData) {
   function assignWikiData(things, ...keys) {
     for (let i = 0; i < things.length; i++) {
+      const thing = things[i];
       for (let j = 0; j < keys.length; j++) {
         const key = keys[j];
-        things[i][key] = wikiData[key];
+        thing[key] = wikiData[key];
       }
     }
   }
@@ -1169,32 +1133,11 @@ export function linkWikiDataArrays(wikiData) {
 
   assignWikiData([WD.wikiInfo], 'groupData');
 
-  assignWikiData(
-    WD.albumData,
-    'artistData',
-    'artTagData',
-    'groupData',
-    'trackData'
-  );
-  WD.albumData.forEach((album) =>
-    assignWikiData(album.trackGroups, 'trackData')
-  );
-
-  assignWikiData(
-    WD.trackData,
-    'albumData',
-    'artistData',
-    'artTagData',
-    'flashData',
-    'trackData'
-  );
-  assignWikiData(
-    WD.artistData,
-    'albumData',
-    'artistData',
-    'flashData',
-    'trackData'
-  );
+  assignWikiData(WD.albumData, 'artistData', 'artTagData', 'groupData', 'trackData');
+  WD.albumData.forEach((album) => assignWikiData(album.trackGroups, 'trackData'));
+
+  assignWikiData(WD.trackData, 'albumData', 'artistData', 'artTagData', 'flashData', 'trackData');
+  assignWikiData(WD.artistData, 'albumData', 'artistData', 'flashData', 'trackData');
   assignWikiData(WD.groupData, 'albumData', 'groupCategoryData');
   assignWikiData(WD.groupCategoryData, 'groupData');
   assignWikiData(WD.flashData, 'artistData', 'flashActData', 'trackData');
@@ -1236,48 +1179,47 @@ export function filterDuplicateDirectories(wikiData) {
   const aggregate = openAggregate({message: `Duplicate directories found`});
   for (const thingDataProp of deduplicateSpec) {
     const thingData = wikiData[thingDataProp];
-    aggregate.nest(
-      {
-        message: `Duplicate directories found in ${color.green(
-          'wikiData.' + thingDataProp
-        )}`,
-      },
-      ({call}) => {
-        const directoryPlaces = Object.create(null);
-        const duplicateDirectories = [];
-        for (const thing of thingData) {
-          const {directory} = thing;
-          if (directory in directoryPlaces) {
-            directoryPlaces[directory].push(thing);
-            duplicateDirectories.push(directory);
-          } else {
-            directoryPlaces[directory] = [thing];
-          }
+    aggregate.nest({message: `Duplicate directories found in ${color.green('wikiData.' + thingDataProp)}`}, ({call}) => {
+      const directoryPlaces = Object.create(null);
+      const duplicateDirectories = [];
+
+      for (const thing of thingData) {
+        const {directory} = thing;
+        if (directory in directoryPlaces) {
+          directoryPlaces[directory].push(thing);
+          duplicateDirectories.push(directory);
+        } else {
+          directoryPlaces[directory] = [thing];
         }
-        if (empty(duplicateDirectories)) return;
-        duplicateDirectories.sort((a, b) => {
-          const aL = a.toLowerCase();
-          const bL = b.toLowerCase();
-          return aL < bL ? -1 : aL > bL ? 1 : 0;
+      }
+
+      if (empty(duplicateDirectories)) return;
+
+      duplicateDirectories.sort((a, b) => {
+        const aL = a.toLowerCase();
+        const bL = b.toLowerCase();
+        return aL < bL ? -1 : aL > bL ? 1 : 0;
+      });
+
+      for (const directory of duplicateDirectories) {
+        const places = directoryPlaces[directory];
+        call(() => {
+          throw new Error(
+            `Duplicate directory ${color.green(directory)}:\n` +
+              places.map((thing) => ` - ` + inspect(thing)).join('\n')
+          );
         });
-        for (const directory of duplicateDirectories) {
-          const places = directoryPlaces[directory];
-          call(() => {
-            throw new Error(
-              `Duplicate directory ${color.green(directory)}:\n` +
-                places.map((thing) => ` - ` + inspect(thing)).join('\n')
-            );
-          });
-        }
-        const allDuplicatedThings = Object.values(directoryPlaces)
-          .filter((arr) => arr.length > 1)
-          .flat();
-        const filteredThings = thingData.filter(
-          (thing) => !allDuplicatedThings.includes(thing)
-        );
-        wikiData[thingDataProp] = filteredThings;
       }
-    );
+
+      const allDuplicatedThings = Object.values(directoryPlaces)
+        .filter((arr) => arr.length > 1)
+        .flat();
+
+      const filteredThings = thingData
+        .filter((thing) => !allDuplicatedThings.includes(thing));
+
+      wikiData[thingDataProp] = filteredThings;
+    });
   }
 
   // TODO: This code closes the aggregate but it generally gets closed again
@@ -1303,67 +1245,46 @@ export function filterDuplicateDirectories(wikiData) {
 // data array.
 export function filterReferenceErrors(wikiData) {
   const referenceSpec = [
-    [
-      'wikiInfo',
-      {
-        divideTrackListsByGroupsByRef: 'group',
-      },
-    ],
-
-    [
-      'albumData',
-      {
-        artistContribsByRef: '_contrib',
-        coverArtistContribsByRef: '_contrib',
-        trackCoverArtistContribsByRef: '_contrib',
-        wallpaperArtistContribsByRef: '_contrib',
-        bannerArtistContribsByRef: '_contrib',
-        groupsByRef: 'group',
-        artTagsByRef: 'artTag',
-      },
-    ],
-
-    [
-      'trackData',
-      {
-        artistContribsByRef: '_contrib',
-        contributorContribsByRef: '_contrib',
-        coverArtistContribsByRef: '_contrib',
-        referencedTracksByRef: 'track',
-        artTagsByRef: 'artTag',
-        originalReleaseTrackByRef: 'track',
-      },
-    ],
-
-    [
-      'groupCategoryData',
-      {
-        groupsByRef: 'group',
-      },
-    ],
-
-    [
-      'homepageLayout.rows',
-      {
-        sourceGroupsByRef: 'group',
-        sourceAlbumsByRef: 'album',
-      },
-    ],
-
-    [
-      'flashData',
-      {
-        contributorContribsByRef: '_contrib',
-        featuredTracksByRef: 'track',
-      },
-    ],
-
-    [
-      'flashActData',
-      {
-        flashesByRef: 'flash',
-      },
-    ],
+    ['wikiInfo', {
+      divideTrackListsByGroupsByRef: 'group',
+    }],
+
+    ['albumData', {
+      artistContribsByRef: '_contrib',
+      coverArtistContribsByRef: '_contrib',
+      trackCoverArtistContribsByRef: '_contrib',
+      wallpaperArtistContribsByRef: '_contrib',
+      bannerArtistContribsByRef: '_contrib',
+      groupsByRef: 'group',
+      artTagsByRef: 'artTag',
+    }],
+
+    ['trackData', {
+      artistContribsByRef: '_contrib',
+      contributorContribsByRef: '_contrib',
+      coverArtistContribsByRef: '_contrib',
+      referencedTracksByRef: 'track',
+      artTagsByRef: 'artTag',
+      originalReleaseTrackByRef: 'track',
+    }],
+
+    ['groupCategoryData', {
+      groupsByRef: 'group',
+    }],
+
+    ['homepageLayout.rows', {
+      sourceGroupsByRef: 'group',
+      sourceAlbumsByRef: 'album',
+    }],
+
+    ['flashData', {
+      contributorContribsByRef: '_contrib',
+      featuredTracksByRef: 'track',
+    }],
+
+    ['flashActData', {
+      flashesByRef: 'flash',
+    }],
   ];
 
   function getNestedProp(obj, key) {
@@ -1373,94 +1294,56 @@ export function filterReferenceErrors(wikiData) {
     return recursive(obj, keys);
   }
 
-  const aggregate = openAggregate({
-    message: `Errors validating between-thing references in data`,
-  });
+  const aggregate = openAggregate({message: `Errors validating between-thing references in data`});
   const boundFind = bindFind(wikiData, {mode: 'error'});
   for (const [thingDataProp, propSpec] of referenceSpec) {
     const thingData = getNestedProp(wikiData, thingDataProp);
-    aggregate.nest(
-      {
-        message: `Reference errors in ${color.green(
-          'wikiData.' + thingDataProp
-        )}`,
-      },
-      ({nest}) => {
-        const things = Array.isArray(thingData) ? thingData : [thingData];
-        for (const thing of things) {
-          nest(
-            {message: `Reference errors in ${inspect(thing)}`},
-            ({filter}) => {
-              for (const [property, findFnKey] of Object.entries(propSpec)) {
-                if (!thing[property]) continue;
-                if (findFnKey === '_contrib') {
-                  thing[property] = filter(
-                    thing[property],
-                    decorateErrorWithIndex(({who}) => {
-                      const alias = find.artist(who, wikiData.artistAliasData, {
-                        mode: 'quiet',
-                      });
-                      if (alias) {
-                        const original = find.artist(
-                          alias.aliasedArtistRef,
-                          wikiData.artistData,
-                          {
-                            mode: 'quiet',
-                          }
-                        );
-                        throw new Error(
-                          `Reference ${color.red(
-                            who
-                          )} is to an alias, should be ${color.green(
-                            original.name
-                          )}`
-                        );
-                      }
-                      return boundFind.artist(who);
-                    }),
-                    {
-                      message: `Reference errors in contributions ${color.green(
-                        property
-                      )} (${color.green('find.artist')})`,
-                    }
-                  );
-                  continue;
-                }
-                const findFn = boundFind[findFnKey];
-                const value = thing[property];
-                if (Array.isArray(value)) {
-                  thing[property] = filter(
-                    value,
-                    decorateErrorWithIndex(findFn),
-                    {
-                      message: `Reference errors in property ${color.green(
-                        property
-                      )} (${color.green('find.' + findFnKey)})`,
-                    }
-                  );
-                } else {
-                  nest(
-                    {
-                      message: `Reference error in property ${color.green(
-                        property
-                      )} (${color.green('find.' + findFnKey)})`,
-                    },
-                    ({call}) => {
-                      try {
-                        call(findFn, value);
-                      } catch (error) {
-                        thing[property] = null;
-                        throw error;
-                      }
-                    }
-                  );
+
+    aggregate.nest({message: `Reference errors in ${color.green('wikiData.' + thingDataProp)}`}, ({nest}) => {
+      const things = Array.isArray(thingData) ? thingData : [thingData];
+
+      for (const thing of things) {
+        nest({message: `Reference errors in ${inspect(thing)}`}, ({filter}) => {
+          for (const [property, findFnKey] of Object.entries(propSpec)) {
+            if (!thing[property]) continue;
+
+            if (findFnKey === '_contrib') {
+              thing[property] = filter(
+                thing[property],
+                decorateErrorWithIndex(({who}) => {
+                  const alias = find.artist(who, wikiData.artistAliasData, {mode: 'quiet'});
+                  if (alias) {
+                    const original = find.artist(alias.aliasedArtistRef, wikiData.artistData, {mode: 'quiet'});
+                    throw new Error(`Reference ${color.red(who)} is to an alias, should be ${color.green(original.name)}`);
+                  }
+                  return boundFind.artist(who);
+                }),
+                {message: `Reference errors in contributions ${color.green(property)} (${color.green('find.artist')})`});
+              continue;
+            }
+
+            const findFn = boundFind[findFnKey];
+            const value = thing[property];
+
+            if (Array.isArray(value)) {
+              thing[property] = filter(
+                value,
+                decorateErrorWithIndex(findFn),
+                {message: `Reference errors in property ${color.green(property)} (${color.green('find.' + findFnKey)})`});
+            } else {
+              nest({message: `Reference error in property ${color.green(property)} (${color.green('find.' + findFnKey)})`}, ({call}) => {
+                try {
+                  call(findFn, value);
+                } catch (error) {
+                  thing[property] = null;
+                  throw error;
                 }
-              }
+              });
             }
-          );
-        }
+          }
+        });
       }
-    );
+    });
   }
 
   return aggregate;
@@ -1472,18 +1355,15 @@ export function filterReferenceErrors(wikiData) {
 // a boilerplate for more specialized output, or as a quick start in utilities
 // where reporting info about data loading isn't as relevant as during the
 // main wiki build process.
-export async function quickLoadAllFromYAML(
-  dataPath,
-  {showAggregate: customShowAggregate = showAggregate} = {}
-) {
+export async function quickLoadAllFromYAML(dataPath, {
+  showAggregate: customShowAggregate = showAggregate,
+} = {}) {
   const showAggregate = customShowAggregate;
 
   let wikiData;
 
   {
-    const {aggregate, result} = await loadAndProcessDataDocuments({
-      dataPath,
-    });
+    const {aggregate, result} = await loadAndProcessDataDocuments({dataPath});
 
     wikiData = result;