« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package.json1
-rw-r--r--src/data/files/album.js74
-rw-r--r--src/data/files/art-tag.js32
-rw-r--r--src/data/files/artist.js16
-rw-r--r--src/data/files/flash.js78
-rw-r--r--src/data/files/group.js45
-rw-r--r--src/data/files/homepage-layout.js87
-rw-r--r--src/data/files/index.js10
-rw-r--r--src/data/files/news.js16
-rw-r--r--src/data/files/sorting-rule.js13
-rw-r--r--src/data/files/static-page.js24
-rw-r--r--src/data/files/wiki-info.js10
-rw-r--r--src/data/thing.js1
-rw-r--r--src/data/things/ArtTag.js35
-rw-r--r--src/data/things/Artist.js18
-rw-r--r--src/data/things/NewsEntry.js18
-rw-r--r--src/data/things/StaticPage.js26
-rw-r--r--src/data/things/Track.js3
-rw-r--r--src/data/things/WikiInfo.js11
-rw-r--r--src/data/things/album/Album.js75
-rw-r--r--src/data/things/flash/FlashSide.js80
-rw-r--r--src/data/things/group/Group.js46
-rw-r--r--src/data/things/homepage-layout/HomepageLayout.js89
-rw-r--r--src/data/things/sorting-rule/SortingRule.js16
-rw-r--r--src/data/yaml.js14
25 files changed, 409 insertions, 429 deletions
diff --git a/package.json b/package.json
index a4154040..82245438 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
         "#cli": "./src/cli.js",
         "#data-checks": "./src/data/checks.js",
         "#external-links": "./src/external-links.js",
+        "#files": "./src/data/files/index.js",
         "#find": "./src/find.js",
         "#html": "./src/html.js",
         "#import-heck": "./src/import-heck.js",
