« get me outta code hell

data: break up content.js, flash.js, sorting-rule.js - 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>2026-01-26 14:07:13 -0400
committer(quasar) nebula <qznebula@protonmail.com>2026-01-26 14:07:13 -0400
commit1f37e5d6b0c6fccc9c46aabd7bd402375131d452 (patch)
treee441757a73dd2b2cb346ce33b90bf185c614fe7c
parentaa3cb2dd34780fd97873340c3faf7388993fa8d8 (diff)
data: break up content.js, flash.js, sorting-rule.js
-rw-r--r--src/data/things/content/CommentaryEntry.js20
-rw-r--r--src/data/things/content/ContentEntry.js (renamed from src/data/things/content.js)73
-rw-r--r--src/data/things/content/CreditingSourcesEntry.js16
-rw-r--r--src/data/things/content/LyricsEntry.js43
-rw-r--r--src/data/things/content/ReferencingSourcesEntry.js16
-rw-r--r--src/data/things/content/index.js9
-rw-r--r--src/data/things/flash/Flash.js (renamed from src/data/things/flash.js)199
-rw-r--r--src/data/things/flash/FlashAct.js74
-rw-r--r--src/data/things/flash/FlashSide.js136
-rw-r--r--src/data/things/flash/index.js3
-rw-r--r--src/data/things/index.js7
-rw-r--r--src/data/things/sorting-rule/DocumentSortingRule.js (renamed from src/data/things/sorting-rule.js)158
-rw-r--r--src/data/things/sorting-rule/SortingRule.js86
-rw-r--r--src/data/things/sorting-rule/ThingSortingRule.js83
-rw-r--r--src/data/things/sorting-rule/index.js3
15 files changed, 495 insertions, 431 deletions
diff --git a/src/data/things/content/CommentaryEntry.js b/src/data/things/content/CommentaryEntry.js
new file mode 100644
index 00000000..32d69213
--- /dev/null
+++ b/src/data/things/content/CommentaryEntry.js
@@ -0,0 +1,20 @@
+import {V} from '#composite';
+import Thing from '#thing';
+
+import {exposeConstant} from '#composite/control-flow';
+
+import {hasAnnotationPart} from '#composite/things/content';
+
+import {ContentEntry} from './ContentEntry.js';
+
+export class CommentaryEntry extends ContentEntry {
+  static [Thing.wikiData] = 'commentaryData';
+
+  static [Thing.getPropertyDescriptors] = () => ({
+    // Expose only
+
+    isCommentaryEntry: exposeConstant(V(true)),
+
+    isWikiEditorCommentary: hasAnnotationPart(V('wiki editor')),
+  });
+}
diff --git a/src/data/things/content.js b/src/data/things/content/ContentEntry.js
index 64d03e69..7dc81345 100644
--- a/src/data/things/content.js
+++ b/src/data/things/content/ContentEntry.js
@@ -20,7 +20,6 @@ import {
 } from '#composite/control-flow';
 
 import {
-  hasAnnotationPart,
   withAnnotationPartNodeLists,
   withExpressedOrImplicitArtistReferences,
   withWebArchiveDate,
@@ -245,75 +244,3 @@ export class ContentEntry extends Thing {
     },
   };
 }
