« get me outta code hell

data: move sorting & generic functions out of #wiki-data - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2024-02-16 10:16:36 -0400
committer(quasar) nebula <qznebula@protonmail.com>2024-02-16 10:29:50 -0400
commit5457b88f8d3d234af0b08d15f3c6249f6649aac3 (patch)
treec07cee2cbfcd35d8b4a373915f59e6b619531241
parent7a3038b982c4d318e146698527070f096591aa4f (diff)
data: move sorting & generic functions out of #wiki-data
-rw-r--r--package.json1
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js2
-rw-r--r--src/content/dependencies/generateAlbumSecondaryNav.js2
-rw-r--r--src/content/dependencies/generateAlbumSidebarGroupBox.js2
-rw-r--r--src/content/dependencies/generateArtTagGalleryPage.js2
-rw-r--r--src/content/dependencies/generateArtistGalleryPage.js2
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js9
-rw-r--r--src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js9
-rw-r--r--src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js9
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunkedList.js9
-rw-r--r--src/content/dependencies/generateCommentaryIndexPage.js4
-rw-r--r--src/content/dependencies/generateFlashActSidebar.js3
-rw-r--r--src/content/dependencies/generateFooterLocalizationLinks.js2
-rw-r--r--src/content/dependencies/generateGroupGalleryPage.js8
-rw-r--r--src/content/dependencies/generateNewsEntryPage.js2
-rw-r--r--src/content/dependencies/generateNewsIndexPage.js2
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js3
-rw-r--r--src/content/dependencies/listAlbumsByDate.js2
-rw-r--r--src/content/dependencies/listAlbumsByDateAdded.js3
-rw-r--r--src/content/dependencies/listAlbumsByDuration.js5
-rw-r--r--src/content/dependencies/listAlbumsByName.js2
-rw-r--r--src/content/dependencies/listAlbumsByTracks.js4
-rw-r--r--src/content/dependencies/listAllAdditionalFilesTemplate.js4
-rw-r--r--src/content/dependencies/listArtistsByCommentaryEntries.js4
-rw-r--r--src/content/dependencies/listArtistsByContributions.js11
-rw-r--r--src/content/dependencies/listArtistsByDuration.js5
-rw-r--r--src/content/dependencies/listArtistsByGroup.js10
-rw-r--r--src/content/dependencies/listArtistsByLatestContribution.js7
-rw-r--r--src/content/dependencies/listArtistsByName.js3
-rw-r--r--src/content/dependencies/listGroupsByAlbums.js4
-rw-r--r--src/content/dependencies/listGroupsByDuration.js5
-rw-r--r--src/content/dependencies/listGroupsByLatestAlbum.js4
-rw-r--r--src/content/dependencies/listGroupsByName.js2
-rw-r--r--src/content/dependencies/listGroupsByTracks.js4
-rw-r--r--src/content/dependencies/listRandomPageLinks.js2
-rw-r--r--src/content/dependencies/listTagsByName.js2
-rw-r--r--src/content/dependencies/listTagsByUses.js4
-rw-r--r--src/content/dependencies/listTracksByDate.js4
-rw-r--r--src/content/dependencies/listTracksByDuration.js4
-rw-r--r--src/content/dependencies/listTracksByDurationInAlbum.js4
-rw-r--r--src/content/dependencies/listTracksByName.js2
-rw-r--r--src/content/dependencies/listTracksByTimesReferenced.js4
-rw-r--r--src/content/dependencies/listTracksInFlashesByAlbum.js4
-rw-r--r--src/content/dependencies/listTracksInFlashesByFlash.js2
-rw-r--r--src/content/dependencies/listTracksWithExtra.js4
-rw-r--r--src/data/composite/things/album/withTrackSections.js3
-rw-r--r--src/data/composite/things/track/inferredAdditionalNameList.js2
-rw-r--r--src/data/composite/wiki-data/withResolvedContribs.js3
-rw-r--r--src/data/composite/wiki-data/withThingsSortedAlphabetically.js2
-rw-r--r--src/data/things/album.js3
-rw-r--r--src/data/things/art-tag.js2
-rw-r--r--src/data/things/artist.js3
-rw-r--r--src/data/things/flash.js2
-rw-r--r--src/data/things/news-entry.js2
-rw-r--r--src/data/things/static-page.js2
-rw-r--r--src/data/yaml.js2
-rw-r--r--src/gen-thumbs.js17
-rwxr-xr-xsrc/upd8.js2
-rw-r--r--src/util/sort.js405
-rw-r--r--src/util/sugar.js213
-rw-r--r--src/util/wiki-data.js619
61 files changed, 722 insertions, 746 deletions
diff --git a/package.json b/package.json
index bc97e54..6b0d0d5 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
         "#replacer": "./src/util/replacer.js",
         "#serialize": "./src/data/serialize.js",
         "#sugar": "./src/util/sugar.js",