diff --git a/src/data/files/album.js b/src/data/files/album.js
new file mode 100644
index 00000000..84cda226
--- /dev/null
+++ b/src/data/files/album.js
@@ -0,0 +1,74 @@
+import * as path from 'node:path';
+
+import {traverse} from '#node-utils';
+import {sortAlbumsTracksChronologically, sortChronologically} from '#sort';
+import {empty} from '#sugar';
+
+export default ({
+  documentModes: {headerAndEntries},
+  thingConstructors: {Album, Track, TrackSection},
+}) => ({
+  title: `Process album files`,
+
+  files: dataPath =>
+    traverse(path.join(dataPath, 'album'), {
+      filterFile: name => path.extname(name) === '.yaml',
+      prefixPath: 'album',
+    }),
+
+  documentMode: headerAndEntries,
+  headerDocumentThing: Album,
+  entryDocumentThing: document =>
+    ('Section' in document
+      ? TrackSection
+      : Track),
+
+  connect({header: album, entries}) {
+    const trackSections = [];
+
+    let currentTrackSection = new TrackSection();
+    let currentTrackSectionTracks = [];
+
+    Object.assign(currentTrackSection, {
+      name: `Default Track Section`,
+      isDefaultTrackSection: true,
+    });
+
+    const closeCurrentTrackSection = () => {
+      if (
+        currentTrackSection.isDefaultTrackSection &&
+        empty(currentTrackSectionTracks)
+      ) {
+        return;
+      }
+
+      currentTrackSection.tracks = currentTrackSectionTracks;
+      currentTrackSection.album = album;
+
+      trackSections.push(currentTrackSection);
+    };
+
+    for (const entry of entries) {
+      if (entry instanceof TrackSection) {
+        closeCurrentTrackSection();
+        currentTrackSection = entry;
+        currentTrackSectionTracks = [];
+        continue;
+      }
+
+      entry.album = album;
+      entry.trackSection = currentTrackSection;
+
+      currentTrackSectionTracks.push(entry);
+    }
+
+    closeCurrentTrackSection();
+
+    album.trackSections = trackSections;
+  },
+
+  sort({albumData, trackData}) {
+    sortChronologically(albumData);
+    sortAlbumsTracksChronologically(trackData);
+  },
+});
diff --git a/src/data/files/art-tag.js b/src/data/files/art-tag.js
new file mode 100644
index 00000000..67a22ca7
--- /dev/null
+++ b/src/data/files/art-tag.js
@@ -0,0 +1,32 @@
+import {readFile} from 'node:fs/promises';
+import * as path from 'node:path';
+
+import {traverse} from '#node-utils';
+import {sortAlphabetically} from '#sort';
+
+export default ({
+  documentModes: {allTogether},
+  thingConstructors: {ArtTag},
+}) => ({
+  title: `Process art tags file`,
+
+  files: dataPath =>
+    Promise.allSettled([
+      readFile(path.join(dataPath, 'tags.yaml'))
+        .then(() => ['tags.yaml']),
+
+      traverse(path.join(dataPath, 'art-tags'), {
+        filterFile: name => path.extname(name) === '.yaml',
+        prefixPath: 'art-tags',
+      }),
+    ]).then(results => results
+        .filter(({status}) => status === 'fulfilled')
+        .flatMap(({value}) => value)),
+
+  documentMode: allTogether,
+  documentThing: ArtTag,
+
+  sort({artTagData}) {
+    sortAlphabetically(artTagData);
+  },
+});
diff --git a/src/data/files/artist.js b/src/data/files/artist.js
new file mode 100644
index 00000000..ef971171
--- /dev/null
+++ b/src/data/files/artist.js
@@ -0,0 +1,16 @@
+import {sortAlphabetically} from '#sort';
+
+export default ({
+  documentModes: {allInOne},
+  thingConstructors: {Artist},
+}) => ({
+  title: `Process artists file`,
+  file: 'artists.yaml',
+
+  documentMode: allInOne,
+  documentThing: Artist,
+
+  sort({artistData}) {
+    sortAlphabetically(artistData);
+  },
+});
diff --git a/src/data/files/flash.js b/src/data/files/flash.js
new file mode 100644
index 00000000..3e4f750f
--- /dev/null
+++ b/src/data/files/flash.js
@@ -0,0 +1,78 @@
+import {sortFlashesChronologically} from '#sort';
+
+export default ({
+  documentModes: {allInOne},
+  thingConstructors: {Flash, FlashAct, FlashSide},
+}) => ({
+  title: `Process flashes file`,
+  file: 'flashes.yaml',
+
+  documentMode: allInOne,
+  documentThing: document =>
+    ('Side' in document
+      ? FlashSide
+   : 'Act' in document
+      ? FlashAct
+      : Flash),
+
+  connect(results) {
+    let thing, i;
+
+    for (i = 0; thing = results[i]; i++) {
+      if (thing.isFlashSide) {
+        const side = thing;
+        const acts = [];
+
+        for (i++; thing = results[i]; i++) {
+          if (thing.isFlashAct) {
+            const act = thing;
+            const flashes = [];
+
+            for (i++; thing = results[i]; i++) {
+              if (thing.isFlash) {
+                const flash = thing;
+
+                flash.act = act;
+                flashes.push(flash);
+
+                continue;
+              }
+
+              i--;
+              break;
+            }
+
+            act.side = side;
+            act.flashes = flashes;
+            acts.push(act);
+
+            continue;
+          }
+
+          if (thing.isFlash) {
+            throw new Error(`Flashes must be under an act`);
+          }
+
+          i--;
+          break;
+        }
+
+        side.acts = acts;
+
+        continue;
+      }
+
+      if (thing.isFlashAct) {
+        throw new Error(`Acts must be under a side`);
+      }
+
+      if (thing.isFlash) {
+        throw new Error(`Flashes must be under a side and act`);
+      }
+    }
+  },
+
+  sort({flashData}) {
+    sortFlashesChronologically(flashData);
+  },
+});
diff --git a/src/data/files/group.js b/src/data/files/group.js
new file mode 100644
index 00000000..c10cbf98
--- /dev/null
+++ b/src/data/files/group.js
@@ -0,0 +1,45 @@
+import Thing from '#thing';
+
+export default ({
+  documentModes: {allInOne},
+  thingConstructors: {Group, GroupCategory},
+}) => ({
+  title: `Process groups file`,
+  file: 'groups.yaml',
+
+  documentMode: allInOne,
+  documentThing: document =>
+    ('Category' in document
+      ? GroupCategory
+      : Group),
+
+  connect(results) {
+    let groupCategory;
+    let groupRefs = [];
+
+    if (results[0] && !(results[0] instanceof GroupCategory)) {
+      throw new Error(`Expected a category at top of group data file`);
+    }
+
+    for (const thing of results) {
+      if (thing instanceof GroupCategory) {
+        if (groupCategory) {
+          Object.assign(groupCategory, {groups: groupRefs});
+        }
+
+        groupCategory = thing;
+        groupRefs = [];
+      } else {
+        groupRefs.push(Thing.getReference(thing));
+      }
+    }
+
+    if (groupCategory) {
+      Object.assign(groupCategory, {groups: groupRefs});
+    }
+  },
+
+  // Groups aren't sorted at all, always preserving the order in the data
+  // file as-is.
+  sort: null,
+});
diff --git a/src/data/files/homepage-layout.js b/src/data/files/homepage-layout.js
new file mode 100644
index 00000000..646beff6
--- /dev/null
+++ b/src/data/files/homepage-layout.js
@@ -0,0 +1,87 @@
+import {empty} from '#sugar';
+
+export default ({
+  documentModes: {allInOne},
+  thingConstructors: {
+    HomepageLayout,
+    HomepageLayoutActionsRow,
+    HomepageLayoutAlbumCarouselRow,
+    HomepageLayoutAlbumGridRow,
+    HomepageLayoutRow,
+    HomepageLayoutSection,
+  },
+}) => ({
+  title: `Process homepage layout file`,
+  file: 'homepage.yaml',
+
+  documentMode: allInOne,
+  documentThing: document => {
+    if (document['Homepage']) {
+      return HomepageLayout;
+    }
+
+    if (document['Section']) {
+      return HomepageLayoutSection;
+    }
+
+    if (document['Row']) {
+      switch (document['Row']) {
+        case 'actions':
+          return HomepageLayoutActionsRow;
+        case 'album carousel':
+          return HomepageLayoutAlbumCarouselRow;
+        case 'album grid':
+          return HomepageLayoutAlbumGridRow;
+        default:
+          throw new TypeError(`Unrecognized row type ${document['Row']}`);
+      }
+    }
+
+    return null;
+  },
+
+  connect(results) {
+    if (!empty(results) && !(results[0] instanceof HomepageLayout)) {
+      throw new Error(`Expected 'Homepage' document at top of homepage layout file`);
+    }
+
+    const homepageLayout = results[0];
+    const sections = [];
+
+    let currentSection = null;
+    let currentSectionRows = [];
+
+    const closeCurrentSection = () => {
+      if (currentSection) {
+        for (const row of currentSectionRows) {
+          row.section = currentSection;
+        }
+
+        currentSection.rows = currentSectionRows;
+        sections.push(currentSection);
+
+        currentSection = null;
+        currentSectionRows = [];
+      }
+    };
+
+    for (const entry of results.slice(1)) {
+      if (entry instanceof HomepageLayout) {
+        throw new Error(`Expected only one 'Homepage' document in total`);
+      } else if (entry instanceof HomepageLayoutSection) {
+        closeCurrentSection();
+        currentSection = entry;
+      } else if (entry instanceof HomepageLayoutRow) {
+        if (currentSection) {
+          currentSectionRows.push(entry);
+        } else {
+          throw new Error(`Expected a 'Section' document to add following rows into`);
+        }
+      }
+    }
+
+    closeCurrentSection();
+
+    homepageLayout.sections = sections;
+  },
+});
diff --git a/src/data/files/index.js b/src/data/files/index.js
new file mode 100644
index 00000000..f3efebad
--- /dev/null
+++ b/src/data/files/index.js
@@ -0,0 +1,10 @@
+export {default as getAlbumLoadingSpec} from './album.js';
+export {default as getArtistLoadingSpec} from './artist.js';
+export {default as getArtTagLoadingSpec} from './art-tag.js';
+export {default as getFlashLoadingSpec} from './flash.js';
+export {default as getGroupLoadingSpec} from './group.js';
+export {default as getHomepageLayoutLoadingSpec} from './homepage-layout.js';
+export {default as getNewsLoadingSpec} from './news.js';
+export {default as getSortingRuleLoadingSpec} from './sorting-rule.js';
+export {default as getStaticPageLoadingSpec} from './static-page.js';
+export {default as getWikiInfoLoadingSpec} from './wiki-info.js';
diff --git a/src/data/files/news.js b/src/data/files/news.js
new file mode 100644
index 00000000..5b4a3029
--- /dev/null
+++ b/src/data/files/news.js
@@ -0,0 +1,16 @@
+import {sortChronologically} from '#sort';
+
+export default ({
+  documentModes: {allInOne},
+  thingConstructors: {NewsEntry},
+}) => ({
+  title: `Process news data file`,
+  file: 'news.yaml',
+
+  documentMode: allInOne,
+  documentThing: NewsEntry,
+
+  sort({newsData}) {
+    sortChronologically(newsData, {latestFirst: true});
+  },
+});
diff --git a/src/data/files/sorting-rule.js b/src/data/files/sorting-rule.js
new file mode 100644
index 00000000..61e1df23
--- /dev/null
+++ b/src/data/files/sorting-rule.js
@@ -0,0 +1,13 @@
+export default ({
+  documentModes: {allInOne},
+  thingConstructors: {DocumentSortingRule},
+}) => ({
+  title: `Process sorting rules file`,
+  file: 'sorting-rules.yaml',
+
+  documentMode: allInOne,
+  documentThing: document =>
+    (document['Sort Documents']
+      ? DocumentSortingRule
+      : null),
+});
diff --git a/src/data/files/static-page.js b/src/data/files/static-page.js
new file mode 100644
index 00000000..c7622bc8
--- /dev/null
+++ b/src/data/files/static-page.js
@@ -0,0 +1,24 @@
+import * as path from 'node:path';
+
+import {traverse} from '#node-utils';
+import {sortAlphabetically} from '#sort';
+
+export default ({
+  documentModes: {onePerFile},
+  thingConstructors: {StaticPage},
+}) => ({
+  title: `Process static page files`,
+
+  files: dataPath =>
+    traverse(path.join(dataPath, 'static-page'), {
+      filterFile: name => path.extname(name) === '.yaml',
+      prefixPath: 'static-page',
+    }),
+
+  documentMode: onePerFile,
+  documentThing: StaticPage,
+
+  sort({staticPageData}) {
+    sortAlphabetically(staticPageData);
+  },
+});
diff --git a/src/data/files/wiki-info.js b/src/data/files/wiki-info.js
new file mode 100644
index 00000000..a466ab0b
--- /dev/null
+++ b/src/data/files/wiki-info.js
@@ -0,0 +1,10 @@
+export default ({
+  documentModes: {oneDocumentTotal},
+  thingConstructors: {WikiInfo},
+}) => ({
+  title: `Process wiki info file`,
+  file: 'wiki-info.yaml',
+
+  documentMode: oneDocumentTotal,
+  documentThing: WikiInfo,
+});
diff --git a/src/data/thing.js b/src/data/thing.js
index 32eff4d1..cc7c82b7 100644
--- a/src/data/thing.js
+++ b/src/data/thing.js
@@ -23,7 +23,6 @@ export default class Thing extends CacheableObject {
   static reverseSpecs = Symbol.for('Thing.reverseSpecs');
 
   static yamlDocumentSpec = Symbol.for('Thing.yamlDocumentSpec');
-  static getYamlLoadingSpec = Symbol.for('Thing.getYamlLoadingSpec');
 
   static yamlSourceFilename = Symbol.for('Thing.yamlSourceFilename');
   static yamlSourceDocument = Symbol.for('Thing.yamlSourceDocument');
diff --git a/src/data/things/ArtTag.js b/src/data/things/ArtTag.js
index 91248f77..9d35f54d 100644
--- a/src/data/things/ArtTag.js
+++ b/src/data/things/ArtTag.js
@@ -1,12 +1,4 @@
-const DATA_ART_TAGS_DIRECTORY = 'art-tags';
-const ART_TAG_DATA_FILE = 'tags.yaml';
-
-import {readFile} from 'node:fs/promises';
-import * as path from 'node:path';
-
 import {input, V} from '#composite';
-import {traverse} from '#node-utils';
-import {sortAlphabetically} from '#sort';
 import Thing from '#thing';
 import {unique} from '#sugar';
 import {isName} from '#validators';
@@ -200,31 +192,4 @@ export class ArtTag extends Thing {
       },
     },
   };