-
-export class CommentaryEntry extends ContentEntry {
-  static [Thing.wikiData] = 'commentaryData';
-
-  static [Thing.getPropertyDescriptors] = () => ({
-    // Expose only
-
-    isCommentaryEntry: [
-      exposeConstant({
-        value: input.value(true),
-      }),
-    ],
-
-    isWikiEditorCommentary: hasAnnotationPart({
-      part: input.value('wiki editor'),
-    }),
-  });
-}
-
-export class LyricsEntry extends ContentEntry {
-  static [Thing.wikiData] = 'lyricsData';
-
-  static [Thing.getPropertyDescriptors] = () => ({
-    // Update & expose
-
-    originDetails: contentString(),
-
-    // Expose only
-
-    isLyricsEntry: exposeConstant(V(true)),
-
-    isWikiLyrics: hasAnnotationPart(V('wiki lyrics')),
-    helpNeeded: hasAnnotationPart(V('help needed')),
-
-    hasSquareBracketAnnotations: [
-      exitWithoutDependency('isWikiLyrics', V(false), V('falsy')),
-      exitWithoutDependency('body', V(false)),
-
-      {
-        dependencies: ['body'],
-        compute: ({body}) =>
-          /\[.*\]/m.test(body),
-      },
-    ],
-  });
-
-  static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(ContentEntry, {
-    fields: {
-      'Origin Details': {property: 'originDetails'},
-    },
-  });
-}
-
-export class CreditingSourcesEntry extends ContentEntry {
-  static [Thing.wikiData] = 'creditingSourceData';
-
-  static [Thing.getPropertyDescriptors] = () => ({
-    // Expose only
-
-    isCreditingSourcesEntry: exposeConstant(V(true)),
-  });
-}
-
-export class ReferencingSourcesEntry extends ContentEntry {
-  static [Thing.wikiData] = 'referencingSourceData';
-
-  static [Thing.getPropertyDescriptors] = () => ({
-    // Expose only
-
-    isReferencingSourceEntry: exposeConstant(V(true)),
-  });
-}
diff --git a/src/data/things/content/CreditingSourcesEntry.js b/src/data/things/content/CreditingSourcesEntry.js
new file mode 100644
index 00000000..7331ae8c
--- /dev/null
+++ b/src/data/things/content/CreditingSourcesEntry.js
@@ -0,0 +1,16 @@
+import {V} from '#composite';
+import Thing from '#thing';
+
+import {exposeConstant} from '#composite/control-flow';
+
+import {ContentEntry} from './ContentEntry.js';
+
+export class CreditingSourcesEntry extends ContentEntry {
+  static [Thing.wikiData] = 'creditingSourceData';
+
+  static [Thing.getPropertyDescriptors] = () => ({
+    // Expose only
+
+    isCreditingSourcesEntry: exposeConstant(V(true)),
+  });
+}
diff --git a/src/data/things/content/LyricsEntry.js b/src/data/things/content/LyricsEntry.js
new file mode 100644
index 00000000..88e4464d
--- /dev/null
+++ b/src/data/things/content/LyricsEntry.js
@@ -0,0 +1,43 @@
+import {V} from '#composite';
+import Thing from '#thing';
+
+import {exitWithoutDependency, exposeConstant} from '#composite/control-flow';
+import {contentString} from '#composite/wiki-properties';
+
+import {hasAnnotationPart} from '#composite/things/content';
+
+import {ContentEntry} from './ContentEntry.js';
+
+export class LyricsEntry extends ContentEntry {
+  static [Thing.wikiData] = 'lyricsData';
+
+  static [Thing.getPropertyDescriptors] = () => ({
+    // Update & expose
+
+    originDetails: contentString(),
+
+    // Expose only
+
+    isLyricsEntry: exposeConstant(V(true)),
+
+    isWikiLyrics: hasAnnotationPart(V('wiki lyrics')),
+    helpNeeded: hasAnnotationPart(V('help needed')),
+
+    hasSquareBracketAnnotations: [
+      exitWithoutDependency('isWikiLyrics', V(false), V('falsy')),
+      exitWithoutDependency('body', V(false)),
+
+      {
+        dependencies: ['body'],
+        compute: ({body}) =>
+          /\[.*\]/m.test(body),
+      },
+    ],
+  });
+
+  static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(ContentEntry, {
+    fields: {
+      'Origin Details': {property: 'originDetails'},
+    },
+  });
+}
diff --git a/src/data/things/content/ReferencingSourcesEntry.js b/src/data/things/content/ReferencingSourcesEntry.js
new file mode 100644
index 00000000..76fafbc2
--- /dev/null
+++ b/src/data/things/content/ReferencingSourcesEntry.js
@@ -0,0 +1,16 @@
+import {V} from '#composite';
+import Thing from '#thing';
+
+import {exposeConstant} from '#composite/control-flow';
+
+import {ContentEntry} from './ContentEntry.js';
+
+export class ReferencingSourcesEntry extends ContentEntry {
+  static [Thing.wikiData] = 'referencingSourceData';
+
+  static [Thing.getPropertyDescriptors] = () => ({
+    // Expose only
+
+    isReferencingSourceEntry: exposeConstant(V(true)),
+  });
+}
diff --git a/src/data/things/content/index.js b/src/data/things/content/index.js
new file mode 100644
index 00000000..aaa7f304
--- /dev/null
+++ b/src/data/things/content/index.js
@@ -0,0 +1,9 @@
+// Yet Another Index.js File Descending From A Folder Named Content
+
+export * from './ContentEntry.js';
+
+export * from './CommentaryEntry.js';
+export * from './LyricsEntry.js';
+
+export * from './CreditingSourcesEntry.js';
+export * from './ReferencingSourcesEntry.js';
diff --git a/src/data/things/flash.js b/src/data/things/flash/Flash.js
index b595ec58..1f290b3f 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash/Flash.js
@@ -1,9 +1,6 @@
-export const FLASH_DATA_FILE = 'flashes.yaml';
-
 import {input, V} from '#composite';
-import {sortFlashesChronologically} from '#sort';
 import Thing from '#thing';
-import {anyOf, isColor, isContentString, isDirectory, isNumber, isString}
+import {anyOf, isColor, isDirectory, isNumber, isString}
   from '#validators';
 
 import {
@@ -25,13 +22,10 @@ import {
 } from '#composite/control-flow';
 
 import {
-  color,
   commentatorArtists,
   constitutibleArtwork,
-  contentString,
   contributionList,
   dimensions,
-  directory,
   fileExtension,
   name,
   referenceList,
@@ -250,194 +244,3 @@ export class Flash extends Thing {
     ];
   }
 }
-
-export class FlashAct extends Thing {
-  static [Thing.referenceType] = 'flash-act';
-  static [Thing.friendlyName] = `Flash Act`;
-  static [Thing.wikiData] = 'flashActData';
-
-  static [Thing.getPropertyDescriptors] = ({Flash, FlashSide}) => ({
-    // Update & expose
-
-    side: thing(V(FlashSide)),
-
-    name: name(V('Unnamed Flash Act')),
-    directory: directory(),
-    color: color(),
-
-    listTerminology: [
-      exposeUpdateValueOrContinue({
-        validate: input.value(isContentString),
-      }),
-
-      withPropertyFromObject('side', V('listTerminology')),
-      exposeDependency('#side.listTerminology'),
-    ],
-
-    flashes: thingList(V(Flash)),
-
-    // Update only
-
-    find: soupyFind(),
-    reverse: soupyReverse(),
-
-    // Expose only
-
-    isFlashAct: exposeConstant(V(true)),
-  });
-
-  static [Thing.findSpecs] = {
-    flashAct: {
-      referenceTypes: ['flash-act'],
-      bindTo: 'flashActData',
-    },
-  };
-
-  static [Thing.reverseSpecs] = {
-    flashActsWhoseFlashesInclude: {
-      bindTo: 'flashActData',
-
-      referencing: flashAct => [flashAct],
-      referenced: flashAct => flashAct.flashes,
-    },
-  };
-
-  static [Thing.yamlDocumentSpec] = {
-    fields: {
-      'Act': {property: 'name'},
-      'Directory': {property: 'directory'},
-
-      'Color': {property: 'color'},
-      'List Terminology': {property: 'listTerminology'},
-
-      'Review Points': {ignore: true},
-    },
-  };
-}
-
-export class FlashSide extends Thing {
-  static [Thing.referenceType] = 'flash-side';
-  static [Thing.friendlyName] = `Flash Side`;
-  static [Thing.wikiData] = 'flashSideData';
-
-  static [Thing.getPropertyDescriptors] = ({FlashAct}) => ({
-    // Update & expose
-
-    name: name(V('Unnamed Flash Side')),
-    directory: directory(),
-    color: color(),
-    listTerminology: contentString(),
-
-    acts: thingList(V(FlashAct)),
-
-    // Update only
-
-    find: soupyFind(),
-
-    // Expose only
-
-    isFlashSide: exposeConstant(V(true)),
-  });
-
-  static [Thing.yamlDocumentSpec] = {
-    fields: {
-      'Side': {property: 'name'},
-      'Directory': {property: 'directory'},
-      'Color': {property: 'color'},
-      'List Terminology': {property: 'listTerminology'},
-    },
-  };
-
-  static [Thing.findSpecs] = {
-    flashSide: {
-      referenceTypes: ['flash-side'],
-      bindTo: 'flashSideData',
-    },
-  };
-
-  static [Thing.reverseSpecs] = {
-    flashSidesWhoseActsInclude: {
-      bindTo: 'flashSideData',
-
-      referencing: flashSide => [flashSide],
-      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/flash/FlashAct.js b/src/data/things/flash/FlashAct.js
new file mode 100644
index 00000000..66d4ee1b
--- /dev/null
+++ b/src/data/things/flash/FlashAct.js
@@ -0,0 +1,74 @@
+
+import {input, V} from '#composite';
+import Thing from '#thing';
+import {isContentString} from '#validators';
+
+import {withPropertyFromObject} from '#composite/data';
+import {exposeConstant, exposeDependency, exposeUpdateValueOrContinue}
+  from '#composite/control-flow';
+import {color, directory, name, soupyFind, soupyReverse, thing, thingList}
+  from '#composite/wiki-properties';
+
+export class FlashAct extends Thing {
+  static [Thing.referenceType] = 'flash-act';
+  static [Thing.friendlyName] = `Flash Act`;
+  static [Thing.wikiData] = 'flashActData';
+
+  static [Thing.getPropertyDescriptors] = ({Flash, FlashSide}) => ({
+    // Update & expose
+
+    side: thing(V(FlashSide)),
+
+    name: name(V('Unnamed Flash Act')),
+    directory: directory(),
+    color: color(),
+
+    listTerminology: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isContentString),
+      }),
+
+      withPropertyFromObject('side', V('listTerminology')),
+      exposeDependency('#side.listTerminology'),
+    ],
+
+    flashes: thingList(V(Flash)),
+
+    // Update only
+
+    find: soupyFind(),
+    reverse: soupyReverse(),
+
+    // Expose only
+
+    isFlashAct: exposeConstant(V(true)),
+  });
+
+  static [Thing.findSpecs] = {
+    flashAct: {
+      referenceTypes: ['flash-act'],
+      bindTo: 'flashActData',
+    },
+  };
+
+  static [Thing.reverseSpecs] = {
+    flashActsWhoseFlashesInclude: {
+      bindTo: 'flashActData',
+
+      referencing: flashAct => [flashAct],
+      referenced: flashAct => flashAct.flashes,
+    },
+  };
+
+  static [Thing.yamlDocumentSpec] = {
+    fields: {
+      'Act': {property: 'name'},
+      'Directory': {property: 'directory'},
+
+      'Color': {property: 'color'},
+      'List Terminology': {property: 'listTerminology'},
+
+      'Review Points': {ignore: true},
+    },
+  };
+}
diff --git a/src/data/things/flash/FlashSide.js b/src/data/things/flash/FlashSide.js
new file mode 100644
index 00000000..72782bdd
--- /dev/null
+++ b/src/data/things/flash/FlashSide.js
@@ -0,0 +1,136 @@
+const FLASH_DATA_FILE = 'flashes.yaml';
+
+import {V} from '#composite';
+import {sortFlashesChronologically} from '#sort';
+import Thing from '#thing';
+
+import {exposeConstant} from '#composite/control-flow';
+import {color, contentString, directory, name, soupyFind, thingList}
+  from '#composite/wiki-properties';
+
+export class FlashSide extends Thing {
+  static [Thing.referenceType] = 'flash-side';
+  static [Thing.friendlyName] = `Flash Side`;
+  static [Thing.wikiData] = 'flashSideData';
+
+  static [Thing.getPropertyDescriptors] = ({FlashAct}) => ({
+    // Update & expose
+
+    name: name(V('Unnamed Flash Side')),
+    directory: directory(),
+    color: color(),
+    listTerminology: contentString(),
+
+    acts: thingList(V(FlashAct)),
+
+    // Update only
+
+    find: soupyFind(),
+
+    // Expose only
+
+    isFlashSide: exposeConstant(V(true)),
+  });
+
+  static [Thing.yamlDocumentSpec] = {
+    fields: {
+      'Side': {property: 'name'},
+      'Directory': {property: 'directory'},
+      'Color': {property: 'color'},
+      'List Terminology': {property: 'listTerminology'},
+    },
+  };
+
+  static [Thing.findSpecs] = {
+    flashSide: {
+      referenceTypes: ['flash-side'],
+      bindTo: 'flashSideData',
+    },
+  };
+
+  static [Thing.reverseSpecs] = {
+    flashSidesWhoseActsInclude: {
+      bindTo: 'flashSideData',
+
+      referencing: flashSide => [flashSide],
+      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/flash/index.js b/src/data/things/flash/index.js
new file mode 100644
index 00000000..19b8cc34
--- /dev/null
+++ b/src/data/things/flash/index.js
@@ -0,0 +1,3 @@
+export * from './Flash.js';
+export * from './FlashAct.js';
+export * from './FlashSide.js';
diff --git a/src/data/things/index.js b/src/data/things/index.js
index 4a3d7ad8..bf8a5a33 100644
--- a/src/data/things/index.js
+++ b/src/data/things/index.js
@@ -1,8 +1,11 @@
 // Not actually the entry point for #things - that's init.js in this folder.
 
 export * from './album/index.js';
+export * from './content/index.js';
+export * from './flash/index.js';
 export * from './group/index.js';
 export * from './homepage-layout/index.js';
+export * from './sorting-rule/index.js';
 
 export * from './AdditionalFile.js';
 export * from './AdditionalName.js';
@@ -16,7 +19,3 @@ export * from './NewsEntry.js';
 export * from './StaticPage.js';
 export * from './Track.js';
 export * from './WikiInfo.js';
-
-export * from './content.js';
-export * from './flash.js';
-export * from './sorting-rule.js';
diff --git a/src/data/things/sorting-rule.js b/src/data/things/sorting-rule/DocumentSortingRule.js
index 101a4966..0f67d8f5 100644
--- a/src/data/things/sorting-rule.js
+++ b/src/data/things/sorting-rule/DocumentSortingRule.js
@@ -1,21 +1,12 @@
-export const SORTING_RULE_DATA_FILE = 'sorting-rules.yaml';
-
 import {readFile, writeFile} from 'node:fs/promises';
 import * as path from 'node:path';
 
 import {V} from '#composite';
-import {chunkByProperties, compareArrays, unique} from '#sugar';
+import {chunkByProperties, compareArrays} from '#sugar';
 import Thing from '#thing';
 import {isObject, isStringNonEmpty, anyOf, strictArrayOf} from '#validators';
 
 import {
-  compareCaseLessSensitive,
-  sortByDate,
-  sortByDirectory,
-  sortByName,
-} from '#sort';
-
-import {
   documentModes,
   flattenThingLayoutToDocumentOrder,
   getThingLayoutForFilename,
@@ -23,7 +14,6 @@ import {
 } from '#yaml';
 
 import {exposeConstant} from '#composite/control-flow';
-import {flag} from '#composite/wiki-properties';
 
 function isSelectFollowingEntry(value) {
   isObject(value);
@@ -36,151 +26,7 @@ function isSelectFollowingEntry(value) {
   return true;
 }
 
-export class SortingRule extends Thing {
-  static [Thing.friendlyName] = `Sorting Rule`;
-  static [Thing.wikiData] = 'sortingRules';
-
-  static [Thing.getPropertyDescriptors] = () => ({
-    // Update & expose
-
-    active: flag(V(true)),
-
-    message: {
-      flags: {update: true, expose: true},
-      update: {validate: isStringNonEmpty},
-    },
-
-    // Expose only
-
-    isSortingRule: exposeConstant(V(true)),
-  });
-
-  static [Thing.yamlDocumentSpec] = {
-    fields: {
-      'Message': {property: 'message'},
-      'Active': {property: 'active'},
-    },
-  };
-
-  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);
-  }
-
-  apply(opts) {
-    return this.constructor.apply(this, opts);
-  }
-
-  static check(rule, opts) {
-    const result = this.apply(rule, {...opts, dry: true});
-    if (!result) return true;
-    if (!result.changed) return true;
-    return false;
-  }
-
-  static async apply(_rule, _opts) {
-    throw new Error(`Not implemented`);
-  }
-
-  static async* applyAll(_rules, _opts) {
-    throw new Error(`Not implemented`);
-  }
-
-  static async* go({dataPath, wikiData, dry}) {
-    const rules = wikiData.sortingRules;
-    const constructors = unique(rules.map(rule => rule.constructor));
-
-    for (const constructor of constructors) {
-      yield* constructor.applyAll(
-        rules
-          .filter(rule => rule.active)
-          .filter(rule => rule.constructor === constructor),
-        {dataPath, wikiData, dry});
-    }
-  }
-}
-
-export class ThingSortingRule extends SortingRule {
-  static [Thing.getPropertyDescriptors] = () => ({
-    // Update & expose
-
-    properties: {
-      flags: {update: true, expose: true},
-      update: {
-        validate: strictArrayOf(isStringNonEmpty),
-      },
-    },
-
-    // Expose only
-
-    isThingSortingRule: exposeConstant(V(true)),
-  });
-
-  static [Thing.yamlDocumentSpec] = {
-    fields: {
-      'By Properties': {property: 'properties'},
-    },
-  };
-
-  sort(sortable) {
-    if (this.properties) {
-      for (const property of this.properties.toReversed()) {
-        const get = thing => thing[property];
-        const lc = property.toLowerCase();
-
-        if (lc.endsWith('date')) {
-          sortByDate(sortable, {getDate: get});
-          continue;
-        }
-
-        if (lc.endsWith('directory')) {
-          sortByDirectory(sortable, {getDirectory: get});
-          continue;
-        }
-
-        if (lc.endsWith('name')) {
-          sortByName(sortable, {getName: get});
-          continue;
-        }
-
-        const values = sortable.map(get);
-
-        if (values.every(v => typeof v === 'string')) {
-          sortable.sort((a, b) =>
-            compareCaseLessSensitive(get(a), get(b)));
-          continue;
-        }
-
-        if (values.every(v => typeof v === 'number')) {
-          sortable.sort((a, b) => get(a) - get(b));
-          continue;
-        }
-
-        sortable.sort((a, b) =>
-          (get(a).toString() < get(b).toString()
-            ? -1
-         : get(a).toString() > get(b).toString()
-            ? +1
-            :  0));
-      }
-    }
-
-    return sortable;
-  }
-}
+import {ThingSortingRule} from './ThingSortingRule.js';
 
 export class DocumentSortingRule extends ThingSortingRule {
   static [Thing.getPropertyDescriptors] = () => ({
diff --git a/src/data/things/sorting-rule/SortingRule.js b/src/data/things/sorting-rule/SortingRule.js
new file mode 100644
index 00000000..4ce9d97a
--- /dev/null
+++ b/src/data/things/sorting-rule/SortingRule.js
@@ -0,0 +1,86 @@
+const SORTING_RULE_DATA_FILE = 'sorting-rules.yaml';
+
+import {V} from '#composite';
+import {unique} from '#sugar';
+import Thing from '#thing';
+import {isStringNonEmpty} from '#validators';
+
+import {exposeConstant} from '#composite/control-flow';
+import {flag} from '#composite/wiki-properties';
+
+export class SortingRule extends Thing {
+  static [Thing.friendlyName] = `Sorting Rule`;
+  static [Thing.wikiData] = 'sortingRules';
+
+  static [Thing.getPropertyDescriptors] = () => ({
+    // Update & expose
+
+    active: flag(V(true)),
+
+    message: {
+      flags: {update: true, expose: true},
+      update: {validate: isStringNonEmpty},
+    },
+
+    // Expose only
+
+    isSortingRule: exposeConstant(V(true)),
+  });
+
+  static [Thing.yamlDocumentSpec] = {
+    fields: {
+      'Message': {property: 'message'},
+      'Active': {property: 'active'},
+    },
+  };
+
+  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);
+  }
+
+  apply(opts) {
+    return this.constructor.apply(this, opts);
+  }
+
+  static check(rule, opts) {
+    const result = this.apply(rule, {...opts, dry: true});
+    if (!result) return true;
+    if (!result.changed) return true;
+    return false;
+  }
+
+  static async apply(_rule, _opts) {
+    throw new Error(`Not implemented`);
+  }
+
+  static async* applyAll(_rules, _opts) {
+    throw new Error(`Not implemented`);
+  }
+
+  static async* go({dataPath, wikiData, dry}) {
+    const rules = wikiData.sortingRules;
+    const constructors = unique(rules.map(rule => rule.constructor));
+
+    for (const constructor of constructors) {
+      yield* constructor.applyAll(
+        rules
+          .filter(rule => rule.active)
+          .filter(rule => rule.constructor === constructor),
+        {dataPath, wikiData, dry});
+    }
+  }
+}
diff --git a/src/data/things/sorting-rule/ThingSortingRule.js b/src/data/things/sorting-rule/ThingSortingRule.js
new file mode 100644
index 00000000..b5cc76dc
--- /dev/null
+++ b/src/data/things/sorting-rule/ThingSortingRule.js
@@ -0,0 +1,83 @@
+import {V} from '#composite';
+import Thing from '#thing';
+import {isStringNonEmpty, strictArrayOf} from '#validators';
+
+import {
+  compareCaseLessSensitive,
+  sortByDate,
+  sortByDirectory,
+  sortByName,
+} from '#sort';
+
+import {exposeConstant} from '#composite/control-flow';
+
+import {SortingRule} from './SortingRule.js';
+
+export class ThingSortingRule extends SortingRule {
+  static [Thing.getPropertyDescriptors] = () => ({
+    // Update & expose
+
+    properties: {
+      flags: {update: true, expose: true},
+      update: {
+        validate: strictArrayOf(isStringNonEmpty),
+      },
+    },
+
+    // Expose only
+
+    isThingSortingRule: exposeConstant(V(true)),
+  });
+
+  static [Thing.yamlDocumentSpec] = {
+    fields: {
+      'By Properties': {property: 'properties'},
+    },
+  };
+
+  sort(sortable) {
+    if (this.properties) {
+      for (const property of this.properties.toReversed()) {
+        const get = thing => thing[property];
+        const lc = property.toLowerCase();
+
+        if (lc.endsWith('date')) {
+          sortByDate(sortable, {getDate: get});
+          continue;
+        }
+
+        if (lc.endsWith('directory')) {
+          sortByDirectory(sortable, {getDirectory: get});
+          continue;
+        }
+
+        if (lc.endsWith('name')) {
+          sortByName(sortable, {getName: get});
+          continue;
+        }
+
+        const values = sortable.map(get);
+
+        if (values.every(v => typeof v === 'string')) {
+          sortable.sort((a, b) =>
+            compareCaseLessSensitive(get(a), get(b)));
+          continue;
+        }
+
+        if (values.every(v => typeof v === 'number')) {
+          sortable.sort((a, b) => get(a) - get(b));
+          continue;
+        }
+
+        sortable.sort((a, b) =>
+          (get(a).toString() < get(b).toString()
+            ? -1
+         : get(a).toString() > get(b).toString()
+            ? +1
+            :  0));
+      }
+    }
+
+    return sortable;
+  }
+}
diff --git a/src/data/things/sorting-rule/index.js b/src/data/things/sorting-rule/index.js
new file mode 100644
index 00000000..7b83bd44
--- /dev/null
+++ b/src/data/things/sorting-rule/index.js
@@ -0,0 +1,3 @@
+export * from './SortingRule.js';
+export * from './ThingSortingRule.js';
+export * from './DocumentSortingRule.js';