+        "#sort": "./src/util/sort.js",
         "#test-lib": "./test/lib/index.js",
         "#thing": "./src/data/thing.js",
         "#things": "./src/data/things/index.js",
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index 7fbe4e2..5853f11 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -1,5 +1,5 @@
+import {sortAlbumsTracksChronologically} from '#sort';
 import {empty} from '#sugar';
-import {sortAlbumsTracksChronologically} from '#wiki-data';
 
 import getChronologyRelations from '../util/getChronologyRelations.js';
 
diff --git a/src/content/dependencies/generateAlbumSecondaryNav.js b/src/content/dependencies/generateAlbumSecondaryNav.js
index 6ffb935..5b2e340 100644
--- a/src/content/dependencies/generateAlbumSecondaryNav.js
+++ b/src/content/dependencies/generateAlbumSecondaryNav.js
@@ -1,5 +1,5 @@
+import {sortChronologically} from '#sort';
 import {atOffset, stitchArrays} from '#sugar';
-import {sortChronologically} from '#wiki-data';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js
index 9e1b4cd..93ebf5d 100644
--- a/src/content/dependencies/generateAlbumSidebarGroupBox.js
+++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js
@@ -1,5 +1,5 @@
+import {sortChronologically} from '#sort';
 import {atOffset, empty} from '#sugar';
-import {sortChronologically} from '#wiki-data';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js
index 62b88bc..962f1b7 100644
--- a/src/content/dependencies/generateArtTagGalleryPage.js
+++ b/src/content/dependencies/generateArtTagGalleryPage.js
@@ -1,5 +1,5 @@
+import {sortAlbumsTracksChronologically} from '#sort';
 import {stitchArrays} from '#sugar';
-import {sortAlbumsTracksChronologically} from '#wiki-data';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js
index 11cf8cd..1377915 100644
--- a/src/content/dependencies/generateArtistGalleryPage.js
+++ b/src/content/dependencies/generateArtistGalleryPage.js
@@ -1,5 +1,5 @@
+import {sortAlbumsTracksChronologically} from '#sort';
 import {stitchArrays} from '#sugar';