-
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {allTogether},
-    thingConstructors: {ArtTag},
-  }) => ({
-    title: `Process art tags file`,
-
-    files: dataPath =>
-      Promise.allSettled([
-        readFile(path.join(dataPath, ART_TAG_DATA_FILE))
-          .then(() => [ART_TAG_DATA_FILE]),
-
-        traverse(path.join(dataPath, DATA_ART_TAGS_DIRECTORY), {
-          filterFile: name => path.extname(name) === '.yaml',
-          prefixPath: DATA_ART_TAGS_DIRECTORY,
-        }),
-      ]).then(results => results
-          .filter(({status}) => status === 'fulfilled')
-          .flatMap(({value}) => value)),
-
-    documentMode: allTogether,
-    documentThing: ArtTag,
-
-    sort({artTagData}) {
-      sortAlphabetically(artTagData);
-    },
-  });
 }
diff --git a/src/data/things/Artist.js b/src/data/things/Artist.js
index 85bdc006..f518e31e 100644
--- a/src/data/things/Artist.js
+++ b/src/data/things/Artist.js
@@ -1,5 +1,3 @@
-const ARTIST_DATA_FILE = 'artists.yaml';
-
 import {inspect} from 'node:util';
 
 import CacheableObject from '#cacheable-object';
@@ -11,7 +9,6 @@ import {parseArtistAliases, parseArtwork} from '#yaml';
 import {
   sortAlbumsTracksChronologically,
   sortArtworksChronologically,
-  sortAlphabetically,
   sortContributionsChronologically,
 } from '#sort';
 
@@ -325,21 +322,6 @@ export class Artist extends Thing {
     },
   };
 
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {allInOne},
-    thingConstructors: {Artist},
-  }) => ({
-    title: `Process artists file`,
-    file: ARTIST_DATA_FILE,
-
-    documentMode: allInOne,
-    documentThing: Artist,
-
-    sort({artistData}) {
-      sortAlphabetically(artistData);
-    },
-  });
-
   [inspect.custom]() {
     const parts = [];
 
diff --git a/src/data/things/NewsEntry.js b/src/data/things/NewsEntry.js
index 65fd125b..7cbbfc4b 100644
--- a/src/data/things/NewsEntry.js
+++ b/src/data/things/NewsEntry.js
@@ -1,7 +1,4 @@
-const NEWS_DATA_FILE = 'news.yaml';
-
 import {V} from '#composite';
-import {sortChronologically} from '#sort';
 import Thing from '#thing';
 import {parseDate} from '#yaml';
 
@@ -58,19 +55,4 @@ export class NewsEntry extends Thing {
       'Content': {property: 'content'},
     },
   };
-
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {allInOne},
-    thingConstructors: {NewsEntry},
-  }) => ({
-    title: `Process news data file`,
-    file: NEWS_DATA_FILE,
-
-    documentMode: allInOne,
-    documentThing: NewsEntry,
-
-    sort({newsData}) {
-      sortChronologically(newsData, {latestFirst: true});
-    },
-  });
 }
diff --git a/src/data/things/StaticPage.js b/src/data/things/StaticPage.js
index daa77a7e..5ddddb9d 100644
--- a/src/data/things/StaticPage.js
+++ b/src/data/things/StaticPage.js
@@ -1,10 +1,4 @@
-const DATA_STATIC_PAGE_DIRECTORY = 'static-page';
-
-import * as path from 'node:path';
-
 import {V} from '#composite';
-import {traverse} from '#node-utils';
-import {sortAlphabetically} from '#sort';
 import Thing from '#thing';
 import {isName} from '#validators';
 
@@ -67,24 +61,4 @@ export class StaticPage extends Thing {
       'Review Points': {ignore: true},
     },
   };
-
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {onePerFile},
-    thingConstructors: {StaticPage},
-  }) => ({
-    title: `Process static page files`,
-
-    files: dataPath =>
-      traverse(path.join(dataPath, DATA_STATIC_PAGE_DIRECTORY), {
-        filterFile: name => path.extname(name) === '.yaml',
-        prefixPath: DATA_STATIC_PAGE_DIRECTORY,
-      }),
-
-    documentMode: onePerFile,
-    documentThing: StaticPage,
-
-    sort({staticPageData}) {
-      sortAlphabetically(staticPageData);
-    },
-  });
 }
diff --git a/src/data/things/Track.js b/src/data/things/Track.js
index d776581f..57ff3a94 100644
--- a/src/data/things/Track.js
+++ b/src/data/things/Track.js
@@ -1254,9 +1254,6 @@ export class Track extends Thing {
     },
   };
 
-  // Track YAML loading is handled in album.js.
-  static [Thing.getYamlLoadingSpec] = null;
-
   getOwnAdditionalFilePath(_file, filename) {
     if (!this.album) return null;
 
diff --git a/src/data/things/WikiInfo.js b/src/data/things/WikiInfo.js
index 1d1f90e6..ffb18cd8 100644
--- a/src/data/things/WikiInfo.js
+++ b/src/data/things/WikiInfo.js
@@ -1,5 +1,3 @@
-export const WIKI_INFO_FILE = 'wiki-info.yaml';
-
 import {input, V} from '#composite';
 import Thing from '#thing';
 import {parseContributionPresets, parseWallpaperParts} from '#yaml';
@@ -156,14 +154,5 @@ export class WikiInfo extends Thing {
     },
   };
 
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {oneDocumentTotal},
-    thingConstructors: {WikiInfo},
-  }) => ({
-    title: `Process wiki info file`,
-    file: WIKI_INFO_FILE,
 
-    documentMode: oneDocumentTotal,
-    documentThing: WikiInfo,
-  });
 }
diff --git a/src/data/things/album/Album.js b/src/data/things/album/Album.js
index d5fd1682..e81615d4 100644
--- a/src/data/things/album/Album.js
+++ b/src/data/things/album/Album.js
@@ -1,10 +1,4 @@
-export const DATA_ALBUM_DIRECTORY = 'album';
-
-import * as path from 'node:path';
-
 import {input, V} from '#composite';
-import {traverse} from '#node-utils';
-import {sortAlbumsTracksChronologically, sortChronologically} from '#sort';
 import {empty} from '#sugar';
 import Thing from '#thing';
 import {is, isContributionList, isDate, isDirectory, isNumber}
@@ -794,75 +788,6 @@ export class Album extends Thing {
     ],
   };
 
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {headerAndEntries},
-    thingConstructors: {Album, Track, TrackSection},
-  }) => ({
-    title: `Process album files`,
-
-    files: dataPath =>
-      traverse(path.join(dataPath, DATA_ALBUM_DIRECTORY), {
-        filterFile: name => path.extname(name) === '.yaml',
-        prefixPath: DATA_ALBUM_DIRECTORY,
-      }),
-
-    documentMode: headerAndEntries,
-    headerDocumentThing: Album,
-    entryDocumentThing: document =>
-      ('Section' in document
-        ? TrackSection
-        : Track),
-
-    connect({header: album, entries}) {
-      const trackSections = [];
-
-      let currentTrackSection = new TrackSection();
-      let currentTrackSectionTracks = [];
-
-      Object.assign(currentTrackSection, {
-        name: `Default Track Section`,
-        isDefaultTrackSection: true,
-      });
-
-      const closeCurrentTrackSection = () => {
-        if (
-          currentTrackSection.isDefaultTrackSection &&
-          empty(currentTrackSectionTracks)
-        ) {
-          return;
-        }
-
-        currentTrackSection.tracks = currentTrackSectionTracks;
-        currentTrackSection.album = album;
-
-        trackSections.push(currentTrackSection);
-      };
-
-      for (const entry of entries) {
-        if (entry instanceof TrackSection) {
-          closeCurrentTrackSection();
-          currentTrackSection = entry;
-          currentTrackSectionTracks = [];
-          continue;
-        }
-
-        entry.album = album;
-        entry.trackSection = currentTrackSection;
-
-        currentTrackSectionTracks.push(entry);
-      }
-
-      closeCurrentTrackSection();
-
-      album.trackSections = trackSections;
-    },
-
-    sort({albumData, trackData}) {
-      sortChronologically(albumData);
-      sortAlbumsTracksChronologically(trackData);
-    },
-  });
-
   getOwnAdditionalFilePath(_file, filename) {
     return [
       'media.albumAdditionalFile',
diff --git a/src/data/things/flash/FlashSide.js b/src/data/things/flash/FlashSide.js
index 72782bdd..5e2ea3de 100644
--- a/src/data/things/flash/FlashSide.js
+++ b/src/data/things/flash/FlashSide.js
@@ -1,7 +1,4 @@
-const FLASH_DATA_FILE = 'flashes.yaml';
-
 import {V} from '#composite';
-import {sortFlashesChronologically} from '#sort';
 import Thing from '#thing';
 
 import {exposeConstant} from '#composite/control-flow';
@@ -56,81 +53,4 @@ export class FlashSide extends Thing {
       referenced: flashSide => flashSide.acts,
     },
   };
-
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {allInOne},
-    thingConstructors: {Flash, FlashAct},
-  }) => ({
-    title: `Process flashes file`,
-    file: FLASH_DATA_FILE,
-
-    documentMode: allInOne,
-    documentThing: document =>
-      ('Side' in document
-        ? FlashSide
-     : 'Act' in document
-        ? FlashAct
-        : Flash),
-
-    connect(results) {
-      let thing, i;
-
-      for (i = 0; thing = results[i]; i++) {
-        if (thing.isFlashSide) {
-          const side = thing;
-          const acts = [];
-
-          for (i++; thing = results[i]; i++) {
-            if (thing.isFlashAct) {
-              const act = thing;
-              const flashes = [];
-
-              for (i++; thing = results[i]; i++) {
-                if (thing.isFlash) {
-                  const flash = thing;
-
-                  flash.act = act;
-                  flashes.push(flash);
-
-                  continue;
-                }
-
-                i--;
-                break;
-              }
-
-              act.side = side;
-              act.flashes = flashes;
-              acts.push(act);
-
-              continue;
-            }
-
-            if (thing.isFlash) {
-              throw new Error(`Flashes must be under an act`);
-            }
-
-            i--;
-            break;
-          }
-
-          side.acts = acts;
-
-          continue;
-        }
-
-        if (thing.isFlashAct) {
-          throw new Error(`Acts must be under a side`);
-        }
-
-        if (thing.isFlash) {
-          throw new Error(`Flashes must be under a side and act`);
-        }
-      }
-    },
-
-    sort({flashData}) {
-      sortFlashesChronologically(flashData);
-    },
-  });
 }
diff --git a/src/data/things/group/Group.js b/src/data/things/group/Group.js
index b065f9a3..6f698682 100644
--- a/src/data/things/group/Group.js
+++ b/src/data/things/group/Group.js
@@ -1,5 +1,3 @@
-const GROUP_DATA_FILE = 'groups.yaml';
-
 import {input, V} from '#composite';
 import Thing from '#thing';
 import {isBoolean} from '#validators';
@@ -185,48 +183,4 @@ export class Group extends Thing {
       'Review Points': {ignore: true},
     },
   };
-
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {allInOne},
-    thingConstructors: {Group, GroupCategory},
-  }) => ({
-    title: `Process groups file`,
-    file: GROUP_DATA_FILE,
-
-    documentMode: allInOne,
-    documentThing: document =>
-      ('Category' in document
-        ? GroupCategory
-        : Group),
-
-    connect(results) {
-      let groupCategory;
-      let groupRefs = [];
-
-      if (results[0] && !(results[0] instanceof GroupCategory)) {
-        throw new Error(`Expected a category at top of group data file`);
-      }
-
-      for (const thing of results) {
-        if (thing instanceof GroupCategory) {
-          if (groupCategory) {
-            Object.assign(groupCategory, {groups: groupRefs});
-          }
-
-          groupCategory = thing;
-          groupRefs = [];
-        } else {
-          groupRefs.push(Thing.getReference(thing));
-        }
-      }
-
-      if (groupCategory) {
-        Object.assign(groupCategory, {groups: groupRefs});
-      }
-    },
-
-    // Groups aren't sorted at all, always preserving the order in the data
-    // file as-is.
-    sort: null,
-  });
 }
diff --git a/src/data/things/homepage-layout/HomepageLayout.js b/src/data/things/homepage-layout/HomepageLayout.js
index e144bf80..1c432b53 100644
--- a/src/data/things/homepage-layout/HomepageLayout.js
+++ b/src/data/things/homepage-layout/HomepageLayout.js
@@ -1,8 +1,5 @@
-const HOMEPAGE_LAYOUT_DATA_FILE = 'homepage.yaml';
-
 import {V} from '#composite';
 import Thing from '#thing';
-import {empty} from '#sugar';
 import {isStringNonEmpty, validateArrayItems} from '#validators';
 
 import {exposeConstant} from '#composite/control-flow';
@@ -39,90 +36,4 @@ export class HomepageLayout extends Thing {
       'Navbar Links': {property: 'navbarLinks'},
     },
   };
-
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {allInOne},
-    thingConstructors: {
-      HomepageLayout,
-      HomepageLayoutActionsRow,
-      HomepageLayoutAlbumCarouselRow,
-      HomepageLayoutAlbumGridRow,
-      HomepageLayoutRow,
-      HomepageLayoutSection,
-    },
-  }) => ({
-    title: `Process homepage layout file`,
-    file: HOMEPAGE_LAYOUT_DATA_FILE,
-
-    documentMode: allInOne,
-    documentThing: document => {
-      if (document['Homepage']) {
-        return HomepageLayout;
-      }
-
-      if (document['Section']) {
-        return HomepageLayoutSection;
-      }
-
-      if (document['Row']) {
-        switch (document['Row']) {
-          case 'actions':
-            return HomepageLayoutActionsRow;
-          case 'album carousel':
-            return HomepageLayoutAlbumCarouselRow;
-          case 'album grid':
-            return HomepageLayoutAlbumGridRow;
-          default:
-            throw new TypeError(`Unrecognized row type ${document['Row']}`);
-        }
-      }
-
-      return null;
-    },
-
-    connect(results) {
-      if (!empty(results) && !(results[0] instanceof HomepageLayout)) {
-        throw new Error(`Expected 'Homepage' document at top of homepage layout file`);
-      }
-
-      const homepageLayout = results[0];
-      const sections = [];
-
-      let currentSection = null;
-      let currentSectionRows = [];
-
-      const closeCurrentSection = () => {
-        if (currentSection) {
-          for (const row of currentSectionRows) {
-            row.section = currentSection;
-          }
-
-          currentSection.rows = currentSectionRows;
-          sections.push(currentSection);
-
-          currentSection = null;
-          currentSectionRows = [];
-        }
-      };
-
-      for (const entry of results.slice(1)) {
-        if (entry instanceof HomepageLayout) {
-          throw new Error(`Expected only one 'Homepage' document in total`);
-        } else if (entry instanceof HomepageLayoutSection) {
-          closeCurrentSection();
-          currentSection = entry;
-        } else if (entry instanceof HomepageLayoutRow) {
-          if (currentSection) {
-            currentSectionRows.push(entry);
-          } else {
-            throw new Error(`Expected a 'Section' document to add following rows into`);
-          }
-        }
-      }
-
-      closeCurrentSection();
-
-      homepageLayout.sections = sections;
-    },
-  });
 }
diff --git a/src/data/things/sorting-rule/SortingRule.js b/src/data/things/sorting-rule/SortingRule.js
index 4ce9d97a..5d4bba99 100644
--- a/src/data/things/sorting-rule/SortingRule.js
+++ b/src/data/things/sorting-rule/SortingRule.js
@@ -1,5 +1,3 @@
-const SORTING_RULE_DATA_FILE = 'sorting-rules.yaml';
-
 import {V} from '#composite';
 import {unique} from '#sugar';
 import Thing from '#thing';
@@ -34,20 +32,6 @@ export class SortingRule extends Thing {
     },
   };
 
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {allInOne},
-    thingConstructors: {DocumentSortingRule},
-  }) => ({
-    title: `Process sorting rules file`,
-    file: SORTING_RULE_DATA_FILE,
-
-    documentMode: allInOne,
-    documentThing: document =>
-      (document['Sort Documents']
-        ? DocumentSortingRule
-        : null),
-  });
-
   check(opts) {
     return this.constructor.check(this, opts);
   }
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 908d42c6..50496c00 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -7,6 +7,7 @@ import {inspect as nodeInspect} from 'node:util';
 
 import yaml from 'js-yaml';
 
+import * as fileLoadingSpecs from '#files';
 import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli';
 import {parseContentNodes, splitContentNodesAround} from '#replacer';
 import {sortByName} from '#sort';
@@ -1157,17 +1158,7 @@ export function getAllDataSteps() {
 
   const steps = [];
 
-  const seenLoadingFns = new Set();
-
-  for (const thingConstructor of Object.values(thingConstructors)) {
-    const getSpecFn = thingConstructor[Thing.getYamlLoadingSpec];
-    if (!getSpecFn) continue;
-
-    // Subclasses can expose literally the same static properties
-    // by inheritence. We don't want to double-count those!
-    if (seenLoadingFns.has(getSpecFn)) continue;
-    seenLoadingFns.add(getSpecFn);
-
+  for (const getSpecFn of Object.values(fileLoadingSpecs)) {
     steps.push(getSpecFn({
       documentModes,
       thingConstructors,
@@ -1837,6 +1828,7 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) {
 
     for (const thing of things) {
       if (thing === undefined) continue;
+      if (thing === null) continue;
 
       let hasFind;
       if (constructorHasFindMap.has(thing.constructor)) {