-import {sortAlbumsTracksChronologically} from '#wiki-data';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
index 36daf9c..0beeb27 100644
--- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
@@ -1,10 +1,5 @@
-import {stitchArrays} from '#sugar';
-
-import {
-  chunkByProperties,
-  sortAlbumsTracksChronologically,
-  sortEntryThingPairs,
-} from '#wiki-data';
+import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort';
+import {chunkByProperties, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
index 7c2418b..0bcadc7 100644
--- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
@@ -1,10 +1,5 @@
-import {stitchArrays} from '#sugar';
-
-import {
-  chunkByProperties,
-  sortAlbumsTracksChronologically,
-  sortEntryThingPairs,
-} from '#wiki-data';
+import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort';
+import {chunkByProperties, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js
index 75a1d05..88a97af 100644
--- a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js
@@ -1,10 +1,5 @@
-import {stitchArrays} from '#sugar';
-
-import {
-  chunkByProperties,
-  sortEntryThingPairs,
-  sortFlashesChronologically,
-} from '#wiki-data';
+import {sortEntryThingPairs, sortFlashesChronologically} from '#sort';
+import {chunkByProperties, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
index a820916..d68eba3 100644
--- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
@@ -1,10 +1,5 @@
-import {accumulateSum, empty, stitchArrays} from '#sugar';
-
-import {
-  chunkByProperties,
-  sortAlbumsTracksChronologically,
-  sortEntryThingPairs,
-} from '#wiki-data';
+import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort';
+import {accumulateSum, chunkByProperties, empty, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateCommentaryIndexPage.js b/src/content/dependencies/generateCommentaryIndexPage.js
index 5d38941..3c3504d 100644
--- a/src/content/dependencies/generateCommentaryIndexPage.js
+++ b/src/content/dependencies/generateCommentaryIndexPage.js
@@ -1,5 +1,5 @@
-import {accumulateSum, stitchArrays} from '#sugar';
-import {filterMultipleArrays, sortChronologically} from '#wiki-data';
+import {sortChronologically} from '#sort';
+import {accumulateSum, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generatePageLayout', 'linkAlbumCommentary'],
diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js
index 3c631d9..0bbfa1f 100644
--- a/src/content/dependencies/generateFlashActSidebar.js
+++ b/src/content/dependencies/generateFlashActSidebar.js
@@ -1,6 +1,5 @@
 import find from '#find';
-import {stitchArrays} from '#sugar';
-import {filterMultipleArrays} from '#wiki-data';
+import {filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['linkFlash', 'linkFlashAct', 'linkFlashIndex'],
diff --git a/src/content/dependencies/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js
index 86e6c61..dfd83ae 100644
--- a/src/content/dependencies/generateFooterLocalizationLinks.js
+++ b/src/content/dependencies/generateFooterLocalizationLinks.js
@@ -1,5 +1,5 @@
+import {sortByName} from '#sort';
 import {stitchArrays} from '#sugar';
-import {sortByName} from '#wiki-data';
 
 export default {
   extraDependencies: [
diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js
index 490dcf1..b29c586 100644
--- a/src/content/dependencies/generateGroupGalleryPage.js
+++ b/src/content/dependencies/generateGroupGalleryPage.js
@@ -1,10 +1,6 @@
+import {sortChronologically} from '#sort';
 import {empty, stitchArrays} from '#sugar';
-
-import {
-  filterItemsForCarousel,
-  getTotalDuration,
-  sortChronologically,
-} from '#wiki-data';
+import {filterItemsForCarousel, getTotalDuration} from '#wiki-data';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateNewsEntryPage.js b/src/content/dependencies/generateNewsEntryPage.js
index fa4d68a..bcba719 100644
--- a/src/content/dependencies/generateNewsEntryPage.js
+++ b/src/content/dependencies/generateNewsEntryPage.js
@@ -1,5 +1,5 @@
+import {sortChronologically} from '#sort';
 import {atOffset} from '#sugar';
-import {sortChronologically} from '#wiki-data';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateNewsIndexPage.js b/src/content/dependencies/generateNewsIndexPage.js
index 64279d7..539af80 100644
--- a/src/content/dependencies/generateNewsIndexPage.js
+++ b/src/content/dependencies/generateNewsIndexPage.js
@@ -1,5 +1,5 @@
+import {sortChronologically} from '#sort';
 import {stitchArrays} from '#sugar';
-import {sortChronologically} from '#wiki-data';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index 9cce744..7b70d4f 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -1,5 +1,6 @@
+import {sortAlbumsTracksChronologically, sortFlashesChronologically}
+  from '#sort';
 import {empty, stitchArrays} from '#sugar';
-import {sortAlbumsTracksChronologically, sortFlashesChronologically} from '#wiki-data';
 
 import getChronologyRelations from '../util/getChronologyRelations.js';
 
diff --git a/src/content/dependencies/listAlbumsByDate.js b/src/content/dependencies/listAlbumsByDate.js
index a5e31a0..c83ffc9 100644
--- a/src/content/dependencies/listAlbumsByDate.js
+++ b/src/content/dependencies/listAlbumsByDate.js
@@ -1,5 +1,5 @@
+import {sortChronologically} from '#sort';
 import {stitchArrays} from '#sugar';
-import {sortChronologically} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkAlbum'],
diff --git a/src/content/dependencies/listAlbumsByDateAdded.js b/src/content/dependencies/listAlbumsByDateAdded.js
index 75114a4..d462ad4 100644
--- a/src/content/dependencies/listAlbumsByDateAdded.js
+++ b/src/content/dependencies/listAlbumsByDateAdded.js
@@ -1,4 +1,5 @@
-import {chunkByProperties, sortAlphabetically} from '#wiki-data';
+import {sortAlphabetically} from '#sort';
+import {chunkByProperties} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkAlbum'],
diff --git a/src/content/dependencies/listAlbumsByDuration.js b/src/content/dependencies/listAlbumsByDuration.js
index 1f95f5e..c60685a 100644
--- a/src/content/dependencies/listAlbumsByDuration.js
+++ b/src/content/dependencies/listAlbumsByDuration.js
@@ -1,5 +1,6 @@
-import {stitchArrays} from '#sugar';
-import {filterByCount, getTotalDuration, sortAlphabetically, sortByCount} from '#wiki-data';
+import {sortAlphabetically, sortByCount} from '#sort';
+import {filterByCount, stitchArrays} from '#sugar';
+import {getTotalDuration} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkAlbum'],
diff --git a/src/content/dependencies/listAlbumsByName.js b/src/content/dependencies/listAlbumsByName.js
index 287dc0b..2141953 100644
--- a/src/content/dependencies/listAlbumsByName.js
+++ b/src/content/dependencies/listAlbumsByName.js
@@ -1,5 +1,5 @@
+import {sortAlphabetically} from '#sort';
 import {stitchArrays} from '#sugar';
-import {sortAlphabetically} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkAlbum'],
diff --git a/src/content/dependencies/listAlbumsByTracks.js b/src/content/dependencies/listAlbumsByTracks.js
index abf3c3f..798e6c2 100644
--- a/src/content/dependencies/listAlbumsByTracks.js
+++ b/src/content/dependencies/listAlbumsByTracks.js
@@ -1,5 +1,5 @@
-import {stitchArrays} from '#sugar';
-import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data';
+import {sortAlphabetically, sortByCount} from '#sort';
+import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkAlbum'],
diff --git a/src/content/dependencies/listAllAdditionalFilesTemplate.js b/src/content/dependencies/listAllAdditionalFilesTemplate.js
index 627fdab..bf48c96 100644
--- a/src/content/dependencies/listAllAdditionalFilesTemplate.js
+++ b/src/content/dependencies/listAllAdditionalFilesTemplate.js
@@ -1,5 +1,5 @@
-import {empty, stitchArrays} from '#sugar';
-import {filterMultipleArrays, sortChronologically} from '#wiki-data';
+import {sortChronologically} from '#sort';
+import {empty, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/listArtistsByCommentaryEntries.js b/src/content/dependencies/listArtistsByCommentaryEntries.js
index aac3cfd..eff2dba 100644
--- a/src/content/dependencies/listArtistsByCommentaryEntries.js
+++ b/src/content/dependencies/listArtistsByCommentaryEntries.js
@@ -1,5 +1,5 @@
-import {stitchArrays} from '#sugar';
-import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data';
+import {sortAlphabetically, sortByCount} from '#sort';
+import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkArtist'],
diff --git a/src/content/dependencies/listArtistsByContributions.js b/src/content/dependencies/listArtistsByContributions.js
index 6114115..0af586c 100644
--- a/src/content/dependencies/listArtistsByContributions.js
+++ b/src/content/dependencies/listArtistsByContributions.js
@@ -1,11 +1,6 @@
-import {empty, stitchArrays, unique} from '#sugar';
-
-import {
-  filterByCount,
-  filterMultipleArrays,
-  sortAlphabetically,
-  sortByCount,
-} from '#wiki-data';
+import {sortAlphabetically, sortByCount} from '#sort';
+import {empty, filterByCount, filterMultipleArrays, stitchArrays, unique}
+  from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkArtist'],
diff --git a/src/content/dependencies/listArtistsByDuration.js b/src/content/dependencies/listArtistsByDuration.js
index 056b126..f677d82 100644
--- a/src/content/dependencies/listArtistsByDuration.js
+++ b/src/content/dependencies/listArtistsByDuration.js
@@ -1,5 +1,6 @@
-import {stitchArrays} from '#sugar';
-import {filterByCount, getTotalDuration, sortAlphabetically, sortByCount} from '#wiki-data';
+import {sortAlphabetically, sortByCount} from '#sort';
+import {filterByCount, stitchArrays} from '#sugar';
+import {getTotalDuration} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkArtist'],
diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js
index 69f910c..30884d2 100644
--- a/src/content/dependencies/listArtistsByGroup.js
+++ b/src/content/dependencies/listArtistsByGroup.js
@@ -1,10 +1,6 @@
-import {empty, stitchArrays, unique} from '#sugar';
-
-import {
-  filterMultipleArrays,
-  getArtistNumContributions,
-  sortAlphabetically,
-} from '#wiki-data';
+import {sortAlphabetically} from '#sort';
+import {empty, filterMultipleArrays, stitchArrays, unique} from '#sugar';
+import {getArtistNumContributions} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'],
diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js
index 03c5169..0f70957 100644
--- a/src/content/dependencies/listArtistsByLatestContribution.js
+++ b/src/content/dependencies/listArtistsByLatestContribution.js
@@ -1,13 +1,12 @@
-import {empty, stitchArrays} from '#sugar';
+import {chunkMultipleArrays, empty, sortMultipleArrays, stitchArrays}
+  from '#sugar';
 import T from '#things';
 
 import {
-  chunkMultipleArrays,
   sortAlphabetically,
   sortAlbumsTracksChronologically,
   sortFlashesChronologically,
-  sortMultipleArrays,
-} from '#wiki-data';
+} from '#sort';
 
 const {Album, Flash} = T;
 
diff --git a/src/content/dependencies/listArtistsByName.js b/src/content/dependencies/listArtistsByName.js
index 7704e97..9321849 100644
--- a/src/content/dependencies/listArtistsByName.js
+++ b/src/content/dependencies/listArtistsByName.js
@@ -1,5 +1,6 @@
+import {sortAlphabetically} from '#sort';
 import {stitchArrays} from '#sugar';
-import {getArtistNumContributions, sortAlphabetically} from '#wiki-data';
+import {getArtistNumContributions} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'],
diff --git a/src/content/dependencies/listGroupsByAlbums.js b/src/content/dependencies/listGroupsByAlbums.js
index 063b826..4adfb6d 100644
--- a/src/content/dependencies/listGroupsByAlbums.js
+++ b/src/content/dependencies/listGroupsByAlbums.js
@@ -1,5 +1,5 @@
-import {stitchArrays} from '#sugar';
-import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data';
+import {sortAlphabetically, sortByCount} from '#sort';
+import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkGroup'],
diff --git a/src/content/dependencies/listGroupsByDuration.js b/src/content/dependencies/listGroupsByDuration.js
index e2a023e..da2f26d 100644
--- a/src/content/dependencies/listGroupsByDuration.js
+++ b/src/content/dependencies/listGroupsByDuration.js
@@ -1,5 +1,6 @@
-import {stitchArrays} from '#sugar';
-import {filterByCount, getTotalDuration, sortAlphabetically, sortByCount} from '#wiki-data';
+import {sortAlphabetically, sortByCount} from '#sort';
+import {filterByCount, stitchArrays} from '#sugar';
+import {getTotalDuration} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkGroup'],
diff --git a/src/content/dependencies/listGroupsByLatestAlbum.js b/src/content/dependencies/listGroupsByLatestAlbum.js
index fa22366..4831931 100644
--- a/src/content/dependencies/listGroupsByLatestAlbum.js
+++ b/src/content/dependencies/listGroupsByLatestAlbum.js
@@ -1,5 +1,5 @@
-import {stitchArrays} from '#sugar';
-import {compareDates, filterMultipleArrays, sortChronologically, sortMultipleArrays} from '#wiki-data';
+import {compareDates, sortChronologically} from '#sort';
+import {filterMultipleArrays, sortMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/listGroupsByName.js b/src/content/dependencies/listGroupsByName.js
index 8f0c424..696a49b 100644
--- a/src/content/dependencies/listGroupsByName.js
+++ b/src/content/dependencies/listGroupsByName.js
@@ -1,5 +1,5 @@
+import {sortAlphabetically} from '#sort';
 import {stitchArrays} from '#sugar';
-import {sortAlphabetically} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkGroup', 'linkGroupGallery'],
diff --git a/src/content/dependencies/listGroupsByTracks.js b/src/content/dependencies/listGroupsByTracks.js
index b3c55ca..0b5e4e9 100644
--- a/src/content/dependencies/listGroupsByTracks.js
+++ b/src/content/dependencies/listGroupsByTracks.js
@@ -1,5 +1,5 @@
-import {accumulateSum, stitchArrays} from '#sugar';
-import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data';
+import {sortAlphabetically, sortByCount} from '#sort';
+import {accumulateSum, filterByCount, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkGroup'],
diff --git a/src/content/dependencies/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js
index 1858569..ab2eca9 100644
--- a/src/content/dependencies/listRandomPageLinks.js
+++ b/src/content/dependencies/listRandomPageLinks.js
@@ -1,5 +1,5 @@
+import {sortChronologically} from '#sort';
 import {empty} from '#sugar';
-import {sortChronologically} from '#wiki-data';
 
 export default {
   contentDependencies: [
diff --git a/src/content/dependencies/listTagsByName.js b/src/content/dependencies/listTagsByName.js
index 8571ccd..d7022a5 100644
--- a/src/content/dependencies/listTagsByName.js
+++ b/src/content/dependencies/listTagsByName.js
@@ -1,5 +1,5 @@
+import {sortAlphabetically} from '#sort';
 import {stitchArrays} from '#sugar';
-import {sortAlphabetically} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkArtTag'],
diff --git a/src/content/dependencies/listTagsByUses.js b/src/content/dependencies/listTagsByUses.js
index 98a50b8..00c700a 100644
--- a/src/content/dependencies/listTagsByUses.js
+++ b/src/content/dependencies/listTagsByUses.js
@@ -1,5 +1,5 @@
-import {stitchArrays} from '#sugar';
-import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data';
+import {sortAlphabetically, sortByCount} from '#sort';
+import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkArtTag'],
diff --git a/src/content/dependencies/listTracksByDate.js b/src/content/dependencies/listTracksByDate.js
index 25beb73..01ce4e2 100644
--- a/src/content/dependencies/listTracksByDate.js
+++ b/src/content/dependencies/listTracksByDate.js
@@ -1,5 +1,5 @@
-import {stitchArrays} from '#sugar';
-import {chunkByProperties, sortAlbumsTracksChronologically} from '#wiki-data';
+import {sortAlbumsTracksChronologically} from '#sort';
+import {chunkByProperties, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'],
diff --git a/src/content/dependencies/listTracksByDuration.js b/src/content/dependencies/listTracksByDuration.js
index bff9bd4..64feb4f 100644
--- a/src/content/dependencies/listTracksByDuration.js
+++ b/src/content/dependencies/listTracksByDuration.js
@@ -1,5 +1,5 @@
-import {stitchArrays} from '#sugar';
-import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data';
+import {sortAlphabetically, sortByCount} from '#sort';
+import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkTrack'],
diff --git a/src/content/dependencies/listTracksByDurationInAlbum.js b/src/content/dependencies/listTracksByDurationInAlbum.js
index 4e83e92..c1ea32a 100644
--- a/src/content/dependencies/listTracksByDurationInAlbum.js
+++ b/src/content/dependencies/listTracksByDurationInAlbum.js
@@ -1,5 +1,5 @@
-import {stitchArrays} from '#sugar';
-import {filterByCount, filterMultipleArrays, sortByCount, sortChronologically} from '#wiki-data';
+import {sortByCount, sortChronologically} from '#sort';
+import {filterByCount, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'],
diff --git a/src/content/dependencies/listTracksByName.js b/src/content/dependencies/listTracksByName.js
index caf6886..773b047 100644
--- a/src/content/dependencies/listTracksByName.js
+++ b/src/content/dependencies/listTracksByName.js
@@ -1,4 +1,4 @@
-import {sortAlphabetically} from '#wiki-data';
+import {sortAlphabetically} from '#sort';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkTrack'],
diff --git a/src/content/dependencies/listTracksByTimesReferenced.js b/src/content/dependencies/listTracksByTimesReferenced.js
index 15a3461..5838ded 100644
--- a/src/content/dependencies/listTracksByTimesReferenced.js
+++ b/src/content/dependencies/listTracksByTimesReferenced.js
@@ -1,5 +1,5 @@
-import {stitchArrays} from '#sugar';
-import {filterByCount, sortAlbumsTracksChronologically, sortByCount} from '#wiki-data';
+import {sortAlbumsTracksChronologically, sortByCount} from '#sort';
+import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkTrack'],
diff --git a/src/content/dependencies/listTracksInFlashesByAlbum.js b/src/content/dependencies/listTracksInFlashesByAlbum.js
index 53ceb0e..8ca0d99 100644
--- a/src/content/dependencies/listTracksInFlashesByAlbum.js
+++ b/src/content/dependencies/listTracksInFlashesByAlbum.js
@@ -1,5 +1,5 @@
-import {empty, stitchArrays} from '#sugar';
-import {filterMultipleArrays, sortChronologically} from '#wiki-data';
+import {sortChronologically} from '#sort';
+import {empty, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkAlbum', 'linkFlash', 'linkTrack'],
diff --git a/src/content/dependencies/listTracksInFlashesByFlash.js b/src/content/dependencies/listTracksInFlashesByFlash.js
index c80d582..6ab954e 100644
--- a/src/content/dependencies/listTracksInFlashesByFlash.js
+++ b/src/content/dependencies/listTracksInFlashesByFlash.js
@@ -1,5 +1,5 @@
+import {sortFlashesChronologically} from '#sort';
 import {empty, stitchArrays} from '#sugar';
-import {sortFlashesChronologically} from '#wiki-data';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkAlbum', 'linkFlash', 'linkTrack'],
diff --git a/src/content/dependencies/listTracksWithExtra.js b/src/content/dependencies/listTracksWithExtra.js
index c9f80f3..c7f42f9 100644
--- a/src/content/dependencies/listTracksWithExtra.js
+++ b/src/content/dependencies/listTracksWithExtra.js
@@ -1,5 +1,5 @@
-import {empty, stitchArrays} from '#sugar';
-import {filterMultipleArrays, sortChronologically} from '#wiki-data';
+import {sortChronologically} from '#sort';
+import {empty, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'],
diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js
index 679a09f..0a1ebeb 100644
--- a/src/data/composite/things/album/withTrackSections.js
+++ b/src/data/composite/things/album/withTrackSections.js
@@ -1,8 +1,7 @@
 import {input, templateCompositeFrom} from '#composite';
 import find from '#find';
-import {empty, stitchArrays} from '#sugar';
+import {empty, filterMultipleArrays, stitchArrays} from '#sugar';
 import {isTrackSectionList} from '#validators';
-import {filterMultipleArrays} from '#wiki-data';
 
 import {exitWithoutDependency, exitWithoutUpdateValue}
   from '#composite/control-flow';
diff --git a/src/data/composite/things/track/inferredAdditionalNameList.js b/src/data/composite/things/track/inferredAdditionalNameList.js
index 9cf158c..58e8d2a 100644
--- a/src/data/composite/things/track/inferredAdditionalNameList.js
+++ b/src/data/composite/things/track/inferredAdditionalNameList.js
@@ -4,7 +4,7 @@
 // shares the same name differing from this one's.
 
 import {input, templateCompositeFrom} from '#composite';
-import {chunkByProperties} from '#wiki-data';
+import {chunkByProperties} from '#sugar';
 
 import {exitWithoutDependency} from '#composite/control-flow';
 import {withFilteredList, withPropertyFromList} from '#composite/data';
diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js
index eda2416..77b0f96 100644
--- a/src/data/composite/wiki-data/withResolvedContribs.js
+++ b/src/data/composite/wiki-data/withResolvedContribs.js
@@ -5,9 +5,8 @@
 
 import {input, templateCompositeFrom} from '#composite';
 import find from '#find';
-import {stitchArrays} from '#sugar';
+import {filterMultipleArrays, stitchArrays} from '#sugar';
 import {is, isContributionList} from '#validators';
-import {filterMultipleArrays} from '#wiki-data';
 
 import {
   raiseOutputWithoutDependency,
diff --git a/src/data/composite/wiki-data/withThingsSortedAlphabetically.js b/src/data/composite/wiki-data/withThingsSortedAlphabetically.js
index d2487e4..5e85fa6 100644
--- a/src/data/composite/wiki-data/withThingsSortedAlphabetically.js
+++ b/src/data/composite/wiki-data/withThingsSortedAlphabetically.js
@@ -5,8 +5,8 @@
 // a list so same-name entries are beside each other.
 
 import {input, templateCompositeFrom} from '#composite';
+import {compareCaseLessSensitive, normalizeName} from '#sort';
 import {validateWikiData} from '#validators';
-import {compareCaseLessSensitive, normalizeName} from '#wiki-data';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
 import {withMappedList, withSortedList, withPropertiesFromList}
diff --git a/src/data/things/album.js b/src/data/things/album.js
index d92dd43..01a232d 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -5,11 +5,10 @@ import * as path from 'node:path';
 import {input} from '#composite';
 import find from '#find';
 import {traverse} from '#node-utils';
+import {sortAlbumsTracksChronologically, sortChronologically} from '#sort';
 import {empty} from '#sugar';
 import Thing from '#thing';
 import {isDate} from '#validators';
-import {sortAlbumsTracksChronologically, sortChronologically}
-  from '#wiki-data';
 import {parseAdditionalFiles, parseContributors, parseDate, parseDimensions}
   from '#yaml';
 
diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js
index 69fbb52..3149b31 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -1,9 +1,9 @@
 export const ART_TAG_DATA_FILE = 'tags.yaml';
 
 import {input} from '#composite';
+import {sortAlphabetically, sortAlbumsTracksChronologically} from '#sort';
 import Thing from '#thing';
 import {isName} from '#validators';
-import {sortAlphabetically, sortAlbumsTracksChronologically} from '#wiki-data';
 
 import {exposeUpdateValueOrContinue} from '#composite/control-flow';
 
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 6e61c28..73acba6 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -6,10 +6,11 @@ import CacheableObject from '#cacheable-object';
 import {colors} from '#cli';
 import {input} from '#composite';
 import find from '#find';
+import {sortAlphabetically} from '#sort';
 import {stitchArrays, unique} from '#sugar';
 import Thing from '#thing';
 import {isName, validateArrayItems} from '#validators';
-import {getKebabCase, sortAlphabetically} from '#wiki-data';
+import {getKebabCase} from '#wiki-data';
 
 import {withReverseContributionList} from '#composite/wiki-data';
 
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index 4823f72..81de327 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -2,9 +2,9 @@ export const FLASH_DATA_FILE = 'flashes.yaml';
 
 import {input} from '#composite';
 import find from '#find';
+import {sortFlashesChronologically} from '#sort';
 import Thing from '#thing';
 import {anyOf, isColor, isDirectory, isNumber, isString} from '#validators';
-import {sortFlashesChronologically} from '#wiki-data';
 import {parseDate, parseContributors} from '#yaml';
 
 import {exposeDependency, exposeUpdateValueOrContinue}
diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js
index cb8e364..43d1638 100644
--- a/src/data/things/news-entry.js
+++ b/src/data/things/news-entry.js
@@ -1,7 +1,7 @@
 export const NEWS_DATA_FILE = 'news.yaml';
 
+import {sortChronologically} from '#sort';
 import Thing from '#thing';
-import {sortChronologically} from '#wiki-data';
 import {parseDate} from '#yaml';
 
 import {contentString, directory, name, simpleDate}
diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js
index 69cbfa1..0327497 100644
--- a/src/data/things/static-page.js
+++ b/src/data/things/static-page.js
@@ -3,9 +3,9 @@ export const DATA_STATIC_PAGE_DIRECTORY = 'static-page';
 import * as path from 'node:path';
 
 import {traverse} from '#node-utils';
+import {sortAlphabetically} from '#sort';
 import Thing from '#thing';
 import {isName} from '#validators';
-import {sortAlphabetically} from '#wiki-data';
 
 import {contentString, directory, name, simpleString}
   from '#composite/wiki-properties';
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 100e07b..0f73bdf 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -10,11 +10,11 @@ import yaml from 'js-yaml';
 import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli';
 import {reportDuplicateDirectories, filterReferenceErrors}
   from '#data-checks';
+import {sortByName} from '#sort';
 import {atOffset, empty, filterProperties, typeAppearance, withEntries}
   from '#sugar';
 import Thing from '#thing';
 import thingConstructors from '#things';
-import {sortByName} from '#wiki-data';
 
 import {
   annotateErrorWithFile,
diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js
index cecedc8..8a58269 100644
--- a/src/gen-thumbs.js
+++ b/src/gen-thumbs.js
@@ -156,8 +156,8 @@ import {
 import dimensionsOf from 'image-size';
 
 import CacheableObject from '#cacheable-object';
-import {delay, empty, queue, stitchArrays, unique} from '#sugar';
-import {chunkMultipleArrays, filterMultipleArrays, sortByName} from '#wiki-data';
+import {commandExists, isMain, promisifyProcess, traverse} from '#node-utils';
+import {sortByName} from '#sort';
 
 import {
   colors,
@@ -171,11 +171,14 @@ import {
 } from '#cli';
 
 import {
-  commandExists,
-  isMain,
-  promisifyProcess,
-  traverse,
-} from '#node-utils';
+  delay,
+  empty,
+  chunkMultipleArrays,
+  filterMultipleArrays,
+  queue,
+  stitchArrays,
+  unique,
+} from '#sugar';
 
 export const defaultMagickThreads = 8;
 
diff --git a/src/upd8.js b/src/upd8.js
index 13c625a..790b73e 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -47,9 +47,9 @@ import {bindFind, getAllFindSpecs} from '#find';
 import {processLanguageFile, watchLanguageFile, internalDefaultStringsFile}
   from '#language';
 import {isMain, traverse} from '#node-utils';
+import {sortByName} from '#sort';
 import {empty, withEntries} from '#sugar';
 import {generateURLs, urlSpec} from '#urls';
-import {sortByName} from '#wiki-data';
 import {linkWikiDataArrays, loadAndProcessDataDocuments, sortWikiDataArrays}
   from '#yaml';
 
diff --git a/src/util/sort.js b/src/util/sort.js
new file mode 100644
index 0000000..b3a9081
--- /dev/null
+++ b/src/util/sort.js
@@ -0,0 +1,405 @@
+// 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...
+  sortByDirectory(data, {
+    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;
+}
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 70749d8..46c4879 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -406,6 +406,219 @@ export function bindOpts(fn, bind) {
 
 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
diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js
index cdbffff..89c04a2 100644
--- a/src/util/wiki-data.js
+++ b/src/util/wiki-data.js
@@ -1,6 +1,7 @@
 // Utility functions for interacting with wiki data.
 
-import {accumulateSum, empty, unique} from './sugar.js';
+import {accumulateSum, empty} from './sugar.js';
+import {sortByDate} from './sort.js';
 
 // Generic value operations
 
@@ -15,622 +16,6 @@ export function getKebabCase(name) {
     .toLowerCase();
 }
 
-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;
-}
-
-// 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.)
-
-// 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;
-}
-
-// 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;
-}
-
-// 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;
-}
-
-// 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;
-}
-
-// Corresponding filter function for the above sort. 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);
-}
-
-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...
-  sortByDirectory(data, {
-    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;
-}
-
 // Specific data utilities
 
 // Matches heading details from commentary data in roughly the formats: