From 4075254c9e38be6741527e1fb535eed444e6ad08 Mon Sep 17 00:00:00 2001
From: "(quasar) nebula"
Date: Sun, 26 Jun 2022 16:41:09 -0300
Subject: initial prettier/eslint commit
---
src/data/cacheable-object.js | 393 +++--
src/data/patches.js | 616 +++----
src/data/serialize.js | 30 +-
src/data/things.js | 2751 ++++++++++++++++--------------
src/data/validators.js | 404 ++---
src/data/yaml.js | 2188 +++++++++++++-----------
src/file-size-preloader.js | 128 +-
src/gen-thumbs.js | 555 +++---
src/listing-spec.js | 1623 ++++++++++--------
src/misc-templates.js | 895 +++++-----
src/page/album-commentary.js | 261 +--
src/page/album.js | 960 ++++++-----
src/page/artist-alias.js | 25 +-
src/page/artist.js | 1142 ++++++++-----
src/page/flash.js | 474 +++---
src/page/group.js | 488 +++---
src/page/homepage.js | 269 +--
src/page/index.js | 24 +-
src/page/listing.js | 341 ++--
src/page/news.js | 203 +--
src/page/static.js | 41 +-
src/page/tag.js | 157 +-
src/page/track.js | 674 +++++---
src/repl.js | 108 +-
src/static/client.js | 638 +++----
src/static/lazy-loading.js | 62 +-
src/static/site-basic.css | 12 +-
src/static/site.css | 966 +++++------
src/strings-default.json | 754 ++++-----
src/upd8.js | 3843 +++++++++++++++++++++++-------------------
src/url-spec.js | 127 +-
src/util/cli.js | 418 ++---
src/util/colors.js | 38 +-
src/util/find.js | 254 +--
src/util/html.js | 157 +-
src/util/io.js | 18 +-
src/util/link.js | 189 ++-
src/util/magic-constants.js | 4 +-
src/util/node-utils.js | 49 +-
src/util/replacer.js | 679 ++++----
src/util/serialize.js | 107 +-
src/util/sugar.js | 609 +++----
src/util/urls.js | 196 ++-
src/util/wiki-data.js | 638 +++----
44 files changed, 13304 insertions(+), 11204 deletions(-)
(limited to 'src')
diff --git a/src/data/cacheable-object.js b/src/data/cacheable-object.js
index 4afb0368..76efbd83 100644
--- a/src/data/cacheable-object.js
+++ b/src/data/cacheable-object.js
@@ -74,21 +74,21 @@
// function, which provides a mapping of exposed property names to whether
// or not their dependencies are yet met.
-import { color, ENABLE_COLOR } from '../util/cli.js';
+import { color, ENABLE_COLOR } from "../util/cli.js";
-import { inspect as nodeInspect } from 'util';
+import { inspect as nodeInspect } from "util";
function inspect(value) {
- return nodeInspect(value, {colors: ENABLE_COLOR});
+ return nodeInspect(value, { colors: ENABLE_COLOR });
}
export default class CacheableObject {
- static instance = Symbol('CacheableObject `this` instance');
+ static instance = Symbol("CacheableObject `this` instance");
- #propertyUpdateValues = Object.create(null);
- #propertyUpdateCacheInvalidators = Object.create(null);
+ #propertyUpdateValues = Object.create(null);
+ #propertyUpdateCacheInvalidators = Object.create(null);
- /*
+ /*
// Note the constructor doesn't take an initial data source. Due to a quirk
// of JavaScript, private members can't be accessed before the superclass's
// constructor is finished processing - so if we call the overridden
@@ -99,211 +99,238 @@ export default class CacheableObject {
// after constructing the new instance of the Thing (sub)class.
*/
- constructor() {
- this.#defineProperties();
- this.#initializeUpdatingPropertyValues();
-
- if (CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES) {
- return new Proxy(this, {
- get: (obj, key) => {
- if (!Object.hasOwn(obj, key)) {
- if (key !== 'constructor') {
- CacheableObject._invalidAccesses.add(`(${obj.constructor.name}).${key}`);
- }
- }
- return obj[key];
- }
- });
- }
- }
-
- #initializeUpdatingPropertyValues() {
- for (const [ property, descriptor ] of Object.entries(this.constructor.propertyDescriptors)) {
- const { flags, update } = descriptor;
-
- if (!flags.update) {
- continue;
- }
-
- if (update?.default) {
- this[property] = update?.default;
- } else {
- this[property] = null;
+ constructor() {
+ this.#defineProperties();
+ this.#initializeUpdatingPropertyValues();
+
+ if (CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES) {
+ return new Proxy(this, {
+ get: (obj, key) => {
+ if (!Object.hasOwn(obj, key)) {
+ if (key !== "constructor") {
+ CacheableObject._invalidAccesses.add(
+ `(${obj.constructor.name}).${key}`
+ );
}
- }
+ }
+ return obj[key];
+ },
+ });
}
-
- #defineProperties() {
- if (!this.constructor.propertyDescriptors) {
- throw new Error(`Expected constructor ${this.constructor.name} to define propertyDescriptors`);
- }
-
- for (const [ property, descriptor ] of Object.entries(this.constructor.propertyDescriptors)) {
- const { flags } = descriptor;
-
- const definition = {
- configurable: false,
- enumerable: true
- };
-
- if (flags.update) {
- definition.set = this.#getUpdateObjectDefinitionSetterFunction(property);
- }
-
- if (flags.expose) {
- definition.get = this.#getExposeObjectDefinitionGetterFunction(property);
- }
-
- Object.defineProperty(this, property, definition);
- }
-
- Object.seal(this);
+ }
+
+ #initializeUpdatingPropertyValues() {
+ for (const [property, descriptor] of Object.entries(
+ this.constructor.propertyDescriptors
+ )) {
+ const { flags, update } = descriptor;
+
+ if (!flags.update) {
+ continue;
+ }
+
+ if (update?.default) {
+ this[property] = update?.default;
+ } else {
+ this[property] = null;
+ }
}
+ }
- #getUpdateObjectDefinitionSetterFunction(property) {
- const { update } = this.#getPropertyDescriptor(property);
- const validate = update?.validate;
- const allowNull = update?.allowNull;
-
- return (newValue) => {
- const oldValue = this.#propertyUpdateValues[property];
+ #defineProperties() {
+ if (!this.constructor.propertyDescriptors) {
+ throw new Error(
+ `Expected constructor ${this.constructor.name} to define propertyDescriptors`
+ );
+ }
- if (newValue === undefined) {
- throw new TypeError(`Properties cannot be set to undefined`);
- }
+ for (const [property, descriptor] of Object.entries(
+ this.constructor.propertyDescriptors
+ )) {
+ const { flags } = descriptor;
- if (newValue === oldValue) {
- return;
- }
+ const definition = {
+ configurable: false,
+ enumerable: true,
+ };
- if (newValue !== null && validate) {
- try {
- const result = validate(newValue);
- if (result === undefined) {
- throw new TypeError(`Validate function returned undefined`);
- } else if (result !== true) {
- throw new TypeError(`Validation failed for value ${newValue}`);
- }
- } catch (error) {
- error.message = `Property ${color.green(property)} (${inspect(this[property])} -> ${inspect(newValue)}): ${error.message}`;
- throw error;
- }
- }
+ if (flags.update) {
+ definition.set =
+ this.#getUpdateObjectDefinitionSetterFunction(property);
+ }
- this.#propertyUpdateValues[property] = newValue;
- this.#invalidateCachesDependentUpon(property);
- };
- }
+ if (flags.expose) {
+ definition.get =
+ this.#getExposeObjectDefinitionGetterFunction(property);
+ }
- #getUpdatePropertyValidateFunction(property) {
- const descriptor = this.#getPropertyDescriptor(property);
+ Object.defineProperty(this, property, definition);
}
- #getPropertyDescriptor(property) {
- return this.constructor.propertyDescriptors[property];
- }
-
- #invalidateCachesDependentUpon(property) {
- for (const invalidate of this.#propertyUpdateCacheInvalidators[property] || []) {
- invalidate();
- }
- }
-
- #getExposeObjectDefinitionGetterFunction(property) {
- const { flags } = this.#getPropertyDescriptor(property);
- const compute = this.#getExposeComputeFunction(property);
-
- if (compute) {
- let cachedValue;
- const checkCacheValid = this.#getExposeCheckCacheValidFunction(property);
- return () => {
- if (checkCacheValid()) {
- return cachedValue;
- } else {
- return (cachedValue = compute());
- }
- };
- } else if (!flags.update && !compute) {
- throw new Error(`Exposed property ${property} does not update and is missing compute function`);
- } else {
- return () => this.#propertyUpdateValues[property];
+ Object.seal(this);
+ }
+
+ #getUpdateObjectDefinitionSetterFunction(property) {
+ const { update } = this.#getPropertyDescriptor(property);
+ const validate = update?.validate;
+ const allowNull = update?.allowNull;
+
+ return (newValue) => {
+ const oldValue = this.#propertyUpdateValues[property];
+
+ if (newValue === undefined) {
+ throw new TypeError(`Properties cannot be set to undefined`);
+ }
+
+ if (newValue === oldValue) {
+ return;
+ }
+
+ if (newValue !== null && validate) {
+ try {
+ const result = validate(newValue);
+ if (result === undefined) {
+ throw new TypeError(`Validate function returned undefined`);
+ } else if (result !== true) {
+ throw new TypeError(`Validation failed for value ${newValue}`);
+ }
+ } catch (error) {
+ error.message = `Property ${color.green(property)} (${inspect(
+ this[property]
+ )} -> ${inspect(newValue)}): ${error.message}`;
+ throw error;
}
- }
-
- #getExposeComputeFunction(property) {
- const { flags, expose } = this.#getPropertyDescriptor(property);
+ }
- const compute = expose?.compute;
- const transform = expose?.transform;
+ this.#propertyUpdateValues[property] = newValue;
+ this.#invalidateCachesDependentUpon(property);
+ };
+ }
- if (flags.update && !transform) {
- return null;
- } else if (flags.update && compute) {
- throw new Error(`Updating property ${property} has compute function, should be formatted as transform`);
- } else if (!flags.update && !compute) {
- throw new Error(`Exposed property ${property} does not update and is missing compute function`);
- }
+ #getUpdatePropertyValidateFunction(property) {
+ const descriptor = this.#getPropertyDescriptor(property);
+ }
- const dependencyKeys = expose.dependencies || [];
- const dependencyGetters = dependencyKeys.map(key => () => [key, this.#propertyUpdateValues[key]]);
- const getAllDependencies = () => Object.fromEntries(dependencyGetters.map(f => f())
- .concat([[this.constructor.instance, this]]));
+ #getPropertyDescriptor(property) {
+ return this.constructor.propertyDescriptors[property];
+ }
- if (flags.update) {
- return () => transform(this.#propertyUpdateValues[property], getAllDependencies());
+ #invalidateCachesDependentUpon(property) {
+ for (const invalidate of this.#propertyUpdateCacheInvalidators[property] ||
+ []) {
+ invalidate();
+ }
+ }
+
+ #getExposeObjectDefinitionGetterFunction(property) {
+ const { flags } = this.#getPropertyDescriptor(property);
+ const compute = this.#getExposeComputeFunction(property);
+
+ if (compute) {
+ let cachedValue;
+ const checkCacheValid = this.#getExposeCheckCacheValidFunction(property);
+ return () => {
+ if (checkCacheValid()) {
+ return cachedValue;
} else {
- return () => compute(getAllDependencies());
+ return (cachedValue = compute());
}
+ };
+ } else if (!flags.update && !compute) {
+ throw new Error(
+ `Exposed property ${property} does not update and is missing compute function`
+ );
+ } else {
+ return () => this.#propertyUpdateValues[property];
+ }
+ }
+
+ #getExposeComputeFunction(property) {
+ const { flags, expose } = this.#getPropertyDescriptor(property);
+
+ const compute = expose?.compute;
+ const transform = expose?.transform;
+
+ if (flags.update && !transform) {
+ return null;
+ } else if (flags.update && compute) {
+ throw new Error(
+ `Updating property ${property} has compute function, should be formatted as transform`
+ );
+ } else if (!flags.update && !compute) {
+ throw new Error(
+ `Exposed property ${property} does not update and is missing compute function`
+ );
}
- #getExposeCheckCacheValidFunction(property) {
- const { flags, expose } = this.#getPropertyDescriptor(property);
-
- let valid = false;
+ const dependencyKeys = expose.dependencies || [];
+ const dependencyGetters = dependencyKeys.map((key) => () => [
+ key,
+ this.#propertyUpdateValues[key],
+ ]);
+ const getAllDependencies = () =>
+ Object.fromEntries(
+ dependencyGetters
+ .map((f) => f())
+ .concat([[this.constructor.instance, this]])
+ );
+
+ if (flags.update) {
+ return () =>
+ transform(this.#propertyUpdateValues[property], getAllDependencies());
+ } else {
+ return () => compute(getAllDependencies());
+ }
+ }
- const invalidate = () => {
- valid = false;
- };
+ #getExposeCheckCacheValidFunction(property) {
+ const { flags, expose } = this.#getPropertyDescriptor(property);
- const dependencyKeys = new Set(expose?.dependencies);
+ let valid = false;
- if (flags.update) {
- dependencyKeys.add(property);
- }
+ const invalidate = () => {
+ valid = false;
+ };
- for (const key of dependencyKeys) {
- if (this.#propertyUpdateCacheInvalidators[key]) {
- this.#propertyUpdateCacheInvalidators[key].push(invalidate);
- } else {
- this.#propertyUpdateCacheInvalidators[key] = [invalidate];
- }
- }
+ const dependencyKeys = new Set(expose?.dependencies);
- return () => {
- if (!valid) {
- valid = true;
- return false;
- } else {
- return true;
- }
- };
+ if (flags.update) {
+ dependencyKeys.add(property);
}
- static DEBUG_SLOW_TRACK_INVALID_PROPERTIES = false;
- static _invalidAccesses = new Set();
+ for (const key of dependencyKeys) {
+ if (this.#propertyUpdateCacheInvalidators[key]) {
+ this.#propertyUpdateCacheInvalidators[key].push(invalidate);
+ } else {
+ this.#propertyUpdateCacheInvalidators[key] = [invalidate];
+ }
+ }
- static showInvalidAccesses() {
- if (!this.DEBUG_SLOW_TRACK_INVALID_PROPERTIES) {
- return;
- }
+ return () => {
+ if (!valid) {
+ valid = true;
+ return false;
+ } else {
+ return true;
+ }
+ };
+ }
+
+ static DEBUG_SLOW_TRACK_INVALID_PROPERTIES = false;
+ static _invalidAccesses = new Set();
+
+ static showInvalidAccesses() {
+ if (!this.DEBUG_SLOW_TRACK_INVALID_PROPERTIES) {
+ return;
+ }
- if (!this._invalidAccesses.size) {
- return;
- }
+ if (!this._invalidAccesses.size) {
+ return;
+ }
- console.log(`${this._invalidAccesses.size} unique invalid accesses:`);
- for (const line of this._invalidAccesses) {
- console.log(` - ${line}`);
- }
+ console.log(`${this._invalidAccesses.size} unique invalid accesses:`);
+ for (const line of this._invalidAccesses) {
+ console.log(` - ${line}`);
}
+ }
}
diff --git a/src/data/patches.js b/src/data/patches.js
index 3ed4fad0..0ff56ad0 100644
--- a/src/data/patches.js
+++ b/src/data/patches.js
@@ -1,291 +1,309 @@
// --> Patch
export class Patch {
- static INPUT_NONE = 0;
- static INPUT_CONSTANT = 1;
- static INPUT_DIRECT_CONNECTION = 2;
- static INPUT_MANAGED_CONNECTION = 3;
+ static INPUT_NONE = 0;
+ static INPUT_CONSTANT = 1;
+ static INPUT_DIRECT_CONNECTION = 2;
+ static INPUT_MANAGED_CONNECTION = 3;
- static INPUT_UNAVAILABLE = 0;
- static INPUT_AVAILABLE = 1;
+ static INPUT_UNAVAILABLE = 0;
+ static INPUT_AVAILABLE = 1;
- static OUTPUT_UNAVAILABLE = 0;
- static OUTPUT_AVAILABLE = 1;
+ static OUTPUT_UNAVAILABLE = 0;
+ static OUTPUT_AVAILABLE = 1;
- static inputNames = []; inputNames = null;
- static outputNames = []; outputNames = null;
+ static inputNames = [];
+ inputNames = null;
+ static outputNames = [];
+ outputNames = null;
- manager = null;
- inputs = Object.create(null);
+ manager = null;
+ inputs = Object.create(null);
- constructor({
- manager,
+ constructor({
+ manager,
- inputNames,
- outputNames,
+ inputNames,
+ outputNames,
- inputs,
- } = {}) {
- this.inputNames = inputNames ?? this.constructor.inputNames;
- this.outputNames = outputNames ?? this.constructor.outputNames;
+ inputs,
+ } = {}) {
+ this.inputNames = inputNames ?? this.constructor.inputNames;
+ this.outputNames = outputNames ?? this.constructor.outputNames;
- manager?.addManagedPatch(this);
+ manager?.addManagedPatch(this);
- if (inputs) {
- Object.assign(this.inputs, inputs);
- }
-
- this.initializeInputs();
+ if (inputs) {
+ Object.assign(this.inputs, inputs);
}
- initializeInputs() {
- for (const inputName of this.inputNames) {
- if (!this.inputs[inputName]) {
- this.inputs[inputName] = [Patch.INPUT_NONE];
- }
- }
- }
+ this.initializeInputs();
+ }
- computeInputs() {
- const inputs = Object.create(null);
-
- for (const inputName of this.inputNames) {
- const input = this.inputs[inputName];
- switch (input[0]) {
- case Patch.INPUT_NONE:
- inputs[inputName] = [Patch.INPUT_UNAVAILABLE];
- break;
-
- case Patch.INPUT_CONSTANT:
- inputs[inputName] = [Patch.INPUT_AVAILABLE, input[1]];
- break;
-
- case Patch.INPUT_DIRECT_CONNECTION: {
- const patch = input[1];
- const outputName = input[2];
- const output = patch.computeOutputs()[outputName];
- switch (output[0]) {
- case Patch.OUTPUT_UNAVAILABLE:
- inputs[inputName] = [Patch.INPUT_UNAVAILABLE];
- break;
- case Patch.OUTPUT_AVAILABLE:
- inputs[inputName] = [Patch.INPUT_AVAILABLE, output[1]];
- break;
- }
- throw new Error('Unreachable');
- }
-
- case Patch.INPUT_MANAGED_CONNECTION: {
- if (!this.manager) {
- inputs[inputName] = [Patch.INPUT_UNAVAILABLE];
- break;
- }
-
- inputs[inputName] = this.manager.getManagedInput(input[1]);
- break;
- }
- }
+ initializeInputs() {
+ for (const inputName of this.inputNames) {
+ if (!this.inputs[inputName]) {
+ this.inputs[inputName] = [Patch.INPUT_NONE];
+ }
+ }
+ }
+
+ computeInputs() {
+ const inputs = Object.create(null);
+
+ for (const inputName of this.inputNames) {
+ const input = this.inputs[inputName];
+ switch (input[0]) {
+ case Patch.INPUT_NONE:
+ inputs[inputName] = [Patch.INPUT_UNAVAILABLE];
+ break;
+
+ case Patch.INPUT_CONSTANT:
+ inputs[inputName] = [Patch.INPUT_AVAILABLE, input[1]];
+ break;
+
+ case Patch.INPUT_DIRECT_CONNECTION: {
+ const patch = input[1];
+ const outputName = input[2];
+ const output = patch.computeOutputs()[outputName];
+ switch (output[0]) {
+ case Patch.OUTPUT_UNAVAILABLE:
+ inputs[inputName] = [Patch.INPUT_UNAVAILABLE];
+ break;
+ case Patch.OUTPUT_AVAILABLE:
+ inputs[inputName] = [Patch.INPUT_AVAILABLE, output[1]];
+ break;
+ }
+ throw new Error("Unreachable");
}
- return inputs;
- }
+ case Patch.INPUT_MANAGED_CONNECTION: {
+ if (!this.manager) {
+ inputs[inputName] = [Patch.INPUT_UNAVAILABLE];
+ break;
+ }
- computeOutputs() {
- const inputs = this.computeInputs();
- const outputs = Object.create(null);
- console.log(`Compute: ${this.constructor.name}`);
- this.compute(inputs, outputs);
- return outputs;
+ inputs[inputName] = this.manager.getManagedInput(input[1]);
+ break;
+ }
+ }
}
- compute(inputs, outputs) {
- // No-op. Return all outputs as unavailable. This should be overridden
- // in subclasses.
+ return inputs;
+ }
- for (const outputName of this.constructor.outputNames) {
- outputs[outputName] = [Patch.OUTPUT_UNAVAILABLE];
- }
- }
+ computeOutputs() {
+ const inputs = this.computeInputs();
+ const outputs = Object.create(null);
+ console.log(`Compute: ${this.constructor.name}`);
+ this.compute(inputs, outputs);
+ return outputs;
+ }
- attachToManager(manager) {
- manager.addManagedPatch(this);
+ compute(inputs, outputs) {
+ // No-op. Return all outputs as unavailable. This should be overridden
+ // in subclasses.
+
+ for (const outputName of this.constructor.outputNames) {
+ outputs[outputName] = [Patch.OUTPUT_UNAVAILABLE];
}
+ }
- detachFromManager() {
- if (this.manager) {
- this.manager.removeManagedPatch(this);
- }
+ attachToManager(manager) {
+ manager.addManagedPatch(this);
+ }
+
+ detachFromManager() {
+ if (this.manager) {
+ this.manager.removeManagedPatch(this);
}
+ }
}
// --> PatchManager
export class PatchManager extends Patch {
- managedPatches = [];
- managedInputs = {};
-
- #externalInputPatch = null;
- #externalOutputPatch = null;
-
- constructor(...args) {
- super(...args);
-
- this.#externalInputPatch = new PatchManagerExternalInputPatch({manager: this});
- this.#externalOutputPatch = new PatchManagerExternalOutputPatch({manager: this});
+ managedPatches = [];
+ managedInputs = {};
+
+ #externalInputPatch = null;
+ #externalOutputPatch = null;
+
+ constructor(...args) {
+ super(...args);
+
+ this.#externalInputPatch = new PatchManagerExternalInputPatch({
+ manager: this,
+ });
+ this.#externalOutputPatch = new PatchManagerExternalOutputPatch({
+ manager: this,
+ });
+ }
+
+ addManagedPatch(patch) {
+ if (patch.manager === this) {
+ return false;
}
- addManagedPatch(patch) {
- if (patch.manager === this) {
- return false;
- }
-
- patch.detachFromManager();
- patch.manager = this;
+ patch.detachFromManager();
+ patch.manager = this;
- if (patch.manager === this) {
- this.managedPatches.push(patch);
- return true;
- } else {
- return false;
- }
+ if (patch.manager === this) {
+ this.managedPatches.push(patch);
+ return true;
+ } else {
+ return false;
}
+ }
- removeManagedPatch(patch) {
- if (patch.manager !== this) {
- return false;
- }
-
- patch.manager = null;
-
- if (patch.manager === this) {
- return false;
- }
-
- for (const inputNames of patch.inputNames) {
- const input = patch.inputs[inputName];
- if (input[0] === Patch.INPUT_MANAGED_CONNECTION) {
- this.dropManagedInput(input[1]);
- patch.inputs[inputName] = [Patch.INPUT_NONE];
- }
- }
-
- this.managedPatches.splice(this.managedPatches.indexOf(patch), 1);
-
- return true;
+ removeManagedPatch(patch) {
+ if (patch.manager !== this) {
+ return false;
}
- addManagedInput(patchWithInput, inputName, patchWithOutput, outputName) {
- if (patchWithInput.manager !== this || patchWithOutput.manager !== this) {
- throw new Error(`Input and output patches must belong to same manager (this)`);
- }
-
- const input = patchWithInput.inputs[inputName];
- if (input[0] === Patch.INPUT_MANAGED_CONNECTION) {
- this.managedInputs[input[1]] = [patchWithOutput, outputName, {}];
- } else {
- const key = this.getManagedConnectionIdentifier();
- this.managedInputs[key] = [patchWithOutput, outputName, {}];
- patchWithInput.inputs[inputName] = [Patch.INPUT_MANAGED_CONNECTION, key];
- }
+ patch.manager = null;
- return true;
+ if (patch.manager === this) {
+ return false;
}
- dropManagedInput(identifier) {
- return delete this.managedInputs[key];
+ for (const inputNames of patch.inputNames) {
+ const input = patch.inputs[inputName];
+ if (input[0] === Patch.INPUT_MANAGED_CONNECTION) {
+ this.dropManagedInput(input[1]);
+ patch.inputs[inputName] = [Patch.INPUT_NONE];
+ }
}
- getManagedInput(identifier) {
- const connection = this.managedInputs[identifier];
- const patch = connection[0];
- const outputName = connection[1];
- const memory = connection[2];
- return this.computeManagedInput(patch, outputName, memory);
- }
-
- computeManagedInput(patch, outputName, memory) {
- // Override this function in subclasses to alter behavior of the "wire"
- // used for connecting patches.
-
- const output = patch.computeOutputs()[outputName];
- switch (output[0]) {
- case Patch.OUTPUT_UNAVAILABLE:
- return [Patch.INPUT_UNAVAILABLE];
- case Patch.OUTPUT_AVAILABLE:
- return [Patch.INPUT_AVAILABLE, output[1]];
- }
- }
+ this.managedPatches.splice(this.managedPatches.indexOf(patch), 1);
- #managedConnectionIdentifier = 0;
- getManagedConnectionIdentifier() {
- return this.#managedConnectionIdentifier++;
- }
+ return true;
+ }
- addExternalInput(patchWithInput, patchInputName, managerInputName) {
- return this.addManagedInput(patchWithInput, patchInputName, this.#externalInputPatch, managerInputName);
+ addManagedInput(patchWithInput, inputName, patchWithOutput, outputName) {
+ if (patchWithInput.manager !== this || patchWithOutput.manager !== this) {
+ throw new Error(
+ `Input and output patches must belong to same manager (this)`
+ );
}
- setExternalOutput(managerOutputName, patchWithOutput, patchOutputName) {
- return this.addManagedInput(this.#externalOutputPatch, managerOutputName, patchWithOutput, patchOutputName);
+ const input = patchWithInput.inputs[inputName];
+ if (input[0] === Patch.INPUT_MANAGED_CONNECTION) {
+ this.managedInputs[input[1]] = [patchWithOutput, outputName, {}];
+ } else {
+ const key = this.getManagedConnectionIdentifier();
+ this.managedInputs[key] = [patchWithOutput, outputName, {}];
+ patchWithInput.inputs[inputName] = [Patch.INPUT_MANAGED_CONNECTION, key];
}
- compute(inputs, outputs) {
- Object.assign(outputs, this.#externalOutputPatch.computeOutputs());
+ return true;
+ }
+
+ dropManagedInput(identifier) {
+ return delete this.managedInputs[key];
+ }
+
+ getManagedInput(identifier) {
+ const connection = this.managedInputs[identifier];
+ const patch = connection[0];
+ const outputName = connection[1];
+ const memory = connection[2];
+ return this.computeManagedInput(patch, outputName, memory);
+ }
+
+ computeManagedInput(patch, outputName, memory) {
+ // Override this function in subclasses to alter behavior of the "wire"
+ // used for connecting patches.
+
+ const output = patch.computeOutputs()[outputName];
+ switch (output[0]) {
+ case Patch.OUTPUT_UNAVAILABLE:
+ return [Patch.INPUT_UNAVAILABLE];
+ case Patch.OUTPUT_AVAILABLE:
+ return [Patch.INPUT_AVAILABLE, output[1]];
}
+ }
+
+ #managedConnectionIdentifier = 0;
+ getManagedConnectionIdentifier() {
+ return this.#managedConnectionIdentifier++;
+ }
+
+ addExternalInput(patchWithInput, patchInputName, managerInputName) {
+ return this.addManagedInput(
+ patchWithInput,
+ patchInputName,
+ this.#externalInputPatch,
+ managerInputName
+ );
+ }
+
+ setExternalOutput(managerOutputName, patchWithOutput, patchOutputName) {
+ return this.addManagedInput(
+ this.#externalOutputPatch,
+ managerOutputName,
+ patchWithOutput,
+ patchOutputName
+ );
+ }
+
+ compute(inputs, outputs) {
+ Object.assign(outputs, this.#externalOutputPatch.computeOutputs());
+ }
}
class PatchManagerExternalInputPatch extends Patch {
- constructor({manager, ...rest}) {
- super({
- manager,
- inputNames: manager.inputNames,
- outputNames: manager.inputNames,
- ...rest
- });
- }
-
- computeInputs() {
- return this.manager.computeInputs();
- }
-
- compute(inputs, outputs) {
- for (const name of this.inputNames) {
- const input = inputs[name];
- switch (input[0]) {
- case Patch.INPUT_UNAVAILABLE:
- outputs[name] = [Patch.OUTPUT_UNAVAILABLE];
- break;
- case Patch.INPUT_AVAILABLE:
- outputs[name] = [Patch.INPUT_AVAILABLE, input[1]];
- break;
- }
- }
+ constructor({ manager, ...rest }) {
+ super({
+ manager,
+ inputNames: manager.inputNames,
+ outputNames: manager.inputNames,
+ ...rest,
+ });
+ }
+
+ computeInputs() {
+ return this.manager.computeInputs();
+ }
+
+ compute(inputs, outputs) {
+ for (const name of this.inputNames) {
+ const input = inputs[name];
+ switch (input[0]) {
+ case Patch.INPUT_UNAVAILABLE:
+ outputs[name] = [Patch.OUTPUT_UNAVAILABLE];
+ break;
+ case Patch.INPUT_AVAILABLE:
+ outputs[name] = [Patch.INPUT_AVAILABLE, input[1]];
+ break;
+ }
}
+ }
}
class PatchManagerExternalOutputPatch extends Patch {
- constructor({manager, ...rest}) {
- super({
- manager,
- inputNames: manager.outputNames,
- outputNames: manager.outputNames,
- ...rest
- });
- }
-
- compute(inputs, outputs) {
- for (const name of this.inputNames) {
- const input = inputs[name];
- switch (input[0]) {
- case Patch.INPUT_UNAVAILABLE:
- outputs[name] = [Patch.OUTPUT_UNAVAILABLE];
- break;
- case Patch.INPUT_AVAILABLE:
- outputs[name] = [Patch.INPUT_AVAILABLE, input[1]];
- break;
- }
- }
+ constructor({ manager, ...rest }) {
+ super({
+ manager,
+ inputNames: manager.outputNames,
+ outputNames: manager.outputNames,
+ ...rest,
+ });
+ }
+
+ compute(inputs, outputs) {
+ for (const name of this.inputNames) {
+ const input = inputs[name];
+ switch (input[0]) {
+ case Patch.INPUT_UNAVAILABLE:
+ outputs[name] = [Patch.OUTPUT_UNAVAILABLE];
+ break;
+ case Patch.INPUT_AVAILABLE:
+ outputs[name] = [Patch.INPUT_AVAILABLE, input[1]];
+ break;
+ }
}
+ }
}
// --> demo
@@ -295,84 +313,84 @@ const common = Symbol();
const hsmusic = Symbol();
Patch[caches] = {
- WireCachedPatchManager: class extends PatchManager {
- // "Wire" caching for PatchManager: Remembers the last outputs to come
- // from each patch. As long as the inputs for a patch do not change, its
- // cached outputs are reused.
-
- // TODO: This has a unique cache for each managed input. It should
- // re-use a cache for the same patch and output name. How can we ensure
- // the cache is dropped when the patch is removed, though? (Spoilers:
- // probably just override removeManagedPatch)
- computeManagedInput(patch, outputName, memory) {
- let cache = true;
-
- const { previousInputs } = memory;
- const { inputs } = patch;
- if (memory.previousInputs) {
- for (const inputName of patch.inputNames) {
- // TODO: This doesn't account for connections whose values
- // have changed (analogous to bubbling cache invalidation).
- if (inputs[inputName] !== previousInputs[inputName]) {
- cache = false;
- break;
- }
- }
- } else {
- cache = false;
- }
-
- if (cache) {
- return memory.previousOutputs[outputName];
- }
-
- const outputs = patch.computeOutputs();
- memory.previousOutputs = outputs;
- memory.previousInputs = {...inputs};
- return outputs[outputName];
+ WireCachedPatchManager: class extends PatchManager {
+ // "Wire" caching for PatchManager: Remembers the last outputs to come
+ // from each patch. As long as the inputs for a patch do not change, its
+ // cached outputs are reused.
+
+ // TODO: This has a unique cache for each managed input. It should
+ // re-use a cache for the same patch and output name. How can we ensure
+ // the cache is dropped when the patch is removed, though? (Spoilers:
+ // probably just override removeManagedPatch)
+ computeManagedInput(patch, outputName, memory) {
+ let cache = true;
+
+ const { previousInputs } = memory;
+ const { inputs } = patch;
+ if (memory.previousInputs) {
+ for (const inputName of patch.inputNames) {
+ // TODO: This doesn't account for connections whose values
+ // have changed (analogous to bubbling cache invalidation).
+ if (inputs[inputName] !== previousInputs[inputName]) {
+ cache = false;
+ break;
+ }
}
- },
+ } else {
+ cache = false;
+ }
+
+ if (cache) {
+ return memory.previousOutputs[outputName];
+ }
+
+ const outputs = patch.computeOutputs();
+ memory.previousOutputs = outputs;
+ memory.previousInputs = { ...inputs };
+ return outputs[outputName];
+ }
+ },
};
Patch[common] = {
- Stringify: class extends Patch {
- static inputNames = ['value'];
- static outputNames = ['value'];
-
- compute(inputs, outputs) {
- if (inputs.value[0] === Patch.INPUT_AVAILABLE) {
- outputs.value = [Patch.OUTPUT_AVAILABLE, inputs.value[1].toString()];
- } else {
- outputs.value = [Patch.OUTPUT_UNAVAILABLE];
- }
- }
- },
-
- Echo: class extends Patch {
- static inputNames = ['value'];
- static outputNames = ['value'];
-
- compute(inputs, outputs) {
- if (inputs.value[0] === Patch.INPUT_AVAILABLE) {
- outputs.value = [Patch.OUTPUT_AVAILABLE, inputs.value[1]];
- } else {
- outputs.value = [Patch.OUTPUT_UNAVAILABLE];
- }
- }
- },
+ Stringify: class extends Patch {
+ static inputNames = ["value"];
+ static outputNames = ["value"];
+
+ compute(inputs, outputs) {
+ if (inputs.value[0] === Patch.INPUT_AVAILABLE) {
+ outputs.value = [Patch.OUTPUT_AVAILABLE, inputs.value[1].toString()];
+ } else {
+ outputs.value = [Patch.OUTPUT_UNAVAILABLE];
+ }
+ }
+ },
+
+ Echo: class extends Patch {
+ static inputNames = ["value"];
+ static outputNames = ["value"];
+
+ compute(inputs, outputs) {
+ if (inputs.value[0] === Patch.INPUT_AVAILABLE) {
+ outputs.value = [Patch.OUTPUT_AVAILABLE, inputs.value[1]];
+ } else {
+ outputs.value = [Patch.OUTPUT_UNAVAILABLE];
+ }
+ }
+ },
};
const PM = new Patch[caches].WireCachedPatchManager({
- inputNames: ['externalInput'],
- outputNames: ['externalOutput'],
+ inputNames: ["externalInput"],
+ outputNames: ["externalOutput"],
});
-const P1 = new Patch[common].Stringify({manager: PM});
-const P2 = new Patch[common].Echo({manager: PM});
+const P1 = new Patch[common].Stringify({ manager: PM });
+const P2 = new Patch[common].Echo({ manager: PM });
-PM.addExternalInput(P1, 'value', 'externalInput');
-PM.addManagedInput(P2, 'value', P1, 'value');
-PM.setExternalOutput('externalOutput', P2, 'value');
+PM.addExternalInput(P1, "value", "externalInput");
+PM.addManagedInput(P2, "value", P1, "value");
+PM.setExternalOutput("externalOutput", P2, "value");
PM.inputs.externalInput = [Patch.INPUT_CONSTANT, 123];
console.log(PM.computeOutputs());
diff --git a/src/data/serialize.js b/src/data/serialize.js
index 9d4e8885..fc84d1ef 100644
--- a/src/data/serialize.js
+++ b/src/data/serialize.js
@@ -4,19 +4,19 @@
// Utility functions
export function id(x) {
- return x;
+ return x;
}
export function toRef(thing) {
- return thing?.constructor.getReference(thing);
+ return thing?.constructor.getReference(thing);
}
export function toRefs(things) {
- return things?.map(toRef);
+ return things?.map(toRef);
}
export function toContribRefs(contribs) {
- return contribs?.map(({ who, what }) => ({who: toRef(who), what}));
+ return contribs?.map(({ who, what }) => ({ who: toRef(who), what }));
}
// Interface
@@ -24,15 +24,21 @@ export function toContribRefs(contribs) {
export const serializeDescriptors = Symbol();
export function serializeThing(thing) {
- const descriptors = thing.constructor[serializeDescriptors];
- if (!descriptors) {
- throw new Error(`Constructor ${thing.constructor.name} does not provide serialize descriptors`);
- }
-
- return Object.fromEntries(Object.entries(descriptors)
- .map(([ property, transform ]) => [property, transform(thing[property])]));
+ const descriptors = thing.constructor[serializeDescriptors];
+ if (!descriptors) {
+ throw new Error(
+ `Constructor ${thing.constructor.name} does not provide serialize descriptors`
+ );
+ }
+
+ return Object.fromEntries(
+ Object.entries(descriptors).map(([property, transform]) => [
+ property,
+ transform(thing[property]),
+ ])
+ );
}
export function serializeThings(things) {
- return things.map(serializeThing);
+ return things.map(serializeThing);
}
diff --git a/src/data/things.js b/src/data/things.js
index 6a5cdb5e..62c01411 100644
--- a/src/data/things.js
+++ b/src/data/things.js
@@ -1,45 +1,45 @@
// things.js: class definitions for various object types used across the wiki,
// most of which correspond to an output page, such as Track, Album, Artist
-import CacheableObject from './cacheable-object.js';
+import CacheableObject from "./cacheable-object.js";
import {
- isAdditionalFileList,
- isBoolean,
- isColor,
- isCommentary,
- isCountingNumber,
- isContributionList,
- isDate,
- isDimensions,
- isDirectory,
- isDuration,
- isInstance,
- isFileExtension,
- isLanguageCode,
- isName,
- isNumber,
- isURL,
- isString,
- isWholeNumber,
- oneOf,
- validateArrayItems,
- validateInstanceOf,
- validateReference,
- validateReferenceList,
-} from './validators.js';
-
-import * as S from './serialize.js';
+ isAdditionalFileList,
+ isBoolean,
+ isColor,
+ isCommentary,
+ isCountingNumber,
+ isContributionList,
+ isDate,
+ isDimensions,
+ isDirectory,
+ isDuration,
+ isInstance,
+ isFileExtension,
+ isLanguageCode,
+ isName,
+ isNumber,
+ isURL,
+ isString,
+ isWholeNumber,
+ oneOf,
+ validateArrayItems,
+ validateInstanceOf,
+ validateReference,
+ validateReferenceList,
+} from "./validators.js";
+
+import * as S from "./serialize.js";
import {
- getKebabCase,
- sortAlbumsTracksChronologically,
-} from '../util/wiki-data.js';
+ getKebabCase,
+ sortAlbumsTracksChronologically,
+} from "../util/wiki-data.js";
-import find from '../util/find.js';
+import find from "../util/find.js";
-import { inspect } from 'util';
-import { color } from '../util/cli.js';
+import { inspect } from "util";
+import { color } from "../util/cli.js";
// Stub classes (and their exports) at the top of the file - these are
// referenced later when we actually define static class fields. We deliberately
@@ -94,16 +94,16 @@ export class Language extends CacheableObject {}
// Before initializing property descriptors, set additional independent
// constants on the classes (which are referenced later).
-Thing.referenceType = Symbol('Thing.referenceType');
+Thing.referenceType = Symbol("Thing.referenceType");
-Album[Thing.referenceType] = 'album';
-Track[Thing.referenceType] = 'track';
-Artist[Thing.referenceType] = 'artist';
-Group[Thing.referenceType] = 'group';
-ArtTag[Thing.referenceType] = 'tag';
-NewsEntry[Thing.referenceType] = 'news-entry';
-StaticPage[Thing.referenceType] = 'static';
-Flash[Thing.referenceType] = 'flash';
+Album[Thing.referenceType] = "album";
+Track[Thing.referenceType] = "track";
+Artist[Thing.referenceType] = "artist";
+Group[Thing.referenceType] = "group";
+ArtTag[Thing.referenceType] = "tag";
+NewsEntry[Thing.referenceType] = "news-entry";
+StaticPage[Thing.referenceType] = "static";
+Flash[Thing.referenceType] = "flash";
// -> Thing: base class for wiki data types, providing wiki-specific utility
// functions on top of essential CacheableObject behavior.
@@ -112,551 +112,580 @@ Flash[Thing.referenceType] = 'flash';
// duplicating less code across wiki data types. These are specialized utility
// functions, so check each for how its own arguments behave!
Thing.common = {
- name: (defaultName) => ({
- flags: {update: true, expose: true},
- update: {validate: isName, default: defaultName}
- }),
-
- color: () => ({
- flags: {update: true, expose: true},
- update: {validate: isColor}
- }),
-
- directory: () => ({
- flags: {update: true, expose: true},
- update: {validate: isDirectory},
- expose: {
- dependencies: ['name'],
- transform(directory, { name }) {
- if (directory === null && name === null)
- return null;
- else if (directory === null)
- return getKebabCase(name);
- else
- return directory;
- }
- }
- }),
-
- urls: () => ({
- flags: {update: true, expose: true},
- update: {validate: validateArrayItems(isURL)}
- }),
-
- // A file extension! Or the default, if provided when calling this.
- fileExtension: (defaultFileExtension = null) => ({
- flags: {update: true, expose: true},
- update: {validate: isFileExtension},
- expose: {transform: value => value ?? defaultFileExtension}
- }),
-
- // Straightforward flag descriptor for a variety of property purposes.
- // Provide a default value, true or false!
- flag: (defaultValue = false) => {
- if (typeof defaultValue !== 'boolean') {
- throw new TypeError(`Always set explicit defaults for flags!`);
- }
+ name: (defaultName) => ({
+ flags: { update: true, expose: true },
+ update: { validate: isName, default: defaultName },
+ }),
+
+ color: () => ({
+ flags: { update: true, expose: true },
+ update: { validate: isColor },
+ }),
+
+ directory: () => ({
+ flags: { update: true, expose: true },
+ update: { validate: isDirectory },
+ expose: {
+ dependencies: ["name"],
+ transform(directory, { name }) {
+ if (directory === null && name === null) return null;
+ else if (directory === null) return getKebabCase(name);
+ else return directory;
+ },
+ },
+ }),
+
+ urls: () => ({
+ flags: { update: true, expose: true },
+ update: { validate: validateArrayItems(isURL) },
+ }),
+
+ // A file extension! Or the default, if provided when calling this.
+ fileExtension: (defaultFileExtension = null) => ({
+ flags: { update: true, expose: true },
+ update: { validate: isFileExtension },
+ expose: { transform: (value) => value ?? defaultFileExtension },
+ }),
+
+ // Straightforward flag descriptor for a variety of property purposes.
+ // Provide a default value, true or false!
+ flag: (defaultValue = false) => {
+ if (typeof defaultValue !== "boolean") {
+ throw new TypeError(`Always set explicit defaults for flags!`);
+ }
- return {
- flags: {update: true, expose: true},
- update: {validate: isBoolean, default: defaultValue}
- };
- },
+ return {
+ flags: { update: true, expose: true },
+ update: { validate: isBoolean, default: defaultValue },
+ };
+ },
+
+ // General date type, used as the descriptor for a bunch of properties.
+ // This isn't dynamic though - it won't inherit from a date stored on
+ // another object, for example.
+ simpleDate: () => ({
+ flags: { update: true, expose: true },
+ update: { validate: isDate },
+ }),
+
+ // General string type. This should probably generally be avoided in favor
+ // of more specific validation, but using it makes it easy to find where we
+ // might want to improve later, and it's a useful shorthand meanwhile.
+ simpleString: () => ({
+ flags: { update: true, expose: true },
+ update: { validate: isString },
+ }),
+
+ // External function. These should only be used as dependencies for other
+ // properties, so they're left unexposed.
+ externalFunction: () => ({
+ flags: { update: true },
+ update: { validate: (t) => typeof t === "function" },
+ }),
+
+ // Super simple "contributions by reference" list, used for a variety of
+ // properties (Artists, Cover Artists, etc). This is the property which is
+ // externally provided, in the form:
+ //
+ // [
+ // {who: 'Artist Name', what: 'Viola'},
+ // {who: 'artist:john-cena', what: null},
+ // ...
+ // ]
+ //
+ // ...processed from YAML, spreadsheet, or any other kind of input.
+ contribsByRef: () => ({
+ flags: { update: true, expose: true },
+ update: { validate: isContributionList },
+ }),
+
+ // Artist commentary! Generally present on tracks and albums.
+ commentary: () => ({
+ flags: { update: true, expose: true },
+ update: { validate: isCommentary },
+ }),
+
+ // This is a somewhat more involved data structure - it's for additional
+ // or "bonus" files associated with albums or tracks (or anything else).
+ // It's got this form:
+ //
+ // [
+ // {title: 'Booklet', files: ['Booklet.pdf']},
+ // {
+ // title: 'Wallpaper',
+ // description: 'Cool Wallpaper!',
+ // files: ['1440x900.png', '1920x1080.png']
+ // },
+ // {title: 'Alternate Covers', description: null, files: [...]},
+ // ...
+ // ]
+ //
+ additionalFiles: () => ({
+ flags: { update: true, expose: true },
+ update: { validate: isAdditionalFileList },
+ }),
+
+ // A reference list! Keep in mind this is for general references to wiki
+ // objects of (usually) other Thing subclasses, not specifically leitmotif
+ // references in tracks (although that property uses referenceList too!).
+ //
+ // The underlying function validateReferenceList expects a string like
+ // 'artist' or 'track', but this utility keeps from having to hard-code the
+ // string in multiple places by referencing the value saved on the class
+ // instead.
+ referenceList: (thingClass) => {
+ const { [Thing.referenceType]: referenceType } = thingClass;
+ if (!referenceType) {
+ throw new Error(
+ `The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`
+ );
+ }
- // General date type, used as the descriptor for a bunch of properties.
- // This isn't dynamic though - it won't inherit from a date stored on
- // another object, for example.
- simpleDate: () => ({
- flags: {update: true, expose: true},
- update: {validate: isDate}
- }),
-
- // General string type. This should probably generally be avoided in favor
- // of more specific validation, but using it makes it easy to find where we
- // might want to improve later, and it's a useful shorthand meanwhile.
- simpleString: () => ({
- flags: {update: true, expose: true},
- update: {validate: isString}
- }),
-
- // External function. These should only be used as dependencies for other
- // properties, so they're left unexposed.
- externalFunction: () => ({
- flags: {update: true},
- update: {validate: t => typeof t === 'function'}
- }),
-
- // Super simple "contributions by reference" list, used for a variety of
- // properties (Artists, Cover Artists, etc). This is the property which is
- // externally provided, in the form:
- //
- // [
- // {who: 'Artist Name', what: 'Viola'},
- // {who: 'artist:john-cena', what: null},
- // ...
- // ]
- //
- // ...processed from YAML, spreadsheet, or any other kind of input.
- contribsByRef: () => ({
- flags: {update: true, expose: true},
- update: {validate: isContributionList}
- }),
-
- // Artist commentary! Generally present on tracks and albums.
- commentary: () => ({
- flags: {update: true, expose: true},
- update: {validate: isCommentary}
- }),
-
- // This is a somewhat more involved data structure - it's for additional
- // or "bonus" files associated with albums or tracks (or anything else).
- // It's got this form:
- //
- // [
- // {title: 'Booklet', files: ['Booklet.pdf']},
- // {
- // title: 'Wallpaper',
- // description: 'Cool Wallpaper!',
- // files: ['1440x900.png', '1920x1080.png']
- // },
- // {title: 'Alternate Covers', description: null, files: [...]},
- // ...
- // ]
- //
- additionalFiles: () => ({
- flags: {update: true, expose: true},
- update: {validate: isAdditionalFileList}
- }),
-
- // A reference list! Keep in mind this is for general references to wiki
- // objects of (usually) other Thing subclasses, not specifically leitmotif
- // references in tracks (although that property uses referenceList too!).
- //
- // The underlying function validateReferenceList expects a string like
- // 'artist' or 'track', but this utility keeps from having to hard-code the
- // string in multiple places by referencing the value saved on the class
- // instead.
- referenceList: thingClass => {
- const { [Thing.referenceType]: referenceType } = thingClass;
- if (!referenceType) {
- throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`);
- }
+ return {
+ flags: { update: true, expose: true },
+ update: { validate: validateReferenceList(referenceType) },
+ };
+ },
+
+ // Corresponding function for a single reference.
+ singleReference: (thingClass) => {
+ const { [Thing.referenceType]: referenceType } = thingClass;
+ if (!referenceType) {
+ throw new Error(
+ `The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`
+ );
+ }
- return {
- flags: {update: true, expose: true},
- update: {validate: validateReferenceList(referenceType)}
- };
- },
+ return {
+ flags: { update: true, expose: true },
+ update: { validate: validateReference(referenceType) },
+ };
+ },
+
+ // Corresponding dynamic property to referenceList, which takes the values
+ // in the provided property and searches the specified wiki data for
+ // matching actual Thing-subclass objects.
+ dynamicThingsFromReferenceList: (
+ referenceListProperty,
+ thingDataProperty,
+ findFn
+ ) => ({
+ flags: { expose: true },
- // Corresponding function for a single reference.
- singleReference: thingClass => {
- const { [Thing.referenceType]: referenceType } = thingClass;
- if (!referenceType) {
- throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`);
- }
+ expose: {
+ dependencies: [referenceListProperty, thingDataProperty],
+ compute: ({
+ [referenceListProperty]: refs,
+ [thingDataProperty]: thingData,
+ }) =>
+ refs && thingData
+ ? refs
+ .map((ref) => findFn(ref, thingData, { mode: "quiet" }))
+ .filter(Boolean)
+ : [],
+ },
+ }),
+
+ // Corresponding function for a single reference.
+ dynamicThingFromSingleReference: (
+ singleReferenceProperty,
+ thingDataProperty,
+ findFn
+ ) => ({
+ flags: { expose: true },
- return {
- flags: {update: true, expose: true},
- update: {validate: validateReference(referenceType)}
- };
- },
+ expose: {
+ dependencies: [singleReferenceProperty, thingDataProperty],
+ compute: ({
+ [singleReferenceProperty]: ref,
+ [thingDataProperty]: thingData,
+ }) =>
+ ref && thingData ? findFn(ref, thingData, { mode: "quiet" }) : null,
+ },
+ }),
+
+ // Corresponding dynamic property to contribsByRef, which takes the values
+ // in the provided property and searches the object's artistData for
+ // matching actual Artist objects. The computed structure has the same form
+ // as contribsByRef, but with Artist objects instead of string references:
+ //
+ // [
+ // {who: (an Artist), what: 'Viola'},
+ // {who: (an Artist), what: null},
+ // ...
+ // ]
+ //
+ // Contributions whose "who" values don't match anything in artistData are
+ // filtered out. (So if the list is all empty, chances are that either the
+ // reference list is somehow messed up, or artistData isn't being provided
+ // properly.)
+ dynamicContribs: (contribsByRefProperty) => ({
+ flags: { expose: true },
+ expose: {
+ dependencies: ["artistData", contribsByRefProperty],
+ compute: ({ artistData, [contribsByRefProperty]: contribsByRef }) =>
+ contribsByRef && artistData
+ ? contribsByRef
+ .map(({ who: ref, what }) => ({
+ who: find.artist(ref, artistData),
+ what,
+ }))
+ .filter(({ who }) => who)
+ : [],
+ },
+ }),
+
+ // Dynamically inherit a contribution list from some other object, if it
+ // hasn't been overridden on this object. This is handy for solo albums
+ // where all tracks have the same artist, for example.
+ //
+ // Note: The arguments of this function aren't currently final! The final
+ // format will look more like (contribsByRef, parentContribsByRef), e.g.
+ // ('artistContribsByRef', '@album/artistContribsByRef').
+ dynamicInheritContribs: (
+ contribsByRefProperty,
+ parentContribsByRefProperty,
+ thingDataProperty,
+ findFn
+ ) => ({
+ flags: { expose: true },
+ expose: {
+ dependencies: [contribsByRefProperty, thingDataProperty, "artistData"],
+ compute({
+ [Thing.instance]: thing,
+ [contribsByRefProperty]: contribsByRef,
+ [thingDataProperty]: thingData,
+ artistData,
+ }) {
+ if (!artistData) return [];
+ const refs =
+ contribsByRef ??
+ findFn(thing, thingData, { mode: "quiet" })?.[
+ parentContribsByRefProperty
+ ];
+ if (!refs) return [];
+ return refs
+ .map(({ who: ref, what }) => ({
+ who: find.artist(ref, artistData),
+ what,
+ }))
+ .filter(({ who }) => who);
+ },
+ },
+ }),
+
+ // Neat little shortcut for "reversing" the reference lists stored on other
+ // things - for example, tracks specify a "referenced tracks" property, and
+ // you would use this to compute a corresponding "referenced *by* tracks"
+ // property. Naturally, the passed ref list property is of the things in the
+ // wiki data provided, not the requesting Thing itself.
+ reverseReferenceList: (wikiDataProperty, referencerRefListProperty) => ({
+ flags: { expose: true },
- // Corresponding dynamic property to referenceList, which takes the values
- // in the provided property and searches the specified wiki data for
- // matching actual Thing-subclass objects.
- dynamicThingsFromReferenceList: (
- referenceListProperty,
- thingDataProperty,
- findFn
- ) => ({
- flags: {expose: true},
-
- expose: {
- dependencies: [referenceListProperty, thingDataProperty],
- compute: ({ [referenceListProperty]: refs, [thingDataProperty]: thingData }) => (
- (refs && thingData
- ? (refs
- .map(ref => findFn(ref, thingData, {mode: 'quiet'}))
- .filter(Boolean))
- : [])
- )
- }
- }),
-
- // Corresponding function for a single reference.
- dynamicThingFromSingleReference: (
- singleReferenceProperty,
- thingDataProperty,
- findFn
- ) => ({
- flags: {expose: true},
-
- expose: {
- dependencies: [singleReferenceProperty, thingDataProperty],
- compute: ({ [singleReferenceProperty]: ref, [thingDataProperty]: thingData }) => (
- (ref && thingData ? findFn(ref, thingData, {mode: 'quiet'}) : null)
- )
- }
- }),
-
- // Corresponding dynamic property to contribsByRef, which takes the values
- // in the provided property and searches the object's artistData for
- // matching actual Artist objects. The computed structure has the same form
- // as contribsByRef, but with Artist objects instead of string references:
- //
- // [
- // {who: (an Artist), what: 'Viola'},
- // {who: (an Artist), what: null},
- // ...
- // ]
- //
- // Contributions whose "who" values don't match anything in artistData are
- // filtered out. (So if the list is all empty, chances are that either the
- // reference list is somehow messed up, or artistData isn't being provided
- // properly.)
- dynamicContribs: (contribsByRefProperty) => ({
- flags: {expose: true},
- expose: {
- dependencies: ['artistData', contribsByRefProperty],
- compute: ({ artistData, [contribsByRefProperty]: contribsByRef }) => (
- ((contribsByRef && artistData)
- ? (contribsByRef
- .map(({ who: ref, what }) => ({
- who: find.artist(ref, artistData),
- what
- }))
- .filter(({ who }) => who))
- : [])
- )
- }
- }),
-
- // Dynamically inherit a contribution list from some other object, if it
- // hasn't been overridden on this object. This is handy for solo albums
- // where all tracks have the same artist, for example.
- //
- // Note: The arguments of this function aren't currently final! The final
- // format will look more like (contribsByRef, parentContribsByRef), e.g.
- // ('artistContribsByRef', '@album/artistContribsByRef').
- dynamicInheritContribs: (
- contribsByRefProperty,
- parentContribsByRefProperty,
- thingDataProperty,
- findFn
- ) => ({
- flags: {expose: true},
- expose: {
- dependencies: [contribsByRefProperty, thingDataProperty, 'artistData'],
- compute({
- [Thing.instance]: thing,
- [contribsByRefProperty]: contribsByRef,
- [thingDataProperty]: thingData,
- artistData
- }) {
- if (!artistData) return [];
- const refs = (contribsByRef ?? findFn(thing, thingData, {mode: 'quiet'})?.[parentContribsByRefProperty]);
- if (!refs) return [];
- return (refs
- .map(({ who: ref, what }) => ({
- who: find.artist(ref, artistData),
- what
- }))
- .filter(({ who }) => who));
- }
- }
- }),
-
- // Neat little shortcut for "reversing" the reference lists stored on other
- // things - for example, tracks specify a "referenced tracks" property, and
- // you would use this to compute a corresponding "referenced *by* tracks"
- // property. Naturally, the passed ref list property is of the things in the
- // wiki data provided, not the requesting Thing itself.
- reverseReferenceList: (wikiDataProperty, referencerRefListProperty) => ({
- flags: {expose: true},
-
- expose: {
- dependencies: [wikiDataProperty],
-
- compute: ({ [wikiDataProperty]: wikiData, [Thing.instance]: thing }) => (
- (wikiData
- ? wikiData.filter(t => t[referencerRefListProperty]?.includes(thing))
- : [])
+ expose: {
+ dependencies: [wikiDataProperty],
+
+ compute: ({ [wikiDataProperty]: wikiData, [Thing.instance]: thing }) =>
+ wikiData
+ ? wikiData.filter((t) =>
+ t[referencerRefListProperty]?.includes(thing)
)
- }
- }),
+ : [],
+ },
+ }),
- // Corresponding function for single references. Note that the return value
- // is still a list - this is for matching all the objects whose single
- // reference (in the given property) matches this Thing.
- reverseSingleReference: (wikiDataProperty, referencerRefListProperty) => ({
- flags: {expose: true},
+ // Corresponding function for single references. Note that the return value
+ // is still a list - this is for matching all the objects whose single
+ // reference (in the given property) matches this Thing.
+ reverseSingleReference: (wikiDataProperty, referencerRefListProperty) => ({
+ flags: { expose: true },
- expose: {
- dependencies: [wikiDataProperty],
+ expose: {
+ dependencies: [wikiDataProperty],
- compute: ({ [wikiDataProperty]: wikiData, [Thing.instance]: thing }) => (
- wikiData?.filter(t => t[referencerRefListProperty] === thing))
- }
- }),
-
- // General purpose wiki data constructor, for properties like artistData,
- // trackData, etc.
- wikiData: (thingClass) => ({
- flags: {update: true},
- update: {
- validate: validateArrayItems(validateInstanceOf(thingClass))
- }
- }),
-
- // This one's kinda tricky: it parses artist "references" from the
- // commentary content, and finds the matching artist for each reference.
- // This is mostly useful for credits and listings on artist pages.
- commentatorArtists: () => ({
- flags: {expose: true},
-
- expose: {
- dependencies: ['artistData', 'commentary'],
-
- compute: ({ artistData, commentary }) => (
- (artistData && commentary
- ? Array.from(new Set((Array
- .from(commentary
- .replace(/<\/?b>/g, '')
- .matchAll(/(?.*?):<\/i>/g))
- .map(({ groups: {who} }) => find.artist(who, artistData, {mode: 'quiet'})))))
- : []))
- }
- }),
+ compute: ({ [wikiDataProperty]: wikiData, [Thing.instance]: thing }) =>
+ wikiData?.filter((t) => t[referencerRefListProperty] === thing),
+ },
+ }),
+
+ // General purpose wiki data constructor, for properties like artistData,
+ // trackData, etc.
+ wikiData: (thingClass) => ({
+ flags: { update: true },
+ update: {
+ validate: validateArrayItems(validateInstanceOf(thingClass)),
+ },
+ }),
+
+ // This one's kinda tricky: it parses artist "references" from the
+ // commentary content, and finds the matching artist for each reference.
+ // This is mostly useful for credits and listings on artist pages.
+ commentatorArtists: () => ({
+ flags: { expose: true },
+
+ expose: {
+ dependencies: ["artistData", "commentary"],
+
+ compute: ({ artistData, commentary }) =>
+ artistData && commentary
+ ? Array.from(
+ new Set(
+ Array.from(
+ commentary
+ .replace(/<\/?b>/g, "")
+ .matchAll(/(?.*?):<\/i>/g)
+ ).map(({ groups: { who } }) =>
+ find.artist(who, artistData, { mode: "quiet" })
+ )
+ )
+ )
+ : [],
+ },
+ }),
};
// Get a reference to a thing (e.g. track:showtime-piano-refrain), using its
// constructor's [Thing.referenceType] as the prefix. This will throw an error
// if the thing's directory isn't yet provided/computable.
-Thing.getReference = function(thing) {
- if (!thing.constructor[Thing.referenceType])
- throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`);
-
- if (!thing.directory)
- throw TypeError(`Passed ${thing.constructor.name} is missing its directory`);
-
- return `${thing.constructor[Thing.referenceType]}:${thing.directory}`;
+Thing.getReference = function (thing) {
+ if (!thing.constructor[Thing.referenceType])
+ throw TypeError(
+ `Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`
+ );
+
+ if (!thing.directory)
+ throw TypeError(
+ `Passed ${thing.constructor.name} is missing its directory`
+ );
+
+ return `${thing.constructor[Thing.referenceType]}:${thing.directory}`;
};
// Default custom inspect function, which may be overridden by Thing subclasses.
// This will be used when displaying aggregate errors and other in command-line
// logging - it's the place to provide information useful in identifying the
// Thing being presented.
-Thing.prototype[inspect.custom] = function() {
- const cname = this.constructor.name;
-
- return (this.name
- ? `${cname} ${color.green(`"${this.name}"`)}`
- : `${cname}`) + (this.directory
- ? ` (${color.blue(Thing.getReference(this))})`
- : '');
+Thing.prototype[inspect.custom] = function () {
+ const cname = this.constructor.name;
+
+ return (
+ (this.name ? `${cname} ${color.green(`"${this.name}"`)}` : `${cname}`) +
+ (this.directory ? ` (${color.blue(Thing.getReference(this))})` : "")
+ );
};
// -> Album
Album.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Album'),
- color: Thing.common.color(),
- directory: Thing.common.directory(),
- urls: Thing.common.urls(),
+ name: Thing.common.name("Unnamed Album"),
+ color: Thing.common.color(),
+ directory: Thing.common.directory(),
+ urls: Thing.common.urls(),
- date: Thing.common.simpleDate(),
- trackArtDate: Thing.common.simpleDate(),
- dateAddedToWiki: Thing.common.simpleDate(),
+ date: Thing.common.simpleDate(),
+ trackArtDate: Thing.common.simpleDate(),
+ dateAddedToWiki: Thing.common.simpleDate(),
- coverArtDate: {
- flags: {update: true, expose: true},
+ coverArtDate: {
+ flags: { update: true, expose: true },
- update: {validate: isDate},
+ update: { validate: isDate },
- expose: {
- dependencies: ['date'],
- transform: (coverArtDate, { date }) => coverArtDate ?? date ?? null
- }
+ expose: {
+ dependencies: ["date"],
+ transform: (coverArtDate, { date }) => coverArtDate ?? date ?? null,
},
+ },
- artistContribsByRef: Thing.common.contribsByRef(),
- coverArtistContribsByRef: Thing.common.contribsByRef(),
- trackCoverArtistContribsByRef: Thing.common.contribsByRef(),
- wallpaperArtistContribsByRef: Thing.common.contribsByRef(),
- bannerArtistContribsByRef: Thing.common.contribsByRef(),
+ artistContribsByRef: Thing.common.contribsByRef(),
+ coverArtistContribsByRef: Thing.common.contribsByRef(),
+ trackCoverArtistContribsByRef: Thing.common.contribsByRef(),
+ wallpaperArtistContribsByRef: Thing.common.contribsByRef(),
+ bannerArtistContribsByRef: Thing.common.contribsByRef(),
- groupsByRef: Thing.common.referenceList(Group),
- artTagsByRef: Thing.common.referenceList(ArtTag),
+ groupsByRef: Thing.common.referenceList(Group),
+ artTagsByRef: Thing.common.referenceList(ArtTag),
- trackGroups: {
- flags: {update: true, expose: true},
+ trackGroups: {
+ flags: { update: true, expose: true },
- update: {
- validate: validateArrayItems(validateInstanceOf(TrackGroup))
- }
+ update: {
+ validate: validateArrayItems(validateInstanceOf(TrackGroup)),
},
+ },
- coverArtFileExtension: Thing.common.fileExtension('jpg'),
- trackCoverArtFileExtension: Thing.common.fileExtension('jpg'),
+ coverArtFileExtension: Thing.common.fileExtension("jpg"),
+ trackCoverArtFileExtension: Thing.common.fileExtension("jpg"),
- wallpaperStyle: Thing.common.simpleString(),
- wallpaperFileExtension: Thing.common.fileExtension('jpg'),
+ wallpaperStyle: Thing.common.simpleString(),
+ wallpaperFileExtension: Thing.common.fileExtension("jpg"),
- bannerStyle: Thing.common.simpleString(),
- bannerFileExtension: Thing.common.fileExtension('jpg'),
- bannerDimensions: {
- flags: {update: true, expose: true},
- update: {validate: isDimensions}
- },
+ bannerStyle: Thing.common.simpleString(),
+ bannerFileExtension: Thing.common.fileExtension("jpg"),
+ bannerDimensions: {
+ flags: { update: true, expose: true },
+ update: { validate: isDimensions },
+ },
- hasCoverArt: Thing.common.flag(true),
- hasTrackArt: Thing.common.flag(true),
- hasTrackNumbers: Thing.common.flag(true),
- isMajorRelease: Thing.common.flag(false),
- isListedOnHomepage: Thing.common.flag(true),
+ hasCoverArt: Thing.common.flag(true),
+ hasTrackArt: Thing.common.flag(true),
+ hasTrackNumbers: Thing.common.flag(true),
+ isMajorRelease: Thing.common.flag(false),
+ isListedOnHomepage: Thing.common.flag(true),
- commentary: Thing.common.commentary(),
- additionalFiles: Thing.common.additionalFiles(),
+ commentary: Thing.common.commentary(),
+ additionalFiles: Thing.common.additionalFiles(),
- // Update only
+ // Update only
- artistData: Thing.common.wikiData(Artist),
- artTagData: Thing.common.wikiData(ArtTag),
- groupData: Thing.common.wikiData(Group),
- trackData: Thing.common.wikiData(Track),
+ artistData: Thing.common.wikiData(Artist),
+ artTagData: Thing.common.wikiData(ArtTag),
+ groupData: Thing.common.wikiData(Group),
+ trackData: Thing.common.wikiData(Track),
- // Expose only
+ // Expose only
- artistContribs: Thing.common.dynamicContribs('artistContribsByRef'),
- coverArtistContribs: Thing.common.dynamicContribs('coverArtistContribsByRef'),
- trackCoverArtistContribs: Thing.common.dynamicContribs('trackCoverArtistContribsByRef'),
- wallpaperArtistContribs: Thing.common.dynamicContribs('wallpaperArtistContribsByRef'),
- bannerArtistContribs: Thing.common.dynamicContribs('bannerArtistContribsByRef'),
+ artistContribs: Thing.common.dynamicContribs("artistContribsByRef"),
+ coverArtistContribs: Thing.common.dynamicContribs("coverArtistContribsByRef"),
+ trackCoverArtistContribs: Thing.common.dynamicContribs(
+ "trackCoverArtistContribsByRef"
+ ),
+ wallpaperArtistContribs: Thing.common.dynamicContribs(
+ "wallpaperArtistContribsByRef"
+ ),
+ bannerArtistContribs: Thing.common.dynamicContribs(
+ "bannerArtistContribsByRef"
+ ),
- commentatorArtists: Thing.common.commentatorArtists(),
+ commentatorArtists: Thing.common.commentatorArtists(),
- tracks: {
- flags: {expose: true},
+ tracks: {
+ flags: { expose: true },
- expose: {
- dependencies: ['trackGroups', 'trackData'],
- compute: ({ trackGroups, trackData }) => (
- (trackGroups && trackData
- ? (trackGroups
- .flatMap(group => group.tracksByRef ?? [])
- .map(ref => find.track(ref, trackData, {mode: 'quiet'}))
- .filter(Boolean))
- : [])
- )
- }
- },
-
- groups: Thing.common.dynamicThingsFromReferenceList('groupsByRef', 'groupData', find.group),
-
- artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag),
+ expose: {
+ dependencies: ["trackGroups", "trackData"],
+ compute: ({ trackGroups, trackData }) =>
+ trackGroups && trackData
+ ? trackGroups
+ .flatMap((group) => group.tracksByRef ?? [])
+ .map((ref) => find.track(ref, trackData, { mode: "quiet" }))
+ .filter(Boolean)
+ : [],
+ },
+ },
+
+ groups: Thing.common.dynamicThingsFromReferenceList(
+ "groupsByRef",
+ "groupData",
+ find.group
+ ),
+
+ artTags: Thing.common.dynamicThingsFromReferenceList(
+ "artTagsByRef",
+ "artTagData",
+ find.artTag
+ ),
};
Album[S.serializeDescriptors] = {
- name: S.id,
- color: S.id,
- directory: S.id,
- urls: S.id,
-
- date: S.id,
- coverArtDate: S.id,
- trackArtDate: S.id,
- dateAddedToWiki: S.id,
-
- artistContribs: S.toContribRefs,
- coverArtistContribs: S.toContribRefs,
- trackCoverArtistContribs: S.toContribRefs,
- wallpaperArtistContribs: S.toContribRefs,
- bannerArtistContribs: S.toContribRefs,
-
- coverArtFileExtension: S.id,
- trackCoverArtFileExtension: S.id,
- wallpaperStyle: S.id,
- wallpaperFileExtension: S.id,
- bannerStyle: S.id,
- bannerFileExtension: S.id,
- bannerDimensions: S.id,
-
- hasTrackArt: S.id,
- isMajorRelease: S.id,
- isListedOnHomepage: S.id,
-
- commentary: S.id,
- additionalFiles: S.id,
-
- tracks: S.toRefs,
- groups: S.toRefs,
- artTags: S.toRefs,
- commentatorArtists: S.toRefs,
+ name: S.id,
+ color: S.id,
+ directory: S.id,
+ urls: S.id,
+
+ date: S.id,
+ coverArtDate: S.id,
+ trackArtDate: S.id,
+ dateAddedToWiki: S.id,
+
+ artistContribs: S.toContribRefs,
+ coverArtistContribs: S.toContribRefs,
+ trackCoverArtistContribs: S.toContribRefs,
+ wallpaperArtistContribs: S.toContribRefs,
+ bannerArtistContribs: S.toContribRefs,
+
+ coverArtFileExtension: S.id,
+ trackCoverArtFileExtension: S.id,
+ wallpaperStyle: S.id,
+ wallpaperFileExtension: S.id,
+ bannerStyle: S.id,
+ bannerFileExtension: S.id,
+ bannerDimensions: S.id,
+
+ hasTrackArt: S.id,
+ isMajorRelease: S.id,
+ isListedOnHomepage: S.id,
+
+ commentary: S.id,
+ additionalFiles: S.id,
+
+ tracks: S.toRefs,
+ groups: S.toRefs,
+ artTags: S.toRefs,
+ commentatorArtists: S.toRefs,
};
TrackGroup.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Track Group'),
+ name: Thing.common.name("Unnamed Track Group"),
- color: {
- flags: {update: true, expose: true},
+ color: {
+ flags: { update: true, expose: true },
- update: {validate: isColor},
+ update: { validate: isColor },
- expose: {
- dependencies: ['album'],
+ expose: {
+ dependencies: ["album"],
- transform(color, { album }) {
- return color ?? album?.color ?? null;
- }
- }
+ transform(color, { album }) {
+ return color ?? album?.color ?? null;
+ },
},
+ },
- dateOriginallyReleased: Thing.common.simpleDate(),
+ dateOriginallyReleased: Thing.common.simpleDate(),
- tracksByRef: Thing.common.referenceList(Track),
+ tracksByRef: Thing.common.referenceList(Track),
- isDefaultTrackGroup: Thing.common.flag(false),
+ isDefaultTrackGroup: Thing.common.flag(false),
- // Update only
+ // Update only
- album: {
- flags: {update: true},
- update: {validate: validateInstanceOf(Album)}
- },
+ album: {
+ flags: { update: true },
+ update: { validate: validateInstanceOf(Album) },
+ },
- trackData: Thing.common.wikiData(Track),
+ trackData: Thing.common.wikiData(Track),
- // Expose only
+ // Expose only
- tracks: {
- flags: {expose: true},
+ tracks: {
+ flags: { expose: true },
- expose: {
- dependencies: ['tracksByRef', 'trackData'],
- compute: ({ tracksByRef, trackData }) => (
- (tracksByRef && trackData
- ? (tracksByRef
- .map(ref => find.track(ref, trackData))
- .filter(Boolean))
- : [])
- )
- }
+ expose: {
+ dependencies: ["tracksByRef", "trackData"],
+ compute: ({ tracksByRef, trackData }) =>
+ tracksByRef && trackData
+ ? tracksByRef.map((ref) => find.track(ref, trackData)).filter(Boolean)
+ : [],
},
+ },
- startIndex: {
- flags: {expose: true},
+ startIndex: {
+ flags: { expose: true },
- expose: {
- dependencies: ['album'],
- compute: ({ album, [TrackGroup.instance]: trackGroup }) => (album.trackGroups
- .slice(0, album.trackGroups.indexOf(trackGroup))
- .reduce((acc, tg) => acc + tg.tracks.length, 0))
- }
+ expose: {
+ dependencies: ["album"],
+ compute: ({ album, [TrackGroup.instance]: trackGroup }) =>
+ album.trackGroups
+ .slice(0, album.trackGroups.indexOf(trackGroup))
+ .reduce((acc, tg) => acc + tg.tracks.length, 0),
},
+ },
};
// -> Track
@@ -665,1059 +694,1191 @@ TrackGroup.propertyDescriptors = {
// several places. Ideally it wouldn't be - we'd just reuse the `album` property
// - but support for that hasn't been coded yet :P
Track.findAlbum = (track, albumData) => {
- return albumData?.find(album => album.tracks.includes(track));
+ return albumData?.find((album) => album.tracks.includes(track));
};
// Another reused utility function. This one's logic is a bit more complicated.
-Track.hasCoverArt = (track, albumData, coverArtistContribsByRef, hasCoverArt) => {
- return (
- hasCoverArt ??
- (coverArtistContribsByRef?.length > 0 || null) ??
- Track.findAlbum(track, albumData)?.hasTrackArt ??
- true);
+Track.hasCoverArt = (
+ track,
+ albumData,
+ coverArtistContribsByRef,
+ hasCoverArt
+) => {
+ return (
+ hasCoverArt ??
+ (coverArtistContribsByRef?.length > 0 || null) ??
+ Track.findAlbum(track, albumData)?.hasTrackArt ??
+ true
+ );
};
Track.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Track'),
- directory: Thing.common.directory(),
+ name: Thing.common.name("Unnamed Track"),
+ directory: Thing.common.directory(),
- duration: {
- flags: {update: true, expose: true},
- update: {validate: isDuration}
- },
+ duration: {
+ flags: { update: true, expose: true },
+ update: { validate: isDuration },
+ },
- urls: Thing.common.urls(),
- dateFirstReleased: Thing.common.simpleDate(),
+ urls: Thing.common.urls(),
+ dateFirstReleased: Thing.common.simpleDate(),
- hasURLs: Thing.common.flag(true),
+ hasURLs: Thing.common.flag(true),
- artistContribsByRef: Thing.common.contribsByRef(),
- contributorContribsByRef: Thing.common.contribsByRef(),
- coverArtistContribsByRef: Thing.common.contribsByRef(),
+ artistContribsByRef: Thing.common.contribsByRef(),
+ contributorContribsByRef: Thing.common.contribsByRef(),
+ coverArtistContribsByRef: Thing.common.contribsByRef(),
- referencedTracksByRef: Thing.common.referenceList(Track),
- artTagsByRef: Thing.common.referenceList(ArtTag),
+ referencedTracksByRef: Thing.common.referenceList(Track),
+ artTagsByRef: Thing.common.referenceList(ArtTag),
- hasCoverArt: {
- flags: {update: true, expose: true},
+ hasCoverArt: {
+ flags: { update: true, expose: true },
- update: {validate: isBoolean},
+ update: { validate: isBoolean },
- expose: {
- dependencies: ['albumData', 'coverArtistContribsByRef'],
- transform: (hasCoverArt, { albumData, coverArtistContribsByRef, [Track.instance]: track }) => (
- Track.hasCoverArt(track, albumData, coverArtistContribsByRef, hasCoverArt))
- }
+ expose: {
+ dependencies: ["albumData", "coverArtistContribsByRef"],
+ transform: (
+ hasCoverArt,
+ { albumData, coverArtistContribsByRef, [Track.instance]: track }
+ ) =>
+ Track.hasCoverArt(
+ track,
+ albumData,
+ coverArtistContribsByRef,
+ hasCoverArt
+ ),
},
+ },
- coverArtFileExtension: {
- flags: {update: true, expose: true},
+ coverArtFileExtension: {
+ flags: { update: true, expose: true },
- update: {validate: isFileExtension},
+ update: { validate: isFileExtension },
- expose: {
- dependencies: ['albumData', 'coverArtistContribsByRef'],
- transform: (coverArtFileExtension, { albumData, coverArtistContribsByRef, hasCoverArt, [Track.instance]: track }) => (
- coverArtFileExtension ??
- (Track.hasCoverArt(track, albumData, coverArtistContribsByRef, hasCoverArt)
- ? Track.findAlbum(track, albumData)?.trackCoverArtFileExtension
- : Track.findAlbum(track, albumData)?.coverArtFileExtension) ??
- 'jpg')
+ expose: {
+ dependencies: ["albumData", "coverArtistContribsByRef"],
+ transform: (
+ coverArtFileExtension,
+ {
+ albumData,
+ coverArtistContribsByRef,
+ hasCoverArt,
+ [Track.instance]: track,
}
+ ) =>
+ coverArtFileExtension ??
+ (Track.hasCoverArt(
+ track,
+ albumData,
+ coverArtistContribsByRef,
+ hasCoverArt
+ )
+ ? Track.findAlbum(track, albumData)?.trackCoverArtFileExtension
+ : Track.findAlbum(track, albumData)?.coverArtFileExtension) ??
+ "jpg",
},
+ },
- // Previously known as: (track).aka
- originalReleaseTrackByRef: Thing.common.singleReference(Track),
+ // Previously known as: (track).aka
+ originalReleaseTrackByRef: Thing.common.singleReference(Track),
- dataSourceAlbumByRef: Thing.common.singleReference(Album),
+ dataSourceAlbumByRef: Thing.common.singleReference(Album),
- commentary: Thing.common.commentary(),
- lyrics: Thing.common.simpleString(),
- additionalFiles: Thing.common.additionalFiles(),
+ commentary: Thing.common.commentary(),
+ lyrics: Thing.common.simpleString(),
+ additionalFiles: Thing.common.additionalFiles(),
- // Update only
+ // Update only
- albumData: Thing.common.wikiData(Album),
- artistData: Thing.common.wikiData(Artist),
- artTagData: Thing.common.wikiData(ArtTag),
- flashData: Thing.common.wikiData(Flash),
- trackData: Thing.common.wikiData(Track),
+ albumData: Thing.common.wikiData(Album),
+ artistData: Thing.common.wikiData(Artist),
+ artTagData: Thing.common.wikiData(ArtTag),
+ flashData: Thing.common.wikiData(Flash),
+ trackData: Thing.common.wikiData(Track),
- // Expose only
+ // Expose only
- commentatorArtists: Thing.common.commentatorArtists(),
+ commentatorArtists: Thing.common.commentatorArtists(),
- album: {
- flags: {expose: true},
+ album: {
+ flags: { expose: true },
- expose: {
- dependencies: ['albumData'],
- compute: ({ [Track.instance]: track, albumData }) => (
- albumData?.find(album => album.tracks.includes(track)) ?? null)
- }
- },
+ expose: {
+ dependencies: ["albumData"],
+ compute: ({ [Track.instance]: track, albumData }) =>
+ albumData?.find((album) => album.tracks.includes(track)) ?? null,
+ },
+ },
+
+ // Note - this is an internal property used only to help identify a track.
+ // It should not be assumed in general that the album and dataSourceAlbum match
+ // (i.e. a track may dynamically be moved from one album to another, at
+ // which point dataSourceAlbum refers to where it was originally from, and is
+ // not generally relevant information). It's also not guaranteed that
+ // dataSourceAlbum is available (depending on the Track creator to optionally
+ // provide dataSourceAlbumByRef).
+ dataSourceAlbum: Thing.common.dynamicThingFromSingleReference(
+ "dataSourceAlbumByRef",
+ "albumData",
+ find.album
+ ),
+
+ date: {
+ flags: { expose: true },
- // Note - this is an internal property used only to help identify a track.
- // It should not be assumed in general that the album and dataSourceAlbum match
- // (i.e. a track may dynamically be moved from one album to another, at
- // which point dataSourceAlbum refers to where it was originally from, and is
- // not generally relevant information). It's also not guaranteed that
- // dataSourceAlbum is available (depending on the Track creator to optionally
- // provide dataSourceAlbumByRef).
- dataSourceAlbum: Thing.common.dynamicThingFromSingleReference('dataSourceAlbumByRef', 'albumData', find.album),
-
- date: {
- flags: {expose: true},
-
- expose: {
- dependencies: ['albumData', 'dateFirstReleased'],
- compute: ({ albumData, dateFirstReleased, [Track.instance]: track }) => (
- dateFirstReleased ??
- Track.findAlbum(track, albumData)?.date ??
- null
- )
- }
+ expose: {
+ dependencies: ["albumData", "dateFirstReleased"],
+ compute: ({ albumData, dateFirstReleased, [Track.instance]: track }) =>
+ dateFirstReleased ?? Track.findAlbum(track, albumData)?.date ?? null,
},
+ },
- color: {
- flags: {expose: true},
+ color: {
+ flags: { expose: true },
- expose: {
- dependencies: ['albumData'],
+ expose: {
+ dependencies: ["albumData"],
- compute: ({ albumData, [Track.instance]: track }) => (
- (Track.findAlbum(track, albumData)?.trackGroups
- .find(tg => tg.tracks.includes(track))?.color)
- ?? null
- )
- }
+ compute: ({ albumData, [Track.instance]: track }) =>
+ Track.findAlbum(track, albumData)?.trackGroups.find((tg) =>
+ tg.tracks.includes(track)
+ )?.color ?? null,
},
+ },
- coverArtDate: {
- flags: {update: true, expose: true},
+ coverArtDate: {
+ flags: { update: true, expose: true },
- update: {validate: isDate},
+ update: { validate: isDate },
- expose: {
- dependencies: ['albumData', 'dateFirstReleased'],
- transform: (coverArtDate, { albumData, dateFirstReleased, [Track.instance]: track }) => (
- coverArtDate ??
- dateFirstReleased ??
- Track.findAlbum(track, albumData)?.trackArtDate ??
- Track.findAlbum(track, albumData)?.date ??
- null
- )
- }
- },
-
- originalReleaseTrack: Thing.common.dynamicThingFromSingleReference('originalReleaseTrackByRef', 'trackData', find.track),
-
- otherReleases: {
- flags: {expose: true},
-
- expose: {
- dependencies: ['originalReleaseTrackByRef', 'trackData'],
-
- compute: ({ originalReleaseTrackByRef: t1origRef, trackData, [Track.instance]: t1 }) => {
- if (!trackData) {
- return [];
- }
-
- const t1orig = find.track(t1origRef, trackData);
-
- return [
- t1orig,
- ...trackData.filter(t2 => {
- const { originalReleaseTrack: t2orig } = t2;
- return (
- t2 !== t1 &&
- t2orig &&
- (t2orig === t1orig || t2orig === t1)
- );
- })
- ].filter(Boolean);
- }
- }
- },
+ expose: {
+ dependencies: ["albumData", "dateFirstReleased"],
+ transform: (
+ coverArtDate,
+ { albumData, dateFirstReleased, [Track.instance]: track }
+ ) =>
+ coverArtDate ??
+ dateFirstReleased ??
+ Track.findAlbum(track, albumData)?.trackArtDate ??
+ Track.findAlbum(track, albumData)?.date ??
+ null,
+ },
+ },
+
+ originalReleaseTrack: Thing.common.dynamicThingFromSingleReference(
+ "originalReleaseTrackByRef",
+ "trackData",
+ find.track
+ ),
+
+ otherReleases: {
+ flags: { expose: true },
- // Previously known as: (track).artists
- artistContribs: Thing.common.dynamicInheritContribs('artistContribsByRef', 'artistContribsByRef', 'albumData', Track.findAlbum),
-
- // Previously known as: (track).contributors
- contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'),
-
- // Previously known as: (track).coverArtists
- coverArtistContribs: Thing.common.dynamicInheritContribs('coverArtistContribsByRef', 'trackCoverArtistContribsByRef', 'albumData', Track.findAlbum),
-
- // Previously known as: (track).references
- referencedTracks: Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track),
-
- // Specifically exclude re-releases from this list - while it's useful to
- // get from a re-release to the tracks it references, re-releases aren't
- // generally relevant from the perspective of the tracks being referenced.
- // Filtering them from data here hides them from the corresponding field
- // on the site (obviously), and has the bonus of not counting them when
- // counting the number of times a track has been referenced, for use in
- // the "Tracks - by Times Referenced" listing page (or other data
- // processing).
- referencedByTracks: {
- flags: {expose: true},
-
- expose: {
- dependencies: ['trackData'],
-
- compute: ({ trackData, [Track.instance]: track }) => (trackData
- ? (trackData
- .filter(t => !t.originalReleaseTrack)
- .filter(t => t.referencedTracks?.includes(track)))
- : [])
+ expose: {
+ dependencies: ["originalReleaseTrackByRef", "trackData"],
+
+ compute: ({
+ originalReleaseTrackByRef: t1origRef,
+ trackData,
+ [Track.instance]: t1,
+ }) => {
+ if (!trackData) {
+ return [];
}
- },
- // Previously known as: (track).flashes
- featuredInFlashes: Thing.common.reverseReferenceList('flashData', 'featuredTracks'),
+ const t1orig = find.track(t1origRef, trackData);
+
+ return [
+ t1orig,
+ ...trackData.filter((t2) => {
+ const { originalReleaseTrack: t2orig } = t2;
+ return t2 !== t1 && t2orig && (t2orig === t1orig || t2orig === t1);
+ }),
+ ].filter(Boolean);
+ },
+ },
+ },
+
+ // Previously known as: (track).artists
+ artistContribs: Thing.common.dynamicInheritContribs(
+ "artistContribsByRef",
+ "artistContribsByRef",
+ "albumData",
+ Track.findAlbum
+ ),
+
+ // Previously known as: (track).contributors
+ contributorContribs: Thing.common.dynamicContribs("contributorContribsByRef"),
+
+ // Previously known as: (track).coverArtists
+ coverArtistContribs: Thing.common.dynamicInheritContribs(
+ "coverArtistContribsByRef",
+ "trackCoverArtistContribsByRef",
+ "albumData",
+ Track.findAlbum
+ ),
+
+ // Previously known as: (track).references
+ referencedTracks: Thing.common.dynamicThingsFromReferenceList(
+ "referencedTracksByRef",
+ "trackData",
+ find.track
+ ),
+
+ // Specifically exclude re-releases from this list - while it's useful to
+ // get from a re-release to the tracks it references, re-releases aren't
+ // generally relevant from the perspective of the tracks being referenced.
+ // Filtering them from data here hides them from the corresponding field
+ // on the site (obviously), and has the bonus of not counting them when
+ // counting the number of times a track has been referenced, for use in
+ // the "Tracks - by Times Referenced" listing page (or other data
+ // processing).
+ referencedByTracks: {
+ flags: { expose: true },
- artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag),
+ expose: {
+ dependencies: ["trackData"],
+
+ compute: ({ trackData, [Track.instance]: track }) =>
+ trackData
+ ? trackData
+ .filter((t) => !t.originalReleaseTrack)
+ .filter((t) => t.referencedTracks?.includes(track))
+ : [],
+ },
+ },
+
+ // Previously known as: (track).flashes
+ featuredInFlashes: Thing.common.reverseReferenceList(
+ "flashData",
+ "featuredTracks"
+ ),
+
+ artTags: Thing.common.dynamicThingsFromReferenceList(
+ "artTagsByRef",
+ "artTagData",
+ find.artTag
+ ),
};
-Track.prototype[inspect.custom] = function() {
- const base = Thing.prototype[inspect.custom].apply(this);
+Track.prototype[inspect.custom] = function () {
+ const base = Thing.prototype[inspect.custom].apply(this);
- const { album, dataSourceAlbum } = this;
- const albumName = (album ? album.name : dataSourceAlbum?.name);
- const albumIndex = albumName && (album ? album.tracks.indexOf(this) : dataSourceAlbum.tracks.indexOf(this));
- const trackNum = (albumIndex === -1 ? '#?' : `#${albumIndex + 1}`);
+ const { album, dataSourceAlbum } = this;
+ const albumName = album ? album.name : dataSourceAlbum?.name;
+ const albumIndex =
+ albumName &&
+ (album ? album.tracks.indexOf(this) : dataSourceAlbum.tracks.indexOf(this));
+ const trackNum = albumIndex === -1 ? "#?" : `#${albumIndex + 1}`;
- return (albumName
- ? base + ` (${color.yellow(trackNum)} in ${color.green(albumName)})`
- : base);
+ return albumName
+ ? base + ` (${color.yellow(trackNum)} in ${color.green(albumName)})`
+ : base;
};
// -> Artist
Artist.filterByContrib = (thingDataProperty, contribsProperty) => ({
- flags: {expose: true},
+ flags: { expose: true },
- expose: {
- dependencies: [thingDataProperty],
+ expose: {
+ dependencies: [thingDataProperty],
- compute: ({ [thingDataProperty]: thingData, [Artist.instance]: artist }) => (
- thingData?.filter(({ [contribsProperty]: contribs }) => (
- contribs?.some(contrib => contrib.who === artist))))
- }
+ compute: ({ [thingDataProperty]: thingData, [Artist.instance]: artist }) =>
+ thingData?.filter(({ [contribsProperty]: contribs }) =>
+ contribs?.some((contrib) => contrib.who === artist)
+ ),
+ },
});
Artist.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Artist'),
- directory: Thing.common.directory(),
- urls: Thing.common.urls(),
- contextNotes: Thing.common.simpleString(),
+ name: Thing.common.name("Unnamed Artist"),
+ directory: Thing.common.directory(),
+ urls: Thing.common.urls(),
+ contextNotes: Thing.common.simpleString(),
- hasAvatar: Thing.common.flag(false),
- avatarFileExtension: Thing.common.fileExtension('jpg'),
+ hasAvatar: Thing.common.flag(false),
+ avatarFileExtension: Thing.common.fileExtension("jpg"),
- aliasNames: {
- flags: {update: true, expose: true},
- update: {
- validate: validateArrayItems(isName)
- }
+ aliasNames: {
+ flags: { update: true, expose: true },
+ update: {
+ validate: validateArrayItems(isName),
},
+ },
- isAlias: Thing.common.flag(),
- aliasedArtistRef: Thing.common.singleReference(Artist),
-
- // Update only
-
- albumData: Thing.common.wikiData(Album),
- artistData: Thing.common.wikiData(Artist),
- flashData: Thing.common.wikiData(Flash),
- trackData: Thing.common.wikiData(Track),
-
- // Expose only
-
- aliasedArtist: {
- flags: {expose: true},
-
- expose: {
- dependencies: ['artistData', 'aliasedArtistRef'],
- compute: ({ artistData, aliasedArtistRef }) => (
- (aliasedArtistRef && artistData
- ? find.artist(aliasedArtistRef, artistData, {mode: 'quiet'})
- : null)
- )
- }
- },
+ isAlias: Thing.common.flag(),
+ aliasedArtistRef: Thing.common.singleReference(Artist),
- tracksAsArtist: Artist.filterByContrib('trackData', 'artistContribs'),
- tracksAsContributor: Artist.filterByContrib('trackData', 'contributorContribs'),
- tracksAsCoverArtist: Artist.filterByContrib('trackData', 'coverArtistContribs'),
+ // Update only
- tracksAsAny: {
- flags: {expose: true},
+ albumData: Thing.common.wikiData(Album),
+ artistData: Thing.common.wikiData(Artist),
+ flashData: Thing.common.wikiData(Flash),
+ trackData: Thing.common.wikiData(Track),
- expose: {
- dependencies: ['trackData'],
+ // Expose only
- compute: ({ trackData, [Artist.instance]: artist }) => (
- trackData?.filter(track => (
- [
- ...track.artistContribs,
- ...track.contributorContribs,
- ...track.coverArtistContribs
- ].some(({ who }) => who === artist))))
- }
- },
+ aliasedArtist: {
+ flags: { expose: true },
- tracksAsCommentator: {
- flags: {expose: true},
+ expose: {
+ dependencies: ["artistData", "aliasedArtistRef"],
+ compute: ({ artistData, aliasedArtistRef }) =>
+ aliasedArtistRef && artistData
+ ? find.artist(aliasedArtistRef, artistData, { mode: "quiet" })
+ : null,
+ },
+ },
+
+ tracksAsArtist: Artist.filterByContrib("trackData", "artistContribs"),
+ tracksAsContributor: Artist.filterByContrib(
+ "trackData",
+ "contributorContribs"
+ ),
+ tracksAsCoverArtist: Artist.filterByContrib(
+ "trackData",
+ "coverArtistContribs"
+ ),
+
+ tracksAsAny: {
+ flags: { expose: true },
- expose: {
- dependencies: ['trackData'],
+ expose: {
+ dependencies: ["trackData"],
- compute: ({ trackData, [Artist.instance]: artist }) => (
- trackData.filter(({ commentatorArtists }) => commentatorArtists?.includes(artist)))
- }
+ compute: ({ trackData, [Artist.instance]: artist }) =>
+ trackData?.filter((track) =>
+ [
+ ...track.artistContribs,
+ ...track.contributorContribs,
+ ...track.coverArtistContribs,
+ ].some(({ who }) => who === artist)
+ ),
},
+ },
- albumsAsAlbumArtist: Artist.filterByContrib('albumData', 'artistContribs'),
- albumsAsCoverArtist: Artist.filterByContrib('albumData', 'coverArtistContribs'),
- albumsAsWallpaperArtist: Artist.filterByContrib('albumData', 'wallpaperArtistContribs'),
- albumsAsBannerArtist: Artist.filterByContrib('albumData', 'bannerArtistContribs'),
+ tracksAsCommentator: {
+ flags: { expose: true },
- albumsAsCommentator: {
- flags: {expose: true},
+ expose: {
+ dependencies: ["trackData"],
+
+ compute: ({ trackData, [Artist.instance]: artist }) =>
+ trackData.filter(({ commentatorArtists }) =>
+ commentatorArtists?.includes(artist)
+ ),
+ },
+ },
+
+ albumsAsAlbumArtist: Artist.filterByContrib("albumData", "artistContribs"),
+ albumsAsCoverArtist: Artist.filterByContrib(
+ "albumData",
+ "coverArtistContribs"
+ ),
+ albumsAsWallpaperArtist: Artist.filterByContrib(
+ "albumData",
+ "wallpaperArtistContribs"
+ ),
+ albumsAsBannerArtist: Artist.filterByContrib(
+ "albumData",
+ "bannerArtistContribs"
+ ),
+
+ albumsAsCommentator: {
+ flags: { expose: true },
- expose: {
- dependencies: ['albumData'],
+ expose: {
+ dependencies: ["albumData"],
- compute: ({ albumData, [Artist.instance]: artist }) => (
- albumData.filter(({ commentatorArtists }) => commentatorArtists?.includes(artist)))
- }
+ compute: ({ albumData, [Artist.instance]: artist }) =>
+ albumData.filter(({ commentatorArtists }) =>
+ commentatorArtists?.includes(artist)
+ ),
},
+ },
- flashesAsContributor: Artist.filterByContrib('flashData', 'contributorContribs'),
+ flashesAsContributor: Artist.filterByContrib(
+ "flashData",
+ "contributorContribs"
+ ),
};
Artist[S.serializeDescriptors] = {
- name: S.id,
- directory: S.id,
- urls: S.id,
- contextNotes: S.id,
+ name: S.id,
+ directory: S.id,
+ urls: S.id,
+ contextNotes: S.id,
- hasAvatar: S.id,
- avatarFileExtension: S.id,
+ hasAvatar: S.id,
+ avatarFileExtension: S.id,
- aliasNames: S.id,
+ aliasNames: S.id,
- tracksAsArtist: S.toRefs,
- tracksAsContributor: S.toRefs,
- tracksAsCoverArtist: S.toRefs,
- tracksAsCommentator: S.toRefs,
+ tracksAsArtist: S.toRefs,
+ tracksAsContributor: S.toRefs,
+ tracksAsCoverArtist: S.toRefs,
+ tracksAsCommentator: S.toRefs,
- albumsAsAlbumArtist: S.toRefs,
- albumsAsCoverArtist: S.toRefs,
- albumsAsWallpaperArtist: S.toRefs,
- albumsAsBannerArtist: S.toRefs,
- albumsAsCommentator: S.toRefs,
+ albumsAsAlbumArtist: S.toRefs,
+ albumsAsCoverArtist: S.toRefs,
+ albumsAsWallpaperArtist: S.toRefs,
+ albumsAsBannerArtist: S.toRefs,
+ albumsAsCommentator: S.toRefs,
- flashesAsContributor: S.toRefs,
+ flashesAsContributor: S.toRefs,
};
// -> Group
Group.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Group'),
- directory: Thing.common.directory(),
+ name: Thing.common.name("Unnamed Group"),
+ directory: Thing.common.directory(),
- description: Thing.common.simpleString(),
+ description: Thing.common.simpleString(),
- urls: Thing.common.urls(),
+ urls: Thing.common.urls(),
- // Update only
+ // Update only
- albumData: Thing.common.wikiData(Album),
- groupCategoryData: Thing.common.wikiData(GroupCategory),
+ albumData: Thing.common.wikiData(Album),
+ groupCategoryData: Thing.common.wikiData(GroupCategory),
- // Expose only
+ // Expose only
- descriptionShort: {
- flags: {expose: true},
+ descriptionShort: {
+ flags: { expose: true },
- expose: {
- dependencies: ['description'],
- compute: ({ description }) => description.split(' ')[0]
- }
+ expose: {
+ dependencies: ["description"],
+ compute: ({ description }) => description.split(' ')[0],
},
+ },
- albums: {
- flags: {expose: true},
+ albums: {
+ flags: { expose: true },
- expose: {
- dependencies: ['albumData'],
- compute: ({ albumData, [Group.instance]: group }) => (
- albumData?.filter(album => album.groups.includes(group)) ?? [])
- }
+ expose: {
+ dependencies: ["albumData"],
+ compute: ({ albumData, [Group.instance]: group }) =>
+ albumData?.filter((album) => album.groups.includes(group)) ?? [],
},
+ },
- color: {
- flags: {expose: true},
+ color: {
+ flags: { expose: true },
- expose: {
- dependencies: ['groupCategoryData'],
+ expose: {
+ dependencies: ["groupCategoryData"],
- compute: ({ groupCategoryData, [Group.instance]: group }) => (
- groupCategoryData.find(category => category.groups.includes(group))?.color ?? null)
- }
+ compute: ({ groupCategoryData, [Group.instance]: group }) =>
+ groupCategoryData.find((category) => category.groups.includes(group))
+ ?.color ?? null,
},
+ },
- category: {
- flags: {expose: true},
+ category: {
+ flags: { expose: true },
- expose: {
- dependencies: ['groupCategoryData'],
- compute: ({ groupCategoryData, [Group.instance]: group }) => (
- groupCategoryData.find(category => category.groups.includes(group)) ?? null)
- }
+ expose: {
+ dependencies: ["groupCategoryData"],
+ compute: ({ groupCategoryData, [Group.instance]: group }) =>
+ groupCategoryData.find((category) => category.groups.includes(group)) ??
+ null,
},
+ },
};
GroupCategory.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Group Category'),
- color: Thing.common.color(),
+ name: Thing.common.name("Unnamed Group Category"),
+ color: Thing.common.color(),
- groupsByRef: Thing.common.referenceList(Group),
+ groupsByRef: Thing.common.referenceList(Group),
- // Update only
+ // Update only
- groupData: Thing.common.wikiData(Group),
+ groupData: Thing.common.wikiData(Group),
- // Expose only
+ // Expose only
- groups: Thing.common.dynamicThingsFromReferenceList('groupsByRef', 'groupData', find.group),
+ groups: Thing.common.dynamicThingsFromReferenceList(
+ "groupsByRef",
+ "groupData",
+ find.group
+ ),
};
// -> ArtTag
ArtTag.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Art Tag'),
- directory: Thing.common.directory(),
- color: Thing.common.color(),
- isContentWarning: Thing.common.flag(false),
+ name: Thing.common.name("Unnamed Art Tag"),
+ directory: Thing.common.directory(),
+ color: Thing.common.color(),
+ isContentWarning: Thing.common.flag(false),
- // Update only
+ // Update only
- albumData: Thing.common.wikiData(Album),
- trackData: Thing.common.wikiData(Track),
+ albumData: Thing.common.wikiData(Album),
+ trackData: Thing.common.wikiData(Track),
- // Expose only
+ // Expose only
- // Previously known as: (tag).things
- taggedInThings: {
- flags: {expose: true},
+ // Previously known as: (tag).things
+ taggedInThings: {
+ flags: { expose: true },
- expose: {
- dependencies: ['albumData', 'trackData'],
- compute: ({ albumData, trackData, [ArtTag.instance]: artTag }) => (
- sortAlbumsTracksChronologically(
- ([...albumData, ...trackData]
- .filter(thing => thing.artTags?.includes(artTag))),
- {getDate: o => o.coverArtDate}))
- }
- }
+ expose: {
+ dependencies: ["albumData", "trackData"],
+ compute: ({ albumData, trackData, [ArtTag.instance]: artTag }) =>
+ sortAlbumsTracksChronologically(
+ [...albumData, ...trackData].filter((thing) =>
+ thing.artTags?.includes(artTag)
+ ),
+ { getDate: (o) => o.coverArtDate }
+ ),
+ },
+ },
};
// -> NewsEntry
NewsEntry.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed News Entry'),
- directory: Thing.common.directory(),
- date: Thing.common.simpleDate(),
+ name: Thing.common.name("Unnamed News Entry"),
+ directory: Thing.common.directory(),
+ date: Thing.common.simpleDate(),
- content: Thing.common.simpleString(),
+ content: Thing.common.simpleString(),
- // Expose only
+ // Expose only
- contentShort: {
- flags: {expose: true},
+ contentShort: {
+ flags: { expose: true },
- expose: {
- dependencies: ['content'],
+ expose: {
+ dependencies: ["content"],
- compute: ({ content }) => content.split(' ')[0]
- }
+ compute: ({ content }) => content.split(' ')[0],
},
+ },
};
// -> StaticPage
StaticPage.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Static Page'),
+ name: Thing.common.name("Unnamed Static Page"),
- nameShort: {
- flags: {update: true, expose: true},
- update: {validate: isName},
+ nameShort: {
+ flags: { update: true, expose: true },
+ update: { validate: isName },
- expose: {
- dependencies: ['name'],
- transform: (value, { name }) => value ?? name
- }
+ expose: {
+ dependencies: ["name"],
+ transform: (value, { name }) => value ?? name,
},
+ },
- directory: Thing.common.directory(),
- content: Thing.common.simpleString(),
- stylesheet: Thing.common.simpleString(),
- showInNavigationBar: Thing.common.flag(true),
+ directory: Thing.common.directory(),
+ content: Thing.common.simpleString(),
+ stylesheet: Thing.common.simpleString(),
+ showInNavigationBar: Thing.common.flag(true),
};
// -> HomepageLayout
HomepageLayout.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- sidebarContent: Thing.common.simpleString(),
+ sidebarContent: Thing.common.simpleString(),
- rows: {
- flags: {update: true, expose: true},
+ rows: {
+ flags: { update: true, expose: true },
- update: {
- validate: validateArrayItems(validateInstanceOf(HomepageLayoutRow))
- }
+ update: {
+ validate: validateArrayItems(validateInstanceOf(HomepageLayoutRow)),
},
+ },
};
HomepageLayoutRow.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Homepage Row'),
+ name: Thing.common.name("Unnamed Homepage Row"),
- type: {
- flags: {update: true, expose: true},
+ type: {
+ flags: { update: true, expose: true },
- update: {
- validate(value) {
- throw new Error(`'type' property validator must be overridden`);
- }
- }
+ update: {
+ validate(value) {
+ throw new Error(`'type' property validator must be overridden`);
+ },
},
+ },
- color: Thing.common.color(),
+ color: Thing.common.color(),
- // Update only
+ // Update only
- // These aren't necessarily used by every HomepageLayoutRow subclass, but
- // for convenience of providing this data, every row accepts all wiki data
- // arrays depended upon by any subclass's behavior.
- albumData: Thing.common.wikiData(Album),
- groupData: Thing.common.wikiData(Group),
+ // These aren't necessarily used by every HomepageLayoutRow subclass, but
+ // for convenience of providing this data, every row accepts all wiki data
+ // arrays depended upon by any subclass's behavior.
+ albumData: Thing.common.wikiData(Album),
+ groupData: Thing.common.wikiData(Group),
};
HomepageLayoutAlbumsRow.propertyDescriptors = {
- ...HomepageLayoutRow.propertyDescriptors,
-
- // Update & expose
+ ...HomepageLayoutRow.propertyDescriptors,
- type: {
- flags: {update: true, expose: true},
- update: {
- validate(value) {
- if (value !== 'albums') {
- throw new TypeError(`Expected 'albums'`);
- }
+ // Update & expose
- return true;
- }
+ type: {
+ flags: { update: true, expose: true },
+ update: {
+ validate(value) {
+ if (value !== "albums") {
+ throw new TypeError(`Expected 'albums'`);
}
+
+ return true;
+ },
},
+ },
- sourceGroupByRef: Thing.common.singleReference(Group),
- sourceAlbumsByRef: Thing.common.referenceList(Album),
+ sourceGroupByRef: Thing.common.singleReference(Group),
+ sourceAlbumsByRef: Thing.common.referenceList(Album),
- countAlbumsFromGroup: {
- flags: {update: true, expose: true},
- update: {validate: isCountingNumber}
- },
+ countAlbumsFromGroup: {
+ flags: { update: true, expose: true },
+ update: { validate: isCountingNumber },
+ },
- actionLinks: {
- flags: {update: true, expose: true},
- update: {validate: validateArrayItems(isString)}
- },
+ actionLinks: {
+ flags: { update: true, expose: true },
+ update: { validate: validateArrayItems(isString) },
+ },
- // Expose only
+ // Expose only
- sourceGroup: Thing.common.dynamicThingFromSingleReference('sourceGroupByRef', 'groupData', find.group),
- sourceAlbums: Thing.common.dynamicThingsFromReferenceList('sourceAlbumsByRef', 'albumData', find.album),
+ sourceGroup: Thing.common.dynamicThingFromSingleReference(
+ "sourceGroupByRef",
+ "groupData",
+ find.group
+ ),
+ sourceAlbums: Thing.common.dynamicThingsFromReferenceList(
+ "sourceAlbumsByRef",
+ "albumData",
+ find.album
+ ),
};
// -> Flash
Flash.propertyDescriptors = {
- // Update & expose
-
- name: Thing.common.name('Unnamed Flash'),
-
- directory: {
- flags: {update: true, expose: true},
- update: {validate: isDirectory},
-
- // Flashes expose directory differently from other Things! Their
- // default directory is dependent on the page number (or ID), not
- // the name.
- expose: {
- dependencies: ['page'],
- transform(directory, { page }) {
- if (directory === null && page === null)
- return null;
- else if (directory === null)
- return page;
- else
- return directory;
- }
- }
+ // Update & expose
+
+ name: Thing.common.name("Unnamed Flash"),
+
+ directory: {
+ flags: { update: true, expose: true },
+ update: { validate: isDirectory },
+
+ // Flashes expose directory differently from other Things! Their
+ // default directory is dependent on the page number (or ID), not
+ // the name.
+ expose: {
+ dependencies: ["page"],
+ transform(directory, { page }) {
+ if (directory === null && page === null) return null;
+ else if (directory === null) return page;
+ else return directory;
+ },
},
+ },
- page: {
- flags: {update: true, expose: true},
- update: {validate: oneOf(isString, isNumber)},
+ page: {
+ flags: { update: true, expose: true },
+ update: { validate: oneOf(isString, isNumber) },
- expose: {
- transform: value => (value === null ? null : value.toString())
- }
+ expose: {
+ transform: (value) => (value === null ? null : value.toString()),
},
+ },
- date: Thing.common.simpleDate(),
+ date: Thing.common.simpleDate(),
- coverArtFileExtension: Thing.common.fileExtension('jpg'),
+ coverArtFileExtension: Thing.common.fileExtension("jpg"),
- contributorContribsByRef: Thing.common.contribsByRef(),
+ contributorContribsByRef: Thing.common.contribsByRef(),
- featuredTracksByRef: Thing.common.referenceList(Track),
+ featuredTracksByRef: Thing.common.referenceList(Track),
- urls: Thing.common.urls(),
+ urls: Thing.common.urls(),
- // Update only
+ // Update only
- artistData: Thing.common.wikiData(Artist),
- trackData: Thing.common.wikiData(Track),
- flashActData: Thing.common.wikiData(FlashAct),
+ artistData: Thing.common.wikiData(Artist),
+ trackData: Thing.common.wikiData(Track),
+ flashActData: Thing.common.wikiData(FlashAct),
- // Expose only
+ // Expose only
- contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'),
+ contributorContribs: Thing.common.dynamicContribs("contributorContribsByRef"),
- featuredTracks: Thing.common.dynamicThingsFromReferenceList('featuredTracksByRef', 'trackData', find.track),
+ featuredTracks: Thing.common.dynamicThingsFromReferenceList(
+ "featuredTracksByRef",
+ "trackData",
+ find.track
+ ),
- act: {
- flags: {expose: true},
+ act: {
+ flags: { expose: true },
- expose: {
- dependencies: ['flashActData'],
+ expose: {
+ dependencies: ["flashActData"],
- compute: ({ flashActData, [Flash.instance]: flash }) => (
- flashActData.find(act => act.flashes.includes(flash)) ?? null)
- }
+ compute: ({ flashActData, [Flash.instance]: flash }) =>
+ flashActData.find((act) => act.flashes.includes(flash)) ?? null,
},
+ },
- color: {
- flags: {expose: true},
+ color: {
+ flags: { expose: true },
- expose: {
- dependencies: ['flashActData'],
+ expose: {
+ dependencies: ["flashActData"],
- compute: ({ flashActData, [Flash.instance]: flash }) => (
- flashActData.find(act => act.flashes.includes(flash))?.color ?? null)
- }
+ compute: ({ flashActData, [Flash.instance]: flash }) =>
+ flashActData.find((act) => act.flashes.includes(flash))?.color ?? null,
},
+ },
};
Flash[S.serializeDescriptors] = {
- name: S.id,
- page: S.id,
- directory: S.id,
- date: S.id,
- contributors: S.toContribRefs,
- tracks: S.toRefs,
- urls: S.id,
- color: S.id,
+ name: S.id,
+ page: S.id,
+ directory: S.id,
+ date: S.id,
+ contributors: S.toContribRefs,
+ tracks: S.toRefs,
+ urls: S.id,
+ color: S.id,
};
FlashAct.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Flash Act'),
- color: Thing.common.color(),
- anchor: Thing.common.simpleString(),
- jump: Thing.common.simpleString(),
- jumpColor: Thing.common.color(),
+ name: Thing.common.name("Unnamed Flash Act"),
+ color: Thing.common.color(),
+ anchor: Thing.common.simpleString(),
+ jump: Thing.common.simpleString(),
+ jumpColor: Thing.common.color(),
- flashesByRef: Thing.common.referenceList(Flash),
+ flashesByRef: Thing.common.referenceList(Flash),
- // Update only
+ // Update only
- flashData: Thing.common.wikiData(Flash),
+ flashData: Thing.common.wikiData(Flash),
- // Expose only
+ // Expose only
- flashes: Thing.common.dynamicThingsFromReferenceList('flashesByRef', 'flashData', find.flash),
+ flashes: Thing.common.dynamicThingsFromReferenceList(
+ "flashesByRef",
+ "flashData",
+ find.flash
+ ),
};
// -> WikiInfo
WikiInfo.propertyDescriptors = {
- // Update & expose
+ // Update & expose
- name: Thing.common.name('Unnamed Wiki'),
+ name: Thing.common.name("Unnamed Wiki"),
- // Displayed in nav bar.
- nameShort: {
- flags: {update: true, expose: true},
- update: {validate: isName},
+ // Displayed in nav bar.
+ nameShort: {
+ flags: { update: true, expose: true },
+ update: { validate: isName },
- expose: {
- dependencies: ['name'],
- transform: (value, { name }) => value ?? name
- }
+ expose: {
+ dependencies: ["name"],
+ transform: (value, { name }) => value ?? name,
},
+ },
- color: Thing.common.color(),
+ color: Thing.common.color(),
- // One-line description used for tag.
- description: Thing.common.simpleString(),
+ // One-line description used for tag.
+ description: Thing.common.simpleString(),
- footerContent: Thing.common.simpleString(),
+ footerContent: Thing.common.simpleString(),
- defaultLanguage: {
- flags: {update: true, expose: true},
- update: {validate: isLanguageCode}
- },
+ defaultLanguage: {
+ flags: { update: true, expose: true },
+ update: { validate: isLanguageCode },
+ },
- canonicalBase: {
- flags: {update: true, expose: true},
- update: {validate: isURL}
- },
+ canonicalBase: {
+ flags: { update: true, expose: true },
+ update: { validate: isURL },
+ },
- divideTrackListsByGroupsByRef: Thing.common.referenceList(Group),
+ divideTrackListsByGroupsByRef: Thing.common.referenceList(Group),
- // Feature toggles
- enableFlashesAndGames: Thing.common.flag(false),
- enableListings: Thing.common.flag(false),
- enableNews: Thing.common.flag(false),
- enableArtTagUI: Thing.common.flag(false),
- enableGroupUI: Thing.common.flag(false),
+ // Feature toggles
+ enableFlashesAndGames: Thing.common.flag(false),
+ enableListings: Thing.common.flag(false),
+ enableNews: Thing.common.flag(false),
+ enableArtTagUI: Thing.common.flag(false),
+ enableGroupUI: Thing.common.flag(false),
- // Update only
+ // Update only
- groupData: Thing.common.wikiData(Group),
+ groupData: Thing.common.wikiData(Group),
- // Expose only
+ // Expose only
- divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList('divideTrackListsByGroupsByRef', 'groupData', find.group),
+ divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList(
+ "divideTrackListsByGroupsByRef",
+ "groupData",
+ find.group
+ ),
};
// -> Language
const intlHelper = (constructor, opts) => ({
- flags: {expose: true},
- expose: {
- dependencies: ['code', 'intlCode'],
- compute: ({ code, intlCode }) => {
- const constructCode = intlCode ?? code;
- if (!constructCode) return null;
- return Reflect.construct(constructor, [constructCode, opts]);
- }
- }
+ flags: { expose: true },
+ expose: {
+ dependencies: ["code", "intlCode"],
+ compute: ({ code, intlCode }) => {
+ const constructCode = intlCode ?? code;
+ if (!constructCode) return null;
+ return Reflect.construct(constructor, [constructCode, opts]);
+ },
+ },
});
Language.propertyDescriptors = {
- // Update & expose
-
- // General language code. This is used to identify the language distinctly
- // from other languages (similar to how "Directory" operates in many data
- // objects).
- code: {
- flags: {update: true, expose: true},
- update: {validate: isLanguageCode}
- },
-
- // Human-readable name. This should be the language's own native name, not
- // localized to any other language.
- name: Thing.common.simpleString(),
-
- // Language code specific to JavaScript's Internationalization (Intl) API.
- // Usually this will be the same as the language's general code, but it
- // may be overridden to provide Intl constructors an alternative value.
- intlCode: {
- flags: {update: true, expose: true},
- update: {validate: isLanguageCode},
- expose: {
- dependencies: ['code'],
- transform: (intlCode, { code }) => intlCode ?? code
- }
- },
-
- // Flag which represents whether or not to hide a language from general
- // access. If a language is hidden, its portion of the website will still
- // be built (with all strings localized to the language), but it won't be
- // included in controls for switching languages or the
- // tags used for search engine optimization. This flag is intended for use
- // with languages that are currently in development and not ready for
- // formal release, or which are just kept hidden as "experimental zones"
- // for wiki development or content testing.
- hidden: Thing.common.flag(false),
-
- // Mapping of translation keys to values (strings). Generally, don't
- // access this object directly - use methods instead.
- strings: {
- flags: {update: true, expose: true},
- update: {validate: t => typeof t === 'object'},
- expose: {
- dependencies: ['inheritedStrings'],
- transform(strings, { inheritedStrings }) {
- if (strings || inheritedStrings) {
- return {...inheritedStrings ?? {}, ...strings ?? {}};
- } else {
- return null;
- }
- }
+ // Update & expose
+
+ // General language code. This is used to identify the language distinctly
+ // from other languages (similar to how "Directory" operates in many data
+ // objects).
+ code: {
+ flags: { update: true, expose: true },
+ update: { validate: isLanguageCode },
+ },
+
+ // Human-readable name. This should be the language's own native name, not
+ // localized to any other language.
+ name: Thing.common.simpleString(),
+
+ // Language code specific to JavaScript's Internationalization (Intl) API.
+ // Usually this will be the same as the language's general code, but it
+ // may be overridden to provide Intl constructors an alternative value.
+ intlCode: {
+ flags: { update: true, expose: true },
+ update: { validate: isLanguageCode },
+ expose: {
+ dependencies: ["code"],
+ transform: (intlCode, { code }) => intlCode ?? code,
+ },
+ },
+
+ // Flag which represents whether or not to hide a language from general
+ // access. If a language is hidden, its portion of the website will still
+ // be built (with all strings localized to the language), but it won't be
+ // included in controls for switching languages or the
+ // tags used for search engine optimization. This flag is intended for use
+ // with languages that are currently in development and not ready for
+ // formal release, or which are just kept hidden as "experimental zones"
+ // for wiki development or content testing.
+ hidden: Thing.common.flag(false),
+
+ // Mapping of translation keys to values (strings). Generally, don't
+ // access this object directly - use methods instead.
+ strings: {
+ flags: { update: true, expose: true },
+ update: { validate: (t) => typeof t === "object" },
+ expose: {
+ dependencies: ["inheritedStrings"],
+ transform(strings, { inheritedStrings }) {
+ if (strings || inheritedStrings) {
+ return { ...(inheritedStrings ?? {}), ...(strings ?? {}) };
+ } else {
+ return null;
}
+ },
},
+ },
- // May be provided to specify "default" strings, generally (but not
- // necessarily) inherited from another Language object.
- inheritedStrings: {
- flags: {update: true, expose: true},
- update: {validate: t => typeof t === 'object'}
- },
+ // May be provided to specify "default" strings, generally (but not
+ // necessarily) inherited from another Language object.
+ inheritedStrings: {
+ flags: { update: true, expose: true },
+ update: { validate: (t) => typeof t === "object" },
+ },
- // Update only
+ // Update only
- escapeHTML: Thing.common.externalFunction(),
+ escapeHTML: Thing.common.externalFunction(),
- // Expose only
+ // Expose only
- intl_date: intlHelper(Intl.DateTimeFormat, {full: true}),
- intl_number: intlHelper(Intl.NumberFormat),
- intl_listConjunction: intlHelper(Intl.ListFormat, {type: 'conjunction'}),
- intl_listDisjunction: intlHelper(Intl.ListFormat, {type: 'disjunction'}),
- intl_listUnit: intlHelper(Intl.ListFormat, {type: 'unit'}),
- intl_pluralCardinal: intlHelper(Intl.PluralRules, {type: 'cardinal'}),
- intl_pluralOrdinal: intlHelper(Intl.PluralRules, {type: 'ordinal'}),
+ intl_date: intlHelper(Intl.DateTimeFormat, { full: true }),
+ intl_number: intlHelper(Intl.NumberFormat),
+ intl_listConjunction: intlHelper(Intl.ListFormat, { type: "conjunction" }),
+ intl_listDisjunction: intlHelper(Intl.ListFormat, { type: "disjunction" }),
+ intl_listUnit: intlHelper(Intl.ListFormat, { type: "unit" }),
+ intl_pluralCardinal: intlHelper(Intl.PluralRules, { type: "cardinal" }),
+ intl_pluralOrdinal: intlHelper(Intl.PluralRules, { type: "ordinal" }),
- validKeys: {
- flags: {expose: true},
-
- expose: {
- dependencies: ['strings', 'inheritedStrings'],
- compute: ({ strings, inheritedStrings }) => Array.from(new Set([
- ...Object.keys(inheritedStrings ?? {}),
- ...Object.keys(strings ?? {})
- ]))
- }
- },
+ validKeys: {
+ flags: { expose: true },
- strings_htmlEscaped: {
- flags: {expose: true},
- expose: {
- dependencies: ['strings', 'inheritedStrings', 'escapeHTML'],
- compute({ strings, inheritedStrings, escapeHTML }) {
- if (!(strings || inheritedStrings) || !escapeHTML) return null;
- const allStrings = {...inheritedStrings ?? {}, ...strings ?? {}};
- return Object.fromEntries(Object.entries(allStrings)
- .map(([ k, v ]) => [k, escapeHTML(v)]));
- }
- }
- },
+ expose: {
+ dependencies: ["strings", "inheritedStrings"],
+ compute: ({ strings, inheritedStrings }) =>
+ Array.from(
+ new Set([
+ ...Object.keys(inheritedStrings ?? {}),
+ ...Object.keys(strings ?? {}),
+ ])
+ ),
+ },
+ },
+
+ strings_htmlEscaped: {
+ flags: { expose: true },
+ expose: {
+ dependencies: ["strings", "inheritedStrings", "escapeHTML"],
+ compute({ strings, inheritedStrings, escapeHTML }) {
+ if (!(strings || inheritedStrings) || !escapeHTML) return null;
+ const allStrings = { ...(inheritedStrings ?? {}), ...(strings ?? {}) };
+ return Object.fromEntries(
+ Object.entries(allStrings).map(([k, v]) => [k, escapeHTML(v)])
+ );
+ },
+ },
+ },
};
-const countHelper = (stringKey, argName = stringKey) => function(value, {unit = false} = {}) {
+const countHelper = (stringKey, argName = stringKey) =>
+ function (value, { unit = false } = {}) {
return this.$(
- (unit
- ? `count.${stringKey}.withUnit.` + this.getUnitForm(value)
- : `count.${stringKey}`),
- {[argName]: this.formatNumber(value)});
-};
+ unit
+ ? `count.${stringKey}.withUnit.` + this.getUnitForm(value)
+ : `count.${stringKey}`,
+ { [argName]: this.formatNumber(value) }
+ );
+ };
Object.assign(Language.prototype, {
- $(key, args = {}) {
- return this.formatString(key, args);
- },
+ $(key, args = {}) {
+ return this.formatString(key, args);
+ },
- assertIntlAvailable(property) {
- if (!this[property]) {
- throw new Error(`Intl API ${property} unavailable`);
- }
- },
-
- getUnitForm(value) {
- this.assertIntlAvailable('intl_pluralCardinal');
- return this.intl_pluralCardinal.select(value);
- },
-
- formatString(key, args = {}) {
- if (this.strings && !this.strings_htmlEscaped) {
- throw new Error(`HTML-escaped strings unavailable - please ensure escapeHTML function is provided`);
- }
-
- return this.formatStringHelper(this.strings_htmlEscaped, key, args);
- },
-
- formatStringNoHTMLEscape(key, args = {}) {
- return this.formatStringHelper(this.strings, key, args);
- },
-
- formatStringHelper(strings, key, args = {}) {
- if (!strings) {
- throw new Error(`Strings unavailable`);
- }
-
- if (!this.validKeys.includes(key)) {
- throw new Error(`Invalid key ${key} accessed`);
- }
-
- const template = strings[key];
-
- // Convert the keys on the args dict from camelCase to CONSTANT_CASE.
- // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut
- // like, who cares, dude?) Also, this is an array, 8ecause it's handy
- // for the iterating we're a8out to do.
- const processedArgs = Object.entries(args)
- .map(([ k, v ]) => [k.replace(/[A-Z]/g, '_$&').toUpperCase(), v]);
-
- // Replacement time! Woot. Reduce comes in handy here!
- const output = processedArgs.reduce(
- (x, [ k, v ]) => x.replaceAll(`{${k}}`, v),
- template);
-
- // Post-processing: if any expected arguments *weren't* replaced, that
- // is almost definitely an error.
- if (output.match(/\{[A-Z_]+\}/)) {
- throw new Error(`Args in ${key} were missing - output: ${output}`);
- }
-
- return output;
- },
-
- formatDate(date) {
- this.assertIntlAvailable('intl_date');
- return this.intl_date.format(date);
- },
-
- formatDateRange(startDate, endDate) {
- this.assertIntlAvailable('intl_date');
- return this.intl_date.formatRange(startDate, endDate);
- },
-
- formatDuration(secTotal, {approximate = false, unit = false} = {}) {
- if (secTotal === 0) {
- return this.formatString('count.duration.missing');
- }
-
- const hour = Math.floor(secTotal / 3600);
- const min = Math.floor((secTotal - hour * 3600) / 60);
- const sec = Math.floor(secTotal - hour * 3600 - min * 60);
-
- const pad = val => val.toString().padStart(2, '0');
-
- const stringSubkey = unit ? '.withUnit' : '';
-
- const duration = (hour > 0
- ? this.formatString('count.duration.hours' + stringSubkey, {
- hours: hour,
- minutes: pad(min),
- seconds: pad(sec)
- })
- : this.formatString('count.duration.minutes' + stringSubkey, {
- minutes: min,
- seconds: pad(sec)
- }));
-
- return (approximate
- ? this.formatString('count.duration.approximate', {duration})
- : duration);
- },
-
- formatIndex(value) {
- this.assertIntlAvailable('intl_pluralOrdinal');
- return this.formatString('count.index.' + this.intl_pluralOrdinal.select(value), {index: value});
- },
-
- formatNumber(value) {
- this.assertIntlAvailable('intl_number');
- return this.intl_number.format(value);
- },
-
- formatWordCount(value) {
- const num = this.formatNumber(value > 1000
- ? Math.floor(value / 100) / 10
- : value);
+ assertIntlAvailable(property) {
+ if (!this[property]) {
+ throw new Error(`Intl API ${property} unavailable`);
+ }
+ },
+
+ getUnitForm(value) {
+ this.assertIntlAvailable("intl_pluralCardinal");
+ return this.intl_pluralCardinal.select(value);
+ },
+
+ formatString(key, args = {}) {
+ if (this.strings && !this.strings_htmlEscaped) {
+ throw new Error(
+ `HTML-escaped strings unavailable - please ensure escapeHTML function is provided`
+ );
+ }
- const words = (value > 1000
- ? this.formatString('count.words.thousand', {words: num})
- : this.formatString('count.words', {words: num}));
+ return this.formatStringHelper(this.strings_htmlEscaped, key, args);
+ },
- return this.formatString('count.words.withUnit.' + this.getUnitForm(value), {words});
- },
+ formatStringNoHTMLEscape(key, args = {}) {
+ return this.formatStringHelper(this.strings, key, args);
+ },
- // Conjunction list: A, B, and C
- formatConjunctionList(array) {
- this.assertIntlAvailable('intl_listConjunction');
- return this.intl_listConjunction.format(array);
- },
+ formatStringHelper(strings, key, args = {}) {
+ if (!strings) {
+ throw new Error(`Strings unavailable`);
+ }
- // Disjunction lists: A, B, or C
- formatDisjunctionList(array) {
- this.assertIntlAvailable('intl_listDisjunction');
- return this.intl_listDisjunction.format(array);
- },
+ if (!this.validKeys.includes(key)) {
+ throw new Error(`Invalid key ${key} accessed`);
+ }
- // Unit lists: A, B, C
- formatUnitList(array) {
- this.assertIntlAvailable('intl_listUnit');
- return this.intl_listUnit.format(array);
- },
+ const template = strings[key];
+
+ // Convert the keys on the args dict from camelCase to CONSTANT_CASE.
+ // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut
+ // like, who cares, dude?) Also, this is an array, 8ecause it's handy
+ // for the iterating we're a8out to do.
+ const processedArgs = Object.entries(args).map(([k, v]) => [
+ k.replace(/[A-Z]/g, "_$&").toUpperCase(),
+ v,
+ ]);
+
+ // Replacement time! Woot. Reduce comes in handy here!
+ const output = processedArgs.reduce(
+ (x, [k, v]) => x.replaceAll(`{${k}}`, v),
+ template
+ );
+
+ // Post-processing: if any expected arguments *weren't* replaced, that
+ // is almost definitely an error.
+ if (output.match(/\{[A-Z_]+\}/)) {
+ throw new Error(`Args in ${key} were missing - output: ${output}`);
+ }
- // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB
- formatFileSize(bytes) {
- if (!bytes) return '';
+ return output;
+ },
- bytes = parseInt(bytes);
- if (isNaN(bytes)) return '';
+ formatDate(date) {
+ this.assertIntlAvailable("intl_date");
+ return this.intl_date.format(date);
+ },
- const round = exp => Math.round(bytes / 10 ** (exp - 1)) / 10;
+ formatDateRange(startDate, endDate) {
+ this.assertIntlAvailable("intl_date");
+ return this.intl_date.formatRange(startDate, endDate);
+ },
- if (bytes >= 10 ** 12) {
- return this.formatString('count.fileSize.terabytes', {terabytes: round(12)});
- } else if (bytes >= 10 ** 9) {
- return this.formatString('count.fileSize.gigabytes', {gigabytes: round(9)});
- } else if (bytes >= 10 ** 6) {
- return this.formatString('count.fileSize.megabytes', {megabytes: round(6)});
- } else if (bytes >= 10 ** 3) {
- return this.formatString('count.fileSize.kilobytes', {kilobytes: round(3)});
- } else {
- return this.formatString('count.fileSize.bytes', {bytes});
- }
- },
+ formatDuration(secTotal, { approximate = false, unit = false } = {}) {
+ if (secTotal === 0) {
+ return this.formatString("count.duration.missing");
+ }
- // TODO: These are hard-coded. Is there a better way?
- countAdditionalFiles: countHelper('additionalFiles', 'files'),
- countAlbums: countHelper('albums'),
- countCommentaryEntries: countHelper('commentaryEntries', 'entries'),
- countContributions: countHelper('contributions'),
- countCoverArts: countHelper('coverArts'),
- countTimesReferenced: countHelper('timesReferenced'),
- countTimesUsed: countHelper('timesUsed'),
- countTracks: countHelper('tracks'),
+ const hour = Math.floor(secTotal / 3600);
+ const min = Math.floor((secTotal - hour * 3600) / 60);
+ const sec = Math.floor(secTotal - hour * 3600 - min * 60);
+
+ const pad = (val) => val.toString().padStart(2, "0");
+
+ const stringSubkey = unit ? ".withUnit" : "";
+
+ const duration =
+ hour > 0
+ ? this.formatString("count.duration.hours" + stringSubkey, {
+ hours: hour,
+ minutes: pad(min),
+ seconds: pad(sec),
+ })
+ : this.formatString("count.duration.minutes" + stringSubkey, {
+ minutes: min,
+ seconds: pad(sec),
+ });
+
+ return approximate
+ ? this.formatString("count.duration.approximate", { duration })
+ : duration;
+ },
+
+ formatIndex(value) {
+ this.assertIntlAvailable("intl_pluralOrdinal");
+ return this.formatString(
+ "count.index." + this.intl_pluralOrdinal.select(value),
+ { index: value }
+ );
+ },
+
+ formatNumber(value) {
+ this.assertIntlAvailable("intl_number");
+ return this.intl_number.format(value);
+ },
+
+ formatWordCount(value) {
+ const num = this.formatNumber(
+ value > 1000 ? Math.floor(value / 100) / 10 : value
+ );
+
+ const words =
+ value > 1000
+ ? this.formatString("count.words.thousand", { words: num })
+ : this.formatString("count.words", { words: num });
+
+ return this.formatString(
+ "count.words.withUnit." + this.getUnitForm(value),
+ { words }
+ );
+ },
+
+ // Conjunction list: A, B, and C
+ formatConjunctionList(array) {
+ this.assertIntlAvailable("intl_listConjunction");
+ return this.intl_listConjunction.format(array);
+ },
+
+ // Disjunction lists: A, B, or C
+ formatDisjunctionList(array) {
+ this.assertIntlAvailable("intl_listDisjunction");
+ return this.intl_listDisjunction.format(array);
+ },
+
+ // Unit lists: A, B, C
+ formatUnitList(array) {
+ this.assertIntlAvailable("intl_listUnit");
+ return this.intl_listUnit.format(array);
+ },
+
+ // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB
+ formatFileSize(bytes) {
+ if (!bytes) return "";
+
+ bytes = parseInt(bytes);
+ if (isNaN(bytes)) return "";
+
+ const round = (exp) => Math.round(bytes / 10 ** (exp - 1)) / 10;
+
+ if (bytes >= 10 ** 12) {
+ return this.formatString("count.fileSize.terabytes", {
+ terabytes: round(12),
+ });
+ } else if (bytes >= 10 ** 9) {
+ return this.formatString("count.fileSize.gigabytes", {
+ gigabytes: round(9),
+ });
+ } else if (bytes >= 10 ** 6) {
+ return this.formatString("count.fileSize.megabytes", {
+ megabytes: round(6),
+ });
+ } else if (bytes >= 10 ** 3) {
+ return this.formatString("count.fileSize.kilobytes", {
+ kilobytes: round(3),
+ });
+ } else {
+ return this.formatString("count.fileSize.bytes", { bytes });
+ }
+ },
+
+ // TODO: These are hard-coded. Is there a better way?
+ countAdditionalFiles: countHelper("additionalFiles", "files"),
+ countAlbums: countHelper("albums"),
+ countCommentaryEntries: countHelper("commentaryEntries", "entries"),
+ countContributions: countHelper("contributions"),
+ countCoverArts: countHelper("coverArts"),
+ countTimesReferenced: countHelper("timesReferenced"),
+ countTimesUsed: countHelper("timesUsed"),
+ countTracks: countHelper("tracks"),
});
diff --git a/src/data/validators.js b/src/data/validators.js
index 0d325aed..714dc3a0 100644
--- a/src/data/validators.js
+++ b/src/data/validators.js
@@ -1,367 +1,387 @@
-import { withAggregate } from '../util/sugar.js';
+import { withAggregate } from "../util/sugar.js";
-import { color, ENABLE_COLOR, decorateTime } from '../util/cli.js';
+import { color, ENABLE_COLOR, decorateTime } from "../util/cli.js";
-import { inspect as nodeInspect } from 'util';
+import { inspect as nodeInspect } from "util";
function inspect(value) {
- return nodeInspect(value, {colors: ENABLE_COLOR});
+ return nodeInspect(value, { colors: ENABLE_COLOR });
}
// Basic types (primitives)
function a(noun) {
- return (/[aeiou]/.test(noun[0]) ? `an ${noun}` : `a ${noun}`);
+ return /[aeiou]/.test(noun[0]) ? `an ${noun}` : `a ${noun}`;
}
function isType(value, type) {
- if (typeof value !== type)
- throw new TypeError(`Expected ${a(type)}, got ${typeof value}`);
+ if (typeof value !== type)
+ throw new TypeError(`Expected ${a(type)}, got ${typeof value}`);
- return true;
+ return true;
}
export function isBoolean(value) {
- return isType(value, 'boolean');
+ return isType(value, "boolean");
}
export function isNumber(value) {
- return isType(value, 'number');
+ return isType(value, "number");
}
export function isPositive(number) {
- isNumber(number);
+ isNumber(number);
- if (number <= 0)
- throw new TypeError(`Expected positive number`);
+ if (number <= 0) throw new TypeError(`Expected positive number`);
- return true;
+ return true;
}
export function isNegative(number) {
- isNumber(number);
+ isNumber(number);
- if (number >= 0)
- throw new TypeError(`Expected negative number`);
+ if (number >= 0) throw new TypeError(`Expected negative number`);
- return true;
+ return true;
}
export function isPositiveOrZero(number) {
- isNumber(number);
+ isNumber(number);
- if (number < 0)
- throw new TypeError(`Expected positive number or zero`);
+ if (number < 0) throw new TypeError(`Expected positive number or zero`);
- return true;
+ return true;
}
export function isNegativeOrZero(number) {
- isNumber(number);
+ isNumber(number);
- if (number > 0)
- throw new TypeError(`Expected negative number or zero`);
+ if (number > 0) throw new TypeError(`Expected negative number or zero`);
- return true;
+ return true;
}
export function isInteger(number) {
- isNumber(number);
+ isNumber(number);
- if (number % 1 !== 0)
- throw new TypeError(`Expected integer`);
+ if (number % 1 !== 0) throw new TypeError(`Expected integer`);
- return true;
+ return true;
}
export function isCountingNumber(number) {
- isInteger(number);
- isPositive(number);
+ isInteger(number);
+ isPositive(number);
- return true;
+ return true;
}
export function isWholeNumber(number) {
- isInteger(number);
- isPositiveOrZero(number);
+ isInteger(number);
+ isPositiveOrZero(number);
- return true;
+ return true;
}
export function isString(value) {
- return isType(value, 'string');
+ return isType(value, "string");
}
export function isStringNonEmpty(value) {
- isString(value);
+ isString(value);
- if (value.trim().length === 0)
- throw new TypeError(`Expected non-empty string`);
+ if (value.trim().length === 0)
+ throw new TypeError(`Expected non-empty string`);
- return true;
+ return true;
}
// Complex types (non-primitives)
export function isInstance(value, constructor) {
- isObject(value);
+ isObject(value);
- if (!(value instanceof constructor))
- throw new TypeError(`Expected ${constructor.name}, got ${value.constructor.name}`);
+ if (!(value instanceof constructor))
+ throw new TypeError(
+ `Expected ${constructor.name}, got ${value.constructor.name}`
+ );
- return true;
+ return true;
}
export function isDate(value) {
- return isInstance(value, Date);
+ return isInstance(value, Date);
}
export function isObject(value) {
- isType(value, 'object');
+ isType(value, "object");
- // Note: Please remember that null is always a valid value for properties
- // held by a CacheableObject. This assertion is exclusively for use in other
- // contexts.
- if (value === null)
- throw new TypeError(`Expected an object, got null`);
+ // Note: Please remember that null is always a valid value for properties
+ // held by a CacheableObject. This assertion is exclusively for use in other
+ // contexts.
+ if (value === null) throw new TypeError(`Expected an object, got null`);
- return true;
+ return true;
}
export function isArray(value) {
- if (typeof value !== 'object' || value === null || !Array.isArray(value))
- throw new TypeError(`Expected an array, got ${value}`);
+ if (typeof value !== "object" || value === null || !Array.isArray(value))
+ throw new TypeError(`Expected an array, got ${value}`);
- return true;
+ return true;
}
function validateArrayItemsHelper(itemValidator) {
- return (item, index) => {
- try {
- const value = itemValidator(item);
-
- if (value !== true) {
- throw new Error(`Expected validator to return true`);
- }
- } catch (error) {
- error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${error.message}`;
- throw error;
- }
- };
+ return (item, index) => {
+ try {
+ const value = itemValidator(item);
+
+ if (value !== true) {
+ throw new Error(`Expected validator to return true`);
+ }
+ } catch (error) {
+ error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${
+ error.message
+ }`;
+ throw error;
+ }
+ };
}
export function validateArrayItems(itemValidator) {
- const fn = validateArrayItemsHelper(itemValidator);
+ const fn = validateArrayItemsHelper(itemValidator);
- return array => {
- isArray(array);
+ return (array) => {
+ isArray(array);
- withAggregate({message: 'Errors validating array items'}, ({ wrap }) => {
- array.forEach(wrap(fn));
- });
+ withAggregate({ message: "Errors validating array items" }, ({ wrap }) => {
+ array.forEach(wrap(fn));
+ });
- return true;
- };
+ return true;
+ };
}
export function validateInstanceOf(constructor) {
- return object => isInstance(object, constructor);
+ return (object) => isInstance(object, constructor);
}
// Wiki data (primitives & non-primitives)
export function isColor(color) {
- isStringNonEmpty(color);
+ isStringNonEmpty(color);
- if (color.startsWith('#')) {
- if (![1 + 3, 1 + 4, 1 + 6, 1 + 8].includes(color.length))
- throw new TypeError(`Expected #rgb, #rgba, #rrggbb, or #rrggbbaa, got length ${color.length}`);
+ if (color.startsWith("#")) {
+ if (![1 + 3, 1 + 4, 1 + 6, 1 + 8].includes(color.length))
+ throw new TypeError(
+ `Expected #rgb, #rgba, #rrggbb, or #rrggbbaa, got length ${color.length}`
+ );
- if (/[^0-9a-fA-F]/.test(color.slice(1)))
- throw new TypeError(`Expected hexadecimal digits`);
+ if (/[^0-9a-fA-F]/.test(color.slice(1)))
+ throw new TypeError(`Expected hexadecimal digits`);
- return true;
- }
+ return true;
+ }
- throw new TypeError(`Unknown color format`);
+ throw new TypeError(`Unknown color format`);
}
export function isCommentary(commentary) {
- return isString(commentary);
+ return isString(commentary);
}
-const isArtistRef = validateReference('artist');
+const isArtistRef = validateReference("artist");
export function validateProperties(spec) {
- const specEntries = Object.entries(spec);
- const specKeys = Object.keys(spec);
-
- return object => {
- isObject(object);
-
- if (Array.isArray(object))
- throw new TypeError(`Expected an object, got array`);
-
- withAggregate({message: `Errors validating object properties`}, ({ call }) => {
- for (const [ specKey, specValidator ] of specEntries) {
- call(() => {
- const value = object[specKey];
- try {
- specValidator(value);
- } catch (error) {
- error.message = `(key: ${color.green(specKey)}, value: ${inspect(value)}) ${error.message}`;
- throw error;
- }
- });
- }
+ const specEntries = Object.entries(spec);
+ const specKeys = Object.keys(spec);
- const unknownKeys = Object.keys(object).filter(key => !specKeys.includes(key));
- if (unknownKeys.length > 0) {
- call(() => {
- throw new Error(`Unknown keys present (${unknownKeys.length}): [${unknownKeys.join(', ')}]`);
- });
+ return (object) => {
+ isObject(object);
+
+ if (Array.isArray(object))
+ throw new TypeError(`Expected an object, got array`);
+
+ withAggregate(
+ { message: `Errors validating object properties` },
+ ({ call }) => {
+ for (const [specKey, specValidator] of specEntries) {
+ call(() => {
+ const value = object[specKey];
+ try {
+ specValidator(value);
+ } catch (error) {
+ error.message = `(key: ${color.green(specKey)}, value: ${inspect(
+ value
+ )}) ${error.message}`;
+ throw error;
}
- });
+ });
+ }
- return true;
- };
-}
+ const unknownKeys = Object.keys(object).filter(
+ (key) => !specKeys.includes(key)
+ );
+ if (unknownKeys.length > 0) {
+ call(() => {
+ throw new Error(
+ `Unknown keys present (${
+ unknownKeys.length
+ }): [${unknownKeys.join(", ")}]`
+ );
+ });
+ }
+ }
+ );
+ return true;
+ };
+}
export const isContribution = validateProperties({
- who: isArtistRef,
- what: value => value === undefined || value === null || isStringNonEmpty(value),
+ who: isArtistRef,
+ what: (value) =>
+ value === undefined || value === null || isStringNonEmpty(value),
});
export const isContributionList = validateArrayItems(isContribution);
export const isAdditionalFile = validateProperties({
- title: isString,
- description: value => (value === undefined || value === null || isString(value)),
- files: validateArrayItems(isString)
+ title: isString,
+ description: (value) =>
+ value === undefined || value === null || isString(value),
+ files: validateArrayItems(isString),
});
export const isAdditionalFileList = validateArrayItems(isAdditionalFile);
export function isDimensions(dimensions) {
- isArray(dimensions);
+ isArray(dimensions);
- if (dimensions.length !== 2)
- throw new TypeError(`Expected 2 item array`);
+ if (dimensions.length !== 2) throw new TypeError(`Expected 2 item array`);
- isPositive(dimensions[0]);
- isInteger(dimensions[0]);
- isPositive(dimensions[1]);
- isInteger(dimensions[1]);
+ isPositive(dimensions[0]);
+ isInteger(dimensions[0]);
+ isPositive(dimensions[1]);
+ isInteger(dimensions[1]);
- return true;
+ return true;
}
export function isDirectory(directory) {
- isStringNonEmpty(directory);
+ isStringNonEmpty(directory);
- if (directory.match(/[^a-zA-Z0-9_\-]/))
- throw new TypeError(`Expected only letters, numbers, dash, and underscore, got "${directory}"`);
+ if (directory.match(/[^a-zA-Z0-9_\-]/))
+ throw new TypeError(
+ `Expected only letters, numbers, dash, and underscore, got "${directory}"`
+ );
- return true;
+ return true;
}
export function isDuration(duration) {
- isNumber(duration);
- isPositiveOrZero(duration);
+ isNumber(duration);
+ isPositiveOrZero(duration);
- return true;
+ return true;
}
export function isFileExtension(string) {
- isStringNonEmpty(string);
+ isStringNonEmpty(string);
- if (string[0] === '.')
- throw new TypeError(`Expected no dot (.) at the start of file extension`);
+ if (string[0] === ".")
+ throw new TypeError(`Expected no dot (.) at the start of file extension`);
- if (string.match(/[^a-zA-Z0-9_]/))
- throw new TypeError(`Expected only alphanumeric and underscore`);
+ if (string.match(/[^a-zA-Z0-9_]/))
+ throw new TypeError(`Expected only alphanumeric and underscore`);
- return true;
+ return true;
}
export function isLanguageCode(string) {
- // TODO: This is a stub function because really we don't need a detailed
- // is-language-code parser right now.
+ // TODO: This is a stub function because really we don't need a detailed
+ // is-language-code parser right now.
- isString(string);
+ isString(string);
- return true;
+ return true;
}
export function isName(name) {
- return isString(name);
+ return isString(name);
}
export function isURL(string) {
- isStringNonEmpty(string);
+ isStringNonEmpty(string);
- new URL(string);
+ new URL(string);
- return true;
+ return true;
}
-export function validateReference(type = 'track') {
- return ref => {
- isStringNonEmpty(ref);
+export function validateReference(type = "track") {
+ return (ref) => {
+ isStringNonEmpty(ref);
- const match = ref.trim().match(/^(?:(?\S+):(?=\S))?(?.+)(?\S+):(?=\S))?(?.+)(? {
- const errorMeta = [];
+ return (value) => {
+ const errorMeta = [];
- for (let i = 0, check; check = checks[i]; i++) {
- try {
- const result = check(value);
-
- if (result !== true) {
- throw new Error(`Check returned false`);
- }
+ for (let i = 0, check; (check = checks[i]); i++) {
+ try {
+ const result = check(value);
- return true;
- } catch (error) {
- errorMeta.push([check, i, error]);
- }
+ if (result !== true) {
+ throw new Error(`Check returned false`);
}
- // Don't process error messages until every check has failed.
- const errors = [];
- for (const [ check, i, error ] of errorMeta) {
- error.message = (check.name
- ? `(#${i} "${check.name}") ${error.message}`
- : `(#${i}) ${error.message}`);
- error.check = check;
- errors.push(error);
- }
- throw new AggregateError(errors, `Expected one of ${checks.length} possible checks, but none were true`);
- };
+ return true;
+ } catch (error) {
+ errorMeta.push([check, i, error]);
+ }
+ }
+
+ // Don't process error messages until every check has failed.
+ const errors = [];
+ for (const [check, i, error] of errorMeta) {
+ error.message = check.name
+ ? `(#${i} "${check.name}") ${error.message}`
+ : `(#${i}) ${error.message}`;
+ error.check = check;
+ errors.push(error);
+ }
+ throw new AggregateError(
+ errors,
+ `Expected one of ${checks.length} possible checks, but none were true`
+ );
+ };
}
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 763dfd28..cfbb985a 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -1,74 +1,69 @@
// yaml.js - specification for HSMusic YAML data file format and utilities for
// loading and processing YAML files and documents
-import * as path from 'path';
-import yaml from 'js-yaml';
+import * as path from "path";
+import yaml from "js-yaml";
-import { readFile } from 'fs/promises';
-import { inspect as nodeInspect } from 'util';
+import { readFile } from "fs/promises";
+import { inspect as nodeInspect } from "util";
import {
- Album,
- Artist,
- ArtTag,
- Flash,
- FlashAct,
- Group,
- GroupCategory,
- HomepageLayout,
- HomepageLayoutAlbumsRow,
- HomepageLayoutRow,
- NewsEntry,
- StaticPage,
- Thing,
- Track,
- TrackGroup,
- WikiInfo,
-} from './things.js';
+ Album,
+ Artist,
+ ArtTag,
+ Flash,
+ FlashAct,
+ Group,
+ GroupCategory,
+ HomepageLayout,
+ HomepageLayoutAlbumsRow,
+ HomepageLayoutRow,
+ NewsEntry,
+ StaticPage,
+ Thing,
+ Track,
+ TrackGroup,
+ WikiInfo,
+} from "./things.js";
+
+import { color, ENABLE_COLOR, logInfo, logWarn } from "../util/cli.js";
import {
- color,
- ENABLE_COLOR,
- logInfo,
- logWarn,
-} from '../util/cli.js';
+ decorateErrorWithIndex,
+ mapAggregate,
+ openAggregate,
+ showAggregate,
+ withAggregate,
+} from "../util/sugar.js";
import {
- decorateErrorWithIndex,
- mapAggregate,
- openAggregate,
- showAggregate,
- withAggregate,
-} from '../util/sugar.js';
+ sortAlbumsTracksChronologically,
+ sortAlphabetically,
+ sortChronologically,
+} from "../util/wiki-data.js";
-import {
- sortAlbumsTracksChronologically,
- sortAlphabetically,
- sortChronologically,
-} from '../util/wiki-data.js';
-
-import find, { bindFind } from '../util/find.js';
-import { findFiles } from '../util/io.js';
+import find, { bindFind } from "../util/find.js";
+import { findFiles } from "../util/io.js";
// --> General supporting stuff
function inspect(value) {
- return nodeInspect(value, {colors: ENABLE_COLOR});
+ return nodeInspect(value, { colors: ENABLE_COLOR });
}
// --> YAML data repository structure constants
-export const WIKI_INFO_FILE = 'wiki-info.yaml';
-export const BUILD_DIRECTIVE_DATA_FILE = 'build-directives.yaml';
-export const HOMEPAGE_LAYOUT_DATA_FILE = 'homepage.yaml';
-export const ARTIST_DATA_FILE = 'artists.yaml';
-export const FLASH_DATA_FILE = 'flashes.yaml';
-export const NEWS_DATA_FILE = 'news.yaml';
-export const ART_TAG_DATA_FILE = 'tags.yaml';
-export const GROUP_DATA_FILE = 'groups.yaml';
-export const STATIC_PAGE_DATA_FILE = 'static-pages.yaml';
+export const WIKI_INFO_FILE = "wiki-info.yaml";
+export const BUILD_DIRECTIVE_DATA_FILE = "build-directives.yaml";
+export const HOMEPAGE_LAYOUT_DATA_FILE = "homepage.yaml";
+export const ARTIST_DATA_FILE = "artists.yaml";
+export const FLASH_DATA_FILE = "flashes.yaml";
+export const NEWS_DATA_FILE = "news.yaml";
+export const ART_TAG_DATA_FILE = "tags.yaml";
+export const GROUP_DATA_FILE = "groups.yaml";
+export const STATIC_PAGE_DATA_FILE = "static-pages.yaml";
-export const DATA_ALBUM_DIRECTORY = 'album';
+export const DATA_ALBUM_DIRECTORY = "album";
// --> Document processing functions
@@ -78,7 +73,9 @@ export const DATA_ALBUM_DIRECTORY = 'album';
// makeProcessDocument is a factory function: the returned function will take a
// document and apply the configuration passed to makeProcessDocument in order
// to construct a Thing subclass.
-function makeProcessDocument(thingClass, {
+function makeProcessDocument(
+ thingClass,
+ {
// Optional early step for transforming field values before providing them
// to the Thing's update() method. This is useful when the input format
// (i.e. values in the document) differ from the format the actual Thing
@@ -101,454 +98,479 @@ function makeProcessDocument(thingClass, {
// they're present in a document, but they won't be used for Thing property
// generation, either. Useful for stuff that's present in data files but not
// yet implemented as part of a Thing's data model!
- ignoredFields = []
-}) {
- if (!propertyFieldMapping) {
- throw new Error(`Expected propertyFieldMapping to be provided`);
- }
-
- const knownFields = Object.values(propertyFieldMapping);
-
- // Invert the property-field mapping, since it'll come in handy for
- // assigning update() source values later.
- const fieldPropertyMapping = Object.fromEntries(
- (Object.entries(propertyFieldMapping)
- .map(([ property, field ]) => [field, property])));
-
- const decorateErrorWithName = fn => {
- const nameField = propertyFieldMapping['name'];
- if (!nameField) return fn;
-
- return document => {
- try {
- return fn(document);
- } catch (error) {
- const name = document[nameField];
- error.message = (name
- ? `(name: ${inspect(name)}) ${error.message}`
- : `(${color.dim(`no name found`)}) ${error.message}`);
- throw error;
- }
- };
+ ignoredFields = [],
+ }
+) {
+ if (!propertyFieldMapping) {
+ throw new Error(`Expected propertyFieldMapping to be provided`);
+ }
+
+ const knownFields = Object.values(propertyFieldMapping);
+
+ // Invert the property-field mapping, since it'll come in handy for
+ // assigning update() source values later.
+ const fieldPropertyMapping = Object.fromEntries(
+ Object.entries(propertyFieldMapping).map(([property, field]) => [
+ field,
+ property,
+ ])
+ );
+
+ const decorateErrorWithName = (fn) => {
+ const nameField = propertyFieldMapping["name"];
+ if (!nameField) return fn;
+
+ return (document) => {
+ try {
+ return fn(document);
+ } catch (error) {
+ const name = document[nameField];
+ error.message = name
+ ? `(name: ${inspect(name)}) ${error.message}`
+ : `(${color.dim(`no name found`)}) ${error.message}`;
+ throw error;
+ }
};
+ };
- return decorateErrorWithName(document => {
- const documentEntries = Object.entries(document)
- .filter(([ field ]) => !ignoredFields.includes(field));
+ return decorateErrorWithName((document) => {
+ const documentEntries = Object.entries(document).filter(
+ ([field]) => !ignoredFields.includes(field)
+ );
- const unknownFields = documentEntries
- .map(([ field ]) => field)
- .filter(field => !knownFields.includes(field));
+ const unknownFields = documentEntries
+ .map(([field]) => field)
+ .filter((field) => !knownFields.includes(field));
- if (unknownFields.length) {
- throw new makeProcessDocument.UnknownFieldsError(unknownFields);
- }
+ if (unknownFields.length) {
+ throw new makeProcessDocument.UnknownFieldsError(unknownFields);
+ }
- const fieldValues = {};
+ const fieldValues = {};
- for (const [ field, value ] of documentEntries) {
- if (Object.hasOwn(fieldTransformations, field)) {
- fieldValues[field] = fieldTransformations[field](value);
- } else {
- fieldValues[field] = value;
- }
- }
+ for (const [field, value] of documentEntries) {
+ if (Object.hasOwn(fieldTransformations, field)) {
+ fieldValues[field] = fieldTransformations[field](value);
+ } else {
+ fieldValues[field] = value;
+ }
+ }
- const sourceProperties = {};
+ const sourceProperties = {};
- for (const [ field, value ] of Object.entries(fieldValues)) {
- const property = fieldPropertyMapping[field];
- sourceProperties[property] = value;
- }
+ for (const [field, value] of Object.entries(fieldValues)) {
+ const property = fieldPropertyMapping[field];
+ sourceProperties[property] = value;
+ }
- const thing = Reflect.construct(thingClass, []);
+ const thing = Reflect.construct(thingClass, []);
- withAggregate({message: `Errors applying ${color.green(thingClass.name)} properties`}, ({ call }) => {
- for (const [ property, value ] of Object.entries(sourceProperties)) {
- call(() => (thing[property] = value));
- }
- });
+ withAggregate(
+ { message: `Errors applying ${color.green(thingClass.name)} properties` },
+ ({ call }) => {
+ for (const [property, value] of Object.entries(sourceProperties)) {
+ call(() => (thing[property] = value));
+ }
+ }
+ );
- return thing;
- });
+ return thing;
+ });
}
-makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends Error {
- constructor(fields) {
- super(`Unknown fields present: ${fields.join(', ')}`);
- this.fields = fields;
- }
+makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends (
+ Error
+) {
+ constructor(fields) {
+ super(`Unknown fields present: ${fields.join(", ")}`);
+ this.fields = fields;
+ }
};
export const processAlbumDocument = makeProcessDocument(Album, {
- fieldTransformations: {
- 'Artists': parseContributors,
- 'Cover Artists': parseContributors,
- 'Default Track Cover Artists': parseContributors,
- 'Wallpaper Artists': parseContributors,
- 'Banner Artists': parseContributors,
-
- 'Date': value => new Date(value),
- 'Date Added': value => new Date(value),
- 'Cover Art Date': value => new Date(value),
- 'Default Track Cover Art Date': value => new Date(value),
-
- 'Banner Dimensions': parseDimensions,
-
- 'Additional Files': parseAdditionalFiles,
- },
-
- propertyFieldMapping: {
- name: 'Album',
-
- color: 'Color',
- directory: 'Directory',
- urls: 'URLs',
-
- artistContribsByRef: 'Artists',
- coverArtistContribsByRef: 'Cover Artists',
- trackCoverArtistContribsByRef: 'Default Track Cover Artists',
-
- coverArtFileExtension: 'Cover Art File Extension',
- trackCoverArtFileExtension: 'Track Art File Extension',
-
- wallpaperArtistContribsByRef: 'Wallpaper Artists',
- wallpaperStyle: 'Wallpaper Style',
- wallpaperFileExtension: 'Wallpaper File Extension',
-
- bannerArtistContribsByRef: 'Banner Artists',
- bannerStyle: 'Banner Style',
- bannerFileExtension: 'Banner File Extension',
- bannerDimensions: 'Banner Dimensions',
-
- date: 'Date',
- trackArtDate: 'Default Track Cover Art Date',
- coverArtDate: 'Cover Art Date',
- dateAddedToWiki: 'Date Added',
-
- hasCoverArt: 'Has Cover Art',
- hasTrackArt: 'Has Track Art',
- hasTrackNumbers: 'Has Track Numbers',
- isMajorRelease: 'Major Release',
- isListedOnHomepage: 'Listed on Homepage',
-
- groupsByRef: 'Groups',
- artTagsByRef: 'Art Tags',
- commentary: 'Commentary',
-
- additionalFiles: 'Additional Files',
- }
+ fieldTransformations: {
+ Artists: parseContributors,
+ "Cover Artists": parseContributors,
+ "Default Track Cover Artists": parseContributors,
+ "Wallpaper Artists": parseContributors,
+ "Banner Artists": parseContributors,
+
+ Date: (value) => new Date(value),
+ "Date Added": (value) => new Date(value),
+ "Cover Art Date": (value) => new Date(value),
+ "Default Track Cover Art Date": (value) => new Date(value),
+
+ "Banner Dimensions": parseDimensions,
+
+ "Additional Files": parseAdditionalFiles,
+ },
+
+ propertyFieldMapping: {
+ name: "Album",
+
+ color: "Color",
+ directory: "Directory",
+ urls: "URLs",
+
+ artistContribsByRef: "Artists",
+ coverArtistContribsByRef: "Cover Artists",
+ trackCoverArtistContribsByRef: "Default Track Cover Artists",
+
+ coverArtFileExtension: "Cover Art File Extension",
+ trackCoverArtFileExtension: "Track Art File Extension",
+
+ wallpaperArtistContribsByRef: "Wallpaper Artists",
+ wallpaperStyle: "Wallpaper Style",
+ wallpaperFileExtension: "Wallpaper File Extension",
+
+ bannerArtistContribsByRef: "Banner Artists",
+ bannerStyle: "Banner Style",
+ bannerFileExtension: "Banner File Extension",
+ bannerDimensions: "Banner Dimensions",
+
+ date: "Date",
+ trackArtDate: "Default Track Cover Art Date",
+ coverArtDate: "Cover Art Date",
+ dateAddedToWiki: "Date Added",
+
+ hasCoverArt: "Has Cover Art",
+ hasTrackArt: "Has Track Art",
+ hasTrackNumbers: "Has Track Numbers",
+ isMajorRelease: "Major Release",
+ isListedOnHomepage: "Listed on Homepage",
+
+ groupsByRef: "Groups",
+ artTagsByRef: "Art Tags",
+ commentary: "Commentary",
+
+ additionalFiles: "Additional Files",
+ },
});
export const processTrackGroupDocument = makeProcessDocument(TrackGroup, {
- fieldTransformations: {
- 'Date Originally Released': value => new Date(value),
- },
-
- propertyFieldMapping: {
- name: 'Group',
- color: 'Color',
- dateOriginallyReleased: 'Date Originally Released',
- }
+ fieldTransformations: {
+ "Date Originally Released": (value) => new Date(value),
+ },
+
+ propertyFieldMapping: {
+ name: "Group",
+ color: "Color",
+ dateOriginallyReleased: "Date Originally Released",
+ },
});
export const processTrackDocument = makeProcessDocument(Track, {
- fieldTransformations: {
- 'Duration': getDurationInSeconds,
+ fieldTransformations: {
+ Duration: getDurationInSeconds,
- 'Date First Released': value => new Date(value),
- 'Cover Art Date': value => new Date(value),
+ "Date First Released": (value) => new Date(value),
+ "Cover Art Date": (value) => new Date(value),
- 'Artists': parseContributors,
- 'Contributors': parseContributors,
- 'Cover Artists': parseContributors,
+ Artists: parseContributors,
+ Contributors: parseContributors,
+ "Cover Artists": parseContributors,
- 'Additional Files': parseAdditionalFiles,
- },
+ "Additional Files": parseAdditionalFiles,
+ },
- propertyFieldMapping: {
- name: 'Track',
+ propertyFieldMapping: {
+ name: "Track",
- directory: 'Directory',
- duration: 'Duration',
- urls: 'URLs',
+ directory: "Directory",
+ duration: "Duration",
+ urls: "URLs",
- coverArtDate: 'Cover Art Date',
- coverArtFileExtension: 'Cover Art File Extension',
- dateFirstReleased: 'Date First Released',
- hasCoverArt: 'Has Cover Art',
- hasURLs: 'Has URLs',
+ coverArtDate: "Cover Art Date",
+ coverArtFileExtension: "Cover Art File Extension",
+ dateFirstReleased: "Date First Released",
+ hasCoverArt: "Has Cover Art",
+ hasURLs: "Has URLs",
- referencedTracksByRef: 'Referenced Tracks',
- artistContribsByRef: 'Artists',
- contributorContribsByRef: 'Contributors',
- coverArtistContribsByRef: 'Cover Artists',
- artTagsByRef: 'Art Tags',
- originalReleaseTrackByRef: 'Originally Released As',
+ referencedTracksByRef: "Referenced Tracks",
+ artistContribsByRef: "Artists",
+ contributorContribsByRef: "Contributors",
+ coverArtistContribsByRef: "Cover Artists",
+ artTagsByRef: "Art Tags",
+ originalReleaseTrackByRef: "Originally Released As",
- commentary: 'Commentary',
- lyrics: 'Lyrics',
+ commentary: "Commentary",
+ lyrics: "Lyrics",
- additionalFiles: 'Additional Files',
- },
+ additionalFiles: "Additional Files",
+ },
- ignoredFields: ['Sampled Tracks']
+ ignoredFields: ["Sampled Tracks"],
});
export const processArtistDocument = makeProcessDocument(Artist, {
- propertyFieldMapping: {
- name: 'Artist',
+ propertyFieldMapping: {
+ name: "Artist",
- directory: 'Directory',
- urls: 'URLs',
- hasAvatar: 'Has Avatar',
- avatarFileExtension: 'Avatar File Extension',
+ directory: "Directory",
+ urls: "URLs",
+ hasAvatar: "Has Avatar",
+ avatarFileExtension: "Avatar File Extension",
- aliasNames: 'Aliases',
+ aliasNames: "Aliases",
- contextNotes: 'Context Notes'
- },
+ contextNotes: "Context Notes",
+ },
- ignoredFields: ['Dead URLs']
+ ignoredFields: ["Dead URLs"],
});
export const processFlashDocument = makeProcessDocument(Flash, {
- fieldTransformations: {
- 'Date': value => new Date(value),
+ fieldTransformations: {
+ Date: (value) => new Date(value),
- 'Contributors': parseContributors,
- },
+ Contributors: parseContributors,
+ },
- propertyFieldMapping: {
- name: 'Flash',
+ propertyFieldMapping: {
+ name: "Flash",
- directory: 'Directory',
- page: 'Page',
- date: 'Date',
- coverArtFileExtension: 'Cover Art File Extension',
+ directory: "Directory",
+ page: "Page",
+ date: "Date",
+ coverArtFileExtension: "Cover Art File Extension",
- featuredTracksByRef: 'Featured Tracks',
- contributorContribsByRef: 'Contributors',
- urls: 'URLs'
- },
+ featuredTracksByRef: "Featured Tracks",
+ contributorContribsByRef: "Contributors",
+ urls: "URLs",
+ },
});
export const processFlashActDocument = makeProcessDocument(FlashAct, {
- propertyFieldMapping: {
- name: 'Act',
- color: 'Color',
- anchor: 'Anchor',
- jump: 'Jump',
- jumpColor: 'Jump Color'
- }
+ propertyFieldMapping: {
+ name: "Act",
+ color: "Color",
+ anchor: "Anchor",
+ jump: "Jump",
+ jumpColor: "Jump Color",
+ },
});
export const processNewsEntryDocument = makeProcessDocument(NewsEntry, {
- fieldTransformations: {
- 'Date': value => new Date(value)
- },
-
- propertyFieldMapping: {
- name: 'Name',
- directory: 'Directory',
- date: 'Date',
- content: 'Content',
- }
+ fieldTransformations: {
+ Date: (value) => new Date(value),
+ },
+
+ propertyFieldMapping: {
+ name: "Name",
+ directory: "Directory",
+ date: "Date",
+ content: "Content",
+ },
});
export const processArtTagDocument = makeProcessDocument(ArtTag, {
- propertyFieldMapping: {
- name: 'Tag',
- directory: 'Directory',
- color: 'Color',
- isContentWarning: 'Is CW'
- }
+ propertyFieldMapping: {
+ name: "Tag",
+ directory: "Directory",
+ color: "Color",
+ isContentWarning: "Is CW",
+ },
});
export const processGroupDocument = makeProcessDocument(Group, {
- propertyFieldMapping: {
- name: 'Group',
- directory: 'Directory',
- description: 'Description',
- urls: 'URLs',
- }
+ propertyFieldMapping: {
+ name: "Group",
+ directory: "Directory",
+ description: "Description",
+ urls: "URLs",
+ },
});
export const processGroupCategoryDocument = makeProcessDocument(GroupCategory, {
- propertyFieldMapping: {
- name: 'Category',
- color: 'Color',
- }
+ propertyFieldMapping: {
+ name: "Category",
+ color: "Color",
+ },
});
export const processStaticPageDocument = makeProcessDocument(StaticPage, {
- propertyFieldMapping: {
- name: 'Name',
- nameShort: 'Short Name',
- directory: 'Directory',
+ propertyFieldMapping: {
+ name: "Name",
+ nameShort: "Short Name",
+ directory: "Directory",
- content: 'Content',
- stylesheet: 'Style',
+ content: "Content",
+ stylesheet: "Style",
- showInNavigationBar: 'Show in Navigation Bar'
- }
+ showInNavigationBar: "Show in Navigation Bar",
+ },
});
export const processWikiInfoDocument = makeProcessDocument(WikiInfo, {
- propertyFieldMapping: {
- name: 'Name',
- nameShort: 'Short Name',
- color: 'Color',
- description: 'Description',
- footerContent: 'Footer Content',
- defaultLanguage: 'Default Language',
- canonicalBase: 'Canonical Base',
- divideTrackListsByGroupsByRef: 'Divide Track Lists By Groups',
- enableFlashesAndGames: 'Enable Flashes & Games',
- enableListings: 'Enable Listings',
- enableNews: 'Enable News',
- enableArtTagUI: 'Enable Art Tag UI',
- enableGroupUI: 'Enable Group UI',
- }
+ propertyFieldMapping: {
+ name: "Name",
+ nameShort: "Short Name",
+ color: "Color",
+ description: "Description",
+ footerContent: "Footer Content",
+ defaultLanguage: "Default Language",
+ canonicalBase: "Canonical Base",
+ divideTrackListsByGroupsByRef: "Divide Track Lists By Groups",
+ enableFlashesAndGames: "Enable Flashes & Games",
+ enableListings: "Enable Listings",
+ enableNews: "Enable News",
+ enableArtTagUI: "Enable Art Tag UI",
+ enableGroupUI: "Enable Group UI",
+ },
});
-export const processHomepageLayoutDocument = makeProcessDocument(HomepageLayout, {
+export const processHomepageLayoutDocument = makeProcessDocument(
+ HomepageLayout,
+ {
propertyFieldMapping: {
- sidebarContent: 'Sidebar Content'
+ sidebarContent: "Sidebar Content",
},
- ignoredFields: ['Homepage']
-});
+ ignoredFields: ["Homepage"],
+ }
+);
export function makeProcessHomepageLayoutRowDocument(rowClass, spec) {
- return makeProcessDocument(rowClass, {
- ...spec,
-
- propertyFieldMapping: {
- name: 'Row',
- color: 'Color',
- type: 'Type',
- ...spec.propertyFieldMapping,
- }
- });
+ return makeProcessDocument(rowClass, {
+ ...spec,
+
+ propertyFieldMapping: {
+ name: "Row",
+ color: "Color",
+ type: "Type",
+ ...spec.propertyFieldMapping,
+ },
+ });
}
export const homepageLayoutRowTypeProcessMapping = {
- albums: makeProcessHomepageLayoutRowDocument(HomepageLayoutAlbumsRow, {
- propertyFieldMapping: {
- sourceGroupByRef: 'Group',
- countAlbumsFromGroup: 'Count',
- sourceAlbumsByRef: 'Albums',
- actionLinks: 'Actions'
- }
- })
+ albums: makeProcessHomepageLayoutRowDocument(HomepageLayoutAlbumsRow, {
+ propertyFieldMapping: {
+ sourceGroupByRef: "Group",
+ countAlbumsFromGroup: "Count",
+ sourceAlbumsByRef: "Albums",
+ actionLinks: "Actions",
+ },
+ }),
};
export function processHomepageLayoutRowDocument(document) {
- const type = document['Type'];
+ const type = document["Type"];
- const match = Object.entries(homepageLayoutRowTypeProcessMapping)
- .find(([ key ]) => key === type);
+ const match = Object.entries(homepageLayoutRowTypeProcessMapping).find(
+ ([key]) => key === type
+ );
- if (!match) {
- throw new TypeError(`No processDocument function for row type ${type}!`);
- }
+ if (!match) {
+ throw new TypeError(`No processDocument function for row type ${type}!`);
+ }
- return match[1](document);
+ return match[1](document);
}
// --> Utilities shared across document parsing functions
export function getDurationInSeconds(string) {
- if (typeof string === 'number') {
- return string;
- }
-
- if (typeof string !== 'string') {
- throw new TypeError(`Expected a string or number, got ${string}`);
- }
-
- const parts = string.split(':').map(n => parseInt(n))
- if (parts.length === 3) {
- return parts[0] * 3600 + parts[1] * 60 + parts[2]
- } else if (parts.length === 2) {
- return parts[0] * 60 + parts[1]
- } else {
- return 0
- }
+ if (typeof string === "number") {
+ return string;
+ }
+
+ if (typeof string !== "string") {
+ throw new TypeError(`Expected a string or number, got ${string}`);
+ }
+
+ const parts = string.split(":").map((n) => parseInt(n));
+ if (parts.length === 3) {
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
+ } else if (parts.length === 2) {
+ return parts[0] * 60 + parts[1];
+ } else {
+ return 0;
+ }
}
export function parseAdditionalFiles(array) {
- if (!array) return null;
- if (!Array.isArray(array)) {
- // Error will be caught when validating against whatever this value is
- return array;
- }
-
- return array.map(item => ({
- title: item['Title'],
- description: item['Description'] ?? null,
- files: item['Files']
- }));
+ if (!array) return null;
+ if (!Array.isArray(array)) {
+ // Error will be caught when validating against whatever this value is
+ return array;
+ }
+
+ return array.map((item) => ({
+ title: item["Title"],
+ description: item["Description"] ?? null,
+ files: item["Files"],
+ }));
}
export function parseCommentary(text) {
- if (text) {
- const lines = String(text).split('\n');
- if (!lines[0].replace(/<\/b>/g, '').includes(': ')) {
- return {error: `An entry is missing commentary citation: "${lines[0].slice(0, 40)}..."`};
- }
- return text;
- } else {
- return null;
+ if (text) {
+ const lines = String(text).split("\n");
+ if (!lines[0].replace(/<\/b>/g, "").includes(": ")) {
+ return {
+ error: `An entry is missing commentary citation: "${lines[0].slice(
+ 0,
+ 40
+ )}..."`,
+ };
}
+ return text;
+ } else {
+ return null;
+ }
}
export function parseContributors(contributors) {
- if (!contributors) {
- return null;
- }
-
- if (contributors.length === 1 && contributors[0].startsWith('')) {
- const arr = [];
- arr.textContent = contributors[0];
- return arr;
+ if (!contributors) {
+ return null;
+ }
+
+ if (contributors.length === 1 && contributors[0].startsWith("")) {
+ const arr = [];
+ arr.textContent = contributors[0];
+ return arr;
+ }
+
+ contributors = contributors.map((contrib) => {
+ // 8asically, the format is "Who (What)", or just "Who". 8e sure to
+ // keep in mind that "what" doesn't necessarily have a value!
+ const match = contrib.match(/^(.*?)( \((.*)\))?$/);
+ if (!match) {
+ return contrib;
}
+ const who = match[1];
+ const what = match[3] || null;
+ return { who, what };
+ });
- contributors = contributors.map(contrib => {
- // 8asically, the format is "Who (What)", or just "Who". 8e sure to
- // keep in mind that "what" doesn't necessarily have a value!
- const match = contrib.match(/^(.*?)( \((.*)\))?$/);
- if (!match) {
- return contrib;
- }
- const who = match[1];
- const what = match[3] || null;
- return {who, what};
- });
-
- const badContributor = contributors.find(val => typeof val === 'string');
- if (badContributor) {
- return {error: `An entry has an incorrectly formatted contributor, "${badContributor}".`};
- }
+ const badContributor = contributors.find((val) => typeof val === "string");
+ if (badContributor) {
+ return {
+ error: `An entry has an incorrectly formatted contributor, "${badContributor}".`,
+ };
+ }
- if (contributors.length === 1 && contributors[0].who === 'none') {
- return null;
- }
+ if (contributors.length === 1 && contributors[0].who === "none") {
+ return null;
+ }
- return contributors;
+ return contributors;
}
function parseDimensions(string) {
- if (!string) {
- return null;
- }
-
- const parts = string.split(/[x,* ]+/g);
- if (parts.length !== 2) throw new Error(`Invalid dimensions: ${string} (expected width & height)`);
- const nums = parts.map(part => Number(part.trim()));
- if (nums.includes(NaN)) throw new Error(`Invalid dimensions: ${string} (couldn't parse as numbers)`);
- return nums;
+ if (!string) {
+ return null;
+ }
+
+ const parts = string.split(/[x,* ]+/g);
+ if (parts.length !== 2)
+ throw new Error(`Invalid dimensions: ${string} (expected width & height)`);
+ const nums = parts.map((part) => Number(part.trim()));
+ if (nums.includes(NaN))
+ throw new Error(
+ `Invalid dimensions: ${string} (couldn't parse as numbers)`
+ );
+ return nums;
}
// --> Data repository loading functions and descriptors
@@ -556,41 +578,41 @@ function parseDimensions(string) {
// documentModes: Symbols indicating sets of behavior for loading and processing
// data files.
export const documentModes = {
- // onePerFile: One document per file. Expects files array (or function) and
- // processDocument function. Obviously, each specified data file should only
- // contain one YAML document (an error will be thrown otherwise). Calls save
- // with an array of processed documents (wiki objects).
- onePerFile: Symbol('Document mode: onePerFile'),
-
- // headerAndEntries: One or more documents per file; the first document is
- // treated as a "header" and represents data which pertains to all following
- // "entry" documents. Expects files array (or function) and
- // processHeaderDocument and processEntryDocument functions. Calls save with
- // an array of {header, entries} objects.
- //
- // Please note that the final results loaded from each file may be "missing"
- // data objects corresponding to entry documents if the processEntryDocument
- // function throws on any entries, resulting in partial data provided to
- // save() - errors will be caught and thrown in the final buildSteps
- // aggregate. However, if the processHeaderDocument function fails, all
- // following documents in the same file will be ignored as well (i.e. an
- // entire file will be excempt from the save() function's input).
- headerAndEntries: Symbol('Document mode: headerAndEntries'),
-
- // allInOne: One or more documents, all contained in one file. Expects file
- // string (or function) and processDocument function. Calls save with an
- // array of processed documents (wiki objects).
- allInOne: Symbol('Document mode: allInOne'),
-
- // oneDocumentTotal: Just a single document, represented in one file.
- // Expects file string (or function) and processDocument function. Calls
- // save with the single processed wiki document (data object).
- //
- // Please note that if the single document fails to process, the save()
- // function won't be called at all, generally resulting in an altogether
- // missing property from the global wikiData object. This should be caught
- // and handled externally.
- oneDocumentTotal: Symbol('Document mode: oneDocumentTotal'),
+ // onePerFile: One document per file. Expects files array (or function) and
+ // processDocument function. Obviously, each specified data file should only
+ // contain one YAML document (an error will be thrown otherwise). Calls save
+ // with an array of processed documents (wiki objects).
+ onePerFile: Symbol("Document mode: onePerFile"),
+
+ // headerAndEntries: One or more documents per file; the first document is
+ // treated as a "header" and represents data which pertains to all following
+ // "entry" documents. Expects files array (or function) and
+ // processHeaderDocument and processEntryDocument functions. Calls save with
+ // an array of {header, entries} objects.
+ //
+ // Please note that the final results loaded from each file may be "missing"
+ // data objects corresponding to entry documents if the processEntryDocument
+ // function throws on any entries, resulting in partial data provided to
+ // save() - errors will be caught and thrown in the final buildSteps
+ // aggregate. However, if the processHeaderDocument function fails, all
+ // following documents in the same file will be ignored as well (i.e. an
+ // entire file will be excempt from the save() function's input).
+ headerAndEntries: Symbol("Document mode: headerAndEntries"),
+
+ // allInOne: One or more documents, all contained in one file. Expects file
+ // string (or function) and processDocument function. Calls save with an
+ // array of processed documents (wiki objects).
+ allInOne: Symbol("Document mode: allInOne"),
+
+ // oneDocumentTotal: Just a single document, represented in one file.
+ // Expects file string (or function) and processDocument function. Calls
+ // save with the single processed wiki document (data object).
+ //
+ // Please note that if the single document fails to process, the save()
+ // function won't be called at all, generally resulting in an altogether
+ // missing property from the global wikiData object. This should be caught
+ // and handled externally.
+ oneDocumentTotal: Symbol("Document mode: oneDocumentTotal"),
};
// dataSteps: Top-level array of "steps" for loading YAML document files.
@@ -626,499 +648,559 @@ export const documentModes = {
// format depends on documentMode.
//
export const dataSteps = [
- {
- title: `Process wiki info file`,
- file: WIKI_INFO_FILE,
+ {
+ title: `Process wiki info file`,
+ file: WIKI_INFO_FILE,
- documentMode: documentModes.oneDocumentTotal,
- processDocument: processWikiInfoDocument,
+ documentMode: documentModes.oneDocumentTotal,
+ processDocument: processWikiInfoDocument,
- save(wikiInfo) {
- if (!wikiInfo) {
- return;
- }
+ save(wikiInfo) {
+ if (!wikiInfo) {
+ return;
+ }
- return {wikiInfo};
- }
+ return { wikiInfo };
+ },
+ },
+
+ {
+ title: `Process album files`,
+ files: async (dataPath) =>
+ (
+ await findFiles(path.join(dataPath, DATA_ALBUM_DIRECTORY), {
+ filter: (f) => path.extname(f) === ".yaml",
+ joinParentDirectory: false,
+ })
+ ).map((file) => path.join(DATA_ALBUM_DIRECTORY, file)),
+
+ documentMode: documentModes.headerAndEntries,
+ processHeaderDocument: processAlbumDocument,
+ processEntryDocument(document) {
+ return "Group" in document
+ ? processTrackGroupDocument(document)
+ : processTrackDocument(document);
},
- {
- title: `Process album files`,
- files: async dataPath => (
- (await findFiles(path.join(dataPath, DATA_ALBUM_DIRECTORY), {
- filter: f => path.extname(f) === '.yaml',
- joinParentDirectory: false
- })).map(file => path.join(DATA_ALBUM_DIRECTORY, file))),
-
- documentMode: documentModes.headerAndEntries,
- processHeaderDocument: processAlbumDocument,
- processEntryDocument(document) {
- return ('Group' in document
- ? processTrackGroupDocument(document)
- : processTrackDocument(document));
- },
-
- save(results) {
- const albumData = [];
- const trackData = [];
-
- for (const { header: album, entries } of results) {
- // We can't mutate an array once it's set as a property
- // value, so prepare the tracks and track groups that will
- // show up in a track list all the way before actually
- // applying them.
- const trackGroups = [];
- let currentTracksByRef = null;
- let currentTrackGroup = null;
-
- const albumRef = Thing.getReference(album);
-
- function closeCurrentTrackGroup() {
- if (currentTracksByRef) {
- let trackGroup;
-
- if (currentTrackGroup) {
- trackGroup = currentTrackGroup;
- } else {
- trackGroup = new TrackGroup();
- trackGroup.name = `Default Track Group`;
- trackGroup.isDefaultTrackGroup = true;
- }
-
- trackGroup.album = album;
- trackGroup.tracksByRef = currentTracksByRef;
- trackGroups.push(trackGroup);
- }
- }
+ save(results) {
+ const albumData = [];
+ const trackData = [];
- for (const entry of entries) {
- if (entry instanceof TrackGroup) {
- closeCurrentTrackGroup();
- currentTracksByRef = [];
- currentTrackGroup = entry;
- continue;
- }
+ for (const { header: album, entries } of results) {
+ // We can't mutate an array once it's set as a property
+ // value, so prepare the tracks and track groups that will
+ // show up in a track list all the way before actually
+ // applying them.
+ const trackGroups = [];
+ let currentTracksByRef = null;
+ let currentTrackGroup = null;
- trackData.push(entry);
+ const albumRef = Thing.getReference(album);
- entry.dataSourceAlbumByRef = albumRef;
+ function closeCurrentTrackGroup() {
+ if (currentTracksByRef) {
+ let trackGroup;
- const trackRef = Thing.getReference(entry);
- if (currentTracksByRef) {
- currentTracksByRef.push(trackRef);
- } else {
- currentTracksByRef = [trackRef];
- }
- }
-
- closeCurrentTrackGroup();
-
- album.trackGroups = trackGroups;
- albumData.push(album);
+ if (currentTrackGroup) {
+ trackGroup = currentTrackGroup;
+ } else {
+ trackGroup = new TrackGroup();
+ trackGroup.name = `Default Track Group`;
+ trackGroup.isDefaultTrackGroup = true;
}
- return {albumData, trackData};
+ trackGroup.album = album;
+ trackGroup.tracksByRef = currentTracksByRef;
+ trackGroups.push(trackGroup);
+ }
}
- },
- {
- title: `Process artists file`,
- file: ARTIST_DATA_FILE,
-
- documentMode: documentModes.allInOne,
- processDocument: processArtistDocument,
-
- save(results) {
- const artistData = results;
-
- const artistAliasData = results.flatMap(artist => {
- const origRef = Thing.getReference(artist);
- return (artist.aliasNames?.map(name => {
- const alias = new Artist();
- alias.name = name;
- alias.isAlias = true;
- alias.aliasedArtistRef = origRef;
- alias.artistData = artistData;
- return alias;
- }) ?? []);
- });
+ for (const entry of entries) {
+ if (entry instanceof TrackGroup) {
+ closeCurrentTrackGroup();
+ currentTracksByRef = [];
+ currentTrackGroup = entry;
+ continue;
+ }
- return {artistData, artistAliasData};
- }
- },
+ trackData.push(entry);
- // TODO: WD.wikiInfo.enableFlashesAndGames &&
- {
- title: `Process flashes file`,
- file: FLASH_DATA_FILE,
+ entry.dataSourceAlbumByRef = albumRef;
- documentMode: documentModes.allInOne,
- processDocument(document) {
- return ('Act' in document
- ? processFlashActDocument(document)
- : processFlashDocument(document));
- },
+ const trackRef = Thing.getReference(entry);
+ if (currentTracksByRef) {
+ currentTracksByRef.push(trackRef);
+ } else {
+ currentTracksByRef = [trackRef];
+ }
+ }
- save(results) {
- let flashAct;
- let flashesByRef = [];
+ closeCurrentTrackGroup();
- if (results[0] && !(results[0] instanceof FlashAct)) {
- throw new Error(`Expected an act at top of flash data file`);
- }
+ album.trackGroups = trackGroups;
+ albumData.push(album);
+ }
- for (const thing of results) {
- if (thing instanceof FlashAct) {
- if (flashAct) {
- Object.assign(flashAct, {flashesByRef});
- }
+ return { albumData, trackData };
+ },
+ },
+
+ {
+ title: `Process artists file`,
+ file: ARTIST_DATA_FILE,
+
+ documentMode: documentModes.allInOne,
+ processDocument: processArtistDocument,
+
+ save(results) {
+ const artistData = results;
+
+ const artistAliasData = results.flatMap((artist) => {
+ const origRef = Thing.getReference(artist);
+ return (
+ artist.aliasNames?.map((name) => {
+ const alias = new Artist();
+ alias.name = name;
+ alias.isAlias = true;
+ alias.aliasedArtistRef = origRef;
+ alias.artistData = artistData;
+ return alias;
+ }) ?? []
+ );
+ });
+
+ return { artistData, artistAliasData };
+ },
+ },
+
+ // TODO: WD.wikiInfo.enableFlashesAndGames &&
+ {
+ title: `Process flashes file`,
+ file: FLASH_DATA_FILE,
+
+ documentMode: documentModes.allInOne,
+ processDocument(document) {
+ return "Act" in document
+ ? processFlashActDocument(document)
+ : processFlashDocument(document);
+ },
- flashAct = thing;
- flashesByRef = [];
- } else {
- flashesByRef.push(Thing.getReference(thing));
- }
- }
+ save(results) {
+ let flashAct;
+ let flashesByRef = [];
- if (flashAct) {
- Object.assign(flashAct, {flashesByRef});
- }
+ if (results[0] && !(results[0] instanceof FlashAct)) {
+ throw new Error(`Expected an act at top of flash data file`);
+ }
- const flashData = results.filter(x => x instanceof Flash);
- const flashActData = results.filter(x => x instanceof FlashAct);
+ for (const thing of results) {
+ if (thing instanceof FlashAct) {
+ if (flashAct) {
+ Object.assign(flashAct, { flashesByRef });
+ }
- return {flashData, flashActData};
+ flashAct = thing;
+ flashesByRef = [];
+ } else {
+ flashesByRef.push(Thing.getReference(thing));
}
- },
+ }
- {
- title: `Process groups file`,
- file: GROUP_DATA_FILE,
+ if (flashAct) {
+ Object.assign(flashAct, { flashesByRef });
+ }
- documentMode: documentModes.allInOne,
- processDocument(document) {
- return ('Category' in document
- ? processGroupCategoryDocument(document)
- : processGroupDocument(document));
- },
+ const flashData = results.filter((x) => x instanceof Flash);
+ const flashActData = results.filter((x) => x instanceof FlashAct);
- save(results) {
- let groupCategory;
- let groupsByRef = [];
+ return { flashData, flashActData };
+ },
+ },
- if (results[0] && !(results[0] instanceof GroupCategory)) {
- throw new Error(`Expected a category at top of group data file`);
- }
+ {
+ title: `Process groups file`,
+ file: GROUP_DATA_FILE,
- for (const thing of results) {
- if (thing instanceof GroupCategory) {
- if (groupCategory) {
- Object.assign(groupCategory, {groupsByRef});
- }
+ documentMode: documentModes.allInOne,
+ processDocument(document) {
+ return "Category" in document
+ ? processGroupCategoryDocument(document)
+ : processGroupDocument(document);
+ },
- groupCategory = thing;
- groupsByRef = [];
- } else {
- groupsByRef.push(Thing.getReference(thing));
- }
- }
+ save(results) {
+ let groupCategory;
+ let groupsByRef = [];
- if (groupCategory) {
- Object.assign(groupCategory, {groupsByRef});
- }
+ if (results[0] && !(results[0] instanceof GroupCategory)) {
+ throw new Error(`Expected a category at top of group data file`);
+ }
- const groupData = results.filter(x => x instanceof Group);
- const groupCategoryData = results.filter(x => x instanceof GroupCategory);
+ for (const thing of results) {
+ if (thing instanceof GroupCategory) {
+ if (groupCategory) {
+ Object.assign(groupCategory, { groupsByRef });
+ }
- return {groupData, groupCategoryData};
+ groupCategory = thing;
+ groupsByRef = [];
+ } else {
+ groupsByRef.push(Thing.getReference(thing));
}
+ }
+
+ if (groupCategory) {
+ Object.assign(groupCategory, { groupsByRef });
+ }
+
+ const groupData = results.filter((x) => x instanceof Group);
+ const groupCategoryData = results.filter(
+ (x) => x instanceof GroupCategory
+ );
+
+ return { groupData, groupCategoryData };
},
+ },
- {
- title: `Process homepage layout file`,
- files: [HOMEPAGE_LAYOUT_DATA_FILE],
+ {
+ title: `Process homepage layout file`,
+ files: [HOMEPAGE_LAYOUT_DATA_FILE],
- documentMode: documentModes.headerAndEntries,
- processHeaderDocument: processHomepageLayoutDocument,
- processEntryDocument: processHomepageLayoutRowDocument,
+ documentMode: documentModes.headerAndEntries,
+ processHeaderDocument: processHomepageLayoutDocument,
+ processEntryDocument: processHomepageLayoutRowDocument,
- save(results) {
- if (!results[0]) {
- return;
- }
+ save(results) {
+ if (!results[0]) {
+ return;
+ }
- const { header: homepageLayout, entries: rows } = results[0];
- Object.assign(homepageLayout, {rows});
- return {homepageLayout};
- }
+ const { header: homepageLayout, entries: rows } = results[0];
+ Object.assign(homepageLayout, { rows });
+ return { homepageLayout };
},
+ },
- // TODO: WD.wikiInfo.enableNews &&
- {
- title: `Process news data file`,
- file: NEWS_DATA_FILE,
+ // TODO: WD.wikiInfo.enableNews &&
+ {
+ title: `Process news data file`,
+ file: NEWS_DATA_FILE,
- documentMode: documentModes.allInOne,
- processDocument: processNewsEntryDocument,
+ documentMode: documentModes.allInOne,
+ processDocument: processNewsEntryDocument,
- save(newsData) {
- sortChronologically(newsData);
- newsData.reverse();
+ save(newsData) {
+ sortChronologically(newsData);
+ newsData.reverse();
- return {newsData};
- }
+ return { newsData };
},
+ },
- {
- title: `Process art tags file`,
- file: ART_TAG_DATA_FILE,
+ {
+ title: `Process art tags file`,
+ file: ART_TAG_DATA_FILE,
- documentMode: documentModes.allInOne,
- processDocument: processArtTagDocument,
+ documentMode: documentModes.allInOne,
+ processDocument: processArtTagDocument,
- save(artTagData) {
- sortAlphabetically(artTagData);
+ save(artTagData) {
+ sortAlphabetically(artTagData);
- return {artTagData};
- }
+ return { artTagData };
},
+ },
- {
- title: `Process static pages file`,
- file: STATIC_PAGE_DATA_FILE,
+ {
+ title: `Process static pages file`,
+ file: STATIC_PAGE_DATA_FILE,
- documentMode: documentModes.allInOne,
- processDocument: processStaticPageDocument,
+ documentMode: documentModes.allInOne,
+ processDocument: processStaticPageDocument,
- save(staticPageData) {
- return {staticPageData};
- }
+ save(staticPageData) {
+ return { staticPageData };
},
+ },
];
-export async function loadAndProcessDataDocuments({
- dataPath,
-}) {
- const processDataAggregate = openAggregate({message: `Errors processing data files`});
- const wikiDataResult = {};
-
- function decorateErrorWithFile(fn) {
- return (x, index, array) => {
- try {
- return fn(x, index, array);
- } catch (error) {
- error.message += (
- (error.message.includes('\n') ? '\n' : ' ') +
- `(file: ${color.bright(color.blue(path.relative(dataPath, x.file)))})`
- );
- throw error;
- }
- };
- }
+export async function loadAndProcessDataDocuments({ dataPath }) {
+ const processDataAggregate = openAggregate({
+ message: `Errors processing data files`,
+ });
+ const wikiDataResult = {};
+
+ function decorateErrorWithFile(fn) {
+ return (x, index, array) => {
+ try {
+ return fn(x, index, array);
+ } catch (error) {
+ error.message +=
+ (error.message.includes("\n") ? "\n" : " ") +
+ `(file: ${color.bright(
+ color.blue(path.relative(dataPath, x.file))
+ )})`;
+ throw error;
+ }
+ };
+ }
- for (const dataStep of dataSteps) {
- await processDataAggregate.nestAsync(
- {message: `Errors during data step: ${dataStep.title}`},
- async ({call, callAsync, map, mapAsync, nest}) => {
- const { documentMode } = dataStep;
+ for (const dataStep of dataSteps) {
+ await processDataAggregate.nestAsync(
+ { message: `Errors during data step: ${dataStep.title}` },
+ async ({ call, callAsync, map, mapAsync, nest }) => {
+ const { documentMode } = dataStep;
- if (!(Object.values(documentModes).includes(documentMode))) {
- throw new Error(`Invalid documentMode: ${documentMode.toString()}`);
- }
+ if (!Object.values(documentModes).includes(documentMode)) {
+ throw new Error(`Invalid documentMode: ${documentMode.toString()}`);
+ }
- if (documentMode === documentModes.allInOne || documentMode === documentModes.oneDocumentTotal) {
- if (!dataStep.file) {
- throw new Error(`Expected 'file' property for ${documentMode.toString()}`);
- }
+ if (
+ documentMode === documentModes.allInOne ||
+ documentMode === documentModes.oneDocumentTotal
+ ) {
+ if (!dataStep.file) {
+ throw new Error(
+ `Expected 'file' property for ${documentMode.toString()}`
+ );
+ }
+
+ const file = path.join(
+ dataPath,
+ typeof dataStep.file === "function"
+ ? await callAsync(dataStep.file, dataPath)
+ : dataStep.file
+ );
- const file = path.join(dataPath,
- (typeof dataStep.file === 'function'
- ? await callAsync(dataStep.file, dataPath)
- : dataStep.file));
+ const readResult = await callAsync(readFile, file, "utf-8");
- const readResult = await callAsync(readFile, file, 'utf-8');
+ if (!readResult) {
+ return;
+ }
- if (!readResult) {
- return;
- }
+ const yamlResult =
+ documentMode === documentModes.oneDocumentTotal
+ ? call(yaml.load, readResult)
+ : call(yaml.loadAll, readResult);
- const yamlResult = (documentMode === documentModes.oneDocumentTotal
- ? call(yaml.load, readResult)
- : call(yaml.loadAll, readResult));
+ if (!yamlResult) {
+ return;
+ }
- if (!yamlResult) {
- return;
- }
+ let processResults;
- let processResults;
-
- if (documentMode === documentModes.oneDocumentTotal) {
- nest({message: `Errors processing document`}, ({ call }) => {
- processResults = call(dataStep.processDocument, yamlResult);
- });
- } else {
- const { result, aggregate } = mapAggregate(
- yamlResult,
- decorateErrorWithIndex(dataStep.processDocument),
- {message: `Errors processing documents`}
- );
- processResults = result;
- call(aggregate.close);
- }
+ if (documentMode === documentModes.oneDocumentTotal) {
+ nest({ message: `Errors processing document` }, ({ call }) => {
+ processResults = call(dataStep.processDocument, yamlResult);
+ });
+ } else {
+ const { result, aggregate } = mapAggregate(
+ yamlResult,
+ decorateErrorWithIndex(dataStep.processDocument),
+ { message: `Errors processing documents` }
+ );
+ processResults = result;
+ call(aggregate.close);
+ }
- if (!processResults) return;
+ if (!processResults) return;
- const saveResult = call(dataStep.save, processResults);
+ const saveResult = call(dataStep.save, processResults);
- if (!saveResult) return;
+ if (!saveResult) return;
- Object.assign(wikiDataResult, saveResult);
+ Object.assign(wikiDataResult, saveResult);
- return;
- }
+ return;
+ }
+
+ if (!dataStep.files) {
+ throw new Error(
+ `Expected 'files' property for ${documentMode.toString()}`
+ );
+ }
- if (!dataStep.files) {
- throw new Error(`Expected 'files' property for ${documentMode.toString()}`);
+ const files = (
+ typeof dataStep.files === "function"
+ ? await callAsync(dataStep.files, dataPath)
+ : dataStep.files
+ ).map((file) => path.join(dataPath, file));
+
+ const readResults = await mapAsync(
+ files,
+ (file) =>
+ readFile(file, "utf-8").then((contents) => ({ file, contents })),
+ { message: `Errors reading data files` }
+ );
+
+ const yamlResults = map(
+ readResults,
+ decorateErrorWithFile(({ file, contents }) => ({
+ file,
+ documents: yaml.loadAll(contents),
+ })),
+ { message: `Errors parsing data files as valid YAML` }
+ );
+
+ let processResults;
+
+ if (documentMode === documentModes.headerAndEntries) {
+ nest(
+ { message: `Errors processing data files as valid documents` },
+ ({ call, map }) => {
+ processResults = [];
+
+ yamlResults.forEach(({ file, documents }) => {
+ const [headerDocument, ...entryDocuments] = documents;
+
+ const header = call(
+ decorateErrorWithFile(({ document }) =>
+ dataStep.processHeaderDocument(document)
+ ),
+ { file, document: headerDocument }
+ );
+
+ // Don't continue processing files whose header
+ // document is invalid - the entire file is excempt
+ // from data in this case.
+ if (!header) {
+ return;
}
- const files = (
- (typeof dataStep.files === 'function'
- ? await callAsync(dataStep.files, dataPath)
- : dataStep.files)
- .map(file => path.join(dataPath, file)));
-
- const readResults = await mapAsync(
- files,
- file => (readFile(file, 'utf-8')
- .then(contents => ({file, contents}))),
- {message: `Errors reading data files`});
-
- const yamlResults = map(
- readResults,
- decorateErrorWithFile(
- ({ file, contents }) => ({file, documents: yaml.loadAll(contents)})),
- {message: `Errors parsing data files as valid YAML`});
-
- let processResults;
-
- if (documentMode === documentModes.headerAndEntries) {
- nest({message: `Errors processing data files as valid documents`}, ({ call, map }) => {
- processResults = [];
-
- yamlResults.forEach(({ file, documents }) => {
- const [ headerDocument, ...entryDocuments ] = documents;
-
- const header = call(
- decorateErrorWithFile(
- ({ document }) => dataStep.processHeaderDocument(document)),
- {file, document: headerDocument});
-
- // Don't continue processing files whose header
- // document is invalid - the entire file is excempt
- // from data in this case.
- if (!header) {
- return;
- }
-
- const entries = map(
- entryDocuments.map(document => ({file, document})),
- decorateErrorWithFile(
- decorateErrorWithIndex(
- ({ document }) => dataStep.processEntryDocument(document))),
- {message: `Errors processing entry documents`});
-
- // Entries may be incomplete (i.e. any errored
- // documents won't have a processed output
- // represented here) - this is intentional! By
- // principle, partial output is preferred over
- // erroring an entire file.
- processResults.push({header, entries});
- });
- });
+ const entries = map(
+ entryDocuments.map((document) => ({ file, document })),
+ decorateErrorWithFile(
+ decorateErrorWithIndex(({ document }) =>
+ dataStep.processEntryDocument(document)
+ )
+ ),
+ { message: `Errors processing entry documents` }
+ );
+
+ // Entries may be incomplete (i.e. any errored
+ // documents won't have a processed output
+ // represented here) - this is intentional! By
+ // principle, partial output is preferred over
+ // erroring an entire file.
+ processResults.push({ header, entries });
+ });
+ }
+ );
+ }
+
+ if (documentMode === documentModes.onePerFile) {
+ nest(
+ { message: `Errors processing data files as valid documents` },
+ ({ call, map }) => {
+ processResults = [];
+
+ yamlResults.forEach(({ file, documents }) => {
+ if (documents.length > 1) {
+ call(
+ decorateErrorWithFile(() => {
+ throw new Error(
+ `Only expected one document to be present per file`
+ );
+ })
+ );
+ return;
}
- if (documentMode === documentModes.onePerFile) {
- nest({message: `Errors processing data files as valid documents`}, ({ call, map }) => {
- processResults = [];
-
- yamlResults.forEach(({ file, documents }) => {
- if (documents.length > 1) {
- call(decorateErrorWithFile(() => {
- throw new Error(`Only expected one document to be present per file`);
- }));
- return;
- }
-
- const result = call(
- decorateErrorWithFile(
- ({ document }) => dataStep.processDocument(document)),
- {file, document: documents[0]});
-
- if (!result) {
- return;
- }
-
- processResults.push(result);
- });
- });
+ const result = call(
+ decorateErrorWithFile(({ document }) =>
+ dataStep.processDocument(document)
+ ),
+ { file, document: documents[0] }
+ );
+
+ if (!result) {
+ return;
}
- const saveResult = call(dataStep.save, processResults);
+ processResults.push(result);
+ });
+ }
+ );
+ }
- if (!saveResult) return;
+ const saveResult = call(dataStep.save, processResults);
- Object.assign(wikiDataResult, saveResult);
- });
- }
+ if (!saveResult) return;
- return {
- aggregate: processDataAggregate,
- result: wikiDataResult
- };
+ Object.assign(wikiDataResult, saveResult);
+ }
+ );
+ }
+
+ return {
+ aggregate: processDataAggregate,
+ result: wikiDataResult,
+ };
}
// Data linking! Basically, provide (portions of) wikiData to the Things which
// require it - they'll expose dynamically computed properties as a result (many
// of which are required for page HTML generation).
export function linkWikiDataArrays(wikiData) {
- function assignWikiData(things, ...keys) {
- for (let i = 0; i < things.length; i++) {
- for (let j = 0; j < keys.length; j++) {
- const key = keys[j];
- things[i][key] = wikiData[key];
- }
- }
+ function assignWikiData(things, ...keys) {
+ for (let i = 0; i < things.length; i++) {
+ for (let j = 0; j < keys.length; j++) {
+ const key = keys[j];
+ things[i][key] = wikiData[key];
+ }
}
-
- const WD = wikiData;
-
- assignWikiData([WD.wikiInfo], 'groupData');
-
- assignWikiData(WD.albumData, 'artistData', 'artTagData', 'groupData', 'trackData');
- WD.albumData.forEach(album => assignWikiData(album.trackGroups, 'trackData'));
-
- assignWikiData(WD.trackData, 'albumData', 'artistData', 'artTagData', 'flashData', 'trackData');
- assignWikiData(WD.artistData, 'albumData', 'artistData', 'flashData', 'trackData');
- assignWikiData(WD.groupData, 'albumData', 'groupCategoryData');
- assignWikiData(WD.groupCategoryData, 'groupData');
- assignWikiData(WD.flashData, 'artistData', 'flashActData', 'trackData');
- assignWikiData(WD.flashActData, 'flashData');
- assignWikiData(WD.artTagData, 'albumData', 'trackData');
- assignWikiData(WD.homepageLayout.rows, 'albumData', 'groupData');
+ }
+
+ const WD = wikiData;
+
+ assignWikiData([WD.wikiInfo], "groupData");
+
+ assignWikiData(
+ WD.albumData,
+ "artistData",
+ "artTagData",
+ "groupData",
+ "trackData"
+ );
+ WD.albumData.forEach((album) =>
+ assignWikiData(album.trackGroups, "trackData")
+ );
+
+ assignWikiData(
+ WD.trackData,
+ "albumData",
+ "artistData",
+ "artTagData",
+ "flashData",
+ "trackData"
+ );
+ assignWikiData(
+ WD.artistData,
+ "albumData",
+ "artistData",
+ "flashData",
+ "trackData"
+ );
+ assignWikiData(WD.groupData, "albumData", "groupCategoryData");
+ assignWikiData(WD.groupCategoryData, "groupData");
+ assignWikiData(WD.flashData, "artistData", "flashActData", "trackData");
+ assignWikiData(WD.flashActData, "flashData");
+ assignWikiData(WD.artTagData, "albumData", "trackData");
+ assignWikiData(WD.homepageLayout.rows, "albumData", "groupData");
}
export function sortWikiDataArrays(wikiData) {
- Object.assign(wikiData, {
- albumData: sortChronologically(wikiData.albumData.slice()),
- trackData: sortAlbumsTracksChronologically(wikiData.trackData.slice()),
- });
-
- // Re-link data arrays, so that every object has the new, sorted versions.
- // Note that the sorting step deliberately creates new arrays (mutating
- // slices instead of the original arrays) - this is so that the object
- // caching system understands that it's working with a new ordering.
- // We still need to actually provide those updated arrays over again!
- linkWikiDataArrays(wikiData);
+ Object.assign(wikiData, {
+ albumData: sortChronologically(wikiData.albumData.slice()),
+ trackData: sortAlbumsTracksChronologically(wikiData.trackData.slice()),
+ });
+
+ // Re-link data arrays, so that every object has the new, sorted versions.
+ // Note that the sorting step deliberately creates new arrays (mutating
+ // slices instead of the original arrays) - this is so that the object
+ // caching system understands that it's working with a new ordering.
+ // We still need to actually provide those updated arrays over again!
+ linkWikiDataArrays(wikiData);
}
// Warn about directories which are reused across more than one of the same type
@@ -1128,63 +1210,76 @@ export function sortWikiDataArrays(wikiData) {
// two tracks share the directory "megalovania", they'll both be skipped for the
// build, for example).
export function filterDuplicateDirectories(wikiData) {
- const deduplicateSpec = [
- 'albumData',
- 'artTagData',
- 'flashData',
- 'groupData',
- 'newsData',
- 'trackData',
- ];
-
- const aggregate = openAggregate({message: `Duplicate directories found`});
- for (const thingDataProp of deduplicateSpec) {
- const thingData = wikiData[thingDataProp];
- aggregate.nest({message: `Duplicate directories found in ${color.green('wikiData.' + thingDataProp)}`}, ({ call }) => {
- const directoryPlaces = Object.create(null);
- const duplicateDirectories = [];
- for (const thing of thingData) {
- const { directory } = thing;
- if (directory in directoryPlaces) {
- directoryPlaces[directory].push(thing);
- duplicateDirectories.push(directory);
- } else {
- directoryPlaces[directory] = [thing];
- }
- }
- if (!duplicateDirectories.length) return;
- duplicateDirectories.sort((a, b) => {
- const aL = a.toLowerCase();
- const bL = b.toLowerCase();
- return aL < bL ? -1 : aL > bL ? 1 : 0;
- });
- for (const directory of duplicateDirectories) {
- const places = directoryPlaces[directory];
- call(() => {
- throw new Error(`Duplicate directory ${color.green(directory)}:\n` +
- places.map(thing => ` - ` + inspect(thing)).join('\n'));
- });
- }
- const allDuplicatedThings = Object.values(directoryPlaces).filter(arr => arr.length > 1).flat();
- const filteredThings = thingData.filter(thing => !allDuplicatedThings.includes(thing));
- wikiData[thingDataProp] = filteredThings;
+ const deduplicateSpec = [
+ "albumData",
+ "artTagData",
+ "flashData",
+ "groupData",
+ "newsData",
+ "trackData",
+ ];
+
+ const aggregate = openAggregate({ message: `Duplicate directories found` });
+ for (const thingDataProp of deduplicateSpec) {
+ const thingData = wikiData[thingDataProp];
+ aggregate.nest(
+ {
+ message: `Duplicate directories found in ${color.green(
+ "wikiData." + thingDataProp
+ )}`,
+ },
+ ({ call }) => {
+ const directoryPlaces = Object.create(null);
+ const duplicateDirectories = [];
+ for (const thing of thingData) {
+ const { directory } = thing;
+ if (directory in directoryPlaces) {
+ directoryPlaces[directory].push(thing);
+ duplicateDirectories.push(directory);
+ } else {
+ directoryPlaces[directory] = [thing];
+ }
+ }
+ if (!duplicateDirectories.length) return;
+ duplicateDirectories.sort((a, b) => {
+ const aL = a.toLowerCase();
+ const bL = b.toLowerCase();
+ return aL < bL ? -1 : aL > bL ? 1 : 0;
});
- }
-
- // TODO: This code closes the aggregate but it generally gets closed again
- // by the caller. This works but it might be weird to assume closing an
- // aggregate twice is okay, maybe there's a better solution? Expose a new
- // function on aggregates for checking if it *would* error?
- // (i.e: errors.length > 0)
- try {
- aggregate.close();
- } catch (error) {
- // Duplicate entries were found and filtered out, resulting in altered
- // wikiData arrays. These must be re-linked so objects receive the new
- // data.
- linkWikiDataArrays(wikiData);
- }
- return aggregate;
+ for (const directory of duplicateDirectories) {
+ const places = directoryPlaces[directory];
+ call(() => {
+ throw new Error(
+ `Duplicate directory ${color.green(directory)}:\n` +
+ places.map((thing) => ` - ` + inspect(thing)).join("\n")
+ );
+ });
+ }
+ const allDuplicatedThings = Object.values(directoryPlaces)
+ .filter((arr) => arr.length > 1)
+ .flat();
+ const filteredThings = thingData.filter(
+ (thing) => !allDuplicatedThings.includes(thing)
+ );
+ wikiData[thingDataProp] = filteredThings;
+ }
+ );
+ }
+
+ // TODO: This code closes the aggregate but it generally gets closed again
+ // by the caller. This works but it might be weird to assume closing an
+ // aggregate twice is okay, maybe there's a better solution? Expose a new
+ // function on aggregates for checking if it *would* error?
+ // (i.e: errors.length > 0)
+ try {
+ aggregate.close();
+ } catch (error) {
+ // Duplicate entries were found and filtered out, resulting in altered
+ // wikiData arrays. These must be re-linked so objects receive the new
+ // data.
+ linkWikiDataArrays(wikiData);
+ }
+ return aggregate;
}
// Warn about references across data which don't match anything. This involves
@@ -1193,102 +1288,166 @@ export function filterDuplicateDirectories(wikiData) {
// any errors). At the same time, we remove errored references from the thing's
// data array.
export function filterReferenceErrors(wikiData) {
- const referenceSpec = [
- ['wikiInfo', {
- divideTrackListsByGroupsByRef: 'group',
- }],
-
- ['albumData', {
- artistContribsByRef: '_contrib',
- coverArtistContribsByRef: '_contrib',
- trackCoverArtistContribsByRef: '_contrib',
- wallpaperArtistContribsByRef: '_contrib',
- bannerArtistContribsByRef: '_contrib',
- groupsByRef: 'group',
- artTagsByRef: 'artTag',
- }],
-
- ['trackData', {
- artistContribsByRef: '_contrib',
- contributorContribsByRef: '_contrib',
- coverArtistContribsByRef: '_contrib',
- referencedTracksByRef: 'track',
- artTagsByRef: 'artTag',
- originalReleaseTrackByRef: 'track',
- }],
-
- ['groupCategoryData', {
- groupsByRef: 'group',
- }],
-
- ['homepageLayout.rows', {
- sourceGroupsByRef: 'group',
- sourceAlbumsByRef: 'album',
- }],
-
- ['flashData', {
- contributorContribsByRef: '_contrib',
- featuredTracksByRef: 'track',
- }],
-
- ['flashActData', {
- flashesByRef: 'flash',
- }],
- ];
-
- function getNestedProp(obj, key) {
- const recursive = (o, k) => (k.length === 1
- ? o[k[0]]
- : recursive(o[k[0]], k.slice(1)));
- const keys = key.split(/(?<=(? {
- const things = Array.isArray(thingData) ? thingData : [thingData];
- for (const thing of things) {
- nest({message: `Reference errors in ${inspect(thing)}`}, ({ filter }) => {
- for (const [ property, findFnKey ] of Object.entries(propSpec)) {
- if (!thing[property]) continue;
- if (findFnKey === '_contrib') {
- thing[property] = filter(thing[property],
- decorateErrorWithIndex(({ who }) => {
- const alias = find.artist(who, wikiData.artistAliasData, {mode: 'quiet'});
- if (alias) {
- const original = find.artist(alias.aliasedArtistRef, wikiData.artistData, {mode: 'quiet'});
- throw new Error(`Reference ${color.red(who)} is to an alias, should be ${color.green(original.name)}`);
- }
- return boundFind.artist(who);
- }),
- {message: `Reference errors in contributions ${color.green(property)} (${color.green('find.artist')})`});
- continue;
- }
- const findFn = boundFind[findFnKey];
- const value = thing[property];
- if (Array.isArray(value)) {
- thing[property] = filter(value, decorateErrorWithIndex(findFn),
- {message: `Reference errors in property ${color.green(property)} (${color.green('find.' + findFnKey)})`});
- } else {
- nest({message: `Reference error in property ${color.green(property)} (${color.green('find.' + findFnKey)})`}, ({ call }) => {
- try {
- call(findFn, value);
- } catch (error) {
- thing[property] = null;
- throw error;
- }
- });
- }
+ const referenceSpec = [
+ [
+ "wikiInfo",
+ {
+ divideTrackListsByGroupsByRef: "group",
+ },
+ ],
+
+ [
+ "albumData",
+ {
+ artistContribsByRef: "_contrib",
+ coverArtistContribsByRef: "_contrib",
+ trackCoverArtistContribsByRef: "_contrib",
+ wallpaperArtistContribsByRef: "_contrib",
+ bannerArtistContribsByRef: "_contrib",
+ groupsByRef: "group",
+ artTagsByRef: "artTag",
+ },
+ ],
+
+ [
+ "trackData",
+ {
+ artistContribsByRef: "_contrib",
+ contributorContribsByRef: "_contrib",
+ coverArtistContribsByRef: "_contrib",
+ referencedTracksByRef: "track",
+ artTagsByRef: "artTag",
+ originalReleaseTrackByRef: "track",
+ },
+ ],
+
+ [
+ "groupCategoryData",
+ {
+ groupsByRef: "group",
+ },
+ ],
+
+ [
+ "homepageLayout.rows",
+ {
+ sourceGroupsByRef: "group",
+ sourceAlbumsByRef: "album",
+ },
+ ],
+
+ [
+ "flashData",
+ {
+ contributorContribsByRef: "_contrib",
+ featuredTracksByRef: "track",
+ },
+ ],
+
+ [
+ "flashActData",
+ {
+ flashesByRef: "flash",
+ },
+ ],
+ ];
+
+ function getNestedProp(obj, key) {
+ const recursive = (o, k) =>
+ k.length === 1 ? o[k[0]] : recursive(o[k[0]], k.slice(1));
+ const keys = key.split(/(?<=(? {
+ const things = Array.isArray(thingData) ? thingData : [thingData];
+ for (const thing of things) {
+ nest(
+ { message: `Reference errors in ${inspect(thing)}` },
+ ({ filter }) => {
+ for (const [property, findFnKey] of Object.entries(propSpec)) {
+ if (!thing[property]) continue;
+ if (findFnKey === "_contrib") {
+ thing[property] = filter(
+ thing[property],
+ decorateErrorWithIndex(({ who }) => {
+ const alias = find.artist(who, wikiData.artistAliasData, {
+ mode: "quiet",
+ });
+ if (alias) {
+ const original = find.artist(
+ alias.aliasedArtistRef,
+ wikiData.artistData,
+ { mode: "quiet" }
+ );
+ throw new Error(
+ `Reference ${color.red(
+ who
+ )} is to an alias, should be ${color.green(
+ original.name
+ )}`
+ );
+ }
+ return boundFind.artist(who);
+ }),
+ {
+ message: `Reference errors in contributions ${color.green(
+ property
+ )} (${color.green("find.artist")})`,
+ }
+ );
+ continue;
+ }
+ const findFn = boundFind[findFnKey];
+ const value = thing[property];
+ if (Array.isArray(value)) {
+ thing[property] = filter(
+ value,
+ decorateErrorWithIndex(findFn),
+ {
+ message: `Reference errors in property ${color.green(
+ property
+ )} (${color.green("find." + findFnKey)})`,
+ }
+ );
+ } else {
+ nest(
+ {
+ message: `Reference error in property ${color.green(
+ property
+ )} (${color.green("find." + findFnKey)})`,
+ },
+ ({ call }) => {
+ try {
+ call(findFn, value);
+ } catch (error) {
+ thing[property] = null;
+ throw error;
+ }
}
- });
+ );
+ }
+ }
}
- });
- }
+ );
+ }
+ }
+ );
+ }
- return aggregate;
+ return aggregate;
}
// Utility function for loading all wiki data from the provided YAML data
@@ -1297,48 +1456,49 @@ export function filterReferenceErrors(wikiData) {
// a boilerplate for more specialized output, or as a quick start in utilities
// where reporting info about data loading isn't as relevant as during the
// main wiki build process.
-export async function quickLoadAllFromYAML(dataPath, {
- showAggregate: customShowAggregate = showAggregate,
-} = {}) {
- const showAggregate = customShowAggregate;
+export async function quickLoadAllFromYAML(
+ dataPath,
+ { showAggregate: customShowAggregate = showAggregate } = {}
+) {
+ const showAggregate = customShowAggregate;
- let wikiData;
+ let wikiData;
- {
- const { aggregate, result } = await loadAndProcessDataDocuments({
- dataPath,
- });
+ {
+ const { aggregate, result } = await loadAndProcessDataDocuments({
+ dataPath,
+ });
- wikiData = result;
-
- try {
- aggregate.close();
- logInfo`Loaded data without errors. (complete data)`;
- } catch (error) {
- showAggregate(error);
- logWarn`Loaded data with errors. (partial data)`;
- }
- }
-
- linkWikiDataArrays(wikiData);
+ wikiData = result;
try {
- filterDuplicateDirectories(wikiData).close();
- logInfo`No duplicate directories found. (complete data)`;
+ aggregate.close();
+ logInfo`Loaded data without errors. (complete data)`;
} catch (error) {
- showAggregate(error);
- logWarn`Duplicate directories found. (partial data)`;
+ showAggregate(error);
+ logWarn`Loaded data with errors. (partial data)`;
}
+ }
- try {
- filterReferenceErrors(wikiData).close();
- logInfo`No reference errors found. (complete data)`;
- } catch (error) {
- showAggregate(error);
- logWarn`Duplicate directories found. (partial data)`;
- }
+ linkWikiDataArrays(wikiData);
+
+ try {
+ filterDuplicateDirectories(wikiData).close();
+ logInfo`No duplicate directories found. (complete data)`;
+ } catch (error) {
+ showAggregate(error);
+ logWarn`Duplicate directories found. (partial data)`;
+ }
+
+ try {
+ filterReferenceErrors(wikiData).close();
+ logInfo`No reference errors found. (complete data)`;
+ } catch (error) {
+ showAggregate(error);
+ logWarn`Duplicate directories found. (partial data)`;
+ }
- sortWikiDataArrays(wikiData);
+ sortWikiDataArrays(wikiData);
- return wikiData;
+ return wikiData;
}
diff --git a/src/file-size-preloader.js b/src/file-size-preloader.js
index d0807cc3..d179e569 100644
--- a/src/file-size-preloader.js
+++ b/src/file-size-preloader.js
@@ -17,84 +17,84 @@
// This only processes files one at a time because I'm lazy and stat calls
// are very, very fast.
-import { stat } from 'fs/promises';
-import { logWarn } from './util/cli.js';
+import { stat } from "fs/promises";
+import { logWarn } from "./util/cli.js";
export default class FileSizePreloader {
- #paths = [];
- #sizes = [];
- #loadedPathIndex = -1;
+ #paths = [];
+ #sizes = [];
+ #loadedPathIndex = -1;
- #loadingPromise = null;
- #resolveLoadingPromise = null;
+ #loadingPromise = null;
+ #resolveLoadingPromise = null;
- loadPaths(...paths) {
- this.#paths.push(...paths.filter(p => !this.#paths.includes(p)));
- return this.#startLoadingPaths();
- }
-
- waitUntilDoneLoading() {
- return this.#loadingPromise ?? Promise.resolve();
- }
-
- #startLoadingPaths() {
- if (this.#loadingPromise) {
- return this.#loadingPromise;
- }
-
- this.#loadingPromise = new Promise((resolve => {
- this.#resolveLoadingPromise = resolve;
- }));
+ loadPaths(...paths) {
+ this.#paths.push(...paths.filter((p) => !this.#paths.includes(p)));
+ return this.#startLoadingPaths();
+ }
- this.#loadNextPath();
+ waitUntilDoneLoading() {
+ return this.#loadingPromise ?? Promise.resolve();
+ }
- return this.#loadingPromise;
+ #startLoadingPaths() {
+ if (this.#loadingPromise) {
+ return this.#loadingPromise;
}
- async #loadNextPath() {
- if (this.#loadedPathIndex === this.#paths.length - 1) {
- return this.#doneLoadingPaths();
- }
+ this.#loadingPromise = new Promise((resolve) => {
+ this.#resolveLoadingPromise = resolve;
+ });
- let size;
+ this.#loadNextPath();
- const path = this.#paths[this.#loadedPathIndex + 1];
+ return this.#loadingPromise;
+ }
- try {
- size = await this.readFileSize(path);
- } catch (error) {
- // Oops! Discard that path, and don't increment the index before
- // moving on, since the next path will now be in its place.
- this.#paths.splice(this.#loadedPathIndex + 1, 1);
- logWarn`Failed to process file size for ${path}: ${error.message}`;
- return this.#loadNextPath();
- }
-
- this.#sizes.push(size);
- this.#loadedPathIndex++;
- return this.#loadNextPath();
+ async #loadNextPath() {
+ if (this.#loadedPathIndex === this.#paths.length - 1) {
+ return this.#doneLoadingPaths();
}
- #doneLoadingPaths() {
- this.#resolveLoadingPromise();
- this.#loadingPromise = null;
- this.#resolveLoadingPromise = null;
- }
+ let size;
- // Override me if you want?
- // The rest of the code here is literally just a queue system, so you could
- // pretty much repurpose it for anything... but there are probably cleaner
- // ways than making an instance or subclass of this and overriding this one
- // method!
- async readFileSize(path) {
- const stats = await stat(path);
- return stats.size;
- }
+ const path = this.#paths[this.#loadedPathIndex + 1];
- getSizeOfPath(path) {
- const index = this.#paths.indexOf(path);
- if (index === -1) return null;
- if (index > this.#loadedPathIndex) return null;
- return this.#sizes[index];
+ try {
+ size = await this.readFileSize(path);
+ } catch (error) {
+ // Oops! Discard that path, and don't increment the index before
+ // moving on, since the next path will now be in its place.
+ this.#paths.splice(this.#loadedPathIndex + 1, 1);
+ logWarn`Failed to process file size for ${path}: ${error.message}`;
+ return this.#loadNextPath();
}
+
+ this.#sizes.push(size);
+ this.#loadedPathIndex++;
+ return this.#loadNextPath();
+ }
+
+ #doneLoadingPaths() {
+ this.#resolveLoadingPromise();
+ this.#loadingPromise = null;
+ this.#resolveLoadingPromise = null;
+ }
+
+ // Override me if you want?
+ // The rest of the code here is literally just a queue system, so you could
+ // pretty much repurpose it for anything... but there are probably cleaner
+ // ways than making an instance or subclass of this and overriding this one
+ // method!
+ async readFileSize(path) {
+ const stats = await stat(path);
+ return stats.size;
+ }
+
+ getSizeOfPath(path) {
+ const index = this.#paths.indexOf(path);
+ if (index === -1) return null;
+ if (index > this.#loadedPathIndex) return null;
+ return this.#sizes[index];
+ }
}
diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js
index 839c1d42..9e78d38d 100644
--- a/src/gen-thumbs.js
+++ b/src/gen-thumbs.js
@@ -72,321 +72,336 @@
// unused). This is just to make the code more porta8le and sta8le, long-term,
// since it avoids a lot of otherwise implic8ted maintenance.
-'use strict';
+"use strict";
-const CACHE_FILE = 'thumbnail-cache.json';
+const CACHE_FILE = "thumbnail-cache.json";
const WARNING_DELAY_TIME = 10000;
-import { spawn } from 'child_process';
-import { createHash } from 'crypto';
-import * as path from 'path';
+import { spawn } from "child_process";
+import { createHash } from "crypto";
+import * as path from "path";
-import {
- readdir,
- readFile,
- writeFile
-} from 'fs/promises'; // Whatcha know! Nice.
-
-import {
- createReadStream
-} from 'fs'; // Still gotta import from 8oth tho, for createReadStream.
+import { readdir, readFile, writeFile } from "fs/promises"; // Whatcha know! Nice.
-import {
- logError,
- logInfo,
- logWarn,
- parseOptions,
- progressPromiseAll
-} from './util/cli.js';
+import { createReadStream } from "fs"; // Still gotta import from 8oth tho, for createReadStream.
import {
- commandExists,
- isMain,
- promisifyProcess,
-} from './util/node-utils.js';
+ logError,
+ logInfo,
+ logWarn,
+ parseOptions,
+ progressPromiseAll,
+} from "./util/cli.js";
+
+import { commandExists, isMain, promisifyProcess } from "./util/node-utils.js";
+
+import { delay, queue } from "./util/sugar.js";
+
+function traverse(
+ startDirPath,
+ { filterFile = () => true, filterDir = () => true } = {}
+) {
+ const recursive = (names, subDirPath) =>
+ Promise.all(
+ names.map((name) =>
+ readdir(path.join(startDirPath, subDirPath, name)).then(
+ (names) =>
+ filterDir(name)
+ ? recursive(names, path.join(subDirPath, name))
+ : [],
+ (err) => (filterFile(name) ? [path.join(subDirPath, name)] : [])
+ )
+ )
+ ).then((pathArrays) => pathArrays.flatMap((x) => x));
-import {
- delay,
- queue,
-} from './util/sugar.js';
-
-function traverse(startDirPath, {
- filterFile = () => true,
- filterDir = () => true
-} = {}) {
- const recursive = (names, subDirPath) => Promise
- .all(names.map(name => readdir(path.join(startDirPath, subDirPath, name)).then(
- names => filterDir(name) ? recursive(names, path.join(subDirPath, name)) : [],
- err => filterFile(name) ? [path.join(subDirPath, name)] : [])))
- .then(pathArrays => pathArrays.flatMap(x => x));
-
- return readdir(startDirPath)
- .then(names => recursive(names, ''));
+ return readdir(startDirPath).then((names) => recursive(names, ""));
}
function readFileMD5(filePath) {
- return new Promise((resolve, reject) => {
- const md5 = createHash('md5');
- const stream = createReadStream(filePath);
- stream.on('data', data => md5.update(data));
- stream.on('end', data => resolve(md5.digest('hex')));
- stream.on('error', err => reject(err));
- });
+ return new Promise((resolve, reject) => {
+ const md5 = createHash("md5");
+ const stream = createReadStream(filePath);
+ stream.on("data", (data) => md5.update(data));
+ stream.on("end", (data) => resolve(md5.digest("hex")));
+ stream.on("error", (err) => reject(err));
+ });
}
async function getImageMagickVersion(spawnConvert) {
- const proc = spawnConvert(['--version'], false);
+ const proc = spawnConvert(["--version"], false);
- let allData = '';
- proc.stdout.on('data', data => {
- allData += data.toString();
- });
+ let allData = "";
+ proc.stdout.on("data", (data) => {
+ allData += data.toString();
+ });
- await promisifyProcess(proc, false);
+ await promisifyProcess(proc, false);
- if (!allData.match(/ImageMagick/i)) {
- return null;
- }
+ if (!allData.match(/ImageMagick/i)) {
+ return null;
+ }
- const match = allData.match(/Version: (.*)/i);
- if (!match) {
- return 'unknown version';
- }
+ const match = allData.match(/Version: (.*)/i);
+ if (!match) {
+ return "unknown version";
+ }
- return match[1];
+ return match[1];
}
async function getSpawnConvert() {
- let fn, description, version;
- if (await commandExists('convert')) {
- fn = args => spawn('convert', args);
- description = 'convert';
- } else if (await commandExists('magick')) {
- fn = (args, prefix = true) => spawn('magick',
- (prefix ? ['convert', ...args] : args));
- description = 'magick convert';
- } else {
- return [`no convert or magick binary`, null];
- }
+ let fn, description, version;
+ if (await commandExists("convert")) {
+ fn = (args) => spawn("convert", args);
+ description = "convert";
+ } else if (await commandExists("magick")) {
+ fn = (args, prefix = true) =>
+ spawn("magick", prefix ? ["convert", ...args] : args);
+ description = "magick convert";
+ } else {
+ return [`no convert or magick binary`, null];
+ }
+
+ version = await getImageMagickVersion(fn);
+
+ if (version === null) {
+ return [`binary --version output didn't indicate it's ImageMagick`];
+ }
+
+ return [`${description} (${version})`, fn];
+}
+
+function generateImageThumbnails(filePath, { spawnConvert }) {
+ const dirname = path.dirname(filePath);
+ const extname = path.extname(filePath);
+ const basename = path.basename(filePath, extname);
+ const output = (name) => path.join(dirname, basename + name + ".jpg");
+
+ const convert = (name, { size, quality }) =>
+ spawnConvert([
+ filePath,
+ "-strip",
+ "-resize",
+ `${size}x${size}>`,
+ "-interlace",
+ "Plane",
+ "-quality",
+ `${quality}%`,
+ output(name),
+ ]);
- version = await getImageMagickVersion(fn);
+ return Promise.all([
+ promisifyProcess(convert(".medium", { size: 400, quality: 95 }), false),
+ promisifyProcess(convert(".small", { size: 250, quality: 85 }), false),
+ ]);
- if (version === null) {
- return [`binary --version output didn't indicate it's ImageMagick`];
+ return new Promise((resolve, reject) => {
+ if (Math.random() < 0.2) {
+ reject(new Error(`Them's the 8r8ks, kiddo!`));
+ } else {
+ resolve();
}
-
- return [`${description} (${version})`, fn];
+ });
}
-function generateImageThumbnails(filePath, {spawnConvert}) {
- const dirname = path.dirname(filePath);
- const extname = path.extname(filePath);
- const basename = path.basename(filePath, extname);
- const output = name => path.join(dirname, basename + name + '.jpg');
-
- const convert = (name, {size, quality}) => spawnConvert([
- filePath,
- '-strip',
- '-resize', `${size}x${size}>`,
- '-interlace', 'Plane',
- '-quality', `${quality}%`,
- output(name)
- ]);
+export default async function genThumbs(
+ mediaPath,
+ { queueSize = 0, quiet = false } = {}
+) {
+ if (!mediaPath) {
+ throw new Error("Expected mediaPath to be passed");
+ }
- return Promise.all([
- promisifyProcess(convert('.medium', {size: 400, quality: 95}), false),
- promisifyProcess(convert('.small', {size: 250, quality: 85}), false)
- ]);
+ const quietInfo = quiet ? () => null : logInfo;
- return new Promise((resolve, reject) => {
- if (Math.random() < 0.2) {
- reject(new Error(`Them's the 8r8ks, kiddo!`));
- } else {
- resolve();
- }
- });
-}
+ const filterFile = (name) => {
+ // TODO: Why is this not working????????
+ // thumbnail-cache.json is 8eing passed through, for some reason.
-export default async function genThumbs(mediaPath, {
- queueSize = 0,
- quiet = false
-} = {}) {
- if (!mediaPath) {
- throw new Error('Expected mediaPath to be passed');
- }
+ const ext = path.extname(name);
+ if (ext !== ".jpg" && ext !== ".png") return false;
- const quietInfo = (quiet
- ? () => null
- : logInfo);
-
- const filterFile = name => {
- // TODO: Why is this not working????????
- // thumbnail-cache.json is 8eing passed through, for some reason.
-
- const ext = path.extname(name);
- if (ext !== '.jpg' && ext !== '.png') return false;
-
- const rest = path.basename(name, ext);
- if (rest.endsWith('.medium') || rest.endsWith('.small')) return false;
-
- return true;
- };
-
- const filterDir = name => {
- if (name === '.git') return false;
- return true;
- };
-
- const [convertInfo, spawnConvert] = await getSpawnConvert() ?? [];
- if (!spawnConvert) {
- logError`${`It looks like you don't have ImageMagick installed.`}`;
- logError`ImageMagick is required to generate thumbnails for display on the wiki.`;
- logError`(Error message: ${convertInfo})`;
- logInfo`You can find info to help install ImageMagick on Linux, Windows, or macOS`;
- logInfo`from its official website: ${`https://imagemagick.org/script/download.php`}`;
- logInfo`If you have trouble working ImageMagick and would like some help, feel free`;
- logInfo`to drop a message in the HSMusic Discord server! ${'https://hsmusic.wiki/discord/'}`;
- return false;
+ const rest = path.basename(name, ext);
+ if (rest.endsWith(".medium") || rest.endsWith(".small")) return false;
+
+ return true;
+ };
+
+ const filterDir = (name) => {
+ if (name === ".git") return false;
+ return true;
+ };
+
+ const [convertInfo, spawnConvert] = (await getSpawnConvert()) ?? [];
+ if (!spawnConvert) {
+ logError`${`It looks like you don't have ImageMagick installed.`}`;
+ logError`ImageMagick is required to generate thumbnails for display on the wiki.`;
+ logError`(Error message: ${convertInfo})`;
+ logInfo`You can find info to help install ImageMagick on Linux, Windows, or macOS`;
+ logInfo`from its official website: ${`https://imagemagick.org/script/download.php`}`;
+ logInfo`If you have trouble working ImageMagick and would like some help, feel free`;
+ logInfo`to drop a message in the HSMusic Discord server! ${"https://hsmusic.wiki/discord/"}`;
+ return false;
+ } else {
+ logInfo`Found ImageMagick binary: ${convertInfo}`;
+ }
+
+ let cache,
+ firstRun = false,
+ failedReadingCache = false;
+ try {
+ cache = JSON.parse(await readFile(path.join(mediaPath, CACHE_FILE)));
+ quietInfo`Cache file successfully read.`;
+ } catch (error) {
+ cache = {};
+ if (error.code === "ENOENT") {
+ firstRun = true;
} else {
- logInfo`Found ImageMagick binary: ${convertInfo}`;
+ failedReadingCache = true;
+ logWarn`Malformed or unreadable cache file: ${error}`;
+ logWarn`You may want to cancel and investigate this!`;
+ logWarn`All-new thumbnails and cache will be generated for this run.`;
+ await delay(WARNING_DELAY_TIME);
}
-
- let cache, firstRun = false, failedReadingCache = false;
- try {
- cache = JSON.parse(await readFile(path.join(mediaPath, CACHE_FILE)));
- quietInfo`Cache file successfully read.`;
- } catch (error) {
- cache = {};
- if (error.code === 'ENOENT') {
- firstRun = true;
- } else {
- failedReadingCache = true;
- logWarn`Malformed or unreadable cache file: ${error}`;
- logWarn`You may want to cancel and investigate this!`;
- logWarn`All-new thumbnails and cache will be generated for this run.`;
- await delay(WARNING_DELAY_TIME);
- }
+ }
+
+ try {
+ await writeFile(path.join(mediaPath, CACHE_FILE), JSON.stringify(cache));
+ quietInfo`Writing to cache file appears to be working.`;
+ } catch (error) {
+ logWarn`Test of cache file writing failed: ${error}`;
+ if (cache) {
+ logWarn`Cache read succeeded: Any newly written thumbs will be unnecessarily regenerated on the next run.`;
+ } else if (firstRun) {
+ logWarn`No cache found: All thumbs will be generated now, and will be unnecessarily regenerated next run.`;
+ } else {
+ logWarn`Cache read failed: All thumbs will be regenerated now, and will be unnecessarily regenerated again next run.`;
}
-
- try {
- await writeFile(path.join(mediaPath, CACHE_FILE), JSON.stringify(cache));
- quietInfo`Writing to cache file appears to be working.`;
- } catch (error) {
- logWarn`Test of cache file writing failed: ${error}`;
- if (cache) {
- logWarn`Cache read succeeded: Any newly written thumbs will be unnecessarily regenerated on the next run.`;
- } else if (firstRun) {
- logWarn`No cache found: All thumbs will be generated now, and will be unnecessarily regenerated next run.`;
- } else {
- logWarn`Cache read failed: All thumbs will be regenerated now, and will be unnecessarily regenerated again next run.`;
- }
- logWarn`You may want to cancel and investigate this!`;
- await delay(WARNING_DELAY_TIME);
+ logWarn`You may want to cancel and investigate this!`;
+ await delay(WARNING_DELAY_TIME);
+ }
+
+ const imagePaths = await traverse(mediaPath, { filterFile, filterDir });
+
+ const imageToMD5Entries = await progressPromiseAll(
+ `Generating MD5s of image files`,
+ queue(
+ imagePaths.map(
+ (imagePath) => () =>
+ readFileMD5(path.join(mediaPath, imagePath)).then(
+ (md5) => [imagePath, md5],
+ (error) => [imagePath, { error }]
+ )
+ ),
+ queueSize
+ )
+ );
+
+ {
+ let error = false;
+ for (const entry of imageToMD5Entries) {
+ if (entry[1].error) {
+ logError`Failed to read ${entry[0]}: ${entry[1].error}`;
+ error = true;
+ }
}
-
- const imagePaths = await traverse(mediaPath, {filterFile, filterDir});
-
- const imageToMD5Entries = await progressPromiseAll(`Generating MD5s of image files`, queue(
- imagePaths.map(imagePath => () => readFileMD5(path.join(mediaPath, imagePath)).then(
- md5 => [imagePath, md5],
- error => [imagePath, {error}]
- )),
- queueSize
- ));
-
- {
- let error = false;
- for (const entry of imageToMD5Entries) {
- if (entry[1].error) {
- logError`Failed to read ${entry[0]}: ${entry[1].error}`;
- error = true;
- }
- }
- if (error) {
- logError`Failed to read at least one image file!`;
- logError`This implies a thumbnail probably won't be generatable.`;
- logError`So, exiting early.`;
- return false;
- } else {
- quietInfo`All image files successfully read.`;
- }
+ if (error) {
+ logError`Failed to read at least one image file!`;
+ logError`This implies a thumbnail probably won't be generatable.`;
+ logError`So, exiting early.`;
+ return false;
+ } else {
+ quietInfo`All image files successfully read.`;
}
+ }
- // Technically we could pro8a8ly mut8te the cache varia8le in-place?
- // 8ut that seems kinda iffy.
- const updatedCache = Object.assign({}, cache);
+ // Technically we could pro8a8ly mut8te the cache varia8le in-place?
+ // 8ut that seems kinda iffy.
+ const updatedCache = Object.assign({}, cache);
- const entriesToGenerate = imageToMD5Entries
- .filter(([filePath, md5]) => md5 !== cache[filePath]);
-
- if (entriesToGenerate.length === 0) {
- logInfo`All image thumbnails are already up-to-date - nice!`;
- return true;
- }
+ const entriesToGenerate = imageToMD5Entries.filter(
+ ([filePath, md5]) => md5 !== cache[filePath]
+ );
- const failed = [];
- const succeeded = [];
- const writeMessageFn = () => `Writing image thumbnails. [failed: ${failed.length}]`;
-
- // This is actually sort of a lie, 8ecause we aren't doing synchronicity.
- // (We pass queueSize = 1 to queue().) 8ut we still use progressPromiseAll,
- // 'cuz the progress indic8tor is very cool and good.
- await progressPromiseAll(writeMessageFn, queue(entriesToGenerate.map(([filePath, md5]) =>
- () => generateImageThumbnails(path.join(mediaPath, filePath)).then(
- () => {
+ if (entriesToGenerate.length === 0) {
+ logInfo`All image thumbnails are already up-to-date - nice!`;
+ return true;
+ }
+
+ const failed = [];
+ const succeeded = [];
+ const writeMessageFn = () =>
+ `Writing image thumbnails. [failed: ${failed.length}]`;
+
+ // This is actually sort of a lie, 8ecause we aren't doing synchronicity.
+ // (We pass queueSize = 1 to queue().) 8ut we still use progressPromiseAll,
+ // 'cuz the progress indic8tor is very cool and good.
+ await progressPromiseAll(
+ writeMessageFn,
+ queue(
+ entriesToGenerate.map(
+ ([filePath, md5]) =>
+ () =>
+ generateImageThumbnails(path.join(mediaPath, filePath)).then(
+ () => {
updatedCache[filePath] = md5;
succeeded.push(filePath);
- },
- error => {
+ },
+ (error) => {
failed.push([filePath, error]);
- }
- )
- )));
-
- if (failed.length > 0) {
- for (const [path, error] of failed) {
- logError`Thumbnails failed to generate for ${path} - ${error}`;
- }
- logWarn`Result is incomplete - the above ${failed.length} thumbnails should be checked for errors.`;
- logWarn`${succeeded.length} successfully generated images won't be regenerated next run, though!`;
- } else {
- logInfo`Generated all (updated) thumbnails successfully!`;
- }
-
- try {
- await writeFile(path.join(mediaPath, CACHE_FILE), JSON.stringify(updatedCache));
- quietInfo`Updated cache file successfully written!`;
- } catch (error) {
- logWarn`Failed to write updated cache file: ${error}`;
- logWarn`Any newly (re)generated thumbnails will be regenerated next run.`;
- logWarn`Sorry about that!`;
+ }
+ )
+ )
+ )
+ );
+
+ if (failed.length > 0) {
+ for (const [path, error] of failed) {
+ logError`Thumbnails failed to generate for ${path} - ${error}`;
}
-
- return true;
+ logWarn`Result is incomplete - the above ${failed.length} thumbnails should be checked for errors.`;
+ logWarn`${succeeded.length} successfully generated images won't be regenerated next run, though!`;
+ } else {
+ logInfo`Generated all (updated) thumbnails successfully!`;
+ }
+
+ try {
+ await writeFile(
+ path.join(mediaPath, CACHE_FILE),
+ JSON.stringify(updatedCache)
+ );
+ quietInfo`Updated cache file successfully written!`;
+ } catch (error) {
+ logWarn`Failed to write updated cache file: ${error}`;
+ logWarn`Any newly (re)generated thumbnails will be regenerated next run.`;
+ logWarn`Sorry about that!`;
+ }
+
+ return true;
}
if (isMain(import.meta.url)) {
- (async function() {
- const miscOptions = await parseOptions(process.argv.slice(2), {
- 'media-path': {
- type: 'value'
- },
- 'queue-size': {
- type: 'value',
- validate(size) {
- if (parseInt(size) !== parseFloat(size)) return 'an integer';
- if (parseInt(size) < 0) return 'a counting number or zero';
- return true;
- }
- },
- queue: {alias: 'queue-size'},
- });
-
- const mediaPath = miscOptions['media-path'] || process.env.HSMUSIC_MEDIA;
- const queueSize = +(miscOptions['queue-size'] ?? 0);
-
- await genThumbs(mediaPath, {queueSize});
- })().catch(err => {
- console.error(err);
+ (async function () {
+ const miscOptions = await parseOptions(process.argv.slice(2), {
+ "media-path": {
+ type: "value",
+ },
+ "queue-size": {
+ type: "value",
+ validate(size) {
+ if (parseInt(size) !== parseFloat(size)) return "an integer";
+ if (parseInt(size) < 0) return "a counting number or zero";
+ return true;
+ },
+ },
+ queue: { alias: "queue-size" },
});
+
+ const mediaPath = miscOptions["media-path"] || process.env.HSMUSIC_MEDIA;
+ const queueSize = +(miscOptions["queue-size"] ?? 0);
+
+ await genThumbs(mediaPath, { queueSize });
+ })().catch((err) => {
+ console.error(err);
+ });
}
diff --git a/src/listing-spec.js b/src/listing-spec.js
index df2b038e..92b9d9db 100644
--- a/src/listing-spec.js
+++ b/src/listing-spec.js
@@ -1,771 +1,968 @@
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
import {
- chunkByProperties,
- getArtistNumContributions,
- getTotalDuration,
- sortAlphabetically,
- sortChronologically,
-} from './util/wiki-data.js';
+ chunkByProperties,
+ getArtistNumContributions,
+ getTotalDuration,
+ sortAlphabetically,
+ sortChronologically,
+} from "./util/wiki-data.js";
const listingSpec = [
- {
- directory: 'albums/by-name',
- stringsKey: 'listAlbums.byName',
-
- data({wikiData}) {
- return sortAlphabetically(wikiData.albumData.slice());
- },
-
- row(album, {link, language}) {
- return language.$('listingPage.listAlbums.byName.item', {
- album: link.album(album),
- tracks: language.countTracks(album.tracks.length, {unit: true})
- });
- }
- },
-
- {
- directory: 'albums/by-tracks',
- stringsKey: 'listAlbums.byTracks',
-
- data({wikiData}) {
- return wikiData.albumData.slice()
- .sort((a, b) => b.tracks.length - a.tracks.length);
- },
-
- row(album, {link, language}) {
- return language.$('listingPage.listAlbums.byTracks.item', {
- album: link.album(album),
- tracks: language.countTracks(album.tracks.length, {unit: true})
- });
- }
- },
-
- {
- directory: 'albums/by-duration',
- stringsKey: 'listAlbums.byDuration',
-
- data({wikiData}) {
- return wikiData.albumData
- .map(album => ({album, duration: getTotalDuration(album.tracks)}))
- .sort((a, b) => b.duration - a.duration);
- },
-
- row({album, duration}, {link, language}) {
- return language.$('listingPage.listAlbums.byDuration.item', {
- album: link.album(album),
- duration: language.formatDuration(duration)
- });
- }
- },
-
- {
- directory: 'albums/by-date',
- stringsKey: 'listAlbums.byDate',
-
- data({wikiData}) {
- return sortChronologically(wikiData.albumData.filter(album => album.date));
- },
-
- row(album, {link, language}) {
- return language.$('listingPage.listAlbums.byDate.item', {
- album: link.album(album),
- date: language.formatDate(album.date)
- });
- }
- },
-
- {
- directory: 'albums/by-date-added',
- stringsKey: 'listAlbums.byDateAdded',
-
- data({wikiData}) {
- return chunkByProperties(wikiData.albumData.filter(a => a.dateAddedToWiki).sort((a, b) => {
- if (a.dateAddedToWiki < b.dateAddedToWiki) return -1;
- if (a.dateAddedToWiki > b.dateAddedToWiki) return 1;
- }), ['dateAddedToWiki']);
- },
-
- html(chunks, {link, language}) {
- return fixWS`
+ {
+ directory: "albums/by-name",
+ stringsKey: "listAlbums.byName",
+
+ data({ wikiData }) {
+ return sortAlphabetically(wikiData.albumData.slice());
+ },
+
+ row(album, { link, language }) {
+ return language.$("listingPage.listAlbums.byName.item", {
+ album: link.album(album),
+ tracks: language.countTracks(album.tracks.length, { unit: true }),
+ });
+ },
+ },
+
+ {
+ directory: "albums/by-tracks",
+ stringsKey: "listAlbums.byTracks",
+
+ data({ wikiData }) {
+ return wikiData.albumData
+ .slice()
+ .sort((a, b) => b.tracks.length - a.tracks.length);
+ },
+
+ row(album, { link, language }) {
+ return language.$("listingPage.listAlbums.byTracks.item", {
+ album: link.album(album),
+ tracks: language.countTracks(album.tracks.length, { unit: true }),
+ });
+ },
+ },
+
+ {
+ directory: "albums/by-duration",
+ stringsKey: "listAlbums.byDuration",
+
+ data({ wikiData }) {
+ return wikiData.albumData
+ .map((album) => ({ album, duration: getTotalDuration(album.tracks) }))
+ .sort((a, b) => b.duration - a.duration);
+ },
+
+ row({ album, duration }, { link, language }) {
+ return language.$("listingPage.listAlbums.byDuration.item", {
+ album: link.album(album),
+ duration: language.formatDuration(duration),
+ });
+ },
+ },
+
+ {
+ directory: "albums/by-date",
+ stringsKey: "listAlbums.byDate",
+
+ data({ wikiData }) {
+ return sortChronologically(
+ wikiData.albumData.filter((album) => album.date)
+ );
+ },
+
+ row(album, { link, language }) {
+ return language.$("listingPage.listAlbums.byDate.item", {
+ album: link.album(album),
+ date: language.formatDate(album.date),
+ });
+ },
+ },
+
+ {
+ directory: "albums/by-date-added",
+ stringsKey: "listAlbums.byDateAdded",
+
+ data({ wikiData }) {
+ return chunkByProperties(
+ wikiData.albumData
+ .filter((a) => a.dateAddedToWiki)
+ .sort((a, b) => {
+ if (a.dateAddedToWiki < b.dateAddedToWiki) return -1;
+ if (a.dateAddedToWiki > b.dateAddedToWiki) return 1;
+ }),
+ ["dateAddedToWiki"]
+ );
+ },
+
+ html(chunks, { link, language }) {
+ return fixWS`
- ${chunks.map(({dateAddedToWiki, chunk: albums}) => fixWS`
- ${language.$('listingPage.listAlbums.byDateAdded.date', {
- date: language.formatDate(dateAddedToWiki)
- })}
+ ${chunks
+ .map(
+ ({ dateAddedToWiki, chunk: albums }) => fixWS`
+ ${language.$(
+ "listingPage.listAlbums.byDateAdded.date",
+ {
+ date: language.formatDate(dateAddedToWiki),
+ }
+ )}
- ${(albums
- .map(album => language.$('listingPage.listAlbums.byDateAdded.album', {
- album: link.album(album)
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ ${albums
+ .map((album) =>
+ language.$(
+ "listingPage.listAlbums.byDateAdded.album",
+ {
+ album: link.album(album),
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
`;
- }
- },
-
- {
- directory: 'artists/by-name',
- stringsKey: 'listArtists.byName',
-
- data({wikiData}) {
- return sortAlphabetically(wikiData.artistData.slice())
- .map(artist => ({artist, contributions: getArtistNumContributions(artist)}));
- },
-
- row({artist, contributions}, {link, language}) {
- return language.$('listingPage.listArtists.byName.item', {
- artist: link.artist(artist),
- contributions: language.countContributions(contributions, {unit: true})
- });
- }
- },
-
- {
- directory: 'artists/by-contribs',
- stringsKey: 'listArtists.byContribs',
-
- data({wikiData}) {
- return {
- toTracks: (wikiData.artistData
- .map(artist => ({
- artist,
- contributions: (
- (artist.tracksAsContributor?.length ?? 0) +
- (artist.tracksAsArtist?.length ?? 0)
- )
- }))
- .sort((a, b) => b.contributions - a.contributions)
- .filter(({ contributions }) => contributions)),
-
- toArtAndFlashes: (wikiData.artistData
- .map(artist => ({
- artist,
- contributions: (
- (artist.tracksAsCoverArtist?.length ?? 0) +
- (artist.albumsAsCoverArtist?.length ?? 0) +
- (artist.albumsAsWallpaperArtist?.length ?? 0) +
- (artist.albumsAsBannerArtist?.length ?? 0) +
- (wikiData.wikiInfo.enableFlashesAndGames
- ? (artist.flashesAsContributor?.length ?? 0)
- : 0)
- )
- }))
- .sort((a, b) => b.contributions - a.contributions)
- .filter(({ contributions }) => contributions)),
-
- // This is a kinda naughty hack, 8ut like, it's the only place
- // we'd 8e passing wikiData to html() otherwise, so like....
- // (Ok we do do this again once later.)
- showAsFlashes: wikiData.wikiInfo.enableFlashesAndGames
- };
- },
-
- html({toTracks, toArtAndFlashes, showAsFlashes}, {link, language}) {
- return fixWS`
+ },
+ },
+
+ {
+ directory: "artists/by-name",
+ stringsKey: "listArtists.byName",
+
+ data({ wikiData }) {
+ return sortAlphabetically(wikiData.artistData.slice()).map((artist) => ({
+ artist,
+ contributions: getArtistNumContributions(artist),
+ }));
+ },
+
+ row({ artist, contributions }, { link, language }) {
+ return language.$("listingPage.listArtists.byName.item", {
+ artist: link.artist(artist),
+ contributions: language.countContributions(contributions, {
+ unit: true,
+ }),
+ });
+ },
+ },
+
+ {
+ directory: "artists/by-contribs",
+ stringsKey: "listArtists.byContribs",
+
+ data({ wikiData }) {
+ return {
+ toTracks: wikiData.artistData
+ .map((artist) => ({
+ artist,
+ contributions:
+ (artist.tracksAsContributor?.length ?? 0) +
+ (artist.tracksAsArtist?.length ?? 0),
+ }))
+ .sort((a, b) => b.contributions - a.contributions)
+ .filter(({ contributions }) => contributions),
+
+ toArtAndFlashes: wikiData.artistData
+ .map((artist) => ({
+ artist,
+ contributions:
+ (artist.tracksAsCoverArtist?.length ?? 0) +
+ (artist.albumsAsCoverArtist?.length ?? 0) +
+ (artist.albumsAsWallpaperArtist?.length ?? 0) +
+ (artist.albumsAsBannerArtist?.length ?? 0) +
+ (wikiData.wikiInfo.enableFlashesAndGames
+ ? artist.flashesAsContributor?.length ?? 0
+ : 0),
+ }))
+ .sort((a, b) => b.contributions - a.contributions)
+ .filter(({ contributions }) => contributions),
+
+ // This is a kinda naughty hack, 8ut like, it's the only place
+ // we'd 8e passing wikiData to html() otherwise, so like....
+ // (Ok we do do this again once later.)
+ showAsFlashes: wikiData.wikiInfo.enableFlashesAndGames,
+ };
+ },
+
+ html({ toTracks, toArtAndFlashes, showAsFlashes }, { link, language }) {
+ return fixWS`
-
${language.$('listingPage.misc.trackContributors')}
+
${language.$(
+ "listingPage.misc.trackContributors"
+ )}
- ${(toTracks
- .map(({ artist, contributions }) => language.$('listingPage.listArtists.byContribs.item', {
+ ${toTracks
+ .map(({ artist, contributions }) =>
+ language.$(
+ "listingPage.listArtists.byContribs.item",
+ {
artist: link.artist(artist),
- contributions: language.countContributions(contributions, {unit: true})
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ contributions: language.countContributions(
+ contributions,
+ { unit: true }
+ ),
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
-
${language.$('listingPage.misc' +
+ ${language.$(
+ "listingPage.misc" +
(showAsFlashes
- ? '.artAndFlashContributors'
- : '.artContributors'))}
+ ? ".artAndFlashContributors"
+ : ".artContributors")
+ )}
- ${(toArtAndFlashes
- .map(({ artist, contributions }) => language.$('listingPage.listArtists.byContribs.item', {
+ ${toArtAndFlashes
+ .map(({ artist, contributions }) =>
+ language.$(
+ "listingPage.listArtists.byContribs.item",
+ {
artist: link.artist(artist),
- contributions: language.countContributions(contributions, {unit: true})
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ contributions: language.countContributions(
+ contributions,
+ { unit: true }
+ ),
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
`;
- }
- },
-
- {
- directory: 'artists/by-commentary',
- stringsKey: 'listArtists.byCommentary',
-
- data({wikiData}) {
- return wikiData.artistData
- .map(artist => ({artist, entries: (
- (artist.tracksAsCommentator?.length ?? 0) +
- (artist.albumsAsCommentator?.length ?? 0)
- )}))
- .filter(({ entries }) => entries)
- .sort((a, b) => b.entries - a.entries);
- },
-
- row({artist, entries}, {link, language}) {
- return language.$('listingPage.listArtists.byCommentary.item', {
- artist: link.artist(artist),
- entries: language.countCommentaryEntries(entries, {unit: true})
- });
- }
- },
-
- {
- directory: 'artists/by-duration',
- stringsKey: 'listArtists.byDuration',
-
- data({wikiData}) {
- return wikiData.artistData
- .map(artist => ({
- artist,
- duration: getTotalDuration([
- ...artist.tracksAsArtist ?? [],
- ...artist.tracksAsContributor ?? []
- ])
- }))
- .filter(({ duration }) => duration > 0)
- .sort((a, b) => b.duration - a.duration);
- },
-
- row({artist, duration}, {link, language}) {
- return language.$('listingPage.listArtists.byDuration.item', {
- artist: link.artist(artist),
- duration: language.formatDuration(duration)
- });
- }
- },
-
- {
- directory: 'artists/by-latest',
- stringsKey: 'listArtists.byLatest',
-
- data({wikiData}) {
- const reversedTracks = sortChronologically(wikiData.trackData.filter(t => t.date)).reverse();
- const reversedArtThings = sortChronologically([...wikiData.trackData, ...wikiData.albumData].filter(t => t.coverArtDate)).reverse();
-
- return {
- toTracks: sortChronologically(wikiData.artistData
- .map(artist => ({
- artist,
- directory: artist.directory,
- name: artist.name,
- date: reversedTracks.find(track => ([
- ...track.artistContribs ?? [],
- ...track.contributorContribs ?? []
- ].some(({ who }) => who === artist)))?.date
- }))
- .filter(({ date }) => date)).reverse(),
-
- toArtAndFlashes: sortChronologically(wikiData.artistData
- .map(artist => {
- const thing = reversedArtThings.find(thing => ([
- ...thing.coverArtistContribs ?? [],
- ...!thing.album && thing.contributorContribs || []
- ].some(({ who }) => who === artist)));
- return thing && {
- artist,
- directory: artist.directory,
- name: artist.name,
- date: (thing.coverArtistContribs?.some(({ who }) => who === artist)
- ? thing.coverArtDate
- : thing.date)
- };
- })
- .filter(Boolean)
- .sort((a, b) => a.name < b.name ? 1 : a.name > b.name ? -1 : 0)
- ).reverse(),
-
- // (Ok we did it again.)
- // This is a kinda naughty hack, 8ut like, it's the only place
- // we'd 8e passing wikiData to html() otherwise, so like....
- showAsFlashes: wikiData.wikiInfo.enableFlashesAndGames
- };
- },
-
- html({toTracks, toArtAndFlashes, showAsFlashes}, {link, language}) {
- return fixWS`
+ },
+ },
+
+ {
+ directory: "artists/by-commentary",
+ stringsKey: "listArtists.byCommentary",
+
+ data({ wikiData }) {
+ return wikiData.artistData
+ .map((artist) => ({
+ artist,
+ entries:
+ (artist.tracksAsCommentator?.length ?? 0) +
+ (artist.albumsAsCommentator?.length ?? 0),
+ }))
+ .filter(({ entries }) => entries)
+ .sort((a, b) => b.entries - a.entries);
+ },
+
+ row({ artist, entries }, { link, language }) {
+ return language.$("listingPage.listArtists.byCommentary.item", {
+ artist: link.artist(artist),
+ entries: language.countCommentaryEntries(entries, { unit: true }),
+ });
+ },
+ },
+
+ {
+ directory: "artists/by-duration",
+ stringsKey: "listArtists.byDuration",
+
+ data({ wikiData }) {
+ return wikiData.artistData
+ .map((artist) => ({
+ artist,
+ duration: getTotalDuration([
+ ...(artist.tracksAsArtist ?? []),
+ ...(artist.tracksAsContributor ?? []),
+ ]),
+ }))
+ .filter(({ duration }) => duration > 0)
+ .sort((a, b) => b.duration - a.duration);
+ },
+
+ row({ artist, duration }, { link, language }) {
+ return language.$("listingPage.listArtists.byDuration.item", {
+ artist: link.artist(artist),
+ duration: language.formatDuration(duration),
+ });
+ },
+ },
+
+ {
+ directory: "artists/by-latest",
+ stringsKey: "listArtists.byLatest",
+
+ data({ wikiData }) {
+ const reversedTracks = sortChronologically(
+ wikiData.trackData.filter((t) => t.date)
+ ).reverse();
+ const reversedArtThings = sortChronologically(
+ [...wikiData.trackData, ...wikiData.albumData].filter(
+ (t) => t.coverArtDate
+ )
+ ).reverse();
+
+ return {
+ toTracks: sortChronologically(
+ wikiData.artistData
+ .map((artist) => ({
+ artist,
+ directory: artist.directory,
+ name: artist.name,
+ date: reversedTracks.find((track) =>
+ [
+ ...(track.artistContribs ?? []),
+ ...(track.contributorContribs ?? []),
+ ].some(({ who }) => who === artist)
+ )?.date,
+ }))
+ .filter(({ date }) => date)
+ ).reverse(),
+
+ toArtAndFlashes: sortChronologically(
+ wikiData.artistData
+ .map((artist) => {
+ const thing = reversedArtThings.find((thing) =>
+ [
+ ...(thing.coverArtistContribs ?? []),
+ ...((!thing.album && thing.contributorContribs) || []),
+ ].some(({ who }) => who === artist)
+ );
+ return (
+ thing && {
+ artist,
+ directory: artist.directory,
+ name: artist.name,
+ date: thing.coverArtistContribs?.some(
+ ({ who }) => who === artist
+ )
+ ? thing.coverArtDate
+ : thing.date,
+ }
+ );
+ })
+ .filter(Boolean)
+ .sort((a, b) => (a.name < b.name ? 1 : a.name > b.name ? -1 : 0))
+ ).reverse(),
+
+ // (Ok we did it again.)
+ // This is a kinda naughty hack, 8ut like, it's the only place
+ // we'd 8e passing wikiData to html() otherwise, so like....
+ showAsFlashes: wikiData.wikiInfo.enableFlashesAndGames,
+ };
+ },
+
+ html({ toTracks, toArtAndFlashes, showAsFlashes }, { link, language }) {
+ return fixWS`
-
${language.$('listingPage.misc.trackContributors')}
+
${language.$(
+ "listingPage.misc.trackContributors"
+ )}
- ${(toTracks
- .map(({ artist, date }) => language.$('listingPage.listArtists.byLatest.item', {
+ ${toTracks
+ .map(({ artist, date }) =>
+ language.$(
+ "listingPage.listArtists.byLatest.item",
+ {
artist: link.artist(artist),
- date: language.formatDate(date)
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ date: language.formatDate(date),
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
-
${language.$('listingPage.misc' +
+ ${language.$(
+ "listingPage.misc" +
(showAsFlashes
- ? '.artAndFlashContributors'
- : '.artContributors'))}
+ ? ".artAndFlashContributors"
+ : ".artContributors")
+ )}
- ${(toArtAndFlashes
- .map(({ artist, date }) => language.$('listingPage.listArtists.byLatest.item', {
+ ${toArtAndFlashes
+ .map(({ artist, date }) =>
+ language.$(
+ "listingPage.listArtists.byLatest.item",
+ {
artist: link.artist(artist),
- date: language.formatDate(date)
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ date: language.formatDate(date),
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
`;
- }
},
-
- {
- directory: 'groups/by-name',
- stringsKey: 'listGroups.byName',
- condition: ({wikiData}) => wikiData.wikiInfo.enableGroupUI,
- data: ({wikiData}) => sortAlphabetically(wikiData.groupData.slice()),
-
- row(group, {link, language}) {
- return language.$('listingPage.listGroups.byCategory.group', {
- group: link.groupInfo(group),
- gallery: link.groupGallery(group, {
- text: language.$('listingPage.listGroups.byCategory.group.gallery')
- })
- });
- }
+ },
+
+ {
+ directory: "groups/by-name",
+ stringsKey: "listGroups.byName",
+ condition: ({ wikiData }) => wikiData.wikiInfo.enableGroupUI,
+ data: ({ wikiData }) => sortAlphabetically(wikiData.groupData.slice()),
+
+ row(group, { link, language }) {
+ return language.$("listingPage.listGroups.byCategory.group", {
+ group: link.groupInfo(group),
+ gallery: link.groupGallery(group, {
+ text: language.$("listingPage.listGroups.byCategory.group.gallery"),
+ }),
+ });
},
+ },
- {
- directory: 'groups/by-category',
- stringsKey: 'listGroups.byCategory',
- condition: ({wikiData}) => wikiData.wikiInfo.enableGroupUI,
- data: ({wikiData}) => wikiData.groupCategoryData,
+ {
+ directory: "groups/by-category",
+ stringsKey: "listGroups.byCategory",
+ condition: ({ wikiData }) => wikiData.wikiInfo.enableGroupUI,
+ data: ({ wikiData }) => wikiData.groupCategoryData,
- html(groupCategoryData, {link, language}) {
- return fixWS`
+ html(groupCategoryData, { link, language }) {
+ return fixWS`
- ${groupCategoryData.map(category => fixWS`
- ${language.$('listingPage.listGroups.byCategory.category', {
- category: link.groupInfo(category.groups[0], {text: category.name})
- })}
+ ${groupCategoryData
+ .map(
+ (category) => fixWS`
+ ${language.$(
+ "listingPage.listGroups.byCategory.category",
+ {
+ category: link.groupInfo(category.groups[0], {
+ text: category.name,
+ }),
+ }
+ )}
- ${(category.groups
- .map(group => language.$('listingPage.listGroups.byCategory.group', {
+ ${category.groups
+ .map((group) =>
+ language.$(
+ "listingPage.listGroups.byCategory.group",
+ {
group: link.groupInfo(group),
gallery: link.groupGallery(group, {
- text: language.$('listingPage.listGroups.byCategory.group.gallery')
- })
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ text: language.$(
+ "listingPage.listGroups.byCategory.group.gallery"
+ ),
+ }),
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
`;
- }
- },
-
- {
- directory: 'groups/by-albums',
- stringsKey: 'listGroups.byAlbums',
- condition: ({wikiData}) => wikiData.wikiInfo.enableGroupUI,
-
- data({wikiData}) {
- return wikiData.groupData
- .map(group => ({group, albums: group.albums.length}))
- .sort((a, b) => b.albums - a.albums);
- },
-
- row({group, albums}, {link, language}) {
- return language.$('listingPage.listGroups.byAlbums.item', {
- group: link.groupInfo(group),
- albums: language.countAlbums(albums, {unit: true})
- });
- }
- },
-
- {
- directory: 'groups/by-tracks',
- stringsKey: 'listGroups.byTracks',
- condition: ({wikiData}) => wikiData.wikiInfo.enableGroupUI,
-
- data({wikiData}) {
- return wikiData.groupData
- .map(group => ({group, tracks: group.albums.reduce((acc, album) => acc + album.tracks.length, 0)}))
- .sort((a, b) => b.tracks - a.tracks);
- },
-
- row({group, tracks}, {link, language}) {
- return language.$('listingPage.listGroups.byTracks.item', {
- group: link.groupInfo(group),
- tracks: language.countTracks(tracks, {unit: true})
- });
- }
- },
-
- {
- directory: 'groups/by-duration',
- stringsKey: 'listGroups.byDuration',
- condition: ({wikiData}) => wikiData.wikiInfo.enableGroupUI,
-
- data({wikiData}) {
- return wikiData.groupData
- .map(group => ({group, duration: getTotalDuration(group.albums.flatMap(album => album.tracks))}))
- .sort((a, b) => b.duration - a.duration);
- },
-
- row({group, duration}, {link, language}) {
- return language.$('listingPage.listGroups.byDuration.item', {
- group: link.groupInfo(group),
- duration: language.formatDuration(duration)
- });
- }
- },
-
- {
- directory: 'groups/by-latest-album',
- stringsKey: 'listGroups.byLatest',
- condition: ({wikiData}) => wikiData.wikiInfo.enableGroupUI,
-
- data({wikiData}) {
- return sortChronologically(wikiData.groupData
- .map(group => {
- const albums = group.albums.filter(a => a.date);
- return albums.length && {
- group,
- directory: group.directory,
- name: group.name,
- date: albums[albums.length - 1].date
- };
- })
- .filter(Boolean)
- // So this is kinda tough to explain, 8ut 8asically, when we
- // reverse the list after sorting it 8y d8te (so that the latest
- // d8tes come first), it also flips the order of groups which
- // share the same d8te. This happens mostly when a single al8um
- // is the l8test in two groups. So, say one such al8um is in the
- // groups "Fandom" and "UMSPAF". Per category order, Fandom is
- // meant to show up 8efore UMSPAF, 8ut when we do the reverse
- // l8ter, that flips them, and UMSPAF ends up displaying 8efore
- // Fandom. So we do an extra reverse here, which will fix that
- // and only affect groups that share the same d8te (8ecause
- // groups that don't will 8e moved 8y the sortChronologically
- // call surrounding this).
- .reverse()).reverse()
- },
-
- row({group, date}, {link, language}) {
- return language.$('listingPage.listGroups.byLatest.item', {
- group: link.groupInfo(group),
- date: language.formatDate(date)
- });
- }
- },
-
- {
- directory: 'tracks/by-name',
- stringsKey: 'listTracks.byName',
-
- data({wikiData}) {
- return sortAlphabetically(wikiData.trackData.slice());
- },
-
- row(track, {link, language}) {
- return language.$('listingPage.listTracks.byName.item', {
- track: link.track(track)
- });
- }
- },
-
- {
- directory: 'tracks/by-album',
- stringsKey: 'listTracks.byAlbum',
- data: ({wikiData}) => wikiData.albumData,
-
- html(albumData, {link, language}) {
- return fixWS`
+ },
+ },
+
+ {
+ directory: "groups/by-albums",
+ stringsKey: "listGroups.byAlbums",
+ condition: ({ wikiData }) => wikiData.wikiInfo.enableGroupUI,
+
+ data({ wikiData }) {
+ return wikiData.groupData
+ .map((group) => ({ group, albums: group.albums.length }))
+ .sort((a, b) => b.albums - a.albums);
+ },
+
+ row({ group, albums }, { link, language }) {
+ return language.$("listingPage.listGroups.byAlbums.item", {
+ group: link.groupInfo(group),
+ albums: language.countAlbums(albums, { unit: true }),
+ });
+ },
+ },
+
+ {
+ directory: "groups/by-tracks",
+ stringsKey: "listGroups.byTracks",
+ condition: ({ wikiData }) => wikiData.wikiInfo.enableGroupUI,
+
+ data({ wikiData }) {
+ return wikiData.groupData
+ .map((group) => ({
+ group,
+ tracks: group.albums.reduce(
+ (acc, album) => acc + album.tracks.length,
+ 0
+ ),
+ }))
+ .sort((a, b) => b.tracks - a.tracks);
+ },
+
+ row({ group, tracks }, { link, language }) {
+ return language.$("listingPage.listGroups.byTracks.item", {
+ group: link.groupInfo(group),
+ tracks: language.countTracks(tracks, { unit: true }),
+ });
+ },
+ },
+
+ {
+ directory: "groups/by-duration",
+ stringsKey: "listGroups.byDuration",
+ condition: ({ wikiData }) => wikiData.wikiInfo.enableGroupUI,
+
+ data({ wikiData }) {
+ return wikiData.groupData
+ .map((group) => ({
+ group,
+ duration: getTotalDuration(
+ group.albums.flatMap((album) => album.tracks)
+ ),
+ }))
+ .sort((a, b) => b.duration - a.duration);
+ },
+
+ row({ group, duration }, { link, language }) {
+ return language.$("listingPage.listGroups.byDuration.item", {
+ group: link.groupInfo(group),
+ duration: language.formatDuration(duration),
+ });
+ },
+ },
+
+ {
+ directory: "groups/by-latest-album",
+ stringsKey: "listGroups.byLatest",
+ condition: ({ wikiData }) => wikiData.wikiInfo.enableGroupUI,
+
+ data({ wikiData }) {
+ return sortChronologically(
+ wikiData.groupData
+ .map((group) => {
+ const albums = group.albums.filter((a) => a.date);
+ return (
+ albums.length && {
+ group,
+ directory: group.directory,
+ name: group.name,
+ date: albums[albums.length - 1].date,
+ }
+ );
+ })
+ .filter(Boolean)
+ // So this is kinda tough to explain, 8ut 8asically, when we
+ // reverse the list after sorting it 8y d8te (so that the latest
+ // d8tes come first), it also flips the order of groups which
+ // share the same d8te. This happens mostly when a single al8um
+ // is the l8test in two groups. So, say one such al8um is in the
+ // groups "Fandom" and "UMSPAF". Per category order, Fandom is
+ // meant to show up 8efore UMSPAF, 8ut when we do the reverse
+ // l8ter, that flips them, and UMSPAF ends up displaying 8efore
+ // Fandom. So we do an extra reverse here, which will fix that
+ // and only affect groups that share the same d8te (8ecause
+ // groups that don't will 8e moved 8y the sortChronologically
+ // call surrounding this).
+ .reverse()
+ ).reverse();
+ },
+
+ row({ group, date }, { link, language }) {
+ return language.$("listingPage.listGroups.byLatest.item", {
+ group: link.groupInfo(group),
+ date: language.formatDate(date),
+ });
+ },
+ },
+
+ {
+ directory: "tracks/by-name",
+ stringsKey: "listTracks.byName",
+
+ data({ wikiData }) {
+ return sortAlphabetically(wikiData.trackData.slice());
+ },
+
+ row(track, { link, language }) {
+ return language.$("listingPage.listTracks.byName.item", {
+ track: link.track(track),
+ });
+ },
+ },
+
+ {
+ directory: "tracks/by-album",
+ stringsKey: "listTracks.byAlbum",
+ data: ({ wikiData }) => wikiData.albumData,
+
+ html(albumData, { link, language }) {
+ return fixWS`
- ${albumData.map(album => fixWS`
- ${language.$('listingPage.listTracks.byAlbum.album', {
- album: link.album(album)
- })}
+ ${albumData
+ .map(
+ (album) => fixWS`
+ ${language.$(
+ "listingPage.listTracks.byAlbum.album",
+ {
+ album: link.album(album),
+ }
+ )}
- ${(album.tracks
- .map(track => language.$('listingPage.listTracks.byAlbum.track', {
- track: link.track(track)
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ ${album.tracks
+ .map((track) =>
+ language.$(
+ "listingPage.listTracks.byAlbum.track",
+ {
+ track: link.track(track),
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
`;
- }
},
+ },
- {
- directory: 'tracks/by-date',
- stringsKey: 'listTracks.byDate',
+ {
+ directory: "tracks/by-date",
+ stringsKey: "listTracks.byDate",
- data({wikiData}) {
- return chunkByProperties(
- sortChronologically(wikiData.trackData.filter(t => t.date)),
- ['album', 'date']
- );
- },
+ data({ wikiData }) {
+ return chunkByProperties(
+ sortChronologically(wikiData.trackData.filter((t) => t.date)),
+ ["album", "date"]
+ );
+ },
- html(chunks, {link, language}) {
- return fixWS`
+ html(chunks, { link, language }) {
+ return fixWS`
- ${chunks.map(({album, date, chunk: tracks}) => fixWS`
- ${language.$('listingPage.listTracks.byDate.album', {
+ ${chunks
+ .map(
+ ({ album, date, chunk: tracks }) => fixWS`
+ ${language.$(
+ "listingPage.listTracks.byDate.album",
+ {
album: link.album(album),
- date: language.formatDate(date)
- })}
+ date: language.formatDate(date),
+ }
+ )}
- ${(tracks
- .map(track => track.aka
- ? `${language.$('listingPage.listTracks.byDate.track.rerelease', {
- track: link.track(track)
- })} `
- : `${language.$('listingPage.listTracks.byDate.track', {
- track: link.track(track)
- })} `)
- .join('\n'))}
+ ${tracks
+ .map((track) =>
+ track.aka
+ ? `${language.$(
+ "listingPage.listTracks.byDate.track.rerelease",
+ {
+ track: link.track(track),
+ }
+ )} `
+ : `${language.$(
+ "listingPage.listTracks.byDate.track",
+ {
+ track: link.track(track),
+ }
+ )} `
+ )
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
`;
- }
},
+ },
- {
- directory: 'tracks/by-duration',
- stringsKey: 'listTracks.byDuration',
+ {
+ directory: "tracks/by-duration",
+ stringsKey: "listTracks.byDuration",
- data({wikiData}) {
- return wikiData.trackData
- .map(track => ({track, duration: track.duration}))
- .filter(({ duration }) => duration > 0)
- .sort((a, b) => b.duration - a.duration);
- },
-
- row({track, duration}, {link, language}) {
- return language.$('listingPage.listTracks.byDuration.item', {
- track: link.track(track),
- duration: language.formatDuration(duration)
- });
- }
+ data({ wikiData }) {
+ return wikiData.trackData
+ .map((track) => ({ track, duration: track.duration }))
+ .filter(({ duration }) => duration > 0)
+ .sort((a, b) => b.duration - a.duration);
},
- {
- directory: 'tracks/by-duration-in-album',
- stringsKey: 'listTracks.byDurationInAlbum',
-
- data({wikiData}) {
- return wikiData.albumData.map(album => ({
- album,
- tracks: album.tracks.slice().sort((a, b) => (b.duration ?? 0) - (a.duration ?? 0))
- }));
- },
+ row({ track, duration }, { link, language }) {
+ return language.$("listingPage.listTracks.byDuration.item", {
+ track: link.track(track),
+ duration: language.formatDuration(duration),
+ });
+ },
+ },
+
+ {
+ directory: "tracks/by-duration-in-album",
+ stringsKey: "listTracks.byDurationInAlbum",
+
+ data({ wikiData }) {
+ return wikiData.albumData.map((album) => ({
+ album,
+ tracks: album.tracks
+ .slice()
+ .sort((a, b) => (b.duration ?? 0) - (a.duration ?? 0)),
+ }));
+ },
- html(albums, {link, language}) {
- return fixWS`
+ html(albums, { link, language }) {
+ return fixWS`
- ${albums.map(({album, tracks}) => fixWS`
- ${language.$('listingPage.listTracks.byDurationInAlbum.album', {
- album: link.album(album)
- })}
+ ${albums
+ .map(
+ ({ album, tracks }) => fixWS`
+ ${language.$(
+ "listingPage.listTracks.byDurationInAlbum.album",
+ {
+ album: link.album(album),
+ }
+ )}
- ${(tracks
- .map(track => language.$('listingPage.listTracks.byDurationInAlbum.track', {
+ ${tracks
+ .map((track) =>
+ language.$(
+ "listingPage.listTracks.byDurationInAlbum.track",
+ {
track: link.track(track),
- duration: language.formatDuration(track.duration ?? 0)
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ duration: language.formatDuration(
+ track.duration ?? 0
+ ),
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
`;
- }
},
-
- {
- directory: 'tracks/by-times-referenced',
- stringsKey: 'listTracks.byTimesReferenced',
-
- data({wikiData}) {
- return wikiData.trackData
- .map(track => ({track, timesReferenced: track.referencedByTracks.length}))
- .filter(({ timesReferenced }) => timesReferenced > 0)
- .sort((a, b) => b.timesReferenced - a.timesReferenced);
- },
-
- row({track, timesReferenced}, {link, language}) {
- return language.$('listingPage.listTracks.byTimesReferenced.item', {
- track: link.track(track),
- timesReferenced: language.countTimesReferenced(timesReferenced, {unit: true})
- });
- }
+ },
+
+ {
+ directory: "tracks/by-times-referenced",
+ stringsKey: "listTracks.byTimesReferenced",
+
+ data({ wikiData }) {
+ return wikiData.trackData
+ .map((track) => ({
+ track,
+ timesReferenced: track.referencedByTracks.length,
+ }))
+ .filter(({ timesReferenced }) => timesReferenced > 0)
+ .sort((a, b) => b.timesReferenced - a.timesReferenced);
},
- {
- directory: 'tracks/in-flashes/by-album',
- stringsKey: 'listTracks.inFlashes.byAlbum',
- condition: ({wikiData}) => wikiData.wikiInfo.enableFlashesAndGames,
-
- data({wikiData}) {
- return chunkByProperties(wikiData.trackData
- .filter(t => t.featuredInFlashes?.length > 0), ['album']);
- },
+ row({ track, timesReferenced }, { link, language }) {
+ return language.$("listingPage.listTracks.byTimesReferenced.item", {
+ track: link.track(track),
+ timesReferenced: language.countTimesReferenced(timesReferenced, {
+ unit: true,
+ }),
+ });
+ },
+ },
+
+ {
+ directory: "tracks/in-flashes/by-album",
+ stringsKey: "listTracks.inFlashes.byAlbum",
+ condition: ({ wikiData }) => wikiData.wikiInfo.enableFlashesAndGames,
+
+ data({ wikiData }) {
+ return chunkByProperties(
+ wikiData.trackData.filter((t) => t.featuredInFlashes?.length > 0),
+ ["album"]
+ );
+ },
- html(chunks, {link, language}) {
- return fixWS`
+ html(chunks, { link, language }) {
+ return fixWS`
- ${chunks.map(({album, chunk: tracks}) => fixWS`
- ${language.$('listingPage.listTracks.inFlashes.byAlbum.album', {
+ ${chunks
+ .map(
+ ({ album, chunk: tracks }) => fixWS`
+ ${language.$(
+ "listingPage.listTracks.inFlashes.byAlbum.album",
+ {
album: link.album(album),
- date: language.formatDate(album.date)
- })}
+ date: language.formatDate(album.date),
+ }
+ )}
- ${(tracks
- .map(track => language.$('listingPage.listTracks.inFlashes.byAlbum.track', {
+ ${tracks
+ .map((track) =>
+ language.$(
+ "listingPage.listTracks.inFlashes.byAlbum.track",
+ {
track: link.track(track),
- flashes: language.formatConjunctionList(track.featuredInFlashes.map(link.flash))
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ flashes: language.formatConjunctionList(
+ track.featuredInFlashes.map(link.flash)
+ ),
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
`;
- }
},
+ },
- {
- directory: 'tracks/in-flashes/by-flash',
- stringsKey: 'listTracks.inFlashes.byFlash',
- condition: ({wikiData}) => wikiData.wikiInfo.enableFlashesAndGames,
- data: ({wikiData}) => wikiData.flashData,
+ {
+ directory: "tracks/in-flashes/by-flash",
+ stringsKey: "listTracks.inFlashes.byFlash",
+ condition: ({ wikiData }) => wikiData.wikiInfo.enableFlashesAndGames,
+ data: ({ wikiData }) => wikiData.flashData,
- html(flashData, {link, language}) {
- return fixWS`
+ html(flashData, { link, language }) {
+ return fixWS`
- ${sortChronologically(flashData.slice()).map(flash => fixWS`
- ${language.$('listingPage.listTracks.inFlashes.byFlash.flash', {
+ ${sortChronologically(flashData.slice())
+ .map(
+ (flash) => fixWS`
+ ${language.$(
+ "listingPage.listTracks.inFlashes.byFlash.flash",
+ {
flash: link.flash(flash),
- date: language.formatDate(flash.date)
- })}
+ date: language.formatDate(flash.date),
+ }
+ )}
- ${(flash.featuredTracks
- .map(track => language.$('listingPage.listTracks.inFlashes.byFlash.track', {
+ ${flash.featuredTracks
+ .map((track) =>
+ language.$(
+ "listingPage.listTracks.inFlashes.byFlash.track",
+ {
track: link.track(track),
- album: link.album(track.album)
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ album: link.album(track.album),
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
`;
- }
+ },
+ },
+
+ {
+ directory: "tracks/with-lyrics",
+ stringsKey: "listTracks.withLyrics",
+
+ data({ wikiData }) {
+ return wikiData.albumData
+ .map((album) => ({
+ album,
+ tracks: album.tracks.filter((t) => t.lyrics),
+ }))
+ .filter(({ tracks }) => tracks.length > 0);
},
- {
- directory: 'tracks/with-lyrics',
- stringsKey: 'listTracks.withLyrics',
-
- data({wikiData}) {
- return wikiData.albumData.map(album => ({
- album,
- tracks: album.tracks.filter(t => t.lyrics)
- })).filter(({ tracks }) => tracks.length > 0);
- },
-
- html(chunks, {link, language}) {
- return fixWS`
+ html(chunks, { link, language }) {
+ return fixWS`
- ${chunks.map(({album, tracks}) => fixWS`
- ${language.$('listingPage.listTracks.withLyrics.album', {
+ ${chunks
+ .map(
+ ({ album, tracks }) => fixWS`
+ ${language.$(
+ "listingPage.listTracks.withLyrics.album",
+ {
album: link.album(album),
- date: language.formatDate(album.date)
- })}
+ date: language.formatDate(album.date),
+ }
+ )}
- ${(tracks
- .map(track => language.$('listingPage.listTracks.withLyrics.track', {
+ ${tracks
+ .map((track) =>
+ language.$(
+ "listingPage.listTracks.withLyrics.track",
+ {
track: link.track(track),
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ }
+ )
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
`;
- }
- },
-
- {
- directory: 'tags/by-name',
- stringsKey: 'listTags.byName',
- condition: ({wikiData}) => wikiData.wikiInfo.enableArtTagUI,
-
- data({wikiData}) {
- return sortAlphabetically(wikiData.artTagData.filter(tag => !tag.isContentWarning))
- .map(tag => ({tag, timesUsed: tag.taggedInThings?.length}));
- },
-
- row({tag, timesUsed}, {link, language}) {
- return language.$('listingPage.listTags.byName.item', {
- tag: link.tag(tag),
- timesUsed: language.countTimesUsed(timesUsed, {unit: true})
- });
- }
- },
-
- {
- directory: 'tags/by-uses',
- stringsKey: 'listTags.byUses',
- condition: ({wikiData}) => wikiData.wikiInfo.enableArtTagUI,
-
- data({wikiData}) {
- return wikiData.artTagData
- .filter(tag => !tag.isContentWarning)
- .map(tag => ({tag, timesUsed: tag.taggedInThings?.length}))
- .sort((a, b) => b.timesUsed - a.timesUsed);
- },
-
- row({tag, timesUsed}, {link, language}) {
- return language.$('listingPage.listTags.byUses.item', {
- tag: link.tag(tag),
- timesUsed: language.countTimesUsed(timesUsed, {unit: true})
- });
- }
- },
-
- {
- directory: 'random',
- stringsKey: 'other.randomPages',
-
- data: ({wikiData}) => ({
- officialAlbumData: wikiData.officialAlbumData,
- fandomAlbumData: wikiData.fandomAlbumData
- }),
+ },
+ },
+
+ {
+ directory: "tags/by-name",
+ stringsKey: "listTags.byName",
+ condition: ({ wikiData }) => wikiData.wikiInfo.enableArtTagUI,
+
+ data({ wikiData }) {
+ return sortAlphabetically(
+ wikiData.artTagData.filter((tag) => !tag.isContentWarning)
+ ).map((tag) => ({ tag, timesUsed: tag.taggedInThings?.length }));
+ },
+
+ row({ tag, timesUsed }, { link, language }) {
+ return language.$("listingPage.listTags.byName.item", {
+ tag: link.tag(tag),
+ timesUsed: language.countTimesUsed(timesUsed, { unit: true }),
+ });
+ },
+ },
+
+ {
+ directory: "tags/by-uses",
+ stringsKey: "listTags.byUses",
+ condition: ({ wikiData }) => wikiData.wikiInfo.enableArtTagUI,
+
+ data({ wikiData }) {
+ return wikiData.artTagData
+ .filter((tag) => !tag.isContentWarning)
+ .map((tag) => ({ tag, timesUsed: tag.taggedInThings?.length }))
+ .sort((a, b) => b.timesUsed - a.timesUsed);
+ },
+
+ row({ tag, timesUsed }, { link, language }) {
+ return language.$("listingPage.listTags.byUses.item", {
+ tag: link.tag(tag),
+ timesUsed: language.countTimesUsed(timesUsed, { unit: true }),
+ });
+ },
+ },
+
+ {
+ directory: "random",
+ stringsKey: "other.randomPages",
+
+ data: ({ wikiData }) => ({
+ officialAlbumData: wikiData.officialAlbumData,
+ fandomAlbumData: wikiData.fandomAlbumData,
+ }),
- html: ({officialAlbumData, fandomAlbumData}, {
- getLinkThemeString,
- language
- }) => fixWS`
+ html: (
+ { officialAlbumData, fandomAlbumData },
+ { getLinkThemeString, language }
+ ) => fixWS`
Choose a link to go to a random page in that category or album! If your browser doesn't support relatively modern JavaScript or you've disabled it, these links won't work - sorry.
(Data files are downloading in the background! Please wait for data to load.)
(Data files have finished being downloaded. The links should work!)
@@ -780,49 +977,73 @@ const listingSpec = [
Random Track (whole site)
${[
- {name: 'Official', albumData: officialAlbumData, code: 'official'},
- {name: 'Fandom', albumData: fandomAlbumData, code: 'fandom'}
- ].map(category => fixWS`
- ${category.name}: (Random Album , Random Track )
- ${category.albumData.map(album => fixWS`
- ${album.name}
- `).join('\n')}
- `).join('\n')}
+ {
+ name: "Official",
+ albumData: officialAlbumData,
+ code: "official",
+ },
+ {
+ name: "Fandom",
+ albumData: fandomAlbumData,
+ code: "fandom",
+ },
+ ]
+ .map(
+ (category) => fixWS`
+ ${category.name}: (Random Album , Random Track )
+
+ `
+ )
+ .join("\n")}
- `
- }
+ `,
+ },
];
-const filterListings = directoryPrefix => listingSpec
- .filter(l => l.directory.startsWith(directoryPrefix));
+const filterListings = (directoryPrefix) =>
+ listingSpec.filter((l) => l.directory.startsWith(directoryPrefix));
const listingTargetSpec = [
- {
- title: ({language}) => language.$('listingPage.target.album'),
- listings: filterListings('album')
- },
- {
- title: ({language}) => language.$('listingPage.target.artist'),
- listings: filterListings('artist')
- },
- {
- title: ({language}) => language.$('listingPage.target.group'),
- listings: filterListings('group')
- },
- {
- title: ({language}) => language.$('listingPage.target.track'),
- listings: filterListings('track')
- },
- {
- title: ({language}) => language.$('listingPage.target.tag'),
- listings: filterListings('tag')
- },
- {
- title: ({language}) => language.$('listingPage.target.other'),
- listings: [
- listingSpec.find(l => l.directory === 'random')
- ]
- }
+ {
+ title: ({ language }) => language.$("listingPage.target.album"),
+ listings: filterListings("album"),
+ },
+ {
+ title: ({ language }) => language.$("listingPage.target.artist"),
+ listings: filterListings("artist"),
+ },
+ {
+ title: ({ language }) => language.$("listingPage.target.group"),
+ listings: filterListings("group"),
+ },
+ {
+ title: ({ language }) => language.$("listingPage.target.track"),
+ listings: filterListings("track"),
+ },
+ {
+ title: ({ language }) => language.$("listingPage.target.tag"),
+ listings: filterListings("tag"),
+ },
+ {
+ title: ({ language }) => language.$("listingPage.target.other"),
+ listings: [listingSpec.find((l) => l.directory === "random")],
+ },
];
-export {listingSpec, listingTargetSpec};
+export { listingSpec, listingTargetSpec };
diff --git a/src/misc-templates.js b/src/misc-templates.js
index 61afa710..4f3a75d0 100644
--- a/src/misc-templates.js
+++ b/src/misc-templates.js
@@ -2,220 +2,283 @@
// These are made available right on a page spec's ({wikiData, language, ...})
// args object!
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
-import * as html from './util/html.js';
+import * as html from "./util/html.js";
-import {
- Track,
- Album,
-} from './data/things.js';
+import { Track, Album } from "./data/things.js";
-import {
- getColors
-} from './util/colors.js';
+import { getColors } from "./util/colors.js";
-import {
- unique
-} from './util/sugar.js';
+import { unique } from "./util/sugar.js";
import {
- getTotalDuration,
- sortAlbumsTracksChronologically,
- sortChronologically,
-} from './util/wiki-data.js';
+ getTotalDuration,
+ sortAlbumsTracksChronologically,
+ sortChronologically,
+} from "./util/wiki-data.js";
-const BANDCAMP_DOMAINS = [
- 'bc.s3m.us',
- 'music.solatrux.com',
-];
+const BANDCAMP_DOMAINS = ["bc.s3m.us", "music.solatrux.com"];
-const MASTODON_DOMAINS = [
- 'types.pl',
-];
+const MASTODON_DOMAINS = ["types.pl"];
// "Additional Files" listing
-export function generateAdditionalFilesShortcut(additionalFiles, {language}) {
- if (!additionalFiles?.length) return '';
+export function generateAdditionalFilesShortcut(additionalFiles, { language }) {
+ if (!additionalFiles?.length) return "";
- return language.$('releaseInfo.additionalFiles.shortcut', {
- anchorLink: `${language.$('releaseInfo.additionalFiles.shortcut.anchorLink')} `,
- titles: language.formatUnitList(additionalFiles.map(g => g.title))
- });
+ return language.$("releaseInfo.additionalFiles.shortcut", {
+ anchorLink: `${language.$(
+ "releaseInfo.additionalFiles.shortcut.anchorLink"
+ )} `,
+ titles: language.formatUnitList(additionalFiles.map((g) => g.title)),
+ });
}
-export function generateAdditionalFilesList(additionalFiles, {language, getFileSize, linkFile}) {
- if (!additionalFiles?.length) return '';
+export function generateAdditionalFilesList(
+ additionalFiles,
+ { language, getFileSize, linkFile }
+) {
+ if (!additionalFiles?.length) return "";
- const fileCount = additionalFiles.flatMap(g => g.files).length;
+ const fileCount = additionalFiles.flatMap((g) => g.files).length;
- return fixWS`
- ${language.$('releaseInfo.additionalFiles.heading', {
- additionalFiles: language.countAdditionalFiles(fileCount, {unit: true})
- })}
+ return fixWS`
+ ${language.$(
+ "releaseInfo.additionalFiles.heading",
+ {
+ additionalFiles: language.countAdditionalFiles(fileCount, {
+ unit: true,
+ }),
+ }
+ )}
- ${additionalFiles.map(({ title, description, files }) => fixWS`
- ${(description
- ? language.$('releaseInfo.additionalFiles.entry.withDescription', {title, description})
- : language.$('releaseInfo.additionalFiles.entry', {title}))}
+ ${additionalFiles
+ .map(
+ ({ title, description, files }) => fixWS`
+ ${
+ description
+ ? language.$(
+ "releaseInfo.additionalFiles.entry.withDescription",
+ { title, description }
+ )
+ : language.$("releaseInfo.additionalFiles.entry", { title })
+ }
- ${files.map(file => {
+ ${files
+ .map((file) => {
const size = getFileSize(file);
- return (size
- ? `${language.$('releaseInfo.additionalFiles.file.withSize', {
+ return size
+ ? ` ${language.$(
+ "releaseInfo.additionalFiles.file.withSize",
+ {
+ file: linkFile(file),
+ size: language.formatFileSize(
+ getFileSize(file)
+ ),
+ }
+ )} `
+ : `${language.$(
+ "releaseInfo.additionalFiles.file",
+ {
file: linkFile(file),
- size: language.formatFileSize(getFileSize(file))
- })} `
- : `${language.$('releaseInfo.additionalFiles.file', {
- file: linkFile(file)
- })} `);
- }).join('\n')}
+ }
+ )}`;
+ })
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
`;
}
// Artist strings
-export function getArtistString(artists, {
- iconifyURL, link, language,
- showIcons = false,
- showContrib = false
-}) {
- return language.formatConjunctionList(artists.map(({ who, what }) => {
- const { urls, directory, name } = who;
- return [
- link.artist(who),
- showContrib && what && `(${what})`,
- showIcons && urls?.length && `(${
- language.formatUnitList(urls.map(url => iconifyURL(url, {language})))
- }) `
- ].filter(Boolean).join(' ');
- }));
+export function getArtistString(
+ artists,
+ { iconifyURL, link, language, showIcons = false, showContrib = false }
+) {
+ return language.formatConjunctionList(
+ artists.map(({ who, what }) => {
+ const { urls, directory, name } = who;
+ return [
+ link.artist(who),
+ showContrib && what && `(${what})`,
+ showIcons &&
+ urls?.length &&
+ `(${language.formatUnitList(
+ urls.map((url) => iconifyURL(url, { language }))
+ )}) `,
+ ]
+ .filter(Boolean)
+ .join(" ");
+ })
+ );
}
// Chronology links
-export function generateChronologyLinks(currentThing, {
- dateKey = 'date',
+export function generateChronologyLinks(
+ currentThing,
+ {
+ dateKey = "date",
contribKey,
getThings,
headingString,
link,
linkAnythingMan,
language,
- wikiData
-}) {
- const { albumData } = wikiData;
-
- const contributions = currentThing[contribKey];
- if (!contributions) {
- return '';
- }
-
- if (contributions.length > 8) {
- return `${language.$('misc.chronology.seeArtistPages')}
`;
- }
-
- return contributions.map(({ who: artist }) => {
- const thingsUnsorted = unique(getThings(artist)).filter(t => t[dateKey]);
-
- // Kinda a hack, but we automatically detect which is (probably) the
- // right function to use here.
- const args = [thingsUnsorted, {getDate: t => t[dateKey]}];
- const things = (thingsUnsorted.every(t => t instanceof Album || t instanceof Track)
- ? sortAlbumsTracksChronologically(...args)
- : sortChronologically(...args));
-
- const index = things.indexOf(currentThing);
-
- if (index === -1) return '';
-
- // TODO: This can pro8a8ly 8e made to use generatePreviousNextLinks?
- // We'd need to make generatePreviousNextLinks use toAnythingMan tho.
- const previous = things[index - 1];
- const next = things[index + 1];
- const parts = [
- previous && linkAnythingMan(previous, {
- color: false,
- text: language.$('misc.nav.previous')
- }),
- next && linkAnythingMan(next, {
- color: false,
- text: language.$('misc.nav.next')
- })
- ].filter(Boolean);
-
- if (!parts.length) {
- return '';
- }
-
- const stringOpts = {
- index: language.formatIndex(index + 1, {language}),
- artist: link.artist(artist)
- };
-
- return fixWS`
+ wikiData,
+ }
+) {
+ const { albumData } = wikiData;
+
+ const contributions = currentThing[contribKey];
+ if (!contributions) {
+ return "";
+ }
+
+ if (contributions.length > 8) {
+ return `${language.$(
+ "misc.chronology.seeArtistPages"
+ )}
`;
+ }
+
+ return contributions
+ .map(({ who: artist }) => {
+ const thingsUnsorted = unique(getThings(artist)).filter(
+ (t) => t[dateKey]
+ );
+
+ // Kinda a hack, but we automatically detect which is (probably) the
+ // right function to use here.
+ const args = [thingsUnsorted, { getDate: (t) => t[dateKey] }];
+ const things = thingsUnsorted.every(
+ (t) => t instanceof Album || t instanceof Track
+ )
+ ? sortAlbumsTracksChronologically(...args)
+ : sortChronologically(...args);
+
+ const index = things.indexOf(currentThing);
+
+ if (index === -1) return "";
+
+ // TODO: This can pro8a8ly 8e made to use generatePreviousNextLinks?
+ // We'd need to make generatePreviousNextLinks use toAnythingMan tho.
+ const previous = things[index - 1];
+ const next = things[index + 1];
+ const parts = [
+ previous &&
+ linkAnythingMan(previous, {
+ color: false,
+ text: language.$("misc.nav.previous"),
+ }),
+ next &&
+ linkAnythingMan(next, {
+ color: false,
+ text: language.$("misc.nav.next"),
+ }),
+ ].filter(Boolean);
+
+ if (!parts.length) {
+ return "";
+ }
+
+ const stringOpts = {
+ index: language.formatIndex(index + 1, { language }),
+ artist: link.artist(artist),
+ };
+
+ return fixWS`
- ${language.$(headingString, stringOpts)}
- ${parts.length && `(${parts.join(', ')}) `}
+ ${language.$(
+ headingString,
+ stringOpts
+ )}
+ ${
+ parts.length &&
+ `(${parts.join(", ")}) `
+ }
`;
- }).filter(Boolean).join('\n');
+ })
+ .filter(Boolean)
+ .join("\n");
}
// Content warning tags
-export function getRevealStringFromWarnings(warnings, {language}) {
- return language.$('misc.contentWarnings', {warnings}) + `${language.$('misc.contentWarnings.reveal')} `
+export function getRevealStringFromWarnings(warnings, { language }) {
+ return (
+ language.$("misc.contentWarnings", { warnings }) +
+ `${language.$(
+ "misc.contentWarnings.reveal"
+ )} `
+ );
}
-export function getRevealStringFromTags(tags, {language}) {
- return tags && tags.some(tag => tag.isContentWarning) && (
- getRevealStringFromWarnings(language.formatUnitList(tags.filter(tag => tag.isContentWarning).map(tag => tag.name)), {language}));
+export function getRevealStringFromTags(tags, { language }) {
+ return (
+ tags &&
+ tags.some((tag) => tag.isContentWarning) &&
+ getRevealStringFromWarnings(
+ language.formatUnitList(
+ tags.filter((tag) => tag.isContentWarning).map((tag) => tag.name)
+ ),
+ { language }
+ )
+ );
}
// Cover art links
export function generateCoverLink({
- img, link, language, to, wikiData,
- src,
- path,
- alt,
- tags = []
+ img,
+ link,
+ language,
+ to,
+ wikiData,
+ src,
+ path,
+ alt,
+ tags = [],
}) {
- const { wikiInfo } = wikiData;
+ const { wikiInfo } = wikiData;
- if (!src && path) {
- src = to(...path);
- }
+ if (!src && path) {
+ src = to(...path);
+ }
- if (!src) {
- throw new Error(`Expected src or path`);
- }
+ if (!src) {
+ throw new Error(`Expected src or path`);
+ }
- return fixWS`
+ return fixWS`
${img({
- src,
- alt,
- thumb: 'medium',
- id: 'cover-art',
- link: true,
- square: true,
- reveal: getRevealStringFromTags(tags, {language})
+ src,
+ alt,
+ thumb: "medium",
+ id: "cover-art",
+ link: true,
+ square: true,
+ reveal: getRevealStringFromTags(tags, { language }),
})}
- ${wikiInfo.enableArtTagUI && tags.filter(tag => !tag.isContentWarning).length && fixWS`
+ ${
+ wikiInfo.enableArtTagUI &&
+ tags.filter((tag) => !tag.isContentWarning).length &&
+ fixWS`
- ${language.$('releaseInfo.artTags')}
- ${(tags
- .filter(tag => !tag.isContentWarning)
- .map(link.tag)
- .join(',\n'))}
+ ${language.$("releaseInfo.artTags")}
+ ${tags
+ .filter((tag) => !tag.isContentWarning)
+ .map(link.tag)
+ .join(",\n")}
- `}
+ `
+ }
`;
}
@@ -223,288 +286,364 @@ export function generateCoverLink({
// CSS & color shenanigans
export function getThemeString(color, additionalVariables = []) {
- if (!color) return '';
+ if (!color) return "";
- const { primary, dim, bg } = getColors(color);
+ const { primary, dim, bg } = getColors(color);
- const variables = [
- `--primary-color: ${primary}`,
- `--dim-color: ${dim}`,
- `--bg-color: ${bg}`,
- ...additionalVariables
- ].filter(Boolean);
+ const variables = [
+ `--primary-color: ${primary}`,
+ `--dim-color: ${dim}`,
+ `--bg-color: ${bg}`,
+ ...additionalVariables,
+ ].filter(Boolean);
- if (!variables.length) return '';
+ if (!variables.length) return "";
- return (
- `:root {\n` +
- variables.map(line => ` ` + line + ';\n').join('') +
- `}`
- );
+ return (
+ `:root {\n` + variables.map((line) => ` ` + line + ";\n").join("") + `}`
+ );
}
-export function getAlbumStylesheet(album, {to}) {
- return [
- album.wallpaperArtistContribs.length && fixWS`
+export function getAlbumStylesheet(album, { to }) {
+ return [
+ album.wallpaperArtistContribs.length &&
+ fixWS`
body::before {
- background-image: url("${to('media.albumWallpaper', album.directory, album.wallpaperFileExtension)}");
+ background-image: url("${to(
+ "media.albumWallpaper",
+ album.directory,
+ album.wallpaperFileExtension
+ )}");
${album.wallpaperStyle}
}
`,
- album.bannerStyle && fixWS`
+ album.bannerStyle &&
+ fixWS`
#banner img {
${album.bannerStyle}
}
- `
- ].filter(Boolean).join('\n');
+ `,
+ ]
+ .filter(Boolean)
+ .join("\n");
}
// Divided track lists
-export function generateTrackListDividedByGroups(tracks, {
- getTrackItem,
- language,
- wikiData,
-}) {
- const { divideTrackListsByGroups: groups } = wikiData.wikiInfo;
+export function generateTrackListDividedByGroups(
+ tracks,
+ { getTrackItem, language, wikiData }
+) {
+ const { divideTrackListsByGroups: groups } = wikiData.wikiInfo;
- if (!groups?.length) {
- return html.tag('ul', tracks.map(t => getTrackItem(t)));
- }
-
- const lists = Object.fromEntries(groups.map(group => [group.directory, {group, tracks: []}]));
- const other = [];
-
- for (const track of tracks) {
- const { album } = track;
- const group = groups.find(g => g.albums.includes(album));
- if (group) {
- lists[group.directory].tracks.push(track);
- } else {
- other.push(track);
- }
+ if (!groups?.length) {
+ return html.tag(
+ "ul",
+ tracks.map((t) => getTrackItem(t))
+ );
+ }
+
+ const lists = Object.fromEntries(
+ groups.map((group) => [group.directory, { group, tracks: [] }])
+ );
+ const other = [];
+
+ for (const track of tracks) {
+ const { album } = track;
+ const group = groups.find((g) => g.albums.includes(album));
+ if (group) {
+ lists[group.directory].tracks.push(track);
+ } else {
+ other.push(track);
}
+ }
- const ddul = tracks => fixWS`
+ const ddul = (tracks) => fixWS`
- ${tracks.map(t => getTrackItem(t)).join('\n')}
+ ${tracks.map((t) => getTrackItem(t)).join("\n")}
`;
- return html.tag('dl', Object.values(lists)
- .filter(({ tracks }) => tracks.length)
- .flatMap(({ group, tracks }) => [
- html.tag('dt', language.formatString('trackList.group', {group: group.name})),
- ddul(tracks)
- ])
- .concat(other.length ? [
- `${language.formatString('trackList.group', {
- group: language.formatString('trackList.group.other')
- })} `,
- ddul(other)
- ] : []));
+ return html.tag(
+ "dl",
+ Object.values(lists)
+ .filter(({ tracks }) => tracks.length)
+ .flatMap(({ group, tracks }) => [
+ html.tag(
+ "dt",
+ language.formatString("trackList.group", { group: group.name })
+ ),
+ ddul(tracks),
+ ])
+ .concat(
+ other.length
+ ? [
+ `${language.formatString("trackList.group", {
+ group: language.formatString("trackList.group.other"),
+ })} `,
+ ddul(other),
+ ]
+ : []
+ )
+ );
}
// Fancy lookin' links
-export function fancifyURL(url, {language, album = false} = {}) {
- let local = Symbol();
- let domain;
- try {
- domain = new URL(url).hostname;
- } catch (error) {
- // No support for relative local URLs yet, sorry! (I.e, local URLs must
- // be absolute relative to the domain name in order to work.)
- domain = local;
- }
- return fixWS`${
- domain === local ? language.$('misc.external.local') :
- domain.includes('bandcamp.com') ? language.$('misc.external.bandcamp') :
- BANDCAMP_DOMAINS.includes(domain) ? language.$('misc.external.bandcamp.domain', {domain}) :
- MASTODON_DOMAINS.includes(domain) ? language.$('misc.external.mastodon.domain', {domain}) :
- domain.includes('youtu') ? (album
- ? (url.includes('list=')
- ? language.$('misc.external.youtube.playlist')
- : language.$('misc.external.youtube.fullAlbum'))
- : language.$('misc.external.youtube')) :
- domain.includes('soundcloud') ? language.$('misc.external.soundcloud') :
- domain.includes('tumblr.com') ? language.$('misc.external.tumblr') :
- domain.includes('twitter.com') ? language.$('misc.external.twitter') :
- domain.includes('deviantart.com') ? language.$('misc.external.deviantart') :
- domain.includes('wikipedia.org') ? language.$('misc.external.wikipedia') :
- domain.includes('poetryfoundation.org') ? language.$('misc.external.poetryFoundation') :
- domain.includes('instagram.com') ? language.$('misc.external.instagram') :
- domain.includes('patreon.com') ? language.$('misc.external.patreon') :
- domain
- } `;
+export function fancifyURL(url, { language, album = false } = {}) {
+ let local = Symbol();
+ let domain;
+ try {
+ domain = new URL(url).hostname;
+ } catch (error) {
+ // No support for relative local URLs yet, sorry! (I.e, local URLs must
+ // be absolute relative to the domain name in order to work.)
+ domain = local;
+ }
+ return fixWS`${
+ domain === local
+ ? language.$("misc.external.local")
+ : domain.includes("bandcamp.com")
+ ? language.$("misc.external.bandcamp")
+ : BANDCAMP_DOMAINS.includes(domain)
+ ? language.$("misc.external.bandcamp.domain", { domain })
+ : MASTODON_DOMAINS.includes(domain)
+ ? language.$("misc.external.mastodon.domain", { domain })
+ : domain.includes("youtu")
+ ? album
+ ? url.includes("list=")
+ ? language.$("misc.external.youtube.playlist")
+ : language.$("misc.external.youtube.fullAlbum")
+ : language.$("misc.external.youtube")
+ : domain.includes("soundcloud")
+ ? language.$("misc.external.soundcloud")
+ : domain.includes("tumblr.com")
+ ? language.$("misc.external.tumblr")
+ : domain.includes("twitter.com")
+ ? language.$("misc.external.twitter")
+ : domain.includes("deviantart.com")
+ ? language.$("misc.external.deviantart")
+ : domain.includes("wikipedia.org")
+ ? language.$("misc.external.wikipedia")
+ : domain.includes("poetryfoundation.org")
+ ? language.$("misc.external.poetryFoundation")
+ : domain.includes("instagram.com")
+ ? language.$("misc.external.instagram")
+ : domain.includes("patreon.com")
+ ? language.$("misc.external.patreon")
+ : domain
+ } `;
}
-export function fancifyFlashURL(url, flash, {language}) {
- const link = fancifyURL(url, {language});
- return `${
- url.includes('homestuck.com') ? (isNaN(Number(flash.page))
- ? language.$('misc.external.flash.homestuck.secret', {link})
- : language.$('misc.external.flash.homestuck.page', {link, page: flash.page})) :
- url.includes('bgreco.net') ? language.$('misc.external.flash.bgreco', {link}) :
- url.includes('youtu') ? language.$('misc.external.flash.youtube', {link}) :
- link
- } `;
+export function fancifyFlashURL(url, flash, { language }) {
+ const link = fancifyURL(url, { language });
+ return `${
+ url.includes("homestuck.com")
+ ? isNaN(Number(flash.page))
+ ? language.$("misc.external.flash.homestuck.secret", { link })
+ : language.$("misc.external.flash.homestuck.page", {
+ link,
+ page: flash.page,
+ })
+ : url.includes("bgreco.net")
+ ? language.$("misc.external.flash.bgreco", { link })
+ : url.includes("youtu")
+ ? language.$("misc.external.flash.youtube", { link })
+ : link
+ } `;
}
-export function iconifyURL(url, {language, to}) {
- const domain = new URL(url).hostname;
- const [ id, msg ] = (
- domain.includes('bandcamp.com') ? ['bandcamp', language.$('misc.external.bandcamp')] :
- BANDCAMP_DOMAINS.includes(domain) ? ['bandcamp', language.$('misc.external.bandcamp.domain', {domain})] :
- MASTODON_DOMAINS.includes(domain) ? ['mastodon', language.$('misc.external.mastodon.domain', {domain})] :
- domain.includes('youtu') ? ['youtube', language.$('misc.external.youtube')] :
- domain.includes('soundcloud') ? ['soundcloud', language.$('misc.external.soundcloud')] :
- domain.includes('tumblr.com') ? ['tumblr', language.$('misc.external.tumblr')] :
- domain.includes('twitter.com') ? ['twitter', language.$('misc.external.twitter')] :
- domain.includes('deviantart.com') ? ['deviantart', language.$('misc.external.deviantart')] :
- domain.includes('instagram.com') ? ['instagram', language.$('misc.external.bandcamp')] :
- ['globe', language.$('misc.external.domain', {domain})]
- );
- return fixWS`${msg} `;
+export function iconifyURL(url, { language, to }) {
+ const domain = new URL(url).hostname;
+ const [id, msg] = domain.includes("bandcamp.com")
+ ? ["bandcamp", language.$("misc.external.bandcamp")]
+ : BANDCAMP_DOMAINS.includes(domain)
+ ? ["bandcamp", language.$("misc.external.bandcamp.domain", { domain })]
+ : MASTODON_DOMAINS.includes(domain)
+ ? ["mastodon", language.$("misc.external.mastodon.domain", { domain })]
+ : domain.includes("youtu")
+ ? ["youtube", language.$("misc.external.youtube")]
+ : domain.includes("soundcloud")
+ ? ["soundcloud", language.$("misc.external.soundcloud")]
+ : domain.includes("tumblr.com")
+ ? ["tumblr", language.$("misc.external.tumblr")]
+ : domain.includes("twitter.com")
+ ? ["twitter", language.$("misc.external.twitter")]
+ : domain.includes("deviantart.com")
+ ? ["deviantart", language.$("misc.external.deviantart")]
+ : domain.includes("instagram.com")
+ ? ["instagram", language.$("misc.external.bandcamp")]
+ : ["globe", language.$("misc.external.domain", { domain })];
+ return fixWS`${msg} `;
}
// Grids
export function getGridHTML({
- img,
- language,
-
- entries,
- srcFn,
- linkFn,
- noSrcTextFn = () => '',
- altFn = () => '',
- detailsFn = null,
- lazy = true
+ img,
+ language,
+
+ entries,
+ srcFn,
+ linkFn,
+ noSrcTextFn = () => "",
+ altFn = () => "",
+ detailsFn = null,
+ lazy = true,
}) {
- return entries.map(({ large, item }, i) => linkFn(item,
- {
- class: ['grid-item', 'box', large && 'large-grid-item'],
- text: fixWS`
+ return entries
+ .map(({ large, item }, i) =>
+ linkFn(item, {
+ class: ["grid-item", "box", large && "large-grid-item"],
+ text: fixWS`
${img({
- src: srcFn(item),
- alt: altFn(item),
- thumb: 'small',
- lazy: (typeof lazy === 'number' ? i >= lazy : lazy),
- square: true,
- reveal: getRevealStringFromTags(item.artTags, {language}),
- noSrcText: noSrcTextFn(item)
+ src: srcFn(item),
+ alt: altFn(item),
+ thumb: "small",
+ lazy: typeof lazy === "number" ? i >= lazy : lazy,
+ square: true,
+ reveal: getRevealStringFromTags(item.artTags, { language }),
+ noSrcText: noSrcTextFn(item),
})}
${item.name}
${detailsFn && `${detailsFn(item)} `}
- `
- })).join('\n');
+ `,
+ })
+ )
+ .join("\n");
}
export function getAlbumGridHTML({
- getAlbumCover, getGridHTML, link, language,
- details = false,
- ...props
+ getAlbumCover,
+ getGridHTML,
+ link,
+ language,
+ details = false,
+ ...props
}) {
- return getGridHTML({
- srcFn: getAlbumCover,
- linkFn: link.album,
- detailsFn: details && (album => language.$('misc.albumGrid.details', {
- tracks: language.countTracks(album.tracks.length, {unit: true}),
- time: language.formatDuration(getTotalDuration(album.tracks))
+ return getGridHTML({
+ srcFn: getAlbumCover,
+ linkFn: link.album,
+ detailsFn:
+ details &&
+ ((album) =>
+ language.$("misc.albumGrid.details", {
+ tracks: language.countTracks(album.tracks.length, { unit: true }),
+ time: language.formatDuration(getTotalDuration(album.tracks)),
})),
- noSrcTextFn: album => language.$('misc.albumGrid.noCoverArt', {
- album: album.name
- }),
- ...props
- });
+ noSrcTextFn: (album) =>
+ language.$("misc.albumGrid.noCoverArt", {
+ album: album.name,
+ }),
+ ...props,
+ });
}
export function getFlashGridHTML({
- getFlashCover, getGridHTML, link,
- ...props
+ getFlashCover,
+ getGridHTML,
+ link,
+ ...props
}) {
- return getGridHTML({
- srcFn: getFlashCover,
- linkFn: link.flash,
- ...props
- });
+ return getGridHTML({
+ srcFn: getFlashCover,
+ linkFn: link.flash,
+ ...props,
+ });
}
// Nav-bar links
-export function generateInfoGalleryLinks(currentThing, isGallery, {
- link, language,
- linkKeyGallery,
- linkKeyInfo
-}) {
- return [
- link[linkKeyInfo](currentThing, {
- class: isGallery ? '' : 'current',
- text: language.$('misc.nav.info')
- }),
- link[linkKeyGallery](currentThing, {
- class: isGallery ? 'current' : '',
- text: language.$('misc.nav.gallery')
- })
- ].join(', ');
+export function generateInfoGalleryLinks(
+ currentThing,
+ isGallery,
+ { link, language, linkKeyGallery, linkKeyInfo }
+) {
+ return [
+ link[linkKeyInfo](currentThing, {
+ class: isGallery ? "" : "current",
+ text: language.$("misc.nav.info"),
+ }),
+ link[linkKeyGallery](currentThing, {
+ class: isGallery ? "current" : "",
+ text: language.$("misc.nav.gallery"),
+ }),
+ ].join(", ");
}
-export function generatePreviousNextLinks(current, {
- data,
- link,
- linkKey,
- language
-}) {
- const linkFn = link[linkKey];
-
- const index = data.indexOf(current);
- const previous = data[index - 1];
- const next = data[index + 1];
-
- return [
- previous && linkFn(previous, {
- attributes: {
- id: 'previous-button',
- title: previous.name
- },
- text: language.$('misc.nav.previous'),
- color: false
- }),
- next && linkFn(next, {
- attributes: {
- id: 'next-button',
- title: next.name
- },
- text: language.$('misc.nav.next'),
- color: false
- })
- ].filter(Boolean).join(', ');
+export function generatePreviousNextLinks(
+ current,
+ { data, link, linkKey, language }
+) {
+ const linkFn = link[linkKey];
+
+ const index = data.indexOf(current);
+ const previous = data[index - 1];
+ const next = data[index + 1];
+
+ return [
+ previous &&
+ linkFn(previous, {
+ attributes: {
+ id: "previous-button",
+ title: previous.name,
+ },
+ text: language.$("misc.nav.previous"),
+ color: false,
+ }),
+ next &&
+ linkFn(next, {
+ attributes: {
+ id: "next-button",
+ title: next.name,
+ },
+ text: language.$("misc.nav.next"),
+ color: false,
+ }),
+ ]
+ .filter(Boolean)
+ .join(", ");
}
// Footer stuff
-export function getFooterLocalizationLinks(pathname, {
- defaultLanguage,
- languages,
- paths,
- language,
- to
-}) {
- const { toPath } = paths;
- const keySuffix = toPath[0].replace(/^localized\./, '.');
- const toArgs = toPath.slice(1);
-
- const links = Object.entries(languages)
- .filter(([ code, language ]) => code !== 'default' && !language.hidden)
- .map(([ code, language ]) => language)
- .sort(({ name: a }, { name: b }) => a < b ? -1 : a > b ? 1 : 0)
- .map(language => html.tag('span', html.tag('a', {
- href: (language === defaultLanguage
- ? to('localizedDefaultLanguage' + keySuffix, ...toArgs)
- : to('localizedWithBaseDirectory' + keySuffix, language.code, ...toArgs))
- }, language.name)));
-
- return html.tag('div',
- {class: 'footer-localization-links'},
- language.$('misc.uiLanguage', {languages: links.join('\n')}));
+export function getFooterLocalizationLinks(
+ pathname,
+ { defaultLanguage, languages, paths, language, to }
+) {
+ const { toPath } = paths;
+ const keySuffix = toPath[0].replace(/^localized\./, ".");
+ const toArgs = toPath.slice(1);
+
+ const links = Object.entries(languages)
+ .filter(([code, language]) => code !== "default" && !language.hidden)
+ .map(([code, language]) => language)
+ .sort(({ name: a }, { name: b }) => (a < b ? -1 : a > b ? 1 : 0))
+ .map((language) =>
+ html.tag(
+ "span",
+ html.tag(
+ "a",
+ {
+ href:
+ language === defaultLanguage
+ ? to("localizedDefaultLanguage" + keySuffix, ...toArgs)
+ : to(
+ "localizedWithBaseDirectory" + keySuffix,
+ language.code,
+ ...toArgs
+ ),
+ },
+ language.name
+ )
+ )
+ );
+
+ return html.tag(
+ "div",
+ { class: "footer-localization-links" },
+ language.$("misc.uiLanguage", { languages: links.join("\n") })
+ );
}
diff --git a/src/page/album-commentary.js b/src/page/album-commentary.js
index 57135a4a..3c197239 100644
--- a/src/page/album-commentary.js
+++ b/src/page/album-commentary.js
@@ -2,143 +2,182 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
-import {
- filterAlbumsByCommentary
-} from '../util/wiki-data.js';
+import { filterAlbumsByCommentary } from "../util/wiki-data.js";
// Page exports
-export function condition({wikiData}) {
- return filterAlbumsByCommentary(wikiData.albumData).length;
+export function condition({ wikiData }) {
+ return filterAlbumsByCommentary(wikiData.albumData).length;
}
-export function targets({wikiData}) {
- return filterAlbumsByCommentary(wikiData.albumData);
+export function targets({ wikiData }) {
+ return filterAlbumsByCommentary(wikiData.albumData);
}
-export function write(album, {wikiData}) {
- const { wikiInfo } = wikiData;
-
- const entries = [album, ...album.tracks].filter(x => x.commentary).map(x => x.commentary);
- const words = entries.join(' ').split(' ').length;
-
- const page = {
- type: 'page',
- path: ['albumCommentary', album.directory],
- page: ({
- getAlbumStylesheet,
- getLinkThemeString,
- getThemeString,
- link,
- language,
- to,
- transformMultiline
- }) => ({
- title: language.$('albumCommentaryPage.title', {album: album.name}),
- stylesheet: getAlbumStylesheet(album),
- theme: getThemeString(album.color),
-
- main: {
- content: fixWS`
+export function write(album, { wikiData }) {
+ const { wikiInfo } = wikiData;
+
+ const entries = [album, ...album.tracks]
+ .filter((x) => x.commentary)
+ .map((x) => x.commentary);
+ const words = entries.join(" ").split(" ").length;
+
+ const page = {
+ type: "page",
+ path: ["albumCommentary", album.directory],
+ page: ({
+ getAlbumStylesheet,
+ getLinkThemeString,
+ getThemeString,
+ link,
+ language,
+ to,
+ transformMultiline,
+ }) => ({
+ title: language.$("albumCommentaryPage.title", { album: album.name }),
+ stylesheet: getAlbumStylesheet(album),
+ theme: getThemeString(album.color),
+
+ main: {
+ content: fixWS`
-
${language.$('albumCommentaryPage.title', {
- album: link.album(album)
+ ${language.$("albumCommentaryPage.title", {
+ album: link.album(album),
})}
- ${language.$('albumCommentaryPage.infoLine', {
- words: `${language.formatWordCount(words, {unit: true})} `,
- entries: `${language.countCommentaryEntries(entries.length, {unit: true})} `
+
${language.$("albumCommentaryPage.infoLine", {
+ words: `${language.formatWordCount(words, {
+ unit: true,
+ })} `,
+ entries: `${language.countCommentaryEntries(
+ entries.length,
+ { unit: true }
+ )} `,
})}
- ${album.commentary && fixWS`
-
${language.$('albumCommentaryPage.entry.title.albumCommentary')}
+ ${
+ album.commentary &&
+ fixWS`
+
${language.$(
+ "albumCommentaryPage.entry.title.albumCommentary"
+ )}
${transformMultiline(album.commentary)}
- `}
- ${album.tracks.filter(t => t.commentary).map(track => fixWS`
-
${language.$('albumCommentaryPage.entry.title.trackCommentary', {
- track: link.track(track)
- })}
-
+ `
+ }
+ ${album.tracks
+ .filter((t) => t.commentary)
+ .map(
+ (track) => fixWS`
+ ${language.$(
+ "albumCommentaryPage.entry.title.trackCommentary",
+ {
+ track: link.track(track),
+ }
+ )}
+
${transformMultiline(track.commentary)}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
- `
- },
-
- nav: {
- linkContainerClasses: ['nav-links-hierarchy'],
- links: [
- {toHome: true},
- {
- path: ['localized.commentaryIndex'],
- title: language.$('commentaryIndex.title')
- },
- {
- html: language.$('albumCommentaryPage.nav.album', {
- album: link.albumCommentary(album, {class: 'current'})
- })
- }
- ]
- }
- })
- };
-
- return [page];
+ `,
+ },
+
+ nav: {
+ linkContainerClasses: ["nav-links-hierarchy"],
+ links: [
+ { toHome: true },
+ {
+ path: ["localized.commentaryIndex"],
+ title: language.$("commentaryIndex.title"),
+ },
+ {
+ html: language.$("albumCommentaryPage.nav.album", {
+ album: link.albumCommentary(album, { class: "current" }),
+ }),
+ },
+ ],
+ },
+ }),
+ };
+
+ return [page];
}
-export function writeTargetless({wikiData}) {
- const data = filterAlbumsByCommentary(wikiData.albumData)
- .map(album => ({
- album,
- entries: [album, ...album.tracks].filter(x => x.commentary).map(x => x.commentary)
- }))
- .map(({ album, entries }) => ({
- album, entries,
- words: entries.join(' ').split(' ').length
- }));
-
- const totalEntries = data.reduce((acc, {entries}) => acc + entries.length, 0);
- const totalWords = data.reduce((acc, {words}) => acc + words, 0);
-
- const page = {
- type: 'page',
- path: ['commentaryIndex'],
- page: ({
- link,
- language
- }) => ({
- title: language.$('commentaryIndex.title'),
-
- main: {
- content: fixWS`
+export function writeTargetless({ wikiData }) {
+ const data = filterAlbumsByCommentary(wikiData.albumData)
+ .map((album) => ({
+ album,
+ entries: [album, ...album.tracks]
+ .filter((x) => x.commentary)
+ .map((x) => x.commentary),
+ }))
+ .map(({ album, entries }) => ({
+ album,
+ entries,
+ words: entries.join(" ").split(" ").length,
+ }));
+
+ const totalEntries = data.reduce(
+ (acc, { entries }) => acc + entries.length,
+ 0
+ );
+ const totalWords = data.reduce((acc, { words }) => acc + words, 0);
+
+ const page = {
+ type: "page",
+ path: ["commentaryIndex"],
+ page: ({ link, language }) => ({
+ title: language.$("commentaryIndex.title"),
+
+ main: {
+ content: fixWS`
-
${language.$('commentaryIndex.title')}
-
${language.$('commentaryIndex.infoLine', {
- words: `${language.formatWordCount(totalWords, {unit: true})} `,
- entries: `${language.countCommentaryEntries(totalEntries, {unit: true})} `
+
${language.$("commentaryIndex.title")}
+
${language.$("commentaryIndex.infoLine", {
+ words: `${language.formatWordCount(totalWords, {
+ unit: true,
+ })} `,
+ entries: `${language.countCommentaryEntries(
+ totalEntries,
+ { unit: true }
+ )} `,
})}
-
${language.$('commentaryIndex.albumList.title')}
+
${language.$("commentaryIndex.albumList.title")}
${data
- .map(({ album, entries, words }) => fixWS`
- ${language.$('commentaryIndex.albumList.item', {
+ .map(
+ ({ album, entries, words }) => fixWS`
+ ${language.$(
+ "commentaryIndex.albumList.item",
+ {
album: link.albumCommentary(album),
- words: language.formatWordCount(words, {unit: true}),
- entries: language.countCommentaryEntries(entries.length, {unit: true})
- })}
- `)
- .join('\n')}
+ words: language.formatWordCount(words, {
+ unit: true,
+ }),
+ entries:
+ language.countCommentaryEntries(
+ entries.length,
+ { unit: true }
+ ),
+ }
+ )}
+ `
+ )
+ .join("\n")}
- `
- },
+ `,
+ },
- nav: {simple: true}
- })
- };
+ nav: { simple: true },
+ }),
+ };
- return [page];
+ return [page];
}
diff --git a/src/page/album.js b/src/page/album.js
index c265fdc6..48747d7f 100644
--- a/src/page/album.js
+++ b/src/page/album.js
@@ -2,297 +2,394 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
-import * as html from '../util/html.js';
+import * as html from "../util/html.js";
-import {
- bindOpts,
- compareArrays,
-} from '../util/sugar.js';
+import { bindOpts, compareArrays } from "../util/sugar.js";
import {
- getAlbumCover,
- getAlbumListTag,
- getTotalDuration,
-} from '../util/wiki-data.js';
+ getAlbumCover,
+ getAlbumListTag,
+ getTotalDuration,
+} from "../util/wiki-data.js";
// Page exports
-export function targets({wikiData}) {
- return wikiData.albumData;
+export function targets({ wikiData }) {
+ return wikiData.albumData;
}
-export function write(album, {wikiData}) {
- const { wikiInfo } = wikiData;
+export function write(album, { wikiData }) {
+ const { wikiInfo } = wikiData;
- const unbound_trackToListItem = (track, {
+ const unbound_trackToListItem = (
+ track,
+ { getArtistString, getLinkThemeString, link, language }
+ ) => {
+ const itemOpts = {
+ duration: language.formatDuration(track.duration ?? 0),
+ track: link.track(track),
+ };
+ return `${
+ compareArrays(
+ track.artistContribs.map((c) => c.who),
+ album.artistContribs.map((c) => c.who),
+ { checkOrder: false }
+ )
+ ? language.$("trackList.item.withDuration", itemOpts)
+ : language.$("trackList.item.withDuration.withArtists", {
+ ...itemOpts,
+ by: `${language.$(
+ "trackList.item.withArtists.by",
+ {
+ artists: getArtistString(track.artistContribs),
+ }
+ )} `,
+ })
+ } `;
+ };
+
+ const hasCommentaryEntries =
+ [album, ...album.tracks].filter((x) => x.commentary).length > 0;
+ const hasAdditionalFiles = album.additionalFiles?.length > 0;
+ const albumDuration = getTotalDuration(album.tracks);
+
+ const listTag = getAlbumListTag(album);
+
+ const data = {
+ type: "data",
+ path: ["album", album.directory],
+ data: ({
+ serializeContribs,
+ serializeCover,
+ serializeGroupsForAlbum,
+ serializeLink,
+ }) => ({
+ name: album.name,
+ directory: album.directory,
+ dates: {
+ released: album.date,
+ trackArtAdded: album.trackArtDate,
+ coverArtAdded: album.coverArtDate,
+ addedToWiki: album.dateAddedToWiki,
+ },
+ duration: albumDuration,
+ color: album.color,
+ cover: serializeCover(album, getAlbumCover),
+ artistContribs: serializeContribs(album.artistContribs),
+ coverArtistContribs: serializeContribs(album.coverArtistContribs),
+ wallpaperArtistContribs: serializeContribs(album.wallpaperArtistContribs),
+ bannerArtistContribs: serializeContribs(album.bannerArtistContribs),
+ groups: serializeGroupsForAlbum(album),
+ trackGroups: album.trackGroups?.map((trackGroup) => ({
+ name: trackGroup.name,
+ color: trackGroup.color,
+ tracks: trackGroup.tracks.map((track) => track.directory),
+ })),
+ tracks: album.tracks.map((track) => ({
+ link: serializeLink(track),
+ duration: track.duration,
+ })),
+ }),
+ };
+
+ const page = {
+ type: "page",
+ path: ["album", album.directory],
+ page: ({
+ fancifyURL,
+ generateAdditionalFilesShortcut,
+ generateAdditionalFilesList,
+ generateChronologyLinks,
+ generateCoverLink,
+ getAlbumCover,
+ getAlbumStylesheet,
+ getArtistString,
+ getLinkThemeString,
+ getSizeOfAdditionalFile,
+ getThemeString,
+ link,
+ language,
+ transformMultiline,
+ urls,
+ }) => {
+ const trackToListItem = bindOpts(unbound_trackToListItem, {
getArtistString,
getLinkThemeString,
link,
- language
- }) => {
- const itemOpts = {
- duration: language.formatDuration(track.duration ?? 0),
- track: link.track(track)
- };
- return `${
- (compareArrays(
- track.artistContribs.map(c => c.who),
- album.artistContribs.map(c => c.who),
- {checkOrder: false})
- ? language.$('trackList.item.withDuration', itemOpts)
- : language.$('trackList.item.withDuration.withArtists', {
- ...itemOpts,
- by: `${
- language.$('trackList.item.withArtists.by', {
- artists: getArtistString(track.artistContribs)
- })
- } `
- }))
- } `;
- };
-
- const hasCommentaryEntries = ([album, ...album.tracks].filter(x => x.commentary).length > 0);
- const hasAdditionalFiles = (album.additionalFiles?.length > 0);
- const albumDuration = getTotalDuration(album.tracks);
-
- const listTag = getAlbumListTag(album);
-
- const data = {
- type: 'data',
- path: ['album', album.directory],
- data: ({
- serializeContribs,
- serializeCover,
- serializeGroupsForAlbum,
- serializeLink
- }) => ({
- name: album.name,
- directory: album.directory,
- dates: {
- released: album.date,
- trackArtAdded: album.trackArtDate,
- coverArtAdded: album.coverArtDate,
- addedToWiki: album.dateAddedToWiki
- },
- duration: albumDuration,
- color: album.color,
- cover: serializeCover(album, getAlbumCover),
- artistContribs: serializeContribs(album.artistContribs),
- coverArtistContribs: serializeContribs(album.coverArtistContribs),
- wallpaperArtistContribs: serializeContribs(album.wallpaperArtistContribs),
- bannerArtistContribs: serializeContribs(album.bannerArtistContribs),
- groups: serializeGroupsForAlbum(album),
- trackGroups: album.trackGroups?.map(trackGroup => ({
- name: trackGroup.name,
- color: trackGroup.color,
- tracks: trackGroup.tracks.map(track => track.directory)
- })),
- tracks: album.tracks.map(track => ({
- link: serializeLink(track),
- duration: track.duration
- }))
- })
- };
-
- const page = {
- type: 'page',
- path: ['album', album.directory],
- page: ({
- fancifyURL,
- generateAdditionalFilesShortcut,
- generateAdditionalFilesList,
- generateChronologyLinks,
- generateCoverLink,
- getAlbumCover,
- getAlbumStylesheet,
- getArtistString,
- getLinkThemeString,
- getSizeOfAdditionalFile,
- getThemeString,
- link,
- language,
- transformMultiline,
- urls,
- }) => {
- const trackToListItem = bindOpts(unbound_trackToListItem, {
- getArtistString,
- getLinkThemeString,
- link,
- language
- });
-
- const cover = getAlbumCover(album);
-
- return {
- title: language.$('albumPage.title', {album: album.name}),
- stylesheet: getAlbumStylesheet(album),
- theme: getThemeString(album.color, [
- `--album-directory: ${album.directory}`
- ]),
-
- banner: album.bannerArtistContribs.length && {
- dimensions: album.bannerDimensions,
- path: ['media.albumBanner', album.directory, album.bannerFileExtension],
- alt: language.$('misc.alt.albumBanner'),
- position: 'top'
- },
-
- main: {
- content: fixWS`
- ${cover && generateCoverLink({
+ language,
+ });
+
+ const cover = getAlbumCover(album);
+
+ return {
+ title: language.$("albumPage.title", { album: album.name }),
+ stylesheet: getAlbumStylesheet(album),
+ theme: getThemeString(album.color, [
+ `--album-directory: ${album.directory}`,
+ ]),
+
+ banner: album.bannerArtistContribs.length && {
+ dimensions: album.bannerDimensions,
+ path: [
+ "media.albumBanner",
+ album.directory,
+ album.bannerFileExtension,
+ ],
+ alt: language.$("misc.alt.albumBanner"),
+ position: "top",
+ },
+
+ main: {
+ content: fixWS`
+ ${
+ cover &&
+ generateCoverLink({
src: cover,
- alt: language.$('misc.alt.albumCover'),
- tags: album.artTags
- })}
- ${language.$('albumPage.title', {album: album.name})}
+ alt: language.$("misc.alt.albumCover"),
+ tags: album.artTags,
+ })
+ }
+ ${language.$("albumPage.title", {
+ album: album.name,
+ })}
${[
- album.artistContribs.length && language.$('releaseInfo.by', {
- artists: getArtistString(album.artistContribs, {
- showContrib: true,
- showIcons: true
- })
+ album.artistContribs.length &&
+ language.$("releaseInfo.by", {
+ artists: getArtistString(
+ album.artistContribs,
+ {
+ showContrib: true,
+ showIcons: true,
+ }
+ ),
}),
- album.coverArtistContribs.length && language.$('releaseInfo.coverArtBy', {
- artists: getArtistString(album.coverArtistContribs, {
- showContrib: true,
- showIcons: true
- })
+ album.coverArtistContribs.length &&
+ language.$("releaseInfo.coverArtBy", {
+ artists: getArtistString(
+ album.coverArtistContribs,
+ {
+ showContrib: true,
+ showIcons: true,
+ }
+ ),
}),
- album.wallpaperArtistContribs.length && language.$('releaseInfo.wallpaperArtBy', {
- artists: getArtistString(album.wallpaperArtistContribs, {
- showContrib: true,
- showIcons: true
- })
+ album.wallpaperArtistContribs.length &&
+ language.$("releaseInfo.wallpaperArtBy", {
+ artists: getArtistString(
+ album.wallpaperArtistContribs,
+ {
+ showContrib: true,
+ showIcons: true,
+ }
+ ),
}),
- album.bannerArtistContribs.length && language.$('releaseInfo.bannerArtBy', {
- artists: getArtistString(album.bannerArtistContribs, {
- showContrib: true,
- showIcons: true
- })
+ album.bannerArtistContribs.length &&
+ language.$("releaseInfo.bannerArtBy", {
+ artists: getArtistString(
+ album.bannerArtistContribs,
+ {
+ showContrib: true,
+ showIcons: true,
+ }
+ ),
}),
- album.date && language.$('releaseInfo.released', {
- date: language.formatDate(album.date)
+ album.date &&
+ language.$("releaseInfo.released", {
+ date: language.formatDate(album.date),
}),
- (album.coverArtDate &&
- +album.coverArtDate !== +album.date &&
- language.$('releaseInfo.artReleased', {
- date: language.formatDate(album.coverArtDate)
- })),
- language.$('releaseInfo.duration', {
- duration: language.formatDuration(albumDuration, {approximate: album.tracks.length > 1})
- })
- ].filter(Boolean).join(' \n')}
+ album.coverArtDate &&
+ +album.coverArtDate !== +album.date &&
+ language.$("releaseInfo.artReleased", {
+ date: language.formatDate(album.coverArtDate),
+ }),
+ language.$("releaseInfo.duration", {
+ duration: language.formatDuration(
+ albumDuration,
+ { approximate: album.tracks.length > 1 }
+ ),
+ }),
+ ]
+ .filter(Boolean)
+ .join(" \n")}
- ${(hasAdditionalFiles || hasCommentaryEntries) && fixWS`
+ ${
+ (hasAdditionalFiles || hasCommentaryEntries) &&
+ fixWS`
${[
- hasAdditionalFiles && generateAdditionalFilesShortcut(album.additionalFiles, {language}),
- hasCommentaryEntries && language.$('releaseInfo.viewCommentary', {
- link: link.albumCommentary(album, {
- text: language.$('releaseInfo.viewCommentary.link')
- })
- })
- ].filter(Boolean).join(' \n')
- }
`}
- ${album.urls?.length && `${
- language.$('releaseInfo.listenOn', {
- links: language.formatDisjunctionList(album.urls.map(url => fancifyURL(url, {album: true})))
- })
- }
`}
- ${album.trackGroups && (album.trackGroups.length > 1 || !album.trackGroups[0].isDefaultTrackGroup) ? fixWS`
+ hasAdditionalFiles &&
+ generateAdditionalFilesShortcut(
+ album.additionalFiles,
+ { language }
+ ),
+ hasCommentaryEntries &&
+ language.$("releaseInfo.viewCommentary", {
+ link: link.albumCommentary(album, {
+ text: language.$(
+ "releaseInfo.viewCommentary.link"
+ ),
+ }),
+ }),
+ ]
+ .filter(Boolean)
+ .join(" \n")}
`
+ }
+ ${
+ album.urls?.length &&
+ `${language.$("releaseInfo.listenOn", {
+ links: language.formatDisjunctionList(
+ album.urls.map((url) =>
+ fancifyURL(url, { album: true })
+ )
+ ),
+ })}
`
+ }
+ ${
+ album.trackGroups &&
+ (album.trackGroups.length > 1 ||
+ !album.trackGroups[0].isDefaultTrackGroup)
+ ? fixWS`
- ${album.trackGroups.map(({ name, color, startIndex, tracks }) => fixWS`
- ${
- language.$('trackList.section.withDuration', {
- duration: language.formatDuration(getTotalDuration(tracks), {approximate: tracks.length > 1}),
- section: name
- })
- }
- <${listTag === 'ol' ? `ol start="${startIndex + 1}"` : listTag}>
- ${tracks.map(trackToListItem).join('\n')}
+ ${album.trackGroups
+ .map(
+ ({
+ name,
+ color,
+ startIndex,
+ tracks,
+ }) => fixWS`
+ ${language.$(
+ "trackList.section.withDuration",
+ {
+ duration: language.formatDuration(
+ getTotalDuration(tracks),
+ { approximate: tracks.length > 1 }
+ ),
+ section: name,
+ }
+ )}
+ <${
+ listTag === "ol"
+ ? `ol start="${startIndex + 1}"`
+ : listTag
+ }>
+ ${tracks
+ .map(trackToListItem)
+ .join("\n")}
${listTag}>
- `).join('\n')}
+ `
+ )
+ .join("\n")}
- ` : fixWS`
+ `
+ : fixWS`
<${listTag}>
- ${album.tracks.map(trackToListItem).join('\n')}
+ ${album.tracks.map(trackToListItem).join("\n")}
${listTag}>
- `}
- ${album.dateAddedToWiki && fixWS`
+ `
+ }
+ ${
+ album.dateAddedToWiki &&
+ fixWS`
${[
- language.$('releaseInfo.addedToWiki', {
- date: language.formatDate(album.dateAddedToWiki)
- })
- ].filter(Boolean).join(' \n')}
+ language.$("releaseInfo.addedToWiki", {
+ date: language.formatDate(
+ album.dateAddedToWiki
+ ),
+ }),
+ ]
+ .filter(Boolean)
+ .join(" \n")}
- `}
- ${hasAdditionalFiles && generateAdditionalFilesList(album.additionalFiles, {
+ `
+ }
+ ${
+ hasAdditionalFiles &&
+ generateAdditionalFilesList(album.additionalFiles, {
// TODO: Kinda near the metal here...
- getFileSize: file => getSizeOfAdditionalFile(urls
- .from('media.root')
- .to('media.albumAdditionalFile', album.directory, file)),
- linkFile: file => link.albumAdditionalFile({album, file}),
- })}
- ${album.commentary && fixWS`
- ${language.$('releaseInfo.artistCommentary')}
+ getFileSize: (file) =>
+ getSizeOfAdditionalFile(
+ urls
+ .from("media.root")
+ .to(
+ "media.albumAdditionalFile",
+ album.directory,
+ file
+ )
+ ),
+ linkFile: (file) =>
+ link.albumAdditionalFile({ album, file }),
+ })
+ }
+ ${
+ album.commentary &&
+ fixWS`
+ ${language.$("releaseInfo.artistCommentary")}
${transformMultiline(album.commentary)}
- `}
- `
- },
-
- sidebarLeft: generateAlbumSidebar(album, null, {
- fancifyURL,
- getLinkThemeString,
- link,
- language,
- transformMultiline,
- wikiData
- }),
-
- nav: {
- linkContainerClasses: ['nav-links-hierarchy'],
- links: [
- {toHome: true},
- {
- html: language.$('albumPage.nav.album', {
- album: link.album(album, {class: 'current'})
- })
- },
- ],
- bottomRowContent: generateAlbumNavLinks(album, null, {language}),
- content: generateAlbumChronologyLinks(album, null, {generateChronologyLinks}),
- },
-
- secondaryNav: generateAlbumSecondaryNav(album, null, {
- language,
- link,
- getLinkThemeString,
- }),
- };
- }
- };
+ `
+ }
+ `,
+ },
+
+ sidebarLeft: generateAlbumSidebar(album, null, {
+ fancifyURL,
+ getLinkThemeString,
+ link,
+ language,
+ transformMultiline,
+ wikiData,
+ }),
- return [page, data];
+ nav: {
+ linkContainerClasses: ["nav-links-hierarchy"],
+ links: [
+ { toHome: true },
+ {
+ html: language.$("albumPage.nav.album", {
+ album: link.album(album, { class: "current" }),
+ }),
+ },
+ ],
+ bottomRowContent: generateAlbumNavLinks(album, null, { language }),
+ content: generateAlbumChronologyLinks(album, null, {
+ generateChronologyLinks,
+ }),
+ },
+
+ secondaryNav: generateAlbumSecondaryNav(album, null, {
+ language,
+ link,
+ getLinkThemeString,
+ }),
+ };
+ },
+ };
+
+ return [page, data];
}
// Utility functions
-export function generateAlbumSidebar(album, currentTrack, {
+export function generateAlbumSidebar(
+ album,
+ currentTrack,
+ {
fancifyURL,
getLinkThemeString,
link,
language,
transformMultiline,
- wikiData
-}) {
- const listTag = getAlbumListTag(album);
+ wikiData,
+ }
+) {
+ const listTag = getAlbumListTag(album);
- /*
+ /*
const trackGroups = album.trackGroups || [{
name: language.$('albumSidebar.trackList.fallbackGroupName'),
color: album.color,
@@ -301,185 +398,254 @@ export function generateAlbumSidebar(album, currentTrack, {
}];
*/
- const { trackGroups } = album;
+ const { trackGroups } = album;
- const trackToListItem = track => html.tag('li',
- {class: track === currentTrack && 'current'},
- language.$('albumSidebar.trackList.item', {
- track: link.track(track)
- }));
+ const trackToListItem = (track) =>
+ html.tag(
+ "li",
+ { class: track === currentTrack && "current" },
+ language.$("albumSidebar.trackList.item", {
+ track: link.track(track),
+ })
+ );
- const nameOrDefault = (isDefaultTrackGroup, name) =>
- (isDefaultTrackGroup
- ? language.$('albumSidebar.trackList.fallbackGroupName')
- : name);
+ const nameOrDefault = (isDefaultTrackGroup, name) =>
+ isDefaultTrackGroup
+ ? language.$("albumSidebar.trackList.fallbackGroupName")
+ : name;
- const trackListPart = fixWS`
+ const trackListPart = fixWS`
${link.album(album)}
- ${trackGroups.map(({ name, color, startIndex, tracks, isDefaultTrackGroup }) =>
- html.tag('details', {
+ ${trackGroups
+ .map(({ name, color, startIndex, tracks, isDefaultTrackGroup }) =>
+ html.tag(
+ "details",
+ {
// Leave side8ar track groups collapsed on al8um homepage,
// since there's already a view of all the groups expanded
// in the main content area.
open: currentTrack && tracks.includes(currentTrack),
- class: tracks.includes(currentTrack) && 'current'
- }, [
- html.tag('summary',
- {style: getLinkThemeString(color)},
- (listTag === 'ol'
- ? language.$('albumSidebar.trackList.group.withRange', {
- group: `${nameOrDefault(isDefaultTrackGroup, name)} `,
- range: `${startIndex + 1}–${startIndex + tracks.length}`
- })
- : language.$('albumSidebar.trackList.group', {
- group: `${nameOrDefault(isDefaultTrackGroup, name)} `
- }))
+ class: tracks.includes(currentTrack) && "current",
+ },
+ [
+ html.tag(
+ "summary",
+ { style: getLinkThemeString(color) },
+ listTag === "ol"
+ ? language.$("albumSidebar.trackList.group.withRange", {
+ group: `${nameOrDefault(
+ isDefaultTrackGroup,
+ name
+ )} `,
+ range: `${startIndex + 1}–${
+ startIndex + tracks.length
+ }`,
+ })
+ : language.$("albumSidebar.trackList.group", {
+ group: `${nameOrDefault(
+ isDefaultTrackGroup,
+ name
+ )} `,
+ })
),
fixWS`
- <${listTag === 'ol' ? `ol start="${startIndex + 1}"` : listTag}>
- ${tracks.map(trackToListItem).join('\n')}
+ <${
+ listTag === "ol"
+ ? `ol start="${startIndex + 1}"`
+ : listTag
+ }>
+ ${tracks.map(trackToListItem).join("\n")}
${listTag}>
- `
- ])).join('\n')}
+ `,
+ ]
+ )
+ )
+ .join("\n")}
`;
- const { groups } = album;
-
- const groupParts = groups.map(group => {
- const albums = group.albums.filter(album => album.date);
- const index = albums.indexOf(album);
- const next = index >= 0 && albums[index + 1];
- const previous = index > 0 && albums[index - 1];
- return {group, next, previous};
- }).map(({group, next, previous}) => fixWS`
- ${
- language.$('albumSidebar.groupBox.title', {
- group: link.groupInfo(group)
- })
- }
+ const { groups } = album;
+
+ const groupParts = groups
+ .map((group) => {
+ const albums = group.albums.filter((album) => album.date);
+ const index = albums.indexOf(album);
+ const next = index >= 0 && albums[index + 1];
+ const previous = index > 0 && albums[index - 1];
+ return { group, next, previous };
+ })
+ .map(
+ ({ group, next, previous }) => fixWS`
+ ${language.$("albumSidebar.groupBox.title", {
+ group: link.groupInfo(group),
+ })}
${!currentTrack && transformMultiline(group.descriptionShort)}
- ${group.urls?.length && `${
- language.$('releaseInfo.visitOn', {
- links: language.formatDisjunctionList(group.urls.map(url => fancifyURL(url)))
- })
- }
`}
- ${!currentTrack && fixWS`
- ${next && `${
- language.$('albumSidebar.groupBox.next', {
- album: link.album(next)
- })
- }
`}
- ${previous && `${
- language.$('albumSidebar.groupBox.previous', {
- album: link.album(previous)
- })
- }
`}
- `}
- `);
-
- if (groupParts.length) {
- if (currentTrack) {
- const combinedGroupPart = groupParts.join('\n \n');
- return {
- multiple: [
- trackListPart,
- combinedGroupPart
- ]
- };
- } else {
- return {
- multiple: [
- ...groupParts,
- trackListPart
- ]
- };
+ ${
+ group.urls?.length &&
+ `${language.$("releaseInfo.visitOn", {
+ links: language.formatDisjunctionList(
+ group.urls.map((url) => fancifyURL(url))
+ ),
+ })}
`
+ }
+ ${
+ !currentTrack &&
+ fixWS`
+ ${
+ next &&
+ `${language.$(
+ "albumSidebar.groupBox.next",
+ {
+ album: link.album(next),
+ }
+ )}
`
+ }
+ ${
+ previous &&
+ `${language.$(
+ "albumSidebar.groupBox.previous",
+ {
+ album: link.album(previous),
+ }
+ )}
`
+ }
+ `
}
+ `
+ );
+
+ if (groupParts.length) {
+ if (currentTrack) {
+ const combinedGroupPart = groupParts.join("\n \n");
+ return {
+ multiple: [trackListPart, combinedGroupPart],
+ };
} else {
- return {
- content: trackListPart
- };
+ return {
+ multiple: [...groupParts, trackListPart],
+ };
}
+ } else {
+ return {
+ content: trackListPart,
+ };
+ }
}
-export function generateAlbumSecondaryNav(album, currentTrack, {
- link,
- language,
- getLinkThemeString,
-}) {
- const { groups } = album;
-
- if (!groups.length) {
- return null;
- }
-
- const groupParts = groups.map(group => {
- const albums = group.albums.filter(album => album.date);
- const index = albums.indexOf(album);
- const next = index >= 0 && albums[index + 1];
- const previous = index > 0 && albums[index - 1];
- return {group, next, previous};
- }).map(({group, next, previous}) => {
- const previousNext = !currentTrack && [
- previous && link.album(previous, {color: false, text: language.$('misc.nav.previous')}),
- next && link.album(next, {color: false, text: language.$('misc.nav.next')})
- ].filter(Boolean);
- return html.tag('span', {style: getLinkThemeString(group.color)}, [
- language.$('albumSidebar.groupBox.title', {
- group: link.groupInfo(group)
+export function generateAlbumSecondaryNav(
+ album,
+ currentTrack,
+ { link, language, getLinkThemeString }
+) {
+ const { groups } = album;
+
+ if (!groups.length) {
+ return null;
+ }
+
+ const groupParts = groups
+ .map((group) => {
+ const albums = group.albums.filter((album) => album.date);
+ const index = albums.indexOf(album);
+ const next = index >= 0 && albums[index + 1];
+ const previous = index > 0 && albums[index - 1];
+ return { group, next, previous };
+ })
+ .map(({ group, next, previous }) => {
+ const previousNext =
+ !currentTrack &&
+ [
+ previous &&
+ link.album(previous, {
+ color: false,
+ text: language.$("misc.nav.previous"),
+ }),
+ next &&
+ link.album(next, {
+ color: false,
+ text: language.$("misc.nav.next"),
}),
- previousNext?.length && `(${previousNext.join(',\n')})`
- ]);
+ ].filter(Boolean);
+ return html.tag("span", { style: getLinkThemeString(group.color) }, [
+ language.$("albumSidebar.groupBox.title", {
+ group: link.groupInfo(group),
+ }),
+ previousNext?.length && `(${previousNext.join(",\n")})`,
+ ]);
});
- return {
- classes: ['dot-between-spans'],
- content: groupParts.join('\n'),
- };
+ return {
+ classes: ["dot-between-spans"],
+ content: groupParts.join("\n"),
+ };
}
-export function generateAlbumNavLinks(album, currentTrack, {
- generatePreviousNextLinks,
- language
-}) {
- if (album.tracks.length <= 1) {
- return '';
- }
-
- const previousNextLinks = currentTrack && generatePreviousNextLinks(currentTrack, {
- data: album.tracks,
- linkKey: 'track'
+export function generateAlbumNavLinks(
+ album,
+ currentTrack,
+ { generatePreviousNextLinks, language }
+) {
+ if (album.tracks.length <= 1) {
+ return "";
+ }
+
+ const previousNextLinks =
+ currentTrack &&
+ generatePreviousNextLinks(currentTrack, {
+ data: album.tracks,
+ linkKey: "track",
});
- const randomLink = `${
- (currentTrack
- ? language.$('trackPage.nav.random')
- : language.$('albumPage.nav.randomTrack'))
- } `;
-
- return (previousNextLinks
- ? `(${previousNextLinks}, ${randomLink} )`
- : `(${randomLink}) `);
+ const randomLink = `${
+ currentTrack
+ ? language.$("trackPage.nav.random")
+ : language.$("albumPage.nav.randomTrack")
+ } `;
+
+ return previousNextLinks
+ ? `(${previousNextLinks}, ${randomLink} )`
+ : `(${randomLink}) `;
}
-export function generateAlbumChronologyLinks(album, currentTrack, {generateChronologyLinks}) {
- return html.tag('div', {
- [html.onlyIfContent]: true,
- class: 'nav-chronology-links',
- }, [
- currentTrack && generateChronologyLinks(currentTrack, {
- contribKey: 'artistContribs',
- getThings: artist => [...artist.tracksAsArtist, ...artist.tracksAsContributor],
- headingString: 'misc.chronology.heading.track'
+export function generateAlbumChronologyLinks(
+ album,
+ currentTrack,
+ { generateChronologyLinks }
+) {
+ return html.tag(
+ "div",
+ {
+ [html.onlyIfContent]: true,
+ class: "nav-chronology-links",
+ },
+ [
+ currentTrack &&
+ generateChronologyLinks(currentTrack, {
+ contribKey: "artistContribs",
+ getThings: (artist) => [
+ ...artist.tracksAsArtist,
+ ...artist.tracksAsContributor,
+ ],
+ headingString: "misc.chronology.heading.track",
}),
- currentTrack && generateChronologyLinks(currentTrack, {
- contribKey: 'contributorContribs',
- getThings: artist => [...artist.tracksAsArtist, ...artist.tracksAsContributor],
- headingString: 'misc.chronology.heading.track'
+ currentTrack &&
+ generateChronologyLinks(currentTrack, {
+ contribKey: "contributorContribs",
+ getThings: (artist) => [
+ ...artist.tracksAsArtist,
+ ...artist.tracksAsContributor,
+ ],
+ headingString: "misc.chronology.heading.track",
}),
- generateChronologyLinks(currentTrack || album, {
- contribKey: 'coverArtistContribs',
- dateKey: 'coverArtDate',
- getThings: artist => [...artist.albumsAsCoverArtist, ...artist.tracksAsCoverArtist],
- headingString: 'misc.chronology.heading.coverArt'
- })
- ].filter(Boolean).join('\n'));
+ generateChronologyLinks(currentTrack || album, {
+ contribKey: "coverArtistContribs",
+ dateKey: "coverArtDate",
+ getThings: (artist) => [
+ ...artist.albumsAsCoverArtist,
+ ...artist.tracksAsCoverArtist,
+ ],
+ headingString: "misc.chronology.heading.coverArt",
+ }),
+ ]
+ .filter(Boolean)
+ .join("\n")
+ );
}
diff --git a/src/page/artist-alias.js b/src/page/artist-alias.js
index ac23e902..4fc129e9 100644
--- a/src/page/artist-alias.js
+++ b/src/page/artist-alias.js
@@ -1,22 +1,21 @@
// Artist alias redirect pages.
// (Makes old permalinks bring visitors to the up-to-date page.)
-export function targets({wikiData}) {
- return wikiData.artistAliasData;
+export function targets({ wikiData }) {
+ return wikiData.artistAliasData;
}
-export function write(aliasArtist, {wikiData}) {
- // This function doesn't actually use wikiData, 8ut, um, consistency?
+export function write(aliasArtist, { wikiData }) {
+ // This function doesn't actually use wikiData, 8ut, um, consistency?
- const { aliasedArtist } = aliasArtist;
+ const { aliasedArtist } = aliasArtist;
- const redirect = {
- type: 'redirect',
- fromPath: ['artist', aliasArtist.directory],
- toPath: ['artist', aliasedArtist.directory],
- title: () => aliasedArtist.name
- };
+ const redirect = {
+ type: "redirect",
+ fromPath: ["artist", aliasArtist.directory],
+ toPath: ["artist", aliasedArtist.directory],
+ title: () => aliasedArtist.name,
+ };
- return [redirect];
+ return [redirect];
}
-
diff --git a/src/page/artist.js b/src/page/artist.js
index 6c31a010..8be20106 100644
--- a/src/page/artist.js
+++ b/src/page/artist.js
@@ -4,515 +4,753 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
-import * as html from '../util/html.js';
+import * as html from "../util/html.js";
-import {
- bindOpts,
- unique
-} from '../util/sugar.js';
+import { bindOpts, unique } from "../util/sugar.js";
import {
- chunkByProperties,
- getTotalDuration,
- sortAlbumsTracksChronologically,
- sortByDate,
- sortByDirectory,
- sortChronologically,
-} from '../util/wiki-data.js';
+ chunkByProperties,
+ getTotalDuration,
+ sortAlbumsTracksChronologically,
+ sortByDate,
+ sortByDirectory,
+ sortChronologically,
+} from "../util/wiki-data.js";
// Page exports
-export function targets({wikiData}) {
- return wikiData.artistData;
+export function targets({ wikiData }) {
+ return wikiData.artistData;
}
-export function write(artist, {wikiData}) {
- const { groupData, wikiInfo } = wikiData;
-
- const { name, urls, contextNotes } = artist;
-
- const artThingsAll = sortAlbumsTracksChronologically(unique([
- ...artist.albumsAsCoverArtist ?? [],
- ...artist.albumsAsWallpaperArtist ?? [],
- ...artist.albumsAsBannerArtist ?? [],
- ...artist.tracksAsCoverArtist ?? []
- ]), {getDate: o => o.coverArtDate});
-
- const artThingsGallery = sortAlbumsTracksChronologically([
- ...artist.albumsAsCoverArtist ?? [],
- ...artist.tracksAsCoverArtist ?? []
- ], {getDate: o => o.coverArtDate});
-
- const commentaryThings = sortAlbumsTracksChronologically([
- ...artist.albumsAsCommentator ?? [],
- ...artist.tracksAsCommentator ?? []
- ]);
-
- const hasGallery = artThingsGallery.length > 0;
-
- const getArtistsAndContrib = (thing, key) => ({
- artists: thing[key]?.filter(({ who }) => who !== artist),
- contrib: thing[key]?.find(({ who }) => who === artist),
- thing,
- key
- });
-
- const artListChunks = chunkByProperties(artThingsAll.flatMap(thing =>
- (['coverArtistContribs', 'wallpaperArtistContribs', 'bannerArtistContribs']
- .map(key => getArtistsAndContrib(thing, key))
- .filter(({ contrib }) => contrib)
- .map(props => ({
- album: thing.album || thing,
- track: thing.album ? thing : null,
- date: thing.date,
- ...props
- })))
- ), ['date', 'album']);
-
- const commentaryListChunks = chunkByProperties(commentaryThings.map(thing => ({
- album: thing.album || thing,
- track: thing.album ? thing : null
- })), ['album']);
-
- const allTracks = sortAlbumsTracksChronologically(unique([
- ...artist.tracksAsArtist ?? [],
- ...artist.tracksAsContributor ?? []
- ]));
-
- const chunkTracks = tracks => (
- chunkByProperties(tracks.map(track => ({
- track,
- date: +track.date,
- album: track.album,
- duration: track.duration,
- artists: (track.artistContribs.some(({ who }) => who === artist)
- ? track.artistContribs.filter(({ who }) => who !== artist)
- : track.contributorContribs.filter(({ who }) => who !== artist)),
- contrib: {
- who: artist,
- whatArray: [
- track.artistContribs.find(({ who }) => who === artist)?.what,
- track.contributorContribs.find(({ who }) => who === artist)?.what
- ].filter(Boolean)
+export function write(artist, { wikiData }) {
+ const { groupData, wikiInfo } = wikiData;
+
+ const { name, urls, contextNotes } = artist;
+
+ const artThingsAll = sortAlbumsTracksChronologically(
+ unique([
+ ...(artist.albumsAsCoverArtist ?? []),
+ ...(artist.albumsAsWallpaperArtist ?? []),
+ ...(artist.albumsAsBannerArtist ?? []),
+ ...(artist.tracksAsCoverArtist ?? []),
+ ]),
+ { getDate: (o) => o.coverArtDate }
+ );
+
+ const artThingsGallery = sortAlbumsTracksChronologically(
+ [
+ ...(artist.albumsAsCoverArtist ?? []),
+ ...(artist.tracksAsCoverArtist ?? []),
+ ],
+ { getDate: (o) => o.coverArtDate }
+ );
+
+ const commentaryThings = sortAlbumsTracksChronologically([
+ ...(artist.albumsAsCommentator ?? []),
+ ...(artist.tracksAsCommentator ?? []),
+ ]);
+
+ const hasGallery = artThingsGallery.length > 0;
+
+ const getArtistsAndContrib = (thing, key) => ({
+ artists: thing[key]?.filter(({ who }) => who !== artist),
+ contrib: thing[key]?.find(({ who }) => who === artist),
+ thing,
+ key,
+ });
+
+ const artListChunks = chunkByProperties(
+ artThingsAll.flatMap((thing) =>
+ ["coverArtistContribs", "wallpaperArtistContribs", "bannerArtistContribs"]
+ .map((key) => getArtistsAndContrib(thing, key))
+ .filter(({ contrib }) => contrib)
+ .map((props) => ({
+ album: thing.album || thing,
+ track: thing.album ? thing : null,
+ date: thing.date,
+ ...props,
+ }))
+ ),
+ ["date", "album"]
+ );
+
+ const commentaryListChunks = chunkByProperties(
+ commentaryThings.map((thing) => ({
+ album: thing.album || thing,
+ track: thing.album ? thing : null,
+ })),
+ ["album"]
+ );
+
+ const allTracks = sortAlbumsTracksChronologically(
+ unique([
+ ...(artist.tracksAsArtist ?? []),
+ ...(artist.tracksAsContributor ?? []),
+ ])
+ );
+
+ const chunkTracks = (tracks) =>
+ chunkByProperties(
+ tracks.map((track) => ({
+ track,
+ date: +track.date,
+ album: track.album,
+ duration: track.duration,
+ artists: track.artistContribs.some(({ who }) => who === artist)
+ ? track.artistContribs.filter(({ who }) => who !== artist)
+ : track.contributorContribs.filter(({ who }) => who !== artist),
+ contrib: {
+ who: artist,
+ whatArray: [
+ track.artistContribs.find(({ who }) => who === artist)?.what,
+ track.contributorContribs.find(({ who }) => who === artist)?.what,
+ ].filter(Boolean),
+ },
+ })),
+ ["date", "album"]
+ ).map(({ date, album, chunk }) => ({
+ date,
+ album,
+ chunk,
+ duration: getTotalDuration(chunk),
+ }));
+
+ const trackListChunks = chunkTracks(allTracks);
+ const totalDuration = getTotalDuration(allTracks);
+
+ const countGroups = (things) => {
+ const usedGroups = things.flatMap(
+ (thing) => thing.groups || thing.album?.groups || []
+ );
+ return groupData
+ .map((group) => ({
+ group,
+ contributions: usedGroups.filter((g) => g === group).length,
+ }))
+ .filter(({ contributions }) => contributions > 0)
+ .sort((a, b) => b.contributions - a.contributions);
+ };
+
+ const musicGroups = countGroups(allTracks);
+ const artGroups = countGroups(artThingsAll);
+
+ let flashes, flashListChunks;
+ if (wikiInfo.enableFlashesAndGames) {
+ flashes = sortChronologically(artist.flashesAsContributor?.slice() ?? []);
+ flashListChunks = chunkByProperties(
+ flashes.map((flash) => ({
+ act: flash.act,
+ flash,
+ date: flash.date,
+ // Manual artists/contrib properties here, 8ecause we don't
+ // want to show the full list of other contri8utors inline.
+ // (It can often 8e very, very large!)
+ artists: [],
+ contrib: flash.contributorContribs.find(({ who }) => who === artist),
+ })),
+ ["act"]
+ ).map(({ act, chunk }) => ({
+ act,
+ chunk,
+ dateFirst: chunk[0].date,
+ dateLast: chunk[chunk.length - 1].date,
+ }));
+ }
+
+ const generateEntryAccents = ({
+ getArtistString,
+ language,
+ original,
+ entry,
+ artists,
+ contrib,
+ }) =>
+ original
+ ? language.$("artistPage.creditList.entry.rerelease", { entry })
+ : artists.length
+ ? contrib.what || contrib.whatArray?.length
+ ? language.$(
+ "artistPage.creditList.entry.withArtists.withContribution",
+ {
+ entry,
+ artists: getArtistString(artists),
+ contribution: contrib.whatArray
+ ? language.formatUnitList(contrib.whatArray)
+ : contrib.what,
}
- })), ['date', 'album'])
- .map(({date, album, chunk}) => ({
- date, album, chunk,
- duration: getTotalDuration(chunk),
- })));
-
- const trackListChunks = chunkTracks(allTracks);
- const totalDuration = getTotalDuration(allTracks);
-
- const countGroups = things => {
- const usedGroups = things.flatMap(thing => thing.groups || thing.album?.groups || []);
- return groupData
- .map(group => ({
- group,
- contributions: usedGroups.filter(g => g === group).length
- }))
- .filter(({ contributions }) => contributions > 0)
- .sort((a, b) => b.contributions - a.contributions);
- };
+ )
+ : language.$("artistPage.creditList.entry.withArtists", {
+ entry,
+ artists: getArtistString(artists),
+ })
+ : contrib.what || contrib.whatArray?.length
+ ? language.$("artistPage.creditList.entry.withContribution", {
+ entry,
+ contribution: contrib.whatArray
+ ? language.formatUnitList(contrib.whatArray)
+ : contrib.what,
+ })
+ : entry;
- const musicGroups = countGroups(allTracks);
- const artGroups = countGroups(artThingsAll);
-
- let flashes, flashListChunks;
- if (wikiInfo.enableFlashesAndGames) {
- flashes = sortChronologically(artist.flashesAsContributor?.slice() ?? []);
- flashListChunks = (
- chunkByProperties(flashes.map(flash => ({
- act: flash.act,
- flash,
- date: flash.date,
- // Manual artists/contrib properties here, 8ecause we don't
- // want to show the full list of other contri8utors inline.
- // (It can often 8e very, very large!)
- artists: [],
- contrib: flash.contributorContribs.find(({ who }) => who === artist)
- })), ['act'])
- .map(({ act, chunk }) => ({
- act, chunk,
- dateFirst: chunk[0].date,
- dateLast: chunk[chunk.length - 1].date
- })));
- }
-
- const generateEntryAccents = ({
- getArtistString, language,
- original, entry, artists, contrib
- }) =>
- (original
- ? language.$('artistPage.creditList.entry.rerelease', {entry})
- : (artists.length
- ? ((contrib.what || contrib.whatArray?.length)
- ? language.$('artistPage.creditList.entry.withArtists.withContribution', {
- entry,
- artists: getArtistString(artists),
- contribution: (contrib.whatArray ? language.formatUnitList(contrib.whatArray) : contrib.what)
- })
- : language.$('artistPage.creditList.entry.withArtists', {
- entry,
- artists: getArtistString(artists)
- }))
- : ((contrib.what || contrib.whatArray?.length)
- ? language.$('artistPage.creditList.entry.withContribution', {
- entry,
- contribution: (contrib.whatArray ? language.formatUnitList(contrib.whatArray) : contrib.what)
- })
- : entry)));
-
- const unbound_generateTrackList = (chunks, {
- getArtistString, link, language
- }) => fixWS`
+ const unbound_generateTrackList = (
+ chunks,
+ { getArtistString, link, language }
+ ) => fixWS`
- ${chunks.map(({date, album, chunk, duration}) => fixWS`
+ ${chunks
+ .map(
+ ({ date, album, chunk, duration }) => fixWS`
${
- (date && duration) ? language.$('artistPage.creditList.album.withDate.withDuration', {
+ date && duration
+ ? language.$(
+ "artistPage.creditList.album.withDate.withDuration",
+ {
+ album: link.album(album),
+ date: language.formatDate(date),
+ duration: language.formatDuration(duration, {
+ approximate: true,
+ }),
+ }
+ )
+ : date
+ ? language.$("artistPage.creditList.album.withDate", {
album: link.album(album),
date: language.formatDate(date),
- duration: language.formatDuration(duration, {approximate: true})
- }) : date ? language.$('artistPage.creditList.album.withDate', {
+ })
+ : duration
+ ? language.$("artistPage.creditList.album.withDuration", {
album: link.album(album),
- date: language.formatDate(date)
- }) : duration ? language.$('artistPage.creditList.album.withDuration', {
+ duration: language.formatDuration(duration, {
+ approximate: true,
+ }),
+ })
+ : language.$("artistPage.creditList.album", {
album: link.album(album),
- duration: language.formatDuration(duration, {approximate: true})
- }) : language.$('artistPage.creditList.album', {
- album: link.album(album)
- })}
+ })
+ }
- ${(chunk
- .map(({track, ...props}) => ({
- original: track.originalReleaseTrack,
- entry: language.$('artistPage.creditList.entry.track.withDuration', {
- track: link.track(track),
- duration: language.formatDuration(track.duration ?? 0)
- }),
- ...props
- }))
- .map(({original, ...opts}) => html.tag('li',
- {class: original && 'rerelease'},
- generateEntryAccents({getArtistString, language, original, ...opts})))
- .join('\n'))}
+ ${chunk
+ .map(({ track, ...props }) => ({
+ original: track.originalReleaseTrack,
+ entry: language.$(
+ "artistPage.creditList.entry.track.withDuration",
+ {
+ track: link.track(track),
+ duration: language.formatDuration(
+ track.duration ?? 0
+ ),
+ }
+ ),
+ ...props,
+ }))
+ .map(({ original, ...opts }) =>
+ html.tag(
+ "li",
+ { class: original && "rerelease" },
+ generateEntryAccents({
+ getArtistString,
+ language,
+ original,
+ ...opts,
+ })
+ )
+ )
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
`;
- const unbound_serializeArtistsAndContrib = (key, {
- serializeContribs,
- serializeLink
- }) => thing => {
- const { artists, contrib } = getArtistsAndContrib(thing, key);
- const ret = {};
- ret.link = serializeLink(thing);
- if (contrib.what) ret.contribution = contrib.what;
- if (artists.length) ret.otherArtists = serializeContribs(artists);
- return ret;
+ const unbound_serializeArtistsAndContrib =
+ (key, { serializeContribs, serializeLink }) =>
+ (thing) => {
+ const { artists, contrib } = getArtistsAndContrib(thing, key);
+ const ret = {};
+ ret.link = serializeLink(thing);
+ if (contrib.what) ret.contribution = contrib.what;
+ if (artists.length) ret.otherArtists = serializeContribs(artists);
+ return ret;
};
- const unbound_serializeTrackListChunks = (chunks, {serializeLink}) =>
- chunks.map(({date, album, chunk, duration}) => ({
- album: serializeLink(album),
- date,
- duration,
- tracks: chunk.map(({ track }) => ({
- link: serializeLink(track),
- duration: track.duration
- }))
- }));
-
- const data = {
- type: 'data',
- path: ['artist', artist.directory],
- data: ({
- serializeContribs,
- serializeLink
- }) => {
- const serializeArtistsAndContrib = bindOpts(unbound_serializeArtistsAndContrib, {
- serializeContribs,
- serializeLink
- });
-
- const serializeTrackListChunks = bindOpts(unbound_serializeTrackListChunks, {
- serializeLink
- });
-
- return {
- albums: {
- asCoverArtist: artist.albumsAsCoverArtist?.map(serializeArtistsAndContrib('coverArtistContribs')),
- asWallpaperArtist: artist.albumsAsWallpaperArtist?.map(serializeArtistsAndContrib('wallpaperArtistContribs')),
- asBannerArtist: artist.albumsAsBannerArtist?.map(serializeArtistsAndContrib('bannerArtistContribs'))
- },
- flashes: wikiInfo.enableFlashesAndGames ? {
- asContributor: (artist.flashesAsContributor
- ?.map(flash => getArtistsAndContrib(flash, 'contributorContribs'))
- .map(({ contrib, thing: flash }) => ({
- link: serializeLink(flash),
- contribution: contrib.what
- })))
- } : null,
- tracks: {
- asArtist: artist.tracksAsArtist.map(serializeArtistsAndContrib('artistContribs')),
- asContributor: artist.tracksAsContributor.map(serializeArtistsAndContrib('contributorContribs')),
- chunked: serializeTrackListChunks(trackListChunks)
- }
- };
+ const unbound_serializeTrackListChunks = (chunks, { serializeLink }) =>
+ chunks.map(({ date, album, chunk, duration }) => ({
+ album: serializeLink(album),
+ date,
+ duration,
+ tracks: chunk.map(({ track }) => ({
+ link: serializeLink(track),
+ duration: track.duration,
+ })),
+ }));
+
+ const data = {
+ type: "data",
+ path: ["artist", artist.directory],
+ data: ({ serializeContribs, serializeLink }) => {
+ const serializeArtistsAndContrib = bindOpts(
+ unbound_serializeArtistsAndContrib,
+ {
+ serializeContribs,
+ serializeLink,
}
- };
+ );
- const infoPage = {
- type: 'page',
- path: ['artist', artist.directory],
- page: ({
- fancifyURL,
- generateCoverLink,
- generateInfoGalleryLinks,
- getArtistAvatar,
- getArtistString,
- link,
- language,
- to,
- transformMultiline
- }) => {
- const generateTrackList = bindOpts(unbound_generateTrackList, {
- getArtistString,
- link,
- language
- });
-
- return {
- title: language.$('artistPage.title', {artist: name}),
-
- main: {
- content: fixWS`
- ${artist.hasAvatar && generateCoverLink({
+ const serializeTrackListChunks = bindOpts(
+ unbound_serializeTrackListChunks,
+ {
+ serializeLink,
+ }
+ );
+
+ return {
+ albums: {
+ asCoverArtist: artist.albumsAsCoverArtist?.map(
+ serializeArtistsAndContrib("coverArtistContribs")
+ ),
+ asWallpaperArtist: artist.albumsAsWallpaperArtist?.map(
+ serializeArtistsAndContrib("wallpaperArtistContribs")
+ ),
+ asBannerArtist: artist.albumsAsBannerArtist?.map(
+ serializeArtistsAndContrib("bannerArtistContribs")
+ ),
+ },
+ flashes: wikiInfo.enableFlashesAndGames
+ ? {
+ asContributor: artist.flashesAsContributor
+ ?.map((flash) =>
+ getArtistsAndContrib(flash, "contributorContribs")
+ )
+ .map(({ contrib, thing: flash }) => ({
+ link: serializeLink(flash),
+ contribution: contrib.what,
+ })),
+ }
+ : null,
+ tracks: {
+ asArtist: artist.tracksAsArtist.map(
+ serializeArtistsAndContrib("artistContribs")
+ ),
+ asContributor: artist.tracksAsContributor.map(
+ serializeArtistsAndContrib("contributorContribs")
+ ),
+ chunked: serializeTrackListChunks(trackListChunks),
+ },
+ };
+ },
+ };
+
+ const infoPage = {
+ type: "page",
+ path: ["artist", artist.directory],
+ page: ({
+ fancifyURL,
+ generateCoverLink,
+ generateInfoGalleryLinks,
+ getArtistAvatar,
+ getArtistString,
+ link,
+ language,
+ to,
+ transformMultiline,
+ }) => {
+ const generateTrackList = bindOpts(unbound_generateTrackList, {
+ getArtistString,
+ link,
+ language,
+ });
+
+ return {
+ title: language.$("artistPage.title", { artist: name }),
+
+ main: {
+ content: fixWS`
+ ${
+ artist.hasAvatar &&
+ generateCoverLink({
src: getArtistAvatar(artist),
- alt: language.$('misc.alt.artistAvatar')
- })}
- ${language.$('artistPage.title', {artist: name})}
- ${contextNotes && fixWS`
- ${language.$('releaseInfo.note')}
+ alt: language.$("misc.alt.artistAvatar"),
+ })
+ }
+ ${language.$("artistPage.title", {
+ artist: name,
+ })}
+ ${
+ contextNotes &&
+ fixWS`
+ ${language.$("releaseInfo.note")}
${transformMultiline(contextNotes)}
- `}
- ${urls?.length && `${language.$('releaseInfo.visitOn', {
- links: language.formatDisjunctionList(urls.map(url => fancifyURL(url, {language})))
- })}
`}
- ${hasGallery && `${language.$('artistPage.viewArtGallery', {
+ `
+ }
+ ${
+ urls?.length &&
+ `
${language.$("releaseInfo.visitOn", {
+ links: language.formatDisjunctionList(
+ urls.map((url) => fancifyURL(url, { language }))
+ ),
+ })}
`
+ }
+ ${
+ hasGallery &&
+ `${language.$("artistPage.viewArtGallery", {
link: link.artistGallery(artist, {
- text: language.$('artistPage.viewArtGallery.link')
- })
- })}
`}
- ${language.$('misc.jumpTo.withLinks', {
- links: language.formatUnitList([
- allTracks.length && `${language.$('artistPage.trackList.title')} `,
- artThingsAll.length && `${language.$('artistPage.artList.title')} `,
- wikiInfo.enableFlashesAndGames && flashes.length && `${language.$('artistPage.flashList.title')} `,
- commentaryThings.length && `${language.$('artistPage.commentaryList.title')} `
- ].filter(Boolean))
+ text: language.$(
+ "artistPage.viewArtGallery.link"
+ ),
+ }),
+ })}
`
+ }
+ ${language.$("misc.jumpTo.withLinks", {
+ links: language.formatUnitList(
+ [
+ allTracks.length &&
+ `${language.$(
+ "artistPage.trackList.title"
+ )} `,
+ artThingsAll.length &&
+ `${language.$(
+ "artistPage.artList.title"
+ )} `,
+ wikiInfo.enableFlashesAndGames &&
+ flashes.length &&
+ `${language.$(
+ "artistPage.flashList.title"
+ )} `,
+ commentaryThings.length &&
+ `${language.$(
+ "artistPage.commentaryList.title"
+ )} `,
+ ].filter(Boolean)
+ ),
})}
- ${allTracks.length && fixWS`
- ${language.$('artistPage.trackList.title')}
- ${language.$('artistPage.contributedDurationLine', {
+ ${
+ allTracks.length &&
+ fixWS`
+
${language.$(
+ "artistPage.trackList.title"
+ )}
+ ${language.$(
+ "artistPage.contributedDurationLine",
+ {
artist: artist.name,
- duration: language.formatDuration(totalDuration, {approximate: true, unit: true})
- })}
- ${language.$('artistPage.musicGroupsLine', {
- groups: language.formatUnitList(musicGroups
- .map(({ group, contributions }) => language.$('artistPage.groupsLine.item', {
- group: link.groupInfo(group),
- contributions: language.countContributions(contributions)
- })))
+ duration: language.formatDuration(
+ totalDuration,
+ { approximate: true, unit: true }
+ ),
+ }
+ )}
+ ${language.$("artistPage.musicGroupsLine", {
+ groups: language.formatUnitList(
+ musicGroups.map(({ group, contributions }) =>
+ language.$("artistPage.groupsLine.item", {
+ group: link.groupInfo(group),
+ contributions:
+ language.countContributions(
+ contributions
+ ),
+ })
+ )
+ ),
})}
${generateTrackList(trackListChunks)}
- `}
- ${artThingsAll.length && fixWS`
- ${language.$('artistPage.artList.title')}
- ${hasGallery && `${language.$('artistPage.viewArtGallery.orBrowseList', {
- link: link.artistGallery(artist, {
- text: language.$('artistPage.viewArtGallery.link')
- })
- })}
`}
- ${language.$('artistPage.artGroupsLine', {
- groups: language.formatUnitList(artGroups
- .map(({ group, contributions }) => language.$('artistPage.groupsLine.item', {
- group: link.groupInfo(group),
- contributions: language.countContributions(contributions)
- })))
+ `
+ }
+ ${
+ artThingsAll.length &&
+ fixWS`
+
${language.$(
+ "artistPage.artList.title"
+ )}
+ ${
+ hasGallery &&
+ `${language.$(
+ "artistPage.viewArtGallery.orBrowseList",
+ {
+ link: link.artistGallery(artist, {
+ text: language.$(
+ "artistPage.viewArtGallery.link"
+ ),
+ }),
+ }
+ )}
`
+ }
+ ${language.$("artistPage.artGroupsLine", {
+ groups: language.formatUnitList(
+ artGroups.map(({ group, contributions }) =>
+ language.$("artistPage.groupsLine.item", {
+ group: link.groupInfo(group),
+ contributions:
+ language.countContributions(
+ contributions
+ ),
+ })
+ )
+ ),
})}
- ${artListChunks.map(({date, album, chunk}) => fixWS`
- ${language.$('artistPage.creditList.album.withDate', {
+ ${artListChunks
+ .map(
+ ({ date, album, chunk }) => fixWS`
+ ${language.$(
+ "artistPage.creditList.album.withDate",
+ {
album: link.album(album),
- date: language.formatDate(date)
- })}
+ date: language.formatDate(date),
+ }
+ )}
- ${(chunk
- .map(({album, track, key, ...props}) => ({
- entry: (track
- ? language.$('artistPage.creditList.entry.track', {
- track: link.track(track)
- })
- : `${language.$('artistPage.creditList.entry.album.' + {
- wallpaperArtistContribs: 'wallpaperArt',
- bannerArtistContribs: 'bannerArt',
- coverArtistContribs: 'coverArt'
- }[key])} `),
- ...props
- }))
- .map(opts => generateEntryAccents({getArtistString, language, ...opts}))
- .map(row => `${row} `)
- .join('\n'))}
+ ${chunk
+ .map(
+ ({
+ album,
+ track,
+ key,
+ ...props
+ }) => ({
+ entry: track
+ ? language.$(
+ "artistPage.creditList.entry.track",
+ {
+ track: link.track(track),
+ }
+ )
+ : `${language.$(
+ "artistPage.creditList.entry.album." +
+ {
+ wallpaperArtistContribs:
+ "wallpaperArt",
+ bannerArtistContribs:
+ "bannerArt",
+ coverArtistContribs:
+ "coverArt",
+ }[key]
+ )} `,
+ ...props,
+ })
+ )
+ .map((opts) =>
+ generateEntryAccents({
+ getArtistString,
+ language,
+ ...opts,
+ })
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
- `}
- ${wikiInfo.enableFlashesAndGames && flashes.length && fixWS`
- ${language.$('artistPage.flashList.title')}
+ `
+ }
+ ${
+ wikiInfo.enableFlashesAndGames &&
+ flashes.length &&
+ fixWS`
+ ${language.$(
+ "artistPage.flashList.title"
+ )}
- ${flashListChunks.map(({act, chunk, dateFirst, dateLast}) => fixWS`
- ${language.$('artistPage.creditList.flashAct.withDateRange', {
- act: link.flash(chunk[0].flash, {text: act.name}),
- dateRange: language.formatDateRange(dateFirst, dateLast)
- })}
+ ${flashListChunks
+ .map(
+ ({
+ act,
+ chunk,
+ dateFirst,
+ dateLast,
+ }) => fixWS`
+ ${language.$(
+ "artistPage.creditList.flashAct.withDateRange",
+ {
+ act: link.flash(chunk[0].flash, {
+ text: act.name,
+ }),
+ dateRange: language.formatDateRange(
+ dateFirst,
+ dateLast
+ ),
+ }
+ )}
- ${(chunk
- .map(({flash, ...props}) => ({
- entry: language.$('artistPage.creditList.entry.flash', {
- flash: link.flash(flash)
- }),
- ...props
- }))
- .map(opts => generateEntryAccents({getArtistString, language, ...opts}))
- .map(row => `${row} `)
- .join('\n'))}
+ ${chunk
+ .map(({ flash, ...props }) => ({
+ entry: language.$(
+ "artistPage.creditList.entry.flash",
+ {
+ flash: link.flash(flash),
+ }
+ ),
+ ...props,
+ }))
+ .map((opts) =>
+ generateEntryAccents({
+ getArtistString,
+ language,
+ ...opts,
+ })
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
- `}
- ${commentaryThings.length && fixWS`
-
+ `
+ }
+ ${
+ commentaryThings.length &&
+ fixWS`
+
- ${commentaryListChunks.map(({album, chunk}) => fixWS`
- ${language.$('artistPage.creditList.album', {
- album: link.album(album)
- })}
+ ${commentaryListChunks
+ .map(
+ ({ album, chunk }) => fixWS`
+ ${language.$(
+ "artistPage.creditList.album",
+ {
+ album: link.album(album),
+ }
+ )}
- ${(chunk
- .map(({album, track, ...props}) => track
- ? language.$('artistPage.creditList.entry.track', {
- track: link.track(track)
- })
- : `${language.$('artistPage.creditList.entry.album.commentary')} `)
- .map(row => `${row} `)
- .join('\n'))}
+ ${chunk
+ .map(({ album, track, ...props }) =>
+ track
+ ? language.$(
+ "artistPage.creditList.entry.track",
+ {
+ track: link.track(track),
+ }
+ )
+ : `${language.$(
+ "artistPage.creditList.entry.album.commentary"
+ )} `
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
- `}
- `
- },
-
- nav: generateNavForArtist(artist, false, hasGallery, {
- generateInfoGalleryLinks,
- link,
- language,
- wikiData
- })
- };
- }
- };
-
- const galleryPage = hasGallery && {
- type: 'page',
- path: ['artistGallery', artist.directory],
- page: ({
- generateInfoGalleryLinks,
- getAlbumCover,
- getGridHTML,
- getTrackCover,
- link,
- language,
- to
- }) => ({
- title: language.$('artistGalleryPage.title', {artist: name}),
-
- main: {
- classes: ['top-index'],
- content: fixWS`
- ${language.$('artistGalleryPage.title', {artist: name})}
- ${language.$('artistGalleryPage.infoLine', {
- coverArts: language.countCoverArts(artThingsGallery.length, {unit: true})
- })}
+ `
+ }
+ `,
+ },
+
+ nav: generateNavForArtist(artist, false, hasGallery, {
+ generateInfoGalleryLinks,
+ link,
+ language,
+ wikiData,
+ }),
+ };
+ },
+ };
+
+ const galleryPage = hasGallery && {
+ type: "page",
+ path: ["artistGallery", artist.directory],
+ page: ({
+ generateInfoGalleryLinks,
+ getAlbumCover,
+ getGridHTML,
+ getTrackCover,
+ link,
+ language,
+ to,
+ }) => ({
+ title: language.$("artistGalleryPage.title", { artist: name }),
+
+ main: {
+ classes: ["top-index"],
+ content: fixWS`
+ ${language.$("artistGalleryPage.title", {
+ artist: name,
+ })}
+ ${language.$(
+ "artistGalleryPage.infoLine",
+ {
+ coverArts: language.countCoverArts(
+ artThingsGallery.length,
+ { unit: true }
+ ),
+ }
+ )}
${getGridHTML({
- entries: artThingsGallery.map(item => ({item})),
- srcFn: thing => (thing.album
- ? getTrackCover(thing)
- : getAlbumCover(thing)),
- linkFn: (thing, opts) => (thing.album
- ? link.track(thing, opts)
- : link.album(thing, opts))
+ entries: artThingsGallery.map((item) => ({ item })),
+ srcFn: (thing) =>
+ thing.album
+ ? getTrackCover(thing)
+ : getAlbumCover(thing),
+ linkFn: (thing, opts) =>
+ thing.album
+ ? link.track(thing, opts)
+ : link.album(thing, opts),
})}
- `
- },
-
- nav: generateNavForArtist(artist, true, hasGallery, {
- generateInfoGalleryLinks,
- link,
- language,
- wikiData
- })
- })
- };
-
- return [data, infoPage, galleryPage].filter(Boolean);
+ `,
+ },
+
+ nav: generateNavForArtist(artist, true, hasGallery, {
+ generateInfoGalleryLinks,
+ link,
+ language,
+ wikiData,
+ }),
+ }),
+ };
+
+ return [data, infoPage, galleryPage].filter(Boolean);
}
// Utility functions
-function generateNavForArtist(artist, isGallery, hasGallery, {
- generateInfoGalleryLinks,
- link,
- language,
- wikiData
-}) {
- const { wikiInfo } = wikiData;
-
- const infoGalleryLinks = (hasGallery &&
- generateInfoGalleryLinks(artist, isGallery, {
- link, language,
- linkKeyGallery: 'artistGallery',
- linkKeyInfo: 'artist'
- }))
+function generateNavForArtist(
+ artist,
+ isGallery,
+ hasGallery,
+ { generateInfoGalleryLinks, link, language, wikiData }
+) {
+ const { wikiInfo } = wikiData;
+
+ const infoGalleryLinks =
+ hasGallery &&
+ generateInfoGalleryLinks(artist, isGallery, {
+ link,
+ language,
+ linkKeyGallery: "artistGallery",
+ linkKeyInfo: "artist",
+ });
- return {
- linkContainerClasses: ['nav-links-hierarchy'],
- links: [
- {toHome: true},
- wikiInfo.enableListings &&
- {
- path: ['localized.listingIndex'],
- title: language.$('listingIndex.title')
- },
- {
- html: language.$('artistPage.nav.artist', {
- artist: link.artist(artist, {class: 'current'})
- })
- },
- hasGallery &&
- {
- divider: false,
- html: `(${infoGalleryLinks})`
- }
- ]
- };
+ return {
+ linkContainerClasses: ["nav-links-hierarchy"],
+ links: [
+ { toHome: true },
+ wikiInfo.enableListings && {
+ path: ["localized.listingIndex"],
+ title: language.$("listingIndex.title"),
+ },
+ {
+ html: language.$("artistPage.nav.artist", {
+ artist: link.artist(artist, { class: "current" }),
+ }),
+ },
+ hasGallery && {
+ divider: false,
+ html: `(${infoGalleryLinks})`,
+ },
+ ],
+ };
}
diff --git a/src/page/flash.js b/src/page/flash.js
index 21a22b94..4d8b9f11 100644
--- a/src/page/flash.js
+++ b/src/page/flash.js
@@ -2,251 +2,329 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
-import * as html from '../util/html.js';
+import * as html from "../util/html.js";
-import {
- getFlashLink
-} from '../util/wiki-data.js';
+import { getFlashLink } from "../util/wiki-data.js";
// Page exports
-export function condition({wikiData}) {
- return wikiData.wikiInfo.enableFlashesAndGames;
+export function condition({ wikiData }) {
+ return wikiData.wikiInfo.enableFlashesAndGames;
}
-export function targets({wikiData}) {
- return wikiData.flashData;
+export function targets({ wikiData }) {
+ return wikiData.flashData;
}
-export function write(flash, {wikiData}) {
- const page = {
- type: 'page',
- path: ['flash', flash.directory],
- page: ({
- fancifyFlashURL,
- generateChronologyLinks,
- generateCoverLink,
- generatePreviousNextLinks,
- getArtistString,
- getFlashCover,
- getThemeString,
- link,
- language,
- transformInline
- }) => ({
- title: language.$('flashPage.title', {flash: flash.name}),
- theme: getThemeString(flash.color, [
- `--flash-directory: ${flash.directory}`
- ]),
-
- main: {
- content: fixWS`
- ${language.$('flashPage.title', {flash: flash.name})}
+export function write(flash, { wikiData }) {
+ const page = {
+ type: "page",
+ path: ["flash", flash.directory],
+ page: ({
+ fancifyFlashURL,
+ generateChronologyLinks,
+ generateCoverLink,
+ generatePreviousNextLinks,
+ getArtistString,
+ getFlashCover,
+ getThemeString,
+ link,
+ language,
+ transformInline,
+ }) => ({
+ title: language.$("flashPage.title", { flash: flash.name }),
+ theme: getThemeString(flash.color, [
+ `--flash-directory: ${flash.directory}`,
+ ]),
+
+ main: {
+ content: fixWS`
+ ${language.$("flashPage.title", {
+ flash: flash.name,
+ })}
${generateCoverLink({
- src: getFlashCover(flash),
- alt: language.$('misc.alt.flashArt')
+ src: getFlashCover(flash),
+ alt: language.$("misc.alt.flashArt"),
})}
- ${language.$('releaseInfo.released', {date: language.formatDate(flash.date)})}
- ${(flash.page || flash.urls?.length) && `${language.$('releaseInfo.playOn', {
- links: language.formatDisjunctionList([
+
${language.$("releaseInfo.released", {
+ date: language.formatDate(flash.date),
+ })}
+ ${
+ (flash.page || flash.urls?.length) &&
+ `${language.$("releaseInfo.playOn", {
+ links: language.formatDisjunctionList(
+ [
flash.page && getFlashLink(flash),
- ...flash.urls ?? []
- ].map(url => fancifyFlashURL(url, flash)))
- })}
`}
- ${flash.featuredTracks && fixWS`
- Tracks featured in ${flash.name.replace(/\.$/, '')} :
+ ...(flash.urls ?? []),
+ ].map((url) => fancifyFlashURL(url, flash))
+ ),
+ })}`
+ }
+ ${
+ flash.featuredTracks &&
+ fixWS`
+ Tracks featured in ${flash.name.replace(
+ /\.$/,
+ ""
+ )} :
- ${(flash.featuredTracks
- .map(track => language.$('trackList.item.withArtists', {
- track: link.track(track),
- by: `${
- language.$('trackList.item.withArtists.by', {
- artists: getArtistString(track.artistContribs)
- })
- } `
- }))
- .map(row => `${row} `)
- .join('\n'))}
+ ${flash.featuredTracks
+ .map((track) =>
+ language.$("trackList.item.withArtists", {
+ track: link.track(track),
+ by: `${language.$(
+ "trackList.item.withArtists.by",
+ {
+ artists: getArtistString(
+ track.artistContribs
+ ),
+ }
+ )} `,
+ })
+ )
+ .map((row) => `${row} `)
+ .join("\n")}
- `}
- ${flash.contributorContribs.length && fixWS`
- ${language.$('releaseInfo.contributors')}
+ `
+ }
+ ${
+ flash.contributorContribs.length &&
+ fixWS`
+ ${language.$("releaseInfo.contributors")}
${flash.contributorContribs
- .map(contrib => `${getArtistString([contrib], {
+ .map(
+ (contrib) =>
+ ` ${getArtistString([contrib], {
showContrib: true,
- showIcons: true
- })} `)
- .join('\n')}
+ showIcons: true,
+ })}`
+ )
+ .join("\n")}
- `}
- `
- },
-
- sidebarLeft: generateSidebarForFlash(flash, {link, language, wikiData}),
- nav: generateNavForFlash(flash, {
- generateChronologyLinks,
- generatePreviousNextLinks,
- link,
- language,
- wikiData
- })
- })
- };
-
- return [page];
+ `
+ }
+ `,
+ },
+
+ sidebarLeft: generateSidebarForFlash(flash, { link, language, wikiData }),
+ nav: generateNavForFlash(flash, {
+ generateChronologyLinks,
+ generatePreviousNextLinks,
+ link,
+ language,
+ wikiData,
+ }),
+ }),
+ };
+
+ return [page];
}
-export function writeTargetless({wikiData}) {
- const { flashActData } = wikiData;
-
- const page = {
- type: 'page',
- path: ['flashIndex'],
- page: ({
- getFlashGridHTML,
- getLinkThemeString,
- link,
- language
- }) => ({
- title: language.$('flashIndex.title'),
-
- main: {
- classes: ['flash-index'],
- content: fixWS`
- ${language.$('flashIndex.title')}
+export function writeTargetless({ wikiData }) {
+ const { flashActData } = wikiData;
+
+ const page = {
+ type: "page",
+ path: ["flashIndex"],
+ page: ({ getFlashGridHTML, getLinkThemeString, link, language }) => ({
+ title: language.$("flashIndex.title"),
+
+ main: {
+ classes: ["flash-index"],
+ content: fixWS`
+ ${language.$("flashIndex.title")}
-
${language.$('misc.jumpTo')}
+
${language.$("misc.jumpTo")}
- ${flashActData.filter(act => act.jump).map(({ anchor, jump, jumpColor }) => fixWS`
- ${jump}
- `).join('\n')}
+ ${flashActData
+ .filter((act) => act.jump)
+ .map(
+ ({ anchor, jump, jumpColor }) => fixWS`
+ ${jump}
+ `
+ )
+ .join("\n")}
- ${flashActData.map((act, i) => fixWS`
- ${link.flash(act.flashes[0], {text: act.name})}
+ ${flashActData
+ .map(
+ (act, i) => fixWS`
+ ${link.flash(act.flashes[0], {
+ text: act.name,
+ })}
${getFlashGridHTML({
- entries: act.flashes.map(flash => ({item: flash})),
- lazy: i === 0 ? 4 : true
+ entries: act.flashes.map((flash) => ({
+ item: flash,
+ })),
+ lazy: i === 0 ? 4 : true,
})}
- `).join('\n')}
- `
- },
+ `
+ )
+ .join("\n")}
+ `,
+ },
- nav: {simple: true}
- })
- };
+ nav: { simple: true },
+ }),
+ };
- return [page];
+ return [page];
}
// Utility functions
-function generateNavForFlash(flash, {
+function generateNavForFlash(
+ flash,
+ {
generateChronologyLinks,
generatePreviousNextLinks,
link,
language,
- wikiData
-}) {
- const { flashData, wikiInfo } = wikiData;
-
- const previousNextLinks = generatePreviousNextLinks(flash, {
- data: flashData,
- linkKey: 'flash'
- });
-
- return {
- linkContainerClasses: ['nav-links-hierarchy'],
- links: [
- {toHome: true},
- {
- path: ['localized.flashIndex'],
- title: language.$('flashIndex.title')
- },
- {
- html: language.$('flashPage.nav.flash', {
- flash: link.flash(flash, {class: 'current'})
- })
- },
- ],
-
- bottomRowContent: previousNextLinks && `(${previousNextLinks})`,
+ wikiData,
+ }
+) {
+ const { flashData, wikiInfo } = wikiData;
- content: fixWS`
+ const previousNextLinks = generatePreviousNextLinks(flash, {
+ data: flashData,
+ linkKey: "flash",
+ });
+
+ return {
+ linkContainerClasses: ["nav-links-hierarchy"],
+ links: [
+ { toHome: true },
+ {
+ path: ["localized.flashIndex"],
+ title: language.$("flashIndex.title"),
+ },
+ {
+ html: language.$("flashPage.nav.flash", {
+ flash: link.flash(flash, { class: "current" }),
+ }),
+ },
+ ],
+
+ bottomRowContent: previousNextLinks && `(${previousNextLinks})`,
+
+ content: fixWS`
${generateChronologyLinks(flash, {
- headingString: 'misc.chronology.heading.flash',
- contribKey: 'contributorContribs',
- getThings: artist => artist.flashesAsContributor
+ headingString: "misc.chronology.heading.flash",
+ contribKey: "contributorContribs",
+ getThings: (artist) => artist.flashesAsContributor,
})}
- `
- };
+ `,
+ };
}
-function generateSidebarForFlash(flash, {link, language, wikiData}) {
- // all hard-coded, sorry :(
- // this doesnt have a super portable implementation/design...yet!!
-
- const { flashActData } = wikiData;
-
- const act6 = flashActData.findIndex(act => act.name.startsWith('Act 6'));
- const postCanon = flashActData.findIndex(act => act.name.includes('Post Canon'));
- const outsideCanon = postCanon + flashActData.slice(postCanon).findIndex(act => !act.name.includes('Post Canon'));
- const actIndex = flashActData.indexOf(flash.act);
- const side = (
- (actIndex < 0) ? 0 :
- (actIndex < act6) ? 1 :
- (actIndex <= outsideCanon) ? 2 :
- 3
- );
- const currentAct = flash && flash.act;
-
- return {
- content: fixWS`
- ${link.flashIndex('', {text: language.$('flashIndex.title')})}
+function generateSidebarForFlash(flash, { link, language, wikiData }) {
+ // all hard-coded, sorry :(
+ // this doesnt have a super portable implementation/design...yet!!
+
+ const { flashActData } = wikiData;
+
+ const act6 = flashActData.findIndex((act) => act.name.startsWith("Act 6"));
+ const postCanon = flashActData.findIndex((act) =>
+ act.name.includes("Post Canon")
+ );
+ const outsideCanon =
+ postCanon +
+ flashActData
+ .slice(postCanon)
+ .findIndex((act) => !act.name.includes("Post Canon"));
+ const actIndex = flashActData.indexOf(flash.act);
+ const side =
+ actIndex < 0 ? 0 : actIndex < act6 ? 1 : actIndex <= outsideCanon ? 2 : 3;
+ const currentAct = flash && flash.act;
+
+ return {
+ content: fixWS`
+ ${link.flashIndex("", {
+ text: language.$("flashIndex.title"),
+ })}
- ${flashActData.filter(act =>
- act.name.startsWith('Act 1') ||
- act.name.startsWith('Act 6 Act 1') ||
- act.name.startsWith('Hiveswap') ||
- // Sorry not sorry -Yiffy
- (({index = flashActData.indexOf(act)} = {}) => (
- index < act6 ? side === 1 :
- index < outsideCanon ? side === 2 :
- true
- ))()
- ).flatMap(act => [
- act.name.startsWith('Act 1') && html.tag('dt',
- {class: ['side', side === 1 && 'current']},
- link.flash(act.flashes[0], {color: '#4ac925', text: `Side 1 (Acts 1-5)`}))
- || act.name.startsWith('Act 6 Act 1') && html.tag('dt',
- {class: ['side', side === 2 && 'current']},
- link.flash(act.flashes[0], {color: '#1076a2', text: `Side 2 (Acts 6-7)`}))
- || act.name.startsWith('Hiveswap Act 1') && html.tag('dt',
- {class: ['side', side === 3 && 'current']},
- link.flash(act.flashes[0], {color: '#008282', text: `Outside Canon (Misc. Games)`})),
- (({index = flashActData.indexOf(act)} = {}) => (
- index < act6 ? side === 1 :
- index < outsideCanon ? side === 2 :
- true
- ))() && html.tag('dt',
- {class: act === currentAct && 'current'},
- link.flash(act.flashes[0], {text: act.name})),
- act === currentAct && fixWS`
+ ${flashActData
+ .filter(
+ (act) =>
+ act.name.startsWith("Act 1") ||
+ act.name.startsWith("Act 6 Act 1") ||
+ act.name.startsWith("Hiveswap") ||
+ // Sorry not sorry -Yiffy
+ (({ index = flashActData.indexOf(act) } = {}) =>
+ index < act6
+ ? side === 1
+ : index < outsideCanon
+ ? side === 2
+ : true)()
+ )
+ .flatMap((act) => [
+ (act.name.startsWith("Act 1") &&
+ html.tag(
+ "dt",
+ { class: ["side", side === 1 && "current"] },
+ link.flash(act.flashes[0], {
+ color: "#4ac925",
+ text: `Side 1 (Acts 1-5)`,
+ })
+ )) ||
+ (act.name.startsWith("Act 6 Act 1") &&
+ html.tag(
+ "dt",
+ { class: ["side", side === 2 && "current"] },
+ link.flash(act.flashes[0], {
+ color: "#1076a2",
+ text: `Side 2 (Acts 6-7)`,
+ })
+ )) ||
+ (act.name.startsWith("Hiveswap Act 1") &&
+ html.tag(
+ "dt",
+ { class: ["side", side === 3 && "current"] },
+ link.flash(act.flashes[0], {
+ color: "#008282",
+ text: `Outside Canon (Misc. Games)`,
+ })
+ )),
+ (({ index = flashActData.indexOf(act) } = {}) =>
+ index < act6
+ ? side === 1
+ : index < outsideCanon
+ ? side === 2
+ : true)() &&
+ html.tag(
+ "dt",
+ { class: act === currentAct && "current" },
+ link.flash(act.flashes[0], { text: act.name })
+ ),
+ act === currentAct &&
+ fixWS`
- ${act.flashes.map(f => html.tag('li',
- {class: f === flash && 'current'},
- link.flash(f))).join('\n')}
+ ${act.flashes
+ .map((f) =>
+ html.tag(
+ "li",
+ { class: f === flash && "current" },
+ link.flash(f)
+ )
+ )
+ .join("\n")}
- `
- ]).filter(Boolean).join('\n')}
+ `,
+ ])
+ .filter(Boolean)
+ .join("\n")}
- `
- };
+ `,
+ };
}
diff --git a/src/page/group.js b/src/page/group.js
index b83244a3..bea9ecc7 100644
--- a/src/page/group.js
+++ b/src/page/group.js
@@ -2,268 +2,326 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
-import * as html from '../util/html.js';
+import * as html from "../util/html.js";
-import {
- getTotalDuration,
- sortChronologically,
-} from '../util/wiki-data.js';
+import { getTotalDuration, sortChronologically } from "../util/wiki-data.js";
// Page exports
-export function targets({wikiData}) {
- return wikiData.groupData;
+export function targets({ wikiData }) {
+ return wikiData.groupData;
}
-export function write(group, {wikiData}) {
- const { listingSpec, wikiInfo } = wikiData;
+export function write(group, { wikiData }) {
+ const { listingSpec, wikiInfo } = wikiData;
- const { albums } = group;
- const tracks = albums.flatMap(album => album.tracks);
- const totalDuration = getTotalDuration(tracks);
+ const { albums } = group;
+ const tracks = albums.flatMap((album) => album.tracks);
+ const totalDuration = getTotalDuration(tracks);
- const albumLines = group.albums.map(album => ({
- album,
- otherGroup: album.groups.find(g => g !== group)
- }));
+ const albumLines = group.albums.map((album) => ({
+ album,
+ otherGroup: album.groups.find((g) => g !== group),
+ }));
- const infoPage = {
- type: 'page',
- path: ['groupInfo', group.directory],
- page: ({
- generateInfoGalleryLinks,
- generatePreviousNextLinks,
- getLinkThemeString,
- getThemeString,
- fancifyURL,
- link,
- language,
- transformMultiline
- }) => ({
- title: language.$('groupInfoPage.title', {group: group.name}),
- theme: getThemeString(group.color),
+ const infoPage = {
+ type: "page",
+ path: ["groupInfo", group.directory],
+ page: ({
+ generateInfoGalleryLinks,
+ generatePreviousNextLinks,
+ getLinkThemeString,
+ getThemeString,
+ fancifyURL,
+ link,
+ language,
+ transformMultiline,
+ }) => ({
+ title: language.$("groupInfoPage.title", { group: group.name }),
+ theme: getThemeString(group.color),
- main: {
- content: fixWS`
- ${language.$('groupInfoPage.title', {group: group.name})}
- ${group.urls?.length && `${
- language.$('releaseInfo.visitOn', {
- links: language.formatDisjunctionList(group.urls.map(url => fancifyURL(url, {language})))
- })
- }
`}
+ main: {
+ content: fixWS`
+ ${language.$("groupInfoPage.title", {
+ group: group.name,
+ })}
+ ${
+ group.urls?.length &&
+ `${language.$("releaseInfo.visitOn", {
+ links: language.formatDisjunctionList(
+ group.urls.map((url) => fancifyURL(url, { language }))
+ ),
+ })}
`
+ }
${transformMultiline(group.description)}
- ${language.$('groupInfoPage.albumList.title')}
- ${
- language.$('groupInfoPage.viewAlbumGallery', {
- link: link.groupGallery(group, {
- text: language.$('groupInfoPage.viewAlbumGallery.link')
- })
- })
- }
+ ${language.$("groupInfoPage.albumList.title")}
+ ${language.$("groupInfoPage.viewAlbumGallery", {
+ link: link.groupGallery(group, {
+ text: language.$("groupInfoPage.viewAlbumGallery.link"),
+ }),
+ })}
- ${albumLines.map(({ album, otherGroup }) => {
- const item = (album.date
- ? language.$('groupInfoPage.albumList.item', {
- year: album.date.getFullYear(),
- album: link.album(album)
+ ${albumLines
+ .map(({ album, otherGroup }) => {
+ const item = album.date
+ ? language.$("groupInfoPage.albumList.item", {
+ year: album.date.getFullYear(),
+ album: link.album(album),
})
- : language.$('groupInfoPage.albumList.item.withoutYear', {
- album: link.album(album)
- }));
- return html.tag('li', (otherGroup
- ? language.$('groupInfoPage.albumList.item.withAccent', {
- item,
- accent: html.tag('span',
- {class: 'other-group-accent'},
- language.$('groupInfoPage.albumList.item.otherGroupAccent', {
- group: link.groupInfo(otherGroup, {color: false})
- }))
- })
- : item));
- }).join('\n')}
+ : language.$(
+ "groupInfoPage.albumList.item.withoutYear",
+ {
+ album: link.album(album),
+ }
+ );
+ return html.tag(
+ "li",
+ otherGroup
+ ? language.$(
+ "groupInfoPage.albumList.item.withAccent",
+ {
+ item,
+ accent: html.tag(
+ "span",
+ { class: "other-group-accent" },
+ language.$(
+ "groupInfoPage.albumList.item.otherGroupAccent",
+ {
+ group: link.groupInfo(otherGroup, {
+ color: false,
+ }),
+ }
+ )
+ ),
+ }
+ )
+ : item
+ );
+ })
+ .join("\n")}
- `
- },
+ `,
+ },
- sidebarLeft: generateGroupSidebar(group, false, {
- getLinkThemeString,
- link,
- language,
- wikiData
- }),
+ sidebarLeft: generateGroupSidebar(group, false, {
+ getLinkThemeString,
+ link,
+ language,
+ wikiData,
+ }),
- nav: generateGroupNav(group, false, {
- generateInfoGalleryLinks,
- generatePreviousNextLinks,
- link,
- language,
- wikiData
- })
- })
- };
+ nav: generateGroupNav(group, false, {
+ generateInfoGalleryLinks,
+ generatePreviousNextLinks,
+ link,
+ language,
+ wikiData,
+ }),
+ }),
+ };
- const galleryPage = {
- type: 'page',
- path: ['groupGallery', group.directory],
- page: ({
- generateInfoGalleryLinks,
- generatePreviousNextLinks,
- getAlbumGridHTML,
- getLinkThemeString,
- getThemeString,
- link,
- language
- }) => ({
- title: language.$('groupGalleryPage.title', {group: group.name}),
- theme: getThemeString(group.color),
+ const galleryPage = {
+ type: "page",
+ path: ["groupGallery", group.directory],
+ page: ({
+ generateInfoGalleryLinks,
+ generatePreviousNextLinks,
+ getAlbumGridHTML,
+ getLinkThemeString,
+ getThemeString,
+ link,
+ language,
+ }) => ({
+ title: language.$("groupGalleryPage.title", { group: group.name }),
+ theme: getThemeString(group.color),
- main: {
- classes: ['top-index'],
- content: fixWS`
- ${language.$('groupGalleryPage.title', {group: group.name})}
- ${
- language.$('groupGalleryPage.infoLine', {
- tracks: `${language.countTracks(tracks.length, {unit: true})} `,
- albums: `${language.countAlbums(albums.length, {unit: true})} `,
- time: `${language.formatDuration(totalDuration, {unit: true})} `
- })
- }
- ${wikiInfo.enableGroupUI && wikiInfo.enableListings && html.tag('p',
- {class: 'quick-info'},
- language.$('groupGalleryPage.anotherGroupLine', {
- link: link.listing(listingSpec.find(l => l.directory === 'groups/by-category'), {
- text: language.$('groupGalleryPage.anotherGroupLine.link')
- })
+ main: {
+ classes: ["top-index"],
+ content: fixWS`
+ ${language.$("groupGalleryPage.title", {
+ group: group.name,
+ })}
+ ${language.$(
+ "groupGalleryPage.infoLine",
+ {
+ tracks: `${language.countTracks(tracks.length, {
+ unit: true,
+ })} `,
+ albums: `${language.countAlbums(albums.length, {
+ unit: true,
+ })} `,
+ time: `${language.formatDuration(totalDuration, {
+ unit: true,
+ })} `,
+ }
+ )}
+ ${
+ wikiInfo.enableGroupUI &&
+ wikiInfo.enableListings &&
+ html.tag(
+ "p",
+ { class: "quick-info" },
+ language.$("groupGalleryPage.anotherGroupLine", {
+ link: link.listing(
+ listingSpec.find(
+ (l) => l.directory === "groups/by-category"
+ ),
+ {
+ text: language.$(
+ "groupGalleryPage.anotherGroupLine.link"
+ ),
+ }
+ ),
})
- )}
+ )
+ }
${getAlbumGridHTML({
- entries: sortChronologically(group.albums.map(album => ({
- item: album,
- directory: album.directory,
- name: album.name,
- date: album.date,
- }))).reverse(),
- details: true
+ entries: sortChronologically(
+ group.albums.map((album) => ({
+ item: album,
+ directory: album.directory,
+ name: album.name,
+ date: album.date,
+ }))
+ ).reverse(),
+ details: true,
})}
- `
- },
+ `,
+ },
- sidebarLeft: generateGroupSidebar(group, true, {
- getLinkThemeString,
- link,
- language,
- wikiData
- }),
+ sidebarLeft: generateGroupSidebar(group, true, {
+ getLinkThemeString,
+ link,
+ language,
+ wikiData,
+ }),
- nav: generateGroupNav(group, true, {
- generateInfoGalleryLinks,
- generatePreviousNextLinks,
- link,
- language,
- wikiData
- })
- })
- };
+ nav: generateGroupNav(group, true, {
+ generateInfoGalleryLinks,
+ generatePreviousNextLinks,
+ link,
+ language,
+ wikiData,
+ }),
+ }),
+ };
- return [infoPage, galleryPage];
+ return [infoPage, galleryPage];
}
// Utility functions
-function generateGroupSidebar(currentGroup, isGallery, {
- getLinkThemeString,
- link,
- language,
- wikiData
-}) {
- const { groupCategoryData, wikiInfo } = wikiData;
+function generateGroupSidebar(
+ currentGroup,
+ isGallery,
+ { getLinkThemeString, link, language, wikiData }
+) {
+ const { groupCategoryData, wikiInfo } = wikiData;
- if (!wikiInfo.enableGroupUI) {
- return null;
- }
+ if (!wikiInfo.enableGroupUI) {
+ return null;
+ }
- const linkKey = isGallery ? 'groupGallery' : 'groupInfo';
+ const linkKey = isGallery ? "groupGallery" : "groupInfo";
- return {
- content: fixWS`
- ${language.$('groupSidebar.title')}
- ${groupCategoryData.map(category =>
- html.tag('details', {
+ return {
+ content: fixWS`
+ ${language.$("groupSidebar.title")}
+ ${groupCategoryData
+ .map((category) =>
+ html.tag(
+ "details",
+ {
open: category === currentGroup.category,
- class: category === currentGroup.category && 'current'
- }, [
- html.tag('summary',
- {style: getLinkThemeString(category.color)},
- language.$('groupSidebar.groupList.category', {
- category: `${category.name} `
- })),
- html.tag('ul',
- category.groups.map(group => html.tag('li',
- {
- class: group === currentGroup && 'current',
- style: getLinkThemeString(group.color)
- },
- language.$('groupSidebar.groupList.item', {
- group: link[linkKey](group)
- }))))
- ])).join('\n')}
+ class: category === currentGroup.category && "current",
+ },
+ [
+ html.tag(
+ "summary",
+ { style: getLinkThemeString(category.color) },
+ language.$("groupSidebar.groupList.category", {
+ category: `${category.name} `,
+ })
+ ),
+ html.tag(
+ "ul",
+ category.groups.map((group) =>
+ html.tag(
+ "li",
+ {
+ class: group === currentGroup && "current",
+ style: getLinkThemeString(group.color),
+ },
+ language.$("groupSidebar.groupList.item", {
+ group: link[linkKey](group),
+ })
+ )
+ )
+ ),
+ ]
+ )
+ )
+ .join("\n")}
- `
- };
+ `,
+ };
}
-function generateGroupNav(currentGroup, isGallery, {
+function generateGroupNav(
+ currentGroup,
+ isGallery,
+ {
generateInfoGalleryLinks,
generatePreviousNextLinks,
link,
language,
- wikiData
-}) {
- const { groupData, wikiInfo } = wikiData;
+ wikiData,
+ }
+) {
+ const { groupData, wikiInfo } = wikiData;
- if (!wikiInfo.enableGroupUI) {
- return {simple: true};
- }
+ if (!wikiInfo.enableGroupUI) {
+ return { simple: true };
+ }
- const urlKey = isGallery ? 'localized.groupGallery' : 'localized.groupInfo';
- const linkKey = isGallery ? 'groupGallery' : 'groupInfo';
+ const urlKey = isGallery ? "localized.groupGallery" : "localized.groupInfo";
+ const linkKey = isGallery ? "groupGallery" : "groupInfo";
- const infoGalleryLinks = generateInfoGalleryLinks(currentGroup, isGallery, {
- linkKeyGallery: 'groupGallery',
- linkKeyInfo: 'groupInfo'
- });
+ const infoGalleryLinks = generateInfoGalleryLinks(currentGroup, isGallery, {
+ linkKeyGallery: "groupGallery",
+ linkKeyInfo: "groupInfo",
+ });
- const previousNextLinks = generatePreviousNextLinks(currentGroup, {
- data: groupData,
- linkKey
- });
+ const previousNextLinks = generatePreviousNextLinks(currentGroup, {
+ data: groupData,
+ linkKey,
+ });
- return {
- linkContainerClasses: ['nav-links-hierarchy'],
- links: [
- {toHome: true},
- wikiInfo.enableListings &&
- {
- path: ['localized.listingIndex'],
- title: language.$('listingIndex.title')
- },
- {
- html: language.$('groupPage.nav.group', {
- group: link[linkKey](currentGroup, {class: 'current'})
- })
- },
- {
- divider: false,
- html: (previousNextLinks
- ? `(${infoGalleryLinks}; ${previousNextLinks})`
- : `(${previousNextLinks})`)
- }
- ]
- };
+ return {
+ linkContainerClasses: ["nav-links-hierarchy"],
+ links: [
+ { toHome: true },
+ wikiInfo.enableListings && {
+ path: ["localized.listingIndex"],
+ title: language.$("listingIndex.title"),
+ },
+ {
+ html: language.$("groupPage.nav.group", {
+ group: link[linkKey](currentGroup, { class: "current" }),
+ }),
+ },
+ {
+ divider: false,
+ html: previousNextLinks
+ ? `(${infoGalleryLinks}; ${previousNextLinks})`
+ : `(${previousNextLinks})`,
+ },
+ ],
+ };
}
diff --git a/src/page/homepage.js b/src/page/homepage.js
index a19df6cf..ebe3a8d3 100644
--- a/src/page/homepage.js
+++ b/src/page/homepage.js
@@ -2,123 +2,184 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
-import * as html from '../util/html.js';
+import * as html from "../util/html.js";
-import {
- getNewAdditions,
- getNewReleases
-} from '../util/wiki-data.js';
+import { getNewAdditions, getNewReleases } from "../util/wiki-data.js";
// Page exports
-export function writeTargetless({wikiData}) {
- const { newsData, staticPageData, homepageLayout, wikiInfo } = wikiData;
-
- const page = {
- type: 'page',
- path: ['home'],
- page: ({
- getAlbumGridHTML,
- getLinkThemeString,
- link,
- language,
- to,
- transformInline,
- transformMultiline
- }) => ({
- title: wikiInfo.name,
- showWikiNameInTitle: false,
-
- meta: {
- description: wikiInfo.description
- },
-
- main: {
- classes: ['top-index'],
- content: fixWS`
+export function writeTargetless({ wikiData }) {
+ const { newsData, staticPageData, homepageLayout, wikiInfo } = wikiData;
+
+ const page = {
+ type: "page",
+ path: ["home"],
+ page: ({
+ getAlbumGridHTML,
+ getLinkThemeString,
+ link,
+ language,
+ to,
+ transformInline,
+ transformMultiline,
+ }) => ({
+ title: wikiInfo.name,
+ showWikiNameInTitle: false,
+
+ meta: {
+ description: wikiInfo.description,
+ },
+
+ main: {
+ classes: ["top-index"],
+ content: fixWS`
${wikiInfo.name}
- ${homepageLayout.rows?.map((row, i) => fixWS`
-
+ ${homepageLayout.rows
+ ?.map(
+ (row, i) => fixWS`
+
${row.name}
- ${row.type === 'albums' && fixWS`
+ ${
+ row.type === "albums" &&
+ fixWS`
${getAlbumGridHTML({
- entries: (
- row.sourceGroupByRef === 'new-releases' ? getNewReleases(row.countAlbumsFromGroup, {wikiData}) :
- row.sourceGroupByRef === 'new-additions' ? getNewAdditions(row.countAlbumsFromGroup, {wikiData}) :
- ((row.sourceGroup?.albums ?? [])
- .slice()
- .reverse()
- .filter(album => album.isListedOnHomepage)
- .slice(0, row.countAlbumsFromGroup)
- .map(album => ({item: album})))
- ).concat(row.sourceAlbums.map(album => ({item: album}))),
- lazy: i > 0
+ entries: (row.sourceGroupByRef ===
+ "new-releases"
+ ? getNewReleases(
+ row.countAlbumsFromGroup,
+ { wikiData }
+ )
+ : row.sourceGroupByRef ===
+ "new-additions"
+ ? getNewAdditions(
+ row.countAlbumsFromGroup,
+ { wikiData }
+ )
+ : (row.sourceGroup?.albums ?? [])
+ .slice()
+ .reverse()
+ .filter(
+ (album) =>
+ album.isListedOnHomepage
+ )
+ .slice(0, row.countAlbumsFromGroup)
+ .map((album) => ({ item: album }))
+ ).concat(
+ row.sourceAlbums.map((album) => ({
+ item: album,
+ }))
+ ),
+ lazy: i > 0,
})}
- ${row.actionLinks?.length && fixWS`
+ ${
+ row.actionLinks?.length &&
+ fixWS`
- `}
+ `
+ }
- `).join('\n')}
- `
- },
-
- sidebarLeft: homepageLayout.sidebarContent && {
- wide: true,
- collapse: false,
- // This is a pretty filthy hack! 8ut otherwise, the [[news]] part
- // gets treated like it's a reference to the track named "news",
- // which o8viously isn't what we're going for. Gotta catch that
- // 8efore we pass it to transformMultiline, 'cuz otherwise it'll
- // get repl8ced with just the word "news" (or anything else that
- // transformMultiline does with references it can't match) -- and
- // we can't match that for replacing it with the news column!
- //
- // And no, I will not make [[news]] into part of transformMultiline
- // (even though that would 8e hilarious).
- content: (transformMultiline(homepageLayout.sidebarContent.replace('[[news]]', '__GENERATE_NEWS__'))
- .replace('
__GENERATE_NEWS__
', wikiInfo.enableNews ? fixWS`
-
${language.$('homepage.news.title')}
- ${newsData.slice(0, 3).map((entry, i) => html.tag('article',
- {class: ['news-entry', i === 0 && 'first-news-entry']},
- fixWS`
-
${language.formatDate(entry.date)} ${link.newsEntry(entry)}
+ `
+ )
+ .join("\n")}
+ `,
+ },
+
+ sidebarLeft: homepageLayout.sidebarContent && {
+ wide: true,
+ collapse: false,
+ // This is a pretty filthy hack! 8ut otherwise, the [[news]] part
+ // gets treated like it's a reference to the track named "news",
+ // which o8viously isn't what we're going for. Gotta catch that
+ // 8efore we pass it to transformMultiline, 'cuz otherwise it'll
+ // get repl8ced with just the word "news" (or anything else that
+ // transformMultiline does with references it can't match) -- and
+ // we can't match that for replacing it with the news column!
+ //
+ // And no, I will not make [[news]] into part of transformMultiline
+ // (even though that would 8e hilarious).
+ content: transformMultiline(
+ homepageLayout.sidebarContent.replace("[[news]]", "__GENERATE_NEWS__")
+ ).replace(
+ "
__GENERATE_NEWS__
",
+ wikiInfo.enableNews
+ ? fixWS`
+
${language.$("homepage.news.title")}
+ ${newsData
+ .slice(0, 3)
+ .map((entry, i) =>
+ html.tag(
+ "article",
+ {
+ class: [
+ "news-entry",
+ i === 0 && "first-news-entry",
+ ],
+ },
+ fixWS`
+
${language.formatDate(
+ entry.date
+ )} ${link.newsEntry(entry)}
${transformMultiline(entry.contentShort)}
- ${entry.contentShort !== entry.content && link.newsEntry(entry, {
- text: language.$('homepage.news.entry.viewRest')
- })}
- `)).join('\n')}
- ` : `
News requested in content description but this feature isn't enabled
`))
- },
-
- nav: {
- linkContainerClasses: ['nav-links-index'],
- links: [
- link.home('', {text: wikiInfo.nameShort, class: 'current', to}),
-
- wikiInfo.enableListings &&
- link.listingIndex('', {text: language.$('listingIndex.title'), to}),
-
- wikiInfo.enableNews &&
- link.newsIndex('', {text: language.$('newsIndex.title'), to}),
-
- wikiInfo.enableFlashesAndGames &&
- link.flashIndex('', {text: language.$('flashIndex.title'), to}),
-
- ...(staticPageData
- .filter(page => page.showInNavigationBar)
- .map(page => link.staticPage(page, {text: page.nameShort}))),
- ].filter(Boolean).map(html => ({html})),
- }
- })
- };
-
- return [page];
+ ${
+ entry.contentShort !== entry.content &&
+ link.newsEntry(entry, {
+ text: language.$(
+ "homepage.news.entry.viewRest"
+ ),
+ })
+ }
+ `
+ )
+ )
+ .join("\n")}
+ `
+ : `
News requested in content description but this feature isn't enabled
`
+ ),
+ },
+
+ nav: {
+ linkContainerClasses: ["nav-links-index"],
+ links: [
+ link.home("", { text: wikiInfo.nameShort, class: "current", to }),
+
+ wikiInfo.enableListings &&
+ link.listingIndex("", {
+ text: language.$("listingIndex.title"),
+ to,
+ }),
+
+ wikiInfo.enableNews &&
+ link.newsIndex("", { text: language.$("newsIndex.title"), to }),
+
+ wikiInfo.enableFlashesAndGames &&
+ link.flashIndex("", { text: language.$("flashIndex.title"), to }),
+
+ ...staticPageData
+ .filter((page) => page.showInNavigationBar)
+ .map((page) => link.staticPage(page, { text: page.nameShort })),
+ ]
+ .filter(Boolean)
+ .map((html) => ({ html })),
+ },
+ }),
+ };
+
+ return [page];
}
diff --git a/src/page/index.js b/src/page/index.js
index f580cbea..50fbd7a4 100644
--- a/src/page/index.js
+++ b/src/page/index.js
@@ -39,15 +39,15 @@
// These functions should be referenced only from adjacent modules, as they
// pertain only to site page generation.
-export * as album from './album.js';
-export * as albumCommentary from './album-commentary.js';
-export * as artist from './artist.js';
-export * as artistAlias from './artist-alias.js';
-export * as flash from './flash.js';
-export * as group from './group.js';
-export * as homepage from './homepage.js';
-export * as listing from './listing.js';
-export * as news from './news.js';
-export * as static from './static.js';
-export * as tag from './tag.js';
-export * as track from './track.js';
+export * as album from "./album.js";
+export * as albumCommentary from "./album-commentary.js";
+export * as artist from "./artist.js";
+export * as artistAlias from "./artist-alias.js";
+export * as flash from "./flash.js";
+export * as group from "./group.js";
+export * as homepage from "./homepage.js";
+export * as listing from "./listing.js";
+export * as news from "./news.js";
+export * as static from "./static.js";
+export * as tag from "./tag.js";
+export * as track from "./track.js";
diff --git a/src/page/listing.js b/src/page/listing.js
index 447a0c8f..886c8a9d 100644
--- a/src/page/listing.js
+++ b/src/page/listing.js
@@ -10,193 +10,218 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
-import * as html from '../util/html.js';
+import * as html from "../util/html.js";
-import {
- getTotalDuration
-} from '../util/wiki-data.js';
+import { getTotalDuration } from "../util/wiki-data.js";
// Page exports
-export function condition({wikiData}) {
- return wikiData.wikiInfo.enableListings;
+export function condition({ wikiData }) {
+ return wikiData.wikiInfo.enableListings;
}
-export function targets({wikiData}) {
- return wikiData.listingSpec;
+export function targets({ wikiData }) {
+ return wikiData.listingSpec;
}
-export function write(listing, {wikiData}) {
- if (listing.condition && !listing.condition({wikiData})) {
- return null;
- }
+export function write(listing, { wikiData }) {
+ if (listing.condition && !listing.condition({ wikiData })) {
+ return null;
+ }
- const { wikiInfo } = wikiData;
+ const { wikiInfo } = wikiData;
- const data = (listing.data
- ? listing.data({wikiData})
- : null);
+ const data = listing.data ? listing.data({ wikiData }) : null;
- const page = {
- type: 'page',
- path: ['listing', listing.directory],
- page: opts => {
- const { getLinkThemeString, link, language } = opts;
- const titleKey = `listingPage.${listing.stringsKey}.title`;
+ const page = {
+ type: "page",
+ path: ["listing", listing.directory],
+ page: (opts) => {
+ const { getLinkThemeString, link, language } = opts;
+ const titleKey = `listingPage.${listing.stringsKey}.title`;
- return {
- title: language.$(titleKey),
+ return {
+ title: language.$(titleKey),
- main: {
- content: fixWS`
+ main: {
+ content: fixWS`
${language.$(titleKey)}
- ${listing.html && (listing.data
+ ${
+ listing.html &&
+ (listing.data
? listing.html(data, opts)
- : listing.html(opts))}
- ${listing.row && fixWS`
+ : listing.html(opts))
+ }
+ ${
+ listing.row &&
+ fixWS`
- ${(data
- .map(item => listing.row(item, opts))
- .map(row => `${row} `)
- .join('\n'))}
+ ${data
+ .map((item) => listing.row(item, opts))
+ .map((row) => `${row} `)
+ .join("\n")}
- `}
- `
- },
-
- sidebarLeft: {
- content: generateSidebarForListings(listing, {
- getLinkThemeString,
- link,
- language,
- wikiData
- })
- },
-
- nav: {
- linkContainerClasses: ['nav-links-hierarchy'],
- links: [
- {toHome: true},
- {
- path: ['localized.listingIndex'],
- title: language.$('listingIndex.title')
- },
- {toCurrentPage: true}
- ]
- }
- };
- }
- };
-
- return [page];
-}
-
-export function writeTargetless({wikiData}) {
- const { albumData, trackData, wikiInfo } = wikiData;
+ `
+ }
+ `,
+ },
- const totalDuration = getTotalDuration(trackData);
-
- const page = {
- type: 'page',
- path: ['listingIndex'],
- page: ({
+ sidebarLeft: {
+ content: generateSidebarForListings(listing, {
getLinkThemeString,
+ link,
language,
- link
- }) => ({
- title: language.$('listingIndex.title'),
-
- main: {
- content: fixWS`
-
${language.$('listingIndex.title')}
-
${language.$('listingIndex.infoLine', {
- wiki: wikiInfo.name,
- tracks: `${language.countTracks(trackData.length, {unit: true})} `,
- albums: `${language.countAlbums(albumData.length, {unit: true})} `,
- duration: `${language.formatDuration(totalDuration, {approximate: true, unit: true})} `
- })}
-
-
${language.$('listingIndex.exploreList')}
- ${generateLinkIndexForListings(null, false, {link, language, wikiData})}
- `
- },
-
- sidebarLeft: {
- content: generateSidebarForListings(null, {
- getLinkThemeString,
- link,
- language,
- wikiData
- })
+ wikiData,
+ }),
+ },
+
+ nav: {
+ linkContainerClasses: ["nav-links-hierarchy"],
+ links: [
+ { toHome: true },
+ {
+ path: ["localized.listingIndex"],
+ title: language.$("listingIndex.title"),
},
+ { toCurrentPage: true },
+ ],
+ },
+ };
+ },
+ };
+
+ return [page];
+}
- nav: {simple: true}
- })
- };
-
- return [page];
-};
+export function writeTargetless({ wikiData }) {
+ const { albumData, trackData, wikiInfo } = wikiData;
+
+ const totalDuration = getTotalDuration(trackData);
+
+ const page = {
+ type: "page",
+ path: ["listingIndex"],
+ page: ({ getLinkThemeString, language, link }) => ({
+ title: language.$("listingIndex.title"),
+
+ main: {
+ content: fixWS`
+
${language.$("listingIndex.title")}
+
${language.$("listingIndex.infoLine", {
+ wiki: wikiInfo.name,
+ tracks: `${language.countTracks(trackData.length, {
+ unit: true,
+ })} `,
+ albums: `${language.countAlbums(albumData.length, {
+ unit: true,
+ })} `,
+ duration: `${language.formatDuration(totalDuration, {
+ approximate: true,
+ unit: true,
+ })} `,
+ })}
+
+
${language.$("listingIndex.exploreList")}
+ ${generateLinkIndexForListings(null, false, {
+ link,
+ language,
+ wikiData,
+ })}
+ `,
+ },
+
+ sidebarLeft: {
+ content: generateSidebarForListings(null, {
+ getLinkThemeString,
+ link,
+ language,
+ wikiData,
+ }),
+ },
+
+ nav: { simple: true },
+ }),
+ };
+
+ return [page];
+}
// Utility functions
-function generateSidebarForListings(currentListing, {
- getLinkThemeString,
- link,
- language,
- wikiData
-}) {
- return fixWS`
-
${link.listingIndex('', {text: language.$('listingIndex.title')})}
+function generateSidebarForListings(
+ currentListing,
+ { getLinkThemeString, link, language, wikiData }
+) {
+ return fixWS`
+
${link.listingIndex("", {
+ text: language.$("listingIndex.title"),
+ })}
${generateLinkIndexForListings(currentListing, true, {
- getLinkThemeString,
- link,
- language,
- wikiData
+ getLinkThemeString,
+ link,
+ language,
+ wikiData,
})}
`;
}
-function generateLinkIndexForListings(currentListing, forSidebar, {
- getLinkThemeString,
- link,
- language,
- wikiData
-}) {
- const { listingTargetSpec, wikiInfo } = wikiData;
-
- const filteredByCondition = listingTargetSpec
- .map(({ listings, ...rest }) => ({
- ...rest,
- listings: listings.filter(({ condition: c }) => !c || c({wikiData}))
- }))
- .filter(({ listings }) => listings.length > 0);
-
- const genUL = listings => html.tag('ul',
- listings.map(listing => html.tag('li',
- {class: [listing === currentListing && 'current']},
- link.listing(listing, {text: language.$(`listingPage.${listing.stringsKey}.title.short`)})
- )));
-
- if (forSidebar) {
- return filteredByCondition.map(({ title, listings }) =>
- html.tag('details', {
- open: !forSidebar || listings.includes(currentListing),
- class: listings.includes(currentListing) && 'current'
- }, [
- html.tag('summary',
- {style: getLinkThemeString(wikiInfo.color)},
- html.tag('span',
- {class: 'group-name'},
- title({language}))),
- genUL(listings)
- ])).join('\n');
- } else {
- return html.tag('dl',
- filteredByCondition.flatMap(({ title, listings }) => [
- html.tag('dt', title({language})),
- html.tag('dd', genUL(listings))
- ]));
- }
+function generateLinkIndexForListings(
+ currentListing,
+ forSidebar,
+ { getLinkThemeString, link, language, wikiData }
+) {
+ const { listingTargetSpec, wikiInfo } = wikiData;
+
+ const filteredByCondition = listingTargetSpec
+ .map(({ listings, ...rest }) => ({
+ ...rest,
+ listings: listings.filter(({ condition: c }) => !c || c({ wikiData })),
+ }))
+ .filter(({ listings }) => listings.length > 0);
+
+ const genUL = (listings) =>
+ html.tag(
+ "ul",
+ listings.map((listing) =>
+ html.tag(
+ "li",
+ { class: [listing === currentListing && "current"] },
+ link.listing(listing, {
+ text: language.$(`listingPage.${listing.stringsKey}.title.short`),
+ })
+ )
+ )
+ );
+
+ if (forSidebar) {
+ return filteredByCondition
+ .map(({ title, listings }) =>
+ html.tag(
+ "details",
+ {
+ open: !forSidebar || listings.includes(currentListing),
+ class: listings.includes(currentListing) && "current",
+ },
+ [
+ html.tag(
+ "summary",
+ { style: getLinkThemeString(wikiInfo.color) },
+ html.tag("span", { class: "group-name" }, title({ language }))
+ ),
+ genUL(listings),
+ ]
+ )
+ )
+ .join("\n");
+ } else {
+ return html.tag(
+ "dl",
+ filteredByCondition.flatMap(({ title, listings }) => [
+ html.tag("dt", title({ language })),
+ html.tag("dd", genUL(listings)),
+ ])
+ );
+ }
}
diff --git a/src/page/news.js b/src/page/news.js
index 9336506f..2fc5d7b0 100644
--- a/src/page/news.js
+++ b/src/page/news.js
@@ -2,126 +2,135 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
// Page exports
-export function condition({wikiData}) {
- return wikiData.wikiInfo.enableNews;
+export function condition({ wikiData }) {
+ return wikiData.wikiInfo.enableNews;
}
-export function targets({wikiData}) {
- return wikiData.newsData;
+export function targets({ wikiData }) {
+ return wikiData.newsData;
}
-export function write(entry, {wikiData}) {
- const page = {
- type: 'page',
- path: ['newsEntry', entry.directory],
- page: ({
- generatePreviousNextLinks,
- link,
- language,
- transformMultiline,
- }) => ({
- title: language.$('newsEntryPage.title', {entry: entry.name}),
-
- main: {
- content: fixWS`
+export function write(entry, { wikiData }) {
+ const page = {
+ type: "page",
+ path: ["newsEntry", entry.directory],
+ page: ({
+ generatePreviousNextLinks,
+ link,
+ language,
+ transformMultiline,
+ }) => ({
+ title: language.$("newsEntryPage.title", { entry: entry.name }),
+
+ main: {
+ content: fixWS`
-
${language.$('newsEntryPage.title', {entry: entry.name})}
-
${language.$('newsEntryPage.published', {date: language.formatDate(entry.date)})}
+
${language.$("newsEntryPage.title", {
+ entry: entry.name,
+ })}
+
${language.$("newsEntryPage.published", {
+ date: language.formatDate(entry.date),
+ })}
${transformMultiline(entry.content)}
- `
- },
-
- nav: generateNewsEntryNav(entry, {
- generatePreviousNextLinks,
- link,
- language,
- wikiData
- })
- })
- };
-
- return [page];
+ `,
+ },
+
+ nav: generateNewsEntryNav(entry, {
+ generatePreviousNextLinks,
+ link,
+ language,
+ wikiData,
+ }),
+ }),
+ };
+
+ return [page];
}
-export function writeTargetless({wikiData}) {
- const { newsData } = wikiData;
-
- const page = {
- type: 'page',
- path: ['newsIndex'],
- page: ({
- link,
- language,
- transformMultiline
- }) => ({
- title: language.$('newsIndex.title'),
-
- main: {
- content: fixWS`
+export function writeTargetless({ wikiData }) {
+ const { newsData } = wikiData;
+
+ const page = {
+ type: "page",
+ path: ["newsIndex"],
+ page: ({ link, language, transformMultiline }) => ({
+ title: language.$("newsIndex.title"),
+
+ main: {
+ content: fixWS`
-
${language.$('newsIndex.title')}
- ${newsData.map(entry => fixWS`
+
${language.$("newsIndex.title")}
+ ${newsData
+ .map(
+ (entry) => fixWS`
- ${language.formatDate(entry.date)} ${link.newsEntry(entry)}
+ ${language.formatDate(
+ entry.date
+ )} ${link.newsEntry(entry)}
${transformMultiline(entry.contentShort)}
- ${entry.contentShort !== entry.content && `${link.newsEntry(entry, {
- text: language.$('newsIndex.entry.viewRest')
- })}
`}
+ ${
+ entry.contentShort !== entry.content &&
+ `${link.newsEntry(entry, {
+ text: language.$(
+ "newsIndex.entry.viewRest"
+ ),
+ })}
`
+ }
- `).join('\n')}
+ `
+ )
+ .join("\n")}
- `
- },
+ `,
+ },
- nav: {simple: true}
- })
- };
+ nav: { simple: true },
+ }),
+ };
- return [page];
+ return [page];
}
// Utility functions
-function generateNewsEntryNav(entry, {
- generatePreviousNextLinks,
+function generateNewsEntryNav(
+ entry,
+ { generatePreviousNextLinks, link, language, wikiData }
+) {
+ const { wikiInfo, newsData } = wikiData;
+
+ // The newsData list is sorted reverse chronologically (newest ones first),
+ // so the way we find next/previous entries is flipped from normal.
+ const previousNextLinks = generatePreviousNextLinks(entry, {
link,
language,
- wikiData
-}) {
- const { wikiInfo, newsData } = wikiData;
-
- // The newsData list is sorted reverse chronologically (newest ones first),
- // so the way we find next/previous entries is flipped from normal.
- const previousNextLinks = generatePreviousNextLinks(entry, {
- link, language,
- data: newsData.slice().reverse(),
- linkKey: 'newsEntry'
- });
-
- return {
- linkContainerClasses: ['nav-links-hierarchy'],
- links: [
- {toHome: true},
- {
- path: ['localized.newsIndex'],
- title: language.$('newsEntryPage.nav.news')
- },
- {
- html: language.$('newsEntryPage.nav.entry', {
- date: language.formatDate(entry.date),
- entry: link.newsEntry(entry, {class: 'current'})
- })
- },
- previousNextLinks &&
- {
- divider: false,
- html: `(${previousNextLinks})`
- }
- ]
- };
+ data: newsData.slice().reverse(),
+ linkKey: "newsEntry",
+ });
+
+ return {
+ linkContainerClasses: ["nav-links-hierarchy"],
+ links: [
+ { toHome: true },
+ {
+ path: ["localized.newsIndex"],
+ title: language.$("newsEntryPage.nav.news"),
+ },
+ {
+ html: language.$("newsEntryPage.nav.entry", {
+ date: language.formatDate(entry.date),
+ entry: link.newsEntry(entry, { class: "current" }),
+ }),
+ },
+ previousNextLinks && {
+ divider: false,
+ html: `(${previousNextLinks})`,
+ },
+ ],
+ };
}
diff --git a/src/page/static.js b/src/page/static.js
index e9b6a047..39acd64e 100644
--- a/src/page/static.js
+++ b/src/page/static.js
@@ -4,37 +4,34 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
// Page exports
-export function targets({wikiData}) {
- return wikiData.staticPageData;
+export function targets({ wikiData }) {
+ return wikiData.staticPageData;
}
-export function write(staticPage, {wikiData}) {
- const page = {
- type: 'page',
- path: ['staticPage', staticPage.directory],
- page: ({
- language,
- transformMultiline
- }) => ({
- title: staticPage.name,
- stylesheet: staticPage.stylesheet,
-
- main: {
- content: fixWS`
+export function write(staticPage, { wikiData }) {
+ const page = {
+ type: "page",
+ path: ["staticPage", staticPage.directory],
+ page: ({ language, transformMultiline }) => ({
+ title: staticPage.name,
+ stylesheet: staticPage.stylesheet,
+
+ main: {
+ content: fixWS`
${staticPage.name}
${transformMultiline(staticPage.content)}
- `
- },
+ `,
+ },
- nav: {simple: true}
- })
- };
+ nav: { simple: true },
+ }),
+ };
- return [page];
+ return [page];
}
diff --git a/src/page/tag.js b/src/page/tag.js
index 471439da..98b552b3 100644
--- a/src/page/tag.js
+++ b/src/page/tag.js
@@ -2,110 +2,111 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
// Page exports
-export function condition({wikiData}) {
- return wikiData.wikiInfo.enableArtTagUI;
+export function condition({ wikiData }) {
+ return wikiData.wikiInfo.enableArtTagUI;
}
-export function targets({wikiData}) {
- return wikiData.artTagData.filter(tag => !tag.isContentWarning);
+export function targets({ wikiData }) {
+ return wikiData.artTagData.filter((tag) => !tag.isContentWarning);
}
-export function write(tag, {wikiData}) {
- const { wikiInfo } = wikiData;
- const { taggedInThings: things } = tag;
+export function write(tag, { wikiData }) {
+ const { wikiInfo } = wikiData;
+ const { taggedInThings: things } = tag;
- // Display things featuring this art tag in reverse chronological order,
- // sticking the most recent additions near the top!
- const thingsReversed = things.slice().reverse();
+ // Display things featuring this art tag in reverse chronological order,
+ // sticking the most recent additions near the top!
+ const thingsReversed = things.slice().reverse();
- const entries = thingsReversed.map(item => ({item}));
+ const entries = thingsReversed.map((item) => ({ item }));
- const page = {
- type: 'page',
- path: ['tag', tag.directory],
- page: ({
- generatePreviousNextLinks,
- getAlbumCover,
- getGridHTML,
- getThemeString,
- getTrackCover,
- link,
- language,
- to
- }) => ({
- title: language.$('tagPage.title', {tag: tag.name}),
- theme: getThemeString(tag.color),
+ const page = {
+ type: "page",
+ path: ["tag", tag.directory],
+ page: ({
+ generatePreviousNextLinks,
+ getAlbumCover,
+ getGridHTML,
+ getThemeString,
+ getTrackCover,
+ link,
+ language,
+ to,
+ }) => ({
+ title: language.$("tagPage.title", { tag: tag.name }),
+ theme: getThemeString(tag.color),
- main: {
- classes: ['top-index'],
- content: fixWS`
-
${language.$('tagPage.title', {tag: tag.name})}
-
${language.$('tagPage.infoLine', {
- coverArts: language.countCoverArts(things.length, {unit: true})
+ main: {
+ classes: ["top-index"],
+ content: fixWS`
+
${language.$("tagPage.title", { tag: tag.name })}
+
${language.$("tagPage.infoLine", {
+ coverArts: language.countCoverArts(things.length, {
+ unit: true,
+ }),
})}
${getGridHTML({
- entries,
- srcFn: thing => (thing.album
- ? getTrackCover(thing)
- : getAlbumCover(thing)),
- linkFn: (thing, opts) => (thing.album
- ? link.track(thing, opts)
- : link.album(thing, opts))
+ entries,
+ srcFn: (thing) =>
+ thing.album
+ ? getTrackCover(thing)
+ : getAlbumCover(thing),
+ linkFn: (thing, opts) =>
+ thing.album
+ ? link.track(thing, opts)
+ : link.album(thing, opts),
})}
- `
- },
+ `,
+ },
- nav: generateTagNav(tag, {
- generatePreviousNextLinks,
- link,
- language,
- wikiData
- })
- })
- };
+ nav: generateTagNav(tag, {
+ generatePreviousNextLinks,
+ link,
+ language,
+ wikiData,
+ }),
+ }),
+ };
- return [page];
+ return [page];
}
// Utility functions
-function generateTagNav(tag, {
- generatePreviousNextLinks,
- link,
- language,
- wikiData
-}) {
- const previousNextLinks = generatePreviousNextLinks(tag, {
- data: wikiData.artTagData.filter(tag => !tag.isContentWarning),
- linkKey: 'tag'
- });
+function generateTagNav(
+ tag,
+ { generatePreviousNextLinks, link, language, wikiData }
+) {
+ const previousNextLinks = generatePreviousNextLinks(tag, {
+ data: wikiData.artTagData.filter((tag) => !tag.isContentWarning),
+ linkKey: "tag",
+ });
- return {
- linkContainerClasses: ['nav-links-hierarchy'],
- links: [
- {toHome: true},
- wikiData.wikiInfo.enableListings &&
- {
- path: ['localized.listingIndex'],
- title: language.$('listingIndex.title')
- },
- {
- html: language.$('tagPage.nav.tag', {
- tag: link.tag(tag, {class: 'current'})
- })
- },
- /*
+ return {
+ linkContainerClasses: ["nav-links-hierarchy"],
+ links: [
+ { toHome: true },
+ wikiData.wikiInfo.enableListings && {
+ path: ["localized.listingIndex"],
+ title: language.$("listingIndex.title"),
+ },
+ {
+ html: language.$("tagPage.nav.tag", {
+ tag: link.tag(tag, { class: "current" }),
+ }),
+ },
+ /*
previousNextLinks && {
divider: false,
html: `(${previousNextLinks})`
}
*/
- ]
- };
+ ],
+ };
}
diff --git a/src/page/track.js b/src/page/track.js
index c4ec6c59..15316e8f 100644
--- a/src/page/track.js
+++ b/src/page/track.js
@@ -2,186 +2,221 @@
// Imports
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
import {
- generateAlbumChronologyLinks,
- generateAlbumNavLinks,
- generateAlbumSecondaryNav,
- generateAlbumSidebar
-} from './album.js';
+ generateAlbumChronologyLinks,
+ generateAlbumNavLinks,
+ generateAlbumSecondaryNav,
+ generateAlbumSidebar,
+} from "./album.js";
-import * as html from '../util/html.js';
+import * as html from "../util/html.js";
-import {
- bindOpts
-} from '../util/sugar.js';
+import { bindOpts } from "../util/sugar.js";
import {
- getTrackCover,
- getAlbumListTag,
- sortChronologically,
-} from '../util/wiki-data.js';
+ getTrackCover,
+ getAlbumListTag,
+ sortChronologically,
+} from "../util/wiki-data.js";
// Page exports
-export function targets({wikiData}) {
- return wikiData.trackData;
+export function targets({ wikiData }) {
+ return wikiData.trackData;
}
-export function write(track, {wikiData}) {
- const { groupData, wikiInfo } = wikiData;
- const { album, referencedByTracks, referencedTracks, otherReleases } = track;
+export function write(track, { wikiData }) {
+ const { groupData, wikiInfo } = wikiData;
+ const { album, referencedByTracks, referencedTracks, otherReleases } = track;
- const listTag = getAlbumListTag(album);
+ const listTag = getAlbumListTag(album);
- let flashesThatFeature;
- if (wikiInfo.enableFlashesAndGames) {
- flashesThatFeature = sortChronologically([track, ...otherReleases]
- .flatMap(track => track.featuredInFlashes
- .map(flash => ({
- flash,
- as: track,
- directory: flash.directory,
- name: flash.name,
- date: flash.date
- }))));
- }
+ let flashesThatFeature;
+ if (wikiInfo.enableFlashesAndGames) {
+ flashesThatFeature = sortChronologically(
+ [track, ...otherReleases].flatMap((track) =>
+ track.featuredInFlashes.map((flash) => ({
+ flash,
+ as: track,
+ directory: flash.directory,
+ name: flash.name,
+ date: flash.date,
+ }))
+ )
+ );
+ }
- const unbound_getTrackItem = (track, {getArtistString, link, language}) => (
- html.tag('li', language.$('trackList.item.withArtists', {
- track: link.track(track),
- by: `
${language.$('trackList.item.withArtists.by', {
- artists: getArtistString(track.artistContribs)
- })} `
- })));
+ const unbound_getTrackItem = (track, { getArtistString, link, language }) =>
+ html.tag(
+ "li",
+ language.$("trackList.item.withArtists", {
+ track: link.track(track),
+ by: `
${language.$("trackList.item.withArtists.by", {
+ artists: getArtistString(track.artistContribs),
+ })} `,
+ })
+ );
- const unbound_generateTrackList = (tracks, {getArtistString, link, language}) => html.tag('ul',
- tracks.map(track => unbound_getTrackItem(track, {getArtistString, link, language}))
+ const unbound_generateTrackList = (
+ tracks,
+ { getArtistString, link, language }
+ ) =>
+ html.tag(
+ "ul",
+ tracks.map((track) =>
+ unbound_getTrackItem(track, { getArtistString, link, language })
+ )
);
- const hasCommentary = track.commentary || otherReleases.some(t => t.commentary);
- const generateCommentary = ({
- link,
- language,
- transformMultiline
- }) => transformMultiline([
+ const hasCommentary =
+ track.commentary || otherReleases.some((t) => t.commentary);
+ const generateCommentary = ({ link, language, transformMultiline }) =>
+ transformMultiline(
+ [
track.commentary,
- ...otherReleases.map(track =>
- (track.commentary?.split('\n')
- .filter(line => line.replace(/<\/b>/g, '').includes(':'))
- .map(line => fixWS`
+ ...otherReleases.map((track) =>
+ track.commentary
+ ?.split("\n")
+ .filter((line) => line.replace(/<\/b>/g, "").includes(":"))
+ .map(
+ (line) => fixWS`
${line}
- ${language.$('releaseInfo.artistCommentary.seeOriginalRelease', {
- original: link.track(track)
- })}
- `)
- .join('\n')))
- ].filter(Boolean).join('\n'));
+ ${language.$(
+ "releaseInfo.artistCommentary.seeOriginalRelease",
+ {
+ original: link.track(track),
+ }
+ )}
+ `
+ )
+ .join("\n")
+ ),
+ ]
+ .filter(Boolean)
+ .join("\n")
+ );
- const data = {
- type: 'data',
- path: ['track', track.directory],
- data: ({
- serializeContribs,
- serializeCover,
- serializeGroupsForTrack,
- serializeLink
- }) => ({
- name: track.name,
- directory: track.directory,
- dates: {
- released: track.date,
- originallyReleased: track.originalDate,
- coverArtAdded: track.coverArtDate
- },
- duration: track.duration,
- color: track.color,
- cover: serializeCover(track, getTrackCover),
- artistsContribs: serializeContribs(track.artistContribs),
- contributorContribs: serializeContribs(track.contributorContribs),
- coverArtistContribs: serializeContribs(track.coverArtistContribs || []),
- album: serializeLink(track.album),
- groups: serializeGroupsForTrack(track),
- references: track.references.map(serializeLink),
- referencedBy: track.referencedBy.map(serializeLink),
- alsoReleasedAs: otherReleases.map(track => ({
- track: serializeLink(track),
- album: serializeLink(track.album)
- }))
- })
- };
+ const data = {
+ type: "data",
+ path: ["track", track.directory],
+ data: ({
+ serializeContribs,
+ serializeCover,
+ serializeGroupsForTrack,
+ serializeLink,
+ }) => ({
+ name: track.name,
+ directory: track.directory,
+ dates: {
+ released: track.date,
+ originallyReleased: track.originalDate,
+ coverArtAdded: track.coverArtDate,
+ },
+ duration: track.duration,
+ color: track.color,
+ cover: serializeCover(track, getTrackCover),
+ artistsContribs: serializeContribs(track.artistContribs),
+ contributorContribs: serializeContribs(track.contributorContribs),
+ coverArtistContribs: serializeContribs(track.coverArtistContribs || []),
+ album: serializeLink(track.album),
+ groups: serializeGroupsForTrack(track),
+ references: track.references.map(serializeLink),
+ referencedBy: track.referencedBy.map(serializeLink),
+ alsoReleasedAs: otherReleases.map((track) => ({
+ track: serializeLink(track),
+ album: serializeLink(track.album),
+ })),
+ }),
+ };
- const getSocialEmbedDescription = ({
- getArtistString: _getArtistString,
- language,
- }) => {
- const hasArtists = (track.artistContribs?.length > 0);
- const hasCoverArtists = (track.coverArtistContribs?.length > 0);
- const getArtistString = contribs => _getArtistString(contribs, {
- // We don't want to put actual HTML tags in social embeds (sadly
- // they don't get parsed and displayed, generally speaking), so
- // override the link argument so that artist "links" just show
- // their names.
- link: {artist: artist => artist.name}
- });
- if (!hasArtists && !hasCoverArtists) return '';
- return language.formatString(
- 'trackPage.socialEmbed.body' + [
- hasArtists && '.withArtists',
- hasCoverArtists && '.withCoverArtists',
- ].filter(Boolean).join(''),
- Object.fromEntries([
- hasArtists && ['artists', getArtistString(track.artistContribs)],
- hasCoverArtists && ['coverArtists', getArtistString(track.coverArtistContribs)],
- ].filter(Boolean)))
- };
+ const getSocialEmbedDescription = ({
+ getArtistString: _getArtistString,
+ language,
+ }) => {
+ const hasArtists = track.artistContribs?.length > 0;
+ const hasCoverArtists = track.coverArtistContribs?.length > 0;
+ const getArtistString = (contribs) =>
+ _getArtistString(contribs, {
+ // We don't want to put actual HTML tags in social embeds (sadly
+ // they don't get parsed and displayed, generally speaking), so
+ // override the link argument so that artist "links" just show
+ // their names.
+ link: { artist: (artist) => artist.name },
+ });
+ if (!hasArtists && !hasCoverArtists) return "";
+ return language.formatString(
+ "trackPage.socialEmbed.body" +
+ [hasArtists && ".withArtists", hasCoverArtists && ".withCoverArtists"]
+ .filter(Boolean)
+ .join(""),
+ Object.fromEntries(
+ [
+ hasArtists && ["artists", getArtistString(track.artistContribs)],
+ hasCoverArtists && [
+ "coverArtists",
+ getArtistString(track.coverArtistContribs),
+ ],
+ ].filter(Boolean)
+ )
+ );
+ };
- const page = {
- type: 'page',
- path: ['track', track.directory],
- page: ({
- absoluteTo,
- fancifyURL,
- generateChronologyLinks,
- generateCoverLink,
- generatePreviousNextLinks,
- generateTrackListDividedByGroups,
- getAlbumStylesheet,
- getArtistString,
- getLinkThemeString,
- getThemeString,
- getTrackCover,
- link,
- language,
- transformInline,
- transformLyrics,
- transformMultiline,
- to,
- urls,
- }) => {
- const getTrackItem = bindOpts(unbound_getTrackItem, {getArtistString, link, language});
- const cover = getTrackCover(track);
+ const page = {
+ type: "page",
+ path: ["track", track.directory],
+ page: ({
+ absoluteTo,
+ fancifyURL,
+ generateChronologyLinks,
+ generateCoverLink,
+ generatePreviousNextLinks,
+ generateTrackListDividedByGroups,
+ getAlbumStylesheet,
+ getArtistString,
+ getLinkThemeString,
+ getThemeString,
+ getTrackCover,
+ link,
+ language,
+ transformInline,
+ transformLyrics,
+ transformMultiline,
+ to,
+ urls,
+ }) => {
+ const getTrackItem = bindOpts(unbound_getTrackItem, {
+ getArtistString,
+ link,
+ language,
+ });
+ const cover = getTrackCover(track);
- return {
- title: language.$('trackPage.title', {track: track.name}),
- stylesheet: getAlbumStylesheet(album, {to}),
- theme: getThemeString(track.color, [
- `--album-directory: ${album.directory}`,
- `--track-directory: ${track.directory}`
- ]),
+ return {
+ title: language.$("trackPage.title", { track: track.name }),
+ stylesheet: getAlbumStylesheet(album, { to }),
+ theme: getThemeString(track.color, [
+ `--album-directory: ${album.directory}`,
+ `--track-directory: ${track.directory}`,
+ ]),
- socialEmbed: {
- heading: language.$('trackPage.socialEmbed.heading', {album: track.album.name}),
- headingLink: absoluteTo('localized.album', album.directory),
- title: language.$('trackPage.socialEmbed.title', {track: track.name}),
- description: getSocialEmbedDescription({getArtistString, language}),
- image: '/' + getTrackCover(track, {to: urls.from('shared.root').to}),
- color: track.color,
- },
+ socialEmbed: {
+ heading: language.$("trackPage.socialEmbed.heading", {
+ album: track.album.name,
+ }),
+ headingLink: absoluteTo("localized.album", album.directory),
+ title: language.$("trackPage.socialEmbed.title", {
+ track: track.name,
+ }),
+ description: getSocialEmbedDescription({ getArtistString, language }),
+ image:
+ "/" + getTrackCover(track, { to: urls.from("shared.root").to }),
+ color: track.color,
+ },
- // disabled for now! shifting banner position per height of page is disorienting
- /*
+ // disabled for now! shifting banner position per height of page is disorienting
+ /*
banner: album.bannerArtistContribs.length && {
classes: ['dim'],
dimensions: album.bannerDimensions,
@@ -191,156 +226,239 @@ export function write(track, {wikiData}) {
},
*/
- main: {
- content: fixWS`
- ${cover && generateCoverLink({
+ main: {
+ content: fixWS`
+ ${
+ cover &&
+ generateCoverLink({
src: cover,
- alt: language.$('misc.alt.trackCover'),
- tags: track.artTags
- })}
-
${language.$('trackPage.title', {track: track.name})}
+ alt: language.$("misc.alt.trackCover"),
+ tags: track.artTags,
+ })
+ }
+
${language.$("trackPage.title", {
+ track: track.name,
+ })}
${[
- language.$('releaseInfo.by', {
- artists: getArtistString(track.artistContribs, {
- showContrib: true,
- showIcons: true
- })
+ language.$("releaseInfo.by", {
+ artists: getArtistString(track.artistContribs, {
+ showContrib: true,
+ showIcons: true,
}),
- track.coverArtistContribs.length && language.$('releaseInfo.coverArtBy', {
- artists: getArtistString(track.coverArtistContribs, {
- showContrib: true,
- showIcons: true
- })
+ }),
+ track.coverArtistContribs.length &&
+ language.$("releaseInfo.coverArtBy", {
+ artists: getArtistString(
+ track.coverArtistContribs,
+ {
+ showContrib: true,
+ showIcons: true,
+ }
+ ),
+ }),
+ track.date &&
+ language.$("releaseInfo.released", {
+ date: language.formatDate(track.date),
}),
- track.date && language.$('releaseInfo.released', {
- date: language.formatDate(track.date)
+ track.coverArtDate &&
+ +track.coverArtDate !== +track.date &&
+ language.$("releaseInfo.artReleased", {
+ date: language.formatDate(track.coverArtDate),
}),
- (track.coverArtDate &&
- +track.coverArtDate !== +track.date &&
- language.$('releaseInfo.artReleased', {
- date: language.formatDate(track.coverArtDate)
- })),
- track.duration && language.$('releaseInfo.duration', {
- duration: language.formatDuration(track.duration)
- })
- ].filter(Boolean).join(' \n')}
+ track.duration &&
+ language.$("releaseInfo.duration", {
+ duration: language.formatDuration(
+ track.duration
+ ),
+ }),
+ ]
+ .filter(Boolean)
+ .join(" \n")}
${
- (track.urls?.length
- ? language.$('releaseInfo.listenOn', {
- links: language.formatDisjunctionList(track.urls.map(url => fancifyURL(url, {language})))
- })
- : language.$('releaseInfo.listenOn.noLinks'))
+ track.urls?.length
+ ? language.$("releaseInfo.listenOn", {
+ links: language.formatDisjunctionList(
+ track.urls.map((url) =>
+ fancifyURL(url, { language })
+ )
+ ),
+ })
+ : language.$("releaseInfo.listenOn.noLinks")
}
- ${otherReleases.length && fixWS`
-
${language.$('releaseInfo.alsoReleasedAs')}
+ ${
+ otherReleases.length &&
+ fixWS`
+
${language.$("releaseInfo.alsoReleasedAs")}
- ${otherReleases.map(track => fixWS`
- ${language.$('releaseInfo.alsoReleasedAs.item', {
+ ${otherReleases
+ .map(
+ (track) => fixWS`
+ ${language.$(
+ "releaseInfo.alsoReleasedAs.item",
+ {
track: link.track(track),
- album: link.album(track.album)
- })}
- `).join('\n')}
+ album: link.album(track.album),
+ }
+ )}
+ `
+ )
+ .join("\n")}
- `}
- ${track.contributorContribs.length && fixWS`
-
${language.$('releaseInfo.contributors')}
+ `
+ }
+ ${
+ track.contributorContribs.length &&
+ fixWS`
+
${language.$("releaseInfo.contributors")}
- ${(track.contributorContribs
- .map(contrib => `${getArtistString([contrib], {
+ ${track.contributorContribs
+ .map(
+ (contrib) =>
+ ` ${getArtistString([contrib], {
showContrib: true,
- showIcons: true
- })} `)
- .join('\n'))}
+ showIcons: true,
+ })}`
+ )
+ .join("\n")}
- `}
- ${referencedTracks.length && fixWS`
-
${language.$('releaseInfo.tracksReferenced', {track: `${track.name} `})}
- ${html.tag('ul', referencedTracks.map(getTrackItem))}
- `}
- ${referencedByTracks.length && fixWS`
-
${language.$('releaseInfo.tracksThatReference', {track: `${track.name} `})}
- ${generateTrackListDividedByGroups(referencedByTracks, {
+ `
+ }
+ ${
+ referencedTracks.length &&
+ fixWS`
+
${language.$("releaseInfo.tracksReferenced", {
+ track: `${track.name} `,
+ })}
+ ${html.tag(
+ "ul",
+ referencedTracks.map(getTrackItem)
+ )}
+ `
+ }
+ ${
+ referencedByTracks.length &&
+ fixWS`
+
${language.$("releaseInfo.tracksThatReference", {
+ track: `${track.name} `,
+ })}
+ ${generateTrackListDividedByGroups(
+ referencedByTracks,
+ {
getTrackItem,
wikiData,
- })}
- `}
- ${wikiInfo.enableFlashesAndGames && flashesThatFeature.length && fixWS`
-
${language.$('releaseInfo.flashesThatFeature', {track: `${track.name} `})}
+ }
+ )}
+ `
+ }
+ ${
+ wikiInfo.enableFlashesAndGames &&
+ flashesThatFeature.length &&
+ fixWS`
+
${language.$("releaseInfo.flashesThatFeature", {
+ track: `${track.name} `,
+ })}
- ${flashesThatFeature.map(({ flash, as }) => html.tag('li',
- {class: as !== track && 'rerelease'},
- (as === track
- ? language.$('releaseInfo.flashesThatFeature.item', {
- flash: link.flash(flash)
- })
- : language.$('releaseInfo.flashesThatFeature.item.asDifferentRelease', {
- flash: link.flash(flash),
- track: link.track(as)
- })))).join('\n')}
+ ${flashesThatFeature
+ .map(({ flash, as }) =>
+ html.tag(
+ "li",
+ { class: as !== track && "rerelease" },
+ as === track
+ ? language.$(
+ "releaseInfo.flashesThatFeature.item",
+ {
+ flash: link.flash(flash),
+ }
+ )
+ : language.$(
+ "releaseInfo.flashesThatFeature.item.asDifferentRelease",
+ {
+ flash: link.flash(flash),
+ track: link.track(as),
+ }
+ )
+ )
+ )
+ .join("\n")}
- `}
- ${track.lyrics && fixWS`
-
${language.$('releaseInfo.lyrics')}
+ `
+ }
+ ${
+ track.lyrics &&
+ fixWS`
+
${language.$("releaseInfo.lyrics")}
${transformLyrics(track.lyrics)}
- `}
- ${hasCommentary && fixWS`
-
${language.$('releaseInfo.artistCommentary')}
+ `
+ }
+ ${
+ hasCommentary &&
+ fixWS`
+
${language.$("releaseInfo.artistCommentary")}
- ${generateCommentary({link, language, transformMultiline})}
+ ${generateCommentary({
+ link,
+ language,
+ transformMultiline,
+ })}
- `}
- `
- },
+ `
+ }
+ `,
+ },
- sidebarLeft: generateAlbumSidebar(album, track, {
- fancifyURL,
- getLinkThemeString,
- link,
- language,
- transformMultiline,
- wikiData
- }),
+ sidebarLeft: generateAlbumSidebar(album, track, {
+ fancifyURL,
+ getLinkThemeString,
+ link,
+ language,
+ transformMultiline,
+ wikiData,
+ }),
- nav: {
- linkContainerClasses: ['nav-links-hierarchy'],
- links: [
- {toHome: true},
- {
- path: ['localized.album', album.directory],
- title: album.name
- },
- listTag === 'ol' ? {
- html: language.$('trackPage.nav.track.withNumber', {
- number: album.tracks.indexOf(track) + 1,
- track: link.track(track, {class: 'current', to})
- })
- } : {
- html: language.$('trackPage.nav.track', {
- track: link.track(track, {class: 'current', to})
- })
- },
- ].filter(Boolean),
- content: generateAlbumChronologyLinks(album, track, {generateChronologyLinks}),
- bottomRowContent: (album.tracks.length > 1 &&
- generateAlbumNavLinks(album, track, {
- generatePreviousNextLinks,
- language,
- })),
+ nav: {
+ linkContainerClasses: ["nav-links-hierarchy"],
+ links: [
+ { toHome: true },
+ {
+ path: ["localized.album", album.directory],
+ title: album.name,
+ },
+ listTag === "ol"
+ ? {
+ html: language.$("trackPage.nav.track.withNumber", {
+ number: album.tracks.indexOf(track) + 1,
+ track: link.track(track, { class: "current", to }),
+ }),
+ }
+ : {
+ html: language.$("trackPage.nav.track", {
+ track: link.track(track, { class: "current", to }),
+ }),
},
+ ].filter(Boolean),
+ content: generateAlbumChronologyLinks(album, track, {
+ generateChronologyLinks,
+ }),
+ bottomRowContent:
+ album.tracks.length > 1 &&
+ generateAlbumNavLinks(album, track, {
+ generatePreviousNextLinks,
+ language,
+ }),
+ },
- secondaryNav: generateAlbumSecondaryNav(album, track, {
- language,
- link,
- getLinkThemeString,
- }),
- };
- }
- };
+ secondaryNav: generateAlbumSecondaryNav(album, track, {
+ language,
+ link,
+ getLinkThemeString,
+ }),
+ };
+ },
+ };
- return [data, page];
+ return [data, page];
}
-
diff --git a/src/repl.js b/src/repl.js
index cd4c3212..1a694d7e 100644
--- a/src/repl.js
+++ b/src/repl.js
@@ -1,70 +1,70 @@
-import * as os from 'os';
-import * as path from 'path';
-import * as repl from 'repl';
-import { fileURLToPath } from 'url';
-import { promisify } from 'util';
+import * as os from "os";
+import * as path from "path";
+import * as repl from "repl";
+import { fileURLToPath } from "url";
+import { promisify } from "util";
-import { quickLoadAllFromYAML } from './data/yaml.js';
-import { logError, parseOptions } from './util/cli.js';
-import { showAggregate } from './util/sugar.js';
+import { quickLoadAllFromYAML } from "./data/yaml.js";
+import { logError, parseOptions } from "./util/cli.js";
+import { showAggregate } from "./util/sugar.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function main() {
- const miscOptions = await parseOptions(process.argv.slice(2), {
- 'data-path': {
- type: 'value'
- },
+ const miscOptions = await parseOptions(process.argv.slice(2), {
+ "data-path": {
+ type: "value",
+ },
- 'show-traces': {
- type: 'flag'
- },
+ "show-traces": {
+ type: "flag",
+ },
- 'no-history': {
- type: 'flag'
- },
- });
+ "no-history": {
+ type: "flag",
+ },
+ });
- const dataPath = miscOptions['data-path'] || process.env.HSMUSIC_DATA;
- const showAggregateTraces = miscOptions['show-traces'] ?? false;
- const disableHistory = miscOptions['no-history'] ?? false;
+ const dataPath = miscOptions["data-path"] || process.env.HSMUSIC_DATA;
+ const showAggregateTraces = miscOptions["show-traces"] ?? false;
+ const disableHistory = miscOptions["no-history"] ?? false;
- if (!dataPath) {
- logError`Expected --data-path option or HSMUSIC_DATA to be set`;
- return;
- }
+ if (!dataPath) {
+ logError`Expected --data-path option or HSMUSIC_DATA to be set`;
+ return;
+ }
- console.log('HSMusic data REPL');
+ console.log("HSMusic data REPL");
- const wikiData = await quickLoadAllFromYAML(dataPath);
- const replServer = repl.start();
+ const wikiData = await quickLoadAllFromYAML(dataPath);
+ const replServer = repl.start();
- Object.assign(
- replServer.context,
- wikiData,
- {wikiData, WD: wikiData}
- );
+ Object.assign(replServer.context, wikiData, { wikiData, WD: wikiData });
- if (disableHistory) {
- console.log(`\rInput history disabled (--no-history provided)`);
- replServer.displayPrompt(true);
- } else {
- const historyFile = path.join(os.homedir(), '.hsmusic_repl_history');
- replServer.setupHistory(historyFile, err => {
- if (err) {
- console.error(`\rFailed to begin locally logging input history to ${historyFile} (provide --no-history to disable)`);
- } else {
- console.log(`\rLogging input history to ${historyFile} (provide --no-history to disable)`);
- }
- replServer.displayPrompt(true);
- });
- }
+ if (disableHistory) {
+ console.log(`\rInput history disabled (--no-history provided)`);
+ replServer.displayPrompt(true);
+ } else {
+ const historyFile = path.join(os.homedir(), ".hsmusic_repl_history");
+ replServer.setupHistory(historyFile, (err) => {
+ if (err) {
+ console.error(
+ `\rFailed to begin locally logging input history to ${historyFile} (provide --no-history to disable)`
+ );
+ } else {
+ console.log(
+ `\rLogging input history to ${historyFile} (provide --no-history to disable)`
+ );
+ }
+ replServer.displayPrompt(true);
+ });
+ }
}
-main().catch(error => {
- if (error instanceof AggregateError) {
- showAggregate(error)
- } else {
- console.error(error);
- }
+main().catch((error) => {
+ if (error instanceof AggregateError) {
+ showAggregate(error);
+ } else {
+ console.error(error);
+ }
});
diff --git a/src/static/client.js b/src/static/client.js
index 7397735c..72fa9cc2 100644
--- a/src/static/client.js
+++ b/src/static/client.js
@@ -5,13 +5,9 @@
//
// Upd8: As of 04/02/2021, it's now used for info cards too! Nice.
-import {
- getColors
-} from '../util/colors.js';
+import { getColors } from "../util/colors.js";
-import {
- getArtistNumContributions
-} from '../util/wiki-data.js';
+import { getArtistNumContributions } from "../util/wiki-data.js";
let albumData, artistData, flashData;
let officialAlbumData, fandomAlbumData, artistNames;
@@ -20,190 +16,235 @@ let ready = false;
// Localiz8tion nonsense ----------------------------------
-const language = document.documentElement.getAttribute('lang');
+const language = document.documentElement.getAttribute("lang");
let list;
-if (
- typeof Intl === 'object' &&
- typeof Intl.ListFormat === 'function'
-) {
- const getFormat = type => {
- const formatter = new Intl.ListFormat(language, {type});
- return formatter.format.bind(formatter);
- };
-
- list = {
- conjunction: getFormat('conjunction'),
- disjunction: getFormat('disjunction'),
- unit: getFormat('unit')
- };
+if (typeof Intl === "object" && typeof Intl.ListFormat === "function") {
+ const getFormat = (type) => {
+ const formatter = new Intl.ListFormat(language, { type });
+ return formatter.format.bind(formatter);
+ };
+
+ list = {
+ conjunction: getFormat("conjunction"),
+ disjunction: getFormat("disjunction"),
+ unit: getFormat("unit"),
+ };
} else {
- // Not a gr8 mock we've got going here, 8ut it's *mostly* language-free.
- // We use the same mock for every list 'cuz we don't have any of the
- // necessary CLDR info to appropri8tely distinguish 8etween them.
- const arbitraryMock = array => array.join(', ');
-
- list = {
- conjunction: arbitraryMock,
- disjunction: arbitraryMock,
- unit: arbitraryMock
- };
+ // Not a gr8 mock we've got going here, 8ut it's *mostly* language-free.
+ // We use the same mock for every list 'cuz we don't have any of the
+ // necessary CLDR info to appropri8tely distinguish 8etween them.
+ const arbitraryMock = (array) => array.join(", ");
+
+ list = {
+ conjunction: arbitraryMock,
+ disjunction: arbitraryMock,
+ unit: arbitraryMock,
+ };
}
// Miscellaneous helpers ----------------------------------
-function rebase(href, rebaseKey = 'rebaseLocalized') {
- const relative = (document.documentElement.dataset[rebaseKey] || '.') + '/';
- if (relative) {
- return relative + href;
- } else {
- return href;
- }
+function rebase(href, rebaseKey = "rebaseLocalized") {
+ const relative = (document.documentElement.dataset[rebaseKey] || ".") + "/";
+ if (relative) {
+ return relative + href;
+ } else {
+ return href;
+ }
}
function pick(array) {
- return array[Math.floor(Math.random() * array.length)];
+ return array[Math.floor(Math.random() * array.length)];
}
function cssProp(el, key) {
- return getComputedStyle(el).getPropertyValue(key).trim();
+ return getComputedStyle(el).getPropertyValue(key).trim();
}
function getRefDirectory(ref) {
- return ref.split(':')[1];
+ return ref.split(":")[1];
}
function getAlbum(el) {
- const directory = cssProp(el, '--album-directory');
- return albumData.find(album => album.directory === directory);
+ const directory = cssProp(el, "--album-directory");
+ return albumData.find((album) => album.directory === directory);
}
function getFlash(el) {
- const directory = cssProp(el, '--flash-directory');
- return flashData.find(flash => flash.directory === directory);
+ const directory = cssProp(el, "--flash-directory");
+ return flashData.find((flash) => flash.directory === directory);
}
// TODO: These should pro8a8ly access some shared urlSpec path. We'd need to
// separ8te the tooling around that into common-shared code too.
const getLinkHref = (type, directory) => rebase(`${type}/${directory}`);
-const openAlbum = d => rebase(`album/${d}`);
-const openTrack = d => rebase(`track/${d}`);
-const openArtist = d => rebase(`artist/${d}`);
-const openFlash = d => rebase(`flash/${d}`);
+const openAlbum = (d) => rebase(`album/${d}`);
+const openTrack = (d) => rebase(`track/${d}`);
+const openArtist = (d) => rebase(`artist/${d}`);
+const openFlash = (d) => rebase(`flash/${d}`);
function getTrackListAndIndex() {
- const album = getAlbum(document.body);
- const directory = cssProp(document.body, '--track-directory');
- if (!directory && !album) return {};
- if (!directory) return {list: album.tracks};
- const trackIndex = album.tracks.findIndex(track => track.directory === directory);
- return {list: album.tracks, index: trackIndex};
+ const album = getAlbum(document.body);
+ const directory = cssProp(document.body, "--track-directory");
+ if (!directory && !album) return {};
+ if (!directory) return { list: album.tracks };
+ const trackIndex = album.tracks.findIndex(
+ (track) => track.directory === directory
+ );
+ return { list: album.tracks, index: trackIndex };
}
function openRandomTrack() {
- const { list } = getTrackListAndIndex();
- if (!list) return;
- return openTrack(pick(list));
+ const { list } = getTrackListAndIndex();
+ if (!list) return;
+ return openTrack(pick(list));
}
function getFlashListAndIndex() {
- const list = flashData.filter(flash => !flash.act8r8k)
- const flash = getFlash(document.body);
- if (!flash) return {list};
- const flashIndex = list.indexOf(flash);
- return {list, index: flashIndex};
+ const list = flashData.filter((flash) => !flash.act8r8k);
+ const flash = getFlash(document.body);
+ if (!flash) return { list };
+ const flashIndex = list.indexOf(flash);
+ return { list, index: flashIndex };
}
// TODO: This should also use urlSpec.
function fetchData(type, directory) {
- return fetch(rebase(`${type}/${directory}/data.json`, 'rebaseData'))
- .then(res => res.json());
+ return fetch(rebase(`${type}/${directory}/data.json`, "rebaseData")).then(
+ (res) => res.json()
+ );
}
// JS-based links -----------------------------------------
-for (const a of document.body.querySelectorAll('[data-random]')) {
- a.addEventListener('click', evt => {
- if (!ready) {
- evt.preventDefault();
- return;
- }
-
- setTimeout(() => {
- a.href = rebase('js-disabled');
- });
- switch (a.dataset.random) {
- case 'album': return a.href = openAlbum(pick(albumData).directory);
- case 'album-in-fandom': return a.href = openAlbum(pick(fandomAlbumData).directory);
- case 'album-in-official': return a.href = openAlbum(pick(officialAlbumData).directory);
- case 'track': return a.href = openTrack(getRefDirectory(pick(albumData.map(a => a.tracks).reduce((a, b) => a.concat(b), []))));
- case 'track-in-album': return a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks)));
- case 'track-in-fandom': return a.href = openTrack(getRefDirectory(pick(fandomAlbumData.reduce((acc, album) => acc.concat(album.tracks), []))));
- case 'track-in-official': return a.href = openTrack(getRefDirectory(pick(officialAlbumData.reduce((acc, album) => acc.concat(album.tracks), []))));
- case 'artist': return a.href = openArtist(pick(artistData).directory);
- case 'artist-more-than-one-contrib': return a.href = openArtist(pick(artistData.filter(artist => getArtistNumContributions(artist) > 1)).directory);
- }
+for (const a of document.body.querySelectorAll("[data-random]")) {
+ a.addEventListener("click", (evt) => {
+ if (!ready) {
+ evt.preventDefault();
+ return;
+ }
+
+ setTimeout(() => {
+ a.href = rebase("js-disabled");
});
+ switch (a.dataset.random) {
+ case "album":
+ return (a.href = openAlbum(pick(albumData).directory));
+ case "album-in-fandom":
+ return (a.href = openAlbum(pick(fandomAlbumData).directory));
+ case "album-in-official":
+ return (a.href = openAlbum(pick(officialAlbumData).directory));
+ case "track":
+ return (a.href = openTrack(
+ getRefDirectory(
+ pick(
+ albumData.map((a) => a.tracks).reduce((a, b) => a.concat(b), [])
+ )
+ )
+ ));
+ case "track-in-album":
+ return (a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks))));
+ case "track-in-fandom":
+ return (a.href = openTrack(
+ getRefDirectory(
+ pick(
+ fandomAlbumData.reduce(
+ (acc, album) => acc.concat(album.tracks),
+ []
+ )
+ )
+ )
+ ));
+ case "track-in-official":
+ return (a.href = openTrack(
+ getRefDirectory(
+ pick(
+ officialAlbumData.reduce(
+ (acc, album) => acc.concat(album.tracks),
+ []
+ )
+ )
+ )
+ ));
+ case "artist":
+ return (a.href = openArtist(pick(artistData).directory));
+ case "artist-more-than-one-contrib":
+ return (a.href = openArtist(
+ pick(
+ artistData.filter((artist) => getArtistNumContributions(artist) > 1)
+ ).directory
+ ));
+ }
+ });
}
-const next = document.getElementById('next-button');
-const previous = document.getElementById('previous-button');
-const random = document.getElementById('random-button');
+const next = document.getElementById("next-button");
+const previous = document.getElementById("previous-button");
+const random = document.getElementById("random-button");
const prependTitle = (el, prepend) => {
- const existing = el.getAttribute('title');
- if (existing) {
- el.setAttribute('title', prepend + ' ' + existing);
- } else {
- el.setAttribute('title', prepend);
- }
+ const existing = el.getAttribute("title");
+ if (existing) {
+ el.setAttribute("title", prepend + " " + existing);
+ } else {
+ el.setAttribute("title", prepend);
+ }
};
-if (next) prependTitle(next, '(Shift+N)');
-if (previous) prependTitle(previous, '(Shift+P)');
-if (random) prependTitle(random, '(Shift+R)');
-
-document.addEventListener('keypress', event => {
- if (event.shiftKey) {
- if (event.charCode === 'N'.charCodeAt(0)) {
- if (next) next.click();
- } else if (event.charCode === 'P'.charCodeAt(0)) {
- if (previous) previous.click();
- } else if (event.charCode === 'R'.charCodeAt(0)) {
- if (random && ready) random.click();
- }
+if (next) prependTitle(next, "(Shift+N)");
+if (previous) prependTitle(previous, "(Shift+P)");
+if (random) prependTitle(random, "(Shift+R)");
+
+document.addEventListener("keypress", (event) => {
+ if (event.shiftKey) {
+ if (event.charCode === "N".charCodeAt(0)) {
+ if (next) next.click();
+ } else if (event.charCode === "P".charCodeAt(0)) {
+ if (previous) previous.click();
+ } else if (event.charCode === "R".charCodeAt(0)) {
+ if (random && ready) random.click();
}
+ }
});
-for (const reveal of document.querySelectorAll('.reveal')) {
- reveal.addEventListener('click', event => {
- if (!reveal.classList.contains('revealed')) {
- reveal.classList.add('revealed');
- event.preventDefault();
- event.stopPropagation();
- }
- });
+for (const reveal of document.querySelectorAll(".reveal")) {
+ reveal.addEventListener("click", (event) => {
+ if (!reveal.classList.contains("revealed")) {
+ reveal.classList.add("revealed");
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ });
}
-const elements1 = document.getElementsByClassName('js-hide-once-data');
-const elements2 = document.getElementsByClassName('js-show-once-data');
+const elements1 = document.getElementsByClassName("js-hide-once-data");
+const elements2 = document.getElementsByClassName("js-show-once-data");
-for (const element of elements1) element.style.display = 'block';
+for (const element of elements1) element.style.display = "block";
-fetch(rebase('data.json', 'rebaseShared')).then(data => data.json()).then(data => {
+fetch(rebase("data.json", "rebaseShared"))
+ .then((data) => data.json())
+ .then((data) => {
albumData = data.albumData;
artistData = data.artistData;
flashData = data.flashData;
- officialAlbumData = albumData.filter(album => album.groups.includes('group:official'));
- fandomAlbumData = albumData.filter(album => !album.groups.includes('group:official'));
- artistNames = artistData.filter(artist => !artist.alias).map(artist => artist.name);
+ officialAlbumData = albumData.filter((album) =>
+ album.groups.includes("group:official")
+ );
+ fandomAlbumData = albumData.filter(
+ (album) => !album.groups.includes("group:official")
+ );
+ artistNames = artistData
+ .filter((artist) => !artist.alias)
+ .map((artist) => artist.name);
- for (const element of elements1) element.style.display = 'none';
- for (const element of elements2) element.style.display = 'block';
+ for (const element of elements1) element.style.display = "none";
+ for (const element of elements2) element.style.display = "block";
ready = true;
-});
+ });
// Data & info card ---------------------------------------
@@ -216,197 +257,210 @@ let fastHover = false;
let endFastHoverTimeout = null;
function colorLink(a, color) {
- if (color) {
- const { primary, dim } = getColors(color);
- a.style.setProperty('--primary-color', primary);
- a.style.setProperty('--dim-color', dim);
- }
+ if (color) {
+ const { primary, dim } = getColors(color);
+ a.style.setProperty("--primary-color", primary);
+ a.style.setProperty("--dim-color", dim);
+ }
}
-function link(a, type, {name, directory, color}) {
- colorLink(a, color);
- a.innerText = name
- a.href = getLinkHref(type, directory);
+function link(a, type, { name, directory, color }) {
+ colorLink(a, color);
+ a.innerText = name;
+ a.href = getLinkHref(type, directory);
}
function joinElements(type, elements) {
- // We can't use the Intl APIs with elements, 8ecuase it only oper8tes on
- // strings. So instead, we'll pass the element's outer HTML's (which means
- // the entire HTML of that element).
- //
- // That does mean this function returns a string, so always 8e sure to
- // set innerHTML when using it (not appendChild).
-
- return list[type](elements.map(el => el.outerHTML));
+ // We can't use the Intl APIs with elements, 8ecuase it only oper8tes on
+ // strings. So instead, we'll pass the element's outer HTML's (which means
+ // the entire HTML of that element).
+ //
+ // That does mean this function returns a string, so always 8e sure to
+ // set innerHTML when using it (not appendChild).
+
+ return list[type](elements.map((el) => el.outerHTML));
}
const infoCard = (() => {
- const container = document.getElementById('info-card-container');
-
- let cancelShow = false;
- let hideTimeout = null;
- let showing = false;
-
- container.addEventListener('mouseenter', cancelHide);
- container.addEventListener('mouseleave', readyHide);
-
- function show(type, target) {
- cancelShow = false;
-
- fetchData(type, target.dataset[type]).then(data => {
- // Manual DOM 'cuz we're laaaazy.
-
- if (cancelShow) {
- return;
- }
-
- showing = true;
-
- const rect = target.getBoundingClientRect();
-
- container.style.setProperty('--primary-color', data.color);
-
- container.style.top = window.scrollY + rect.bottom + 'px';
- container.style.left = window.scrollX + rect.left + 'px';
-
- // Use a short timeout to let a currently hidden (or not yet shown)
- // info card teleport to the position set a8ove. (If it's currently
- // shown, it'll transition to that position.)
- setTimeout(() => {
- container.classList.remove('hide');
- container.classList.add('show');
- }, 50);
-
- // 8asic details.
-
- const nameLink = container.querySelector('.info-card-name a');
- link(nameLink, 'track', data);
-
- const albumLink = container.querySelector('.info-card-album a');
- link(albumLink, 'album', data.album);
-
- const artistSpan = container.querySelector('.info-card-artists span');
- artistSpan.innerHTML = joinElements('conjunction', data.artists.map(({ artist }) => {
- const a = document.createElement('a');
- a.href = getLinkHref('artist', artist.directory);
- a.innerText = artist.name;
- return a;
- }));
-
- const coverArtistParagraph = container.querySelector('.info-card-cover-artists');
- const coverArtistSpan = coverArtistParagraph.querySelector('span');
- if (data.coverArtists.length) {
- coverArtistParagraph.style.display = 'block';
- coverArtistSpan.innerHTML = joinElements('conjunction', data.coverArtists.map(({ artist }) => {
- const a = document.createElement('a');
- a.href = getLinkHref('artist', artist.directory);
- a.innerText = artist.name;
- return a;
- }));
- } else {
- coverArtistParagraph.style.display = 'none';
- }
-
- // Cover art.
-
- const [ containerNoReveal, containerReveal ] = [
- container.querySelector('.info-card-art-container.no-reveal'),
- container.querySelector('.info-card-art-container.reveal')
- ];
-
- const [ containerShow, containerHide ] = (data.cover.warnings.length
- ? [containerReveal, containerNoReveal]
- : [containerNoReveal, containerReveal]);
-
- containerHide.style.display = 'none';
- containerShow.style.display = 'block';
-
- const img = containerShow.querySelector('.info-card-art');
- img.src = rebase(data.cover.paths.small, 'rebaseMedia');
-
- const imgLink = containerShow.querySelector('a');
- colorLink(imgLink, data.color);
- imgLink.href = rebase(data.cover.paths.original, 'rebaseMedia');
-
- if (containerShow === containerReveal) {
- const cw = containerShow.querySelector('.info-card-art-warnings');
- cw.innerText = list.unit(data.cover.warnings);
-
- const reveal = containerShow.querySelector('.reveal');
- reveal.classList.remove('revealed');
- }
- });
- }
-
- function hide() {
- container.classList.remove('show');
- container.classList.add('hide');
- cancelShow = true;
- showing = false;
- }
-
- function readyHide() {
- if (!hideTimeout && showing) {
- hideTimeout = setTimeout(hide, HIDE_HOVER_DELAY);
- }
+ const container = document.getElementById("info-card-container");
+
+ let cancelShow = false;
+ let hideTimeout = null;
+ let showing = false;
+
+ container.addEventListener("mouseenter", cancelHide);
+ container.addEventListener("mouseleave", readyHide);
+
+ function show(type, target) {
+ cancelShow = false;
+
+ fetchData(type, target.dataset[type]).then((data) => {
+ // Manual DOM 'cuz we're laaaazy.
+
+ if (cancelShow) {
+ return;
+ }
+
+ showing = true;
+
+ const rect = target.getBoundingClientRect();
+
+ container.style.setProperty("--primary-color", data.color);
+
+ container.style.top = window.scrollY + rect.bottom + "px";
+ container.style.left = window.scrollX + rect.left + "px";
+
+ // Use a short timeout to let a currently hidden (or not yet shown)
+ // info card teleport to the position set a8ove. (If it's currently
+ // shown, it'll transition to that position.)
+ setTimeout(() => {
+ container.classList.remove("hide");
+ container.classList.add("show");
+ }, 50);
+
+ // 8asic details.
+
+ const nameLink = container.querySelector(".info-card-name a");
+ link(nameLink, "track", data);
+
+ const albumLink = container.querySelector(".info-card-album a");
+ link(albumLink, "album", data.album);
+
+ const artistSpan = container.querySelector(".info-card-artists span");
+ artistSpan.innerHTML = joinElements(
+ "conjunction",
+ data.artists.map(({ artist }) => {
+ const a = document.createElement("a");
+ a.href = getLinkHref("artist", artist.directory);
+ a.innerText = artist.name;
+ return a;
+ })
+ );
+
+ const coverArtistParagraph = container.querySelector(
+ ".info-card-cover-artists"
+ );
+ const coverArtistSpan = coverArtistParagraph.querySelector("span");
+ if (data.coverArtists.length) {
+ coverArtistParagraph.style.display = "block";
+ coverArtistSpan.innerHTML = joinElements(
+ "conjunction",
+ data.coverArtists.map(({ artist }) => {
+ const a = document.createElement("a");
+ a.href = getLinkHref("artist", artist.directory);
+ a.innerText = artist.name;
+ return a;
+ })
+ );
+ } else {
+ coverArtistParagraph.style.display = "none";
+ }
+
+ // Cover art.
+
+ const [containerNoReveal, containerReveal] = [
+ container.querySelector(".info-card-art-container.no-reveal"),
+ container.querySelector(".info-card-art-container.reveal"),
+ ];
+
+ const [containerShow, containerHide] = data.cover.warnings.length
+ ? [containerReveal, containerNoReveal]
+ : [containerNoReveal, containerReveal];
+
+ containerHide.style.display = "none";
+ containerShow.style.display = "block";
+
+ const img = containerShow.querySelector(".info-card-art");
+ img.src = rebase(data.cover.paths.small, "rebaseMedia");
+
+ const imgLink = containerShow.querySelector("a");
+ colorLink(imgLink, data.color);
+ imgLink.href = rebase(data.cover.paths.original, "rebaseMedia");
+
+ if (containerShow === containerReveal) {
+ const cw = containerShow.querySelector(".info-card-art-warnings");
+ cw.innerText = list.unit(data.cover.warnings);
+
+ const reveal = containerShow.querySelector(".reveal");
+ reveal.classList.remove("revealed");
+ }
+ });
+ }
+
+ function hide() {
+ container.classList.remove("show");
+ container.classList.add("hide");
+ cancelShow = true;
+ showing = false;
+ }
+
+ function readyHide() {
+ if (!hideTimeout && showing) {
+ hideTimeout = setTimeout(hide, HIDE_HOVER_DELAY);
}
+ }
- function cancelHide() {
- if (hideTimeout) {
- clearTimeout(hideTimeout);
- hideTimeout = null;
- }
+ function cancelHide() {
+ if (hideTimeout) {
+ clearTimeout(hideTimeout);
+ hideTimeout = null;
}
-
- return {
- show,
- hide,
- readyHide,
- cancelHide
- };
+ }
+
+ return {
+ show,
+ hide,
+ readyHide,
+ cancelHide,
+ };
})();
function makeInfoCardLinkHandlers(type) {
- let hoverTimeout = null;
-
- return {
- mouseenter(evt) {
- hoverTimeout = setTimeout(() => {
- fastHover = true;
- infoCard.show(type, evt.target);
- }, fastHover ? FAST_HOVER_INFO_DELAY : NORMAL_HOVER_INFO_DELAY);
+ let hoverTimeout = null;
+
+ return {
+ mouseenter(evt) {
+ hoverTimeout = setTimeout(
+ () => {
+ fastHover = true;
+ infoCard.show(type, evt.target);
+ },
+ fastHover ? FAST_HOVER_INFO_DELAY : NORMAL_HOVER_INFO_DELAY
+ );
- clearTimeout(endFastHoverTimeout);
- endFastHoverTimeout = null;
+ clearTimeout(endFastHoverTimeout);
+ endFastHoverTimeout = null;
- infoCard.cancelHide();
- },
+ infoCard.cancelHide();
+ },
- mouseleave(evt) {
- clearTimeout(hoverTimeout);
+ mouseleave(evt) {
+ clearTimeout(hoverTimeout);
- if (fastHover && !endFastHoverTimeout) {
- endFastHoverTimeout = setTimeout(() => {
- endFastHoverTimeout = null;
- fastHover = false;
- }, END_FAST_HOVER_DELAY);
- }
+ if (fastHover && !endFastHoverTimeout) {
+ endFastHoverTimeout = setTimeout(() => {
+ endFastHoverTimeout = null;
+ fastHover = false;
+ }, END_FAST_HOVER_DELAY);
+ }
- infoCard.readyHide();
- }
- };
+ infoCard.readyHide();
+ },
+ };
}
const infoCardLinkHandlers = {
- track: makeInfoCardLinkHandlers('track')
+ track: makeInfoCardLinkHandlers("track"),
};
function addInfoCardLinkHandlers(type) {
- for (const a of document.querySelectorAll(`a[data-${type}]`)) {
- for (const [ eventName, handler ] of Object.entries(infoCardLinkHandlers[type])) {
- a.addEventListener(eventName, handler);
- }
+ for (const a of document.querySelectorAll(`a[data-${type}]`)) {
+ for (const [eventName, handler] of Object.entries(
+ infoCardLinkHandlers[type]
+ )) {
+ a.addEventListener(eventName, handler);
}
+ }
}
// Info cards are disa8led for now since they aren't quite ready for release,
@@ -415,5 +469,5 @@ function addInfoCardLinkHandlers(type) {
// localStorage.tryInfoCards = true;
//
if (localStorage.tryInfoCards) {
- addInfoCardLinkHandlers('track');
+ addInfoCardLinkHandlers("track");
}
diff --git a/src/static/lazy-loading.js b/src/static/lazy-loading.js
index a403d7ca..230dad21 100644
--- a/src/static/lazy-loading.js
+++ b/src/static/lazy-loading.js
@@ -7,45 +7,45 @@
var observer;
function loadImage(image) {
- image.src = image.dataset.original;
+ image.src = image.dataset.original;
}
function lazyLoad(elements) {
- for (var i = 0; i < elements.length; i++) {
- var item = elements[i];
- if (item.intersectionRatio > 0) {
- observer.unobserve(item.target);
- loadImage(item.target);
- }
+ for (var i = 0; i < elements.length; i++) {
+ var item = elements[i];
+ if (item.intersectionRatio > 0) {
+ observer.unobserve(item.target);
+ loadImage(item.target);
}
+ }
}
function lazyLoadMain() {
- // This is a live HTMLCollection! We can't iter8te over it normally 'cuz
- // we'd 8e mutating its value just 8y interacting with the DOM elements it
- // contains. A while loop works just fine, even though you'd think reading
- // over this code that this would 8e an infinitely hanging loop. It isn't!
- var elements = document.getElementsByClassName('js-hide');
- while (elements.length) {
- elements[0].classList.remove('js-hide');
- }
+ // This is a live HTMLCollection! We can't iter8te over it normally 'cuz
+ // we'd 8e mutating its value just 8y interacting with the DOM elements it
+ // contains. A while loop works just fine, even though you'd think reading
+ // over this code that this would 8e an infinitely hanging loop. It isn't!
+ var elements = document.getElementsByClassName("js-hide");
+ while (elements.length) {
+ elements[0].classList.remove("js-hide");
+ }
- var lazyElements = document.getElementsByClassName('lazy');
- if (window.IntersectionObserver) {
- observer = new IntersectionObserver(lazyLoad, {
- rootMargin: '200px',
- threshold: 1.0
- });
- for (var i = 0; i < lazyElements.length; i++) {
- observer.observe(lazyElements[i]);
- }
- } else {
- for (var i = 0; i < lazyElements.length; i++) {
- var element = lazyElements[i];
- var original = element.getAttribute('data-original');
- element.setAttribute('src', original);
- }
+ var lazyElements = document.getElementsByClassName("lazy");
+ if (window.IntersectionObserver) {
+ observer = new IntersectionObserver(lazyLoad, {
+ rootMargin: "200px",
+ threshold: 1.0,
+ });
+ for (var i = 0; i < lazyElements.length; i++) {
+ observer.observe(lazyElements[i]);
+ }
+ } else {
+ for (var i = 0; i < lazyElements.length; i++) {
+ var element = lazyElements[i];
+ var original = element.getAttribute("data-original");
+ element.setAttribute("src", original);
}
+ }
}
-document.addEventListener('DOMContentLoaded', lazyLoadMain);
+document.addEventListener("DOMContentLoaded", lazyLoadMain);
diff --git a/src/static/site-basic.css b/src/static/site-basic.css
index d26584ae..586f37b5 100644
--- a/src/static/site-basic.css
+++ b/src/static/site-basic.css
@@ -4,16 +4,16 @@
*/
html {
- background-color: #222222;
- color: white;
+ background-color: #222222;
+ color: white;
}
body {
- padding: 15px;
+ padding: 15px;
}
main {
- background-color: rgba(0, 0, 0, 0.6);
- border: 1px dotted white;
- padding: 20px;
+ background-color: rgba(0, 0, 0, 0.6);
+ border: 1px dotted white;
+ padding: 20px;
}
diff --git a/src/static/site.css b/src/static/site.css
index e0031351..d80c57c5 100644
--- a/src/static/site.css
+++ b/src/static/site.css
@@ -4,492 +4,503 @@
*/
:root {
- --primary-color: #0088ff;
+ --primary-color: #0088ff;
}
body {
- background: black;
- margin: 10px;
- overflow-y: scroll;
+ background: black;
+ margin: 10px;
+ overflow-y: scroll;
}
body::before {
- content: "";
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: -1;
+ content: "";
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: -1;
- background-image: url("../media/bg.jpg");
- background-position: center;
- background-size: cover;
- opacity: 0.5;
+ background-image: url("../media/bg.jpg");
+ background-position: center;
+ background-size: cover;
+ opacity: 0.5;
}
#page-container {
- background-color: var(--bg-color, rgba(35, 35, 35, 0.80));
- color: #ffffff;
+ background-color: var(--bg-color, rgba(35, 35, 35, 0.8));
+ color: #ffffff;
- max-width: 1100px;
- margin: 10px auto 50px;
- padding: 15px 0;
+ max-width: 1100px;
+ margin: 10px auto 50px;
+ padding: 15px 0;
- box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
+ box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
}
#page-container > * {
- margin-left: 15px;
- margin-right: 15px;
+ margin-left: 15px;
+ margin-right: 15px;
}
#banner {
- margin: 10px 0;
- width: 100%;
- background: black;
- background-color: var(--dim-color);
- border-bottom: 1px solid var(--primary-color);
- position: relative;
+ margin: 10px 0;
+ width: 100%;
+ background: black;
+ background-color: var(--dim-color);
+ border-bottom: 1px solid var(--primary-color);
+ position: relative;
}
#banner::after {
- content: "";
- box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.35);
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- pointer-events: none;
+ content: "";
+ box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.35);
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ pointer-events: none;
}
#banner.dim img {
- opacity: 0.8;
+ opacity: 0.8;
}
#banner img {
- display: block;
- width: 100%;
- height: auto;
+ display: block;
+ width: 100%;
+ height: auto;
}
a {
- color: var(--primary-color);
- text-decoration: none;
+ color: var(--primary-color);
+ text-decoration: none;
}
a:hover {
- text-decoration: underline;
+ text-decoration: underline;
}
#skippers {
- position: absolute;
- left: -10000px;
- top: auto;
- width: 1px;
- height: 1px;
+ position: absolute;
+ left: -10000px;
+ top: auto;
+ width: 1px;
+ height: 1px;
}
#skippers:focus-within {
- position: static;
- width: unset;
- height: unset;
+ position: static;
+ width: unset;
+ height: unset;
}
#skippers > .skipper:not(:last-child)::after {
- content: " \00b7 ";
- font-weight: 800;
+ content: " \00b7 ";
+ font-weight: 800;
}
.layout-columns {
- display: flex;
+ display: flex;
}
-#header, #secondary-nav, #skippers, #footer {
- padding: 5px;
- font-size: 0.85em;
+#header,
+#secondary-nav,
+#skippers,
+#footer {
+ padding: 5px;
+ font-size: 0.85em;
}
-#header, #secondary-nav, #skippers {
- margin-bottom: 10px;
+#header,
+#secondary-nav,
+#skippers {
+ margin-bottom: 10px;
}
#footer {
- margin-top: 10px;
+ margin-top: 10px;
}
#header {
- display: grid;
+ display: grid;
}
#header.nav-has-main-links.nav-has-content {
- grid-template-columns: 2.5fr 3fr;
- grid-template-rows: min-content 1fr;
- grid-template-areas:
- "main-links content"
- "bottom-row content";
+ grid-template-columns: 2.5fr 3fr;
+ grid-template-rows: min-content 1fr;
+ grid-template-areas:
+ "main-links content"
+ "bottom-row content";
}
#header.nav-has-main-links:not(.nav-has-content) {
- grid-template-columns: 1fr;
- grid-template-areas:
- "main-links"
- "bottom-row";
+ grid-template-columns: 1fr;
+ grid-template-areas:
+ "main-links"
+ "bottom-row";
}
.nav-main-links {
- grid-area: main-links;
- margin-right: 20px;
+ grid-area: main-links;
+ margin-right: 20px;
}
.nav-content {
- grid-area: content;
+ grid-area: content;
}
.nav-bottom-row {
- grid-area: bottom-row;
- align-self: start;
+ grid-area: bottom-row;
+ align-self: start;
}
.nav-main-links > span {
- white-space: nowrap;
+ white-space: nowrap;
}
.nav-main-links > span > a.current {
- font-weight: 800;
+ font-weight: 800;
}
.nav-links-index > span:not(:first-child):not(.no-divider)::before {
- content: "\0020\00b7\0020";
- font-weight: 800;
+ content: "\0020\00b7\0020";
+ font-weight: 800;
}
.nav-links-hierarchy > span:not(:first-child):not(.no-divider)::before {
- content: "\0020/\0020";
+ content: "\0020/\0020";
}
#header .chronology {
- display: block;
+ display: block;
}
#header .chronology .heading,
#header .chronology .buttons {
- display: inline-block;
+ display: inline-block;
}
#secondary-nav {
- text-align: center;
+ text-align: center;
}
#secondary-nav:not(.no-hide) {
- display: none;
+ display: none;
}
footer {
- text-align: center;
- font-style: oblique;
+ text-align: center;
+ font-style: oblique;
}
footer > :first-child {
- margin-top: 0;
+ margin-top: 0;
}
footer > :last-child {
- margin-bottom: 0;
+ margin-bottom: 0;
}
.footer-localization-links > span:not(:last-child)::after {
- content: " \00b7 ";
- font-weight: 800;
+ content: " \00b7 ";
+ font-weight: 800;
}
.nowrap {
- white-space: nowrap;
+ white-space: nowrap;
}
.icons {
- font-style: normal;
- white-space: nowrap;
+ font-style: normal;
+ white-space: nowrap;
}
.icon {
- display: inline-block;
- width: 24px;
- height: 1em;
- position: relative;
+ display: inline-block;
+ width: 24px;
+ height: 1em;
+ position: relative;
}
.icon > svg {
- width: 24px;
- height: 24px;
- top: -0.25em;
- position: absolute;
- fill: var(--primary-color);
+ width: 24px;
+ height: 24px;
+ top: -0.25em;
+ position: absolute;
+ fill: var(--primary-color);
}
.rerelease,
.other-group-accent {
- opacity: 0.7;
- font-style: oblique;
+ opacity: 0.7;
+ font-style: oblique;
}
.other-group-accent {
- white-space: nowrap;
+ white-space: nowrap;
}
.content-columns {
- columns: 2;
+ columns: 2;
}
.content-columns .column {
- break-inside: avoid;
+ break-inside: avoid;
}
.content-columns .column h2 {
- margin-top: 0;
- font-size: 1em;
+ margin-top: 0;
+ font-size: 1em;
}
-.sidebar, #content, #header, #secondary-nav, #skippers, #footer {
- background-color: rgba(0, 0, 0, 0.6);
- border: 1px dotted var(--primary-color);
- border-radius: 3px;
+.sidebar,
+#content,
+#header,
+#secondary-nav,
+#skippers,
+#footer {
+ background-color: rgba(0, 0, 0, 0.6);
+ border: 1px dotted var(--primary-color);
+ border-radius: 3px;
}
.sidebar-column {
- flex: 1 1 20%;
- min-width: 150px;
- max-width: 250px;
- flex-basis: 250px;
- height: 100%;
+ flex: 1 1 20%;
+ min-width: 150px;
+ max-width: 250px;
+ flex-basis: 250px;
+ height: 100%;
}
.sidebar-multiple {
- display: flex;
- flex-direction: column;
+ display: flex;
+ flex-direction: column;
}
.sidebar-multiple .sidebar:not(:first-child) {
- margin-top: 10px;
+ margin-top: 10px;
}
.sidebar {
- padding: 5px;
- font-size: 0.85em;
+ padding: 5px;
+ font-size: 0.85em;
}
#sidebar-left {
- margin-right: 10px;
+ margin-right: 10px;
}
#sidebar-right {
- margin-left: 10px;
+ margin-left: 10px;
}
.sidebar.wide {
- max-width: 350px;
- flex-basis: 300px;
- flex-shrink: 0;
- flex-grow: 1;
+ max-width: 350px;
+ flex-basis: 300px;
+ flex-shrink: 0;
+ flex-grow: 1;
}
#content {
- box-sizing: border-box;
- padding: 20px;
- flex-grow: 1;
- flex-shrink: 3;
- overflow-wrap: anywhere;
+ box-sizing: border-box;
+ padding: 20px;
+ flex-grow: 1;
+ flex-shrink: 3;
+ overflow-wrap: anywhere;
}
.sidebar > h1,
.sidebar > h2,
.sidebar > h3,
.sidebar > p {
- text-align: center;
+ text-align: center;
}
.sidebar h1 {
- font-size: 1.25em;
+ font-size: 1.25em;
}
.sidebar h2 {
- font-size: 1.1em;
- margin: 0;
+ font-size: 1.1em;
+ margin: 0;
}
.sidebar h3 {
- font-size: 1.1em;
- font-style: oblique;
- font-variant: small-caps;
- margin-top: 0.3em;
- margin-bottom: 0em;
+ font-size: 1.1em;
+ font-style: oblique;
+ font-variant: small-caps;
+ margin-top: 0.3em;
+ margin-bottom: 0em;
}
.sidebar > p {
- margin: 0.5em 0;
- padding: 0 5px;
+ margin: 0.5em 0;
+ padding: 0 5px;
}
.sidebar hr {
- color: #555;
- margin: 10px 5px;
+ color: #555;
+ margin: 10px 5px;
}
-.sidebar > ol, .sidebar > ul {
- padding-left: 30px;
- padding-right: 15px;
+.sidebar > ol,
+.sidebar > ul {
+ padding-left: 30px;
+ padding-right: 15px;
}
.sidebar > dl {
- padding-right: 15px;
- padding-left: 0;
+ padding-right: 15px;
+ padding-left: 0;
}
.sidebar > dl dt {
- padding-left: 10px;
- margin-top: 0.5em;
+ padding-left: 10px;
+ margin-top: 0.5em;
}
.sidebar > dl dt.current {
- font-weight: 800;
+ font-weight: 800;
}
.sidebar > dl dd {
- margin-left: 0;
+ margin-left: 0;
}
.sidebar > dl dd ul {
- padding-left: 30px;
- margin-left: 0;
+ padding-left: 30px;
+ margin-left: 0;
}
.sidebar > dl .side {
- padding-left: 10px;
+ padding-left: 10px;
}
.sidebar li.current {
- font-weight: 800;
+ font-weight: 800;
}
.sidebar li {
- overflow-wrap: break-word;
+ overflow-wrap: break-word;
}
.sidebar > details.current summary {
- font-weight: 800;
+ font-weight: 800;
}
.sidebar > details summary {
- margin-top: 0.5em;
- padding-left: 5px;
- user-select: none;
+ margin-top: 0.5em;
+ padding-left: 5px;
+ user-select: none;
}
.sidebar > details summary .group-name {
- color: var(--primary-color);
+ color: var(--primary-color);
}
.sidebar > details summary:hover {
- cursor: pointer;
- text-decoration: underline;
- text-decoration-color: var(--primary-color);
+ cursor: pointer;
+ text-decoration: underline;
+ text-decoration-color: var(--primary-color);
}
.sidebar > details ul,
.sidebar > details ol {
- margin-top: 0;
- margin-bottom: 0;
+ margin-top: 0;
+ margin-bottom: 0;
}
.sidebar > details:last-child {
- margin-bottom: 10px;
+ margin-bottom: 10px;
}
.sidebar > details[open] {
- margin-bottom: 1em;
+ margin-bottom: 1em;
}
.sidebar article {
- text-align: left;
- margin: 5px 5px 15px 5px;
+ text-align: left;
+ margin: 5px 5px 15px 5px;
}
.sidebar article:last-child {
- margin-bottom: 5px;
+ margin-bottom: 5px;
}
.sidebar article h2,
.news-index h2 {
- border-bottom: 1px dotted;
+ border-bottom: 1px dotted;
}
.sidebar article h2 time,
.news-index time {
- float: right;
- font-weight: normal;
+ float: right;
+ font-weight: normal;
}
#cover-art-container {
- float: right;
- width: 40%;
- max-width: 400px;
- margin: 0 0 10px 10px;
- font-size: 0.8em;
+ float: right;
+ width: 40%;
+ max-width: 400px;
+ margin: 0 0 10px 10px;
+ font-size: 0.8em;
}
#cover-art img {
- display: block;
- width: 100%;
- height: 100%;
+ display: block;
+ width: 100%;
+ height: 100%;
}
#cover-art-container p {
- margin-top: 5px;
+ margin-top: 5px;
}
.image-container {
- border: 2px solid var(--primary-color);
- box-sizing: border-box;
- position: relative;
- padding: 5px;
- text-align: left;
- background-color: var(--dim-color);
- color: white;
- display: inline-block;
- width: 100%;
- height: 100%;
+ border: 2px solid var(--primary-color);
+ box-sizing: border-box;
+ position: relative;
+ padding: 5px;
+ text-align: left;
+ background-color: var(--dim-color);
+ color: white;
+ display: inline-block;
+ width: 100%;
+ height: 100%;
}
.image-inner-area {
- overflow: hidden;
- width: 100%;
- height: 100%;
- position: relative;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ position: relative;
}
.image-text-area {
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- text-align: center;
- padding: 5px 15px;
- background: rgba(0, 0, 0, 0.65);
- box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) inset;
- line-height: 1.35em;
- color: var(--primary-color);
- font-style: oblique;
- text-shadow: 0 2px 5px rgba(0, 0, 0, 0.75);
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ padding: 5px 15px;
+ background: rgba(0, 0, 0, 0.65);
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) inset;
+ line-height: 1.35em;
+ color: var(--primary-color);
+ font-style: oblique;
+ text-shadow: 0 2px 5px rgba(0, 0, 0, 0.75);
}
img {
- object-fit: cover;
- /* these unfortunately dont take effect while loading, so...
+ object-fit: cover;
+ /* these unfortunately dont take effect while loading, so...
text-align: center;
line-height: 2em;
text-shadow: 0 0 5px black;
@@ -500,525 +511,528 @@ img {
.js-hide,
.js-show-once-data,
.js-hide-once-data {
- display: none;
+ display: none;
}
a.box:focus {
- outline: 3px double var(--primary-color);
+ outline: 3px double var(--primary-color);
}
a.box:focus:not(:focus-visible) {
- outline: none;
+ outline: none;
}
a.box img {
- display: block;
- width: 100%;
- height: 100%;
+ display: block;
+ width: 100%;
+ height: 100%;
}
h1 {
- font-size: 1.5em;
+ font-size: 1.5em;
}
#content li {
- margin-bottom: 4px;
+ margin-bottom: 4px;
}
#content li i {
- white-space: nowrap;
+ white-space: nowrap;
}
.grid-listing {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- align-items: flex-start;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: flex-start;
}
.grid-item {
- display: inline-block;
- margin: 15px;
- text-align: center;
- background-color: #111111;
- border: 1px dotted var(--primary-color);
- border-radius: 2px;
- padding: 5px;
+ display: inline-block;
+ margin: 15px;
+ text-align: center;
+ background-color: #111111;
+ border: 1px dotted var(--primary-color);
+ border-radius: 2px;
+ padding: 5px;
}
.grid-item img {
- width: 100%;
- height: 100%;
- margin-top: auto;
- margin-bottom: auto;
+ width: 100%;
+ height: 100%;
+ margin-top: auto;
+ margin-bottom: auto;
}
.grid-item span {
- overflow-wrap: break-word;
- hyphens: auto;
+ overflow-wrap: break-word;
+ hyphens: auto;
}
.grid-item:hover {
- text-decoration: none;
+ text-decoration: none;
}
.grid-actions .grid-item:hover {
- text-decoration: underline;
+ text-decoration: underline;
}
.grid-item > span {
- display: block;
+ display: block;
}
.grid-item > span:not(:first-child) {
- margin-top: 2px;
+ margin-top: 2px;
}
.grid-item > span:first-of-type {
- margin-top: 6px;
+ margin-top: 6px;
}
.grid-item:hover > span:first-of-type {
- text-decoration: underline;
+ text-decoration: underline;
}
.grid-listing > .grid-item {
- flex: 1 1 26%;
+ flex: 1 1 26%;
}
.grid-actions {
- display: flex;
- flex-direction: column;
- margin: 15px;
- align-self: center;
+ display: flex;
+ flex-direction: column;
+ margin: 15px;
+ align-self: center;
}
.grid-actions > .grid-item {
- flex-basis: unset !important;
- margin: 5px;
- --primary-color: inherit !important;
- --dim-color: inherit !important;
+ flex-basis: unset !important;
+ margin: 5px;
+ --primary-color: inherit !important;
+ --dim-color: inherit !important;
}
.grid-item {
- flex-basis: 240px;
- min-width: 200px;
- max-width: 240px;
+ flex-basis: 240px;
+ min-width: 200px;
+ max-width: 240px;
}
.grid-item:not(.large-grid-item) {
- flex-basis: 180px;
- min-width: 120px;
- max-width: 180px;
- font-size: 0.9em;
+ flex-basis: 180px;
+ min-width: 120px;
+ max-width: 180px;
+ font-size: 0.9em;
}
.square {
- position: relative;
- width: 100%;
+ position: relative;
+ width: 100%;
}
.square::after {
- content: "";
- display: block;
- padding-bottom: 100%;
+ content: "";
+ display: block;
+ padding-bottom: 100%;
}
.square-content {
- position: absolute;
- width: 100%;
- height: 100%;
+ position: absolute;
+ width: 100%;
+ height: 100%;
}
.vertical-square {
- position: relative;
- height: 100%;
+ position: relative;
+ height: 100%;
}
.vertical-square::after {
- content: "";
- display: block;
- padding-right: 100%;
+ content: "";
+ display: block;
+ padding-right: 100%;
}
.reveal {
- position: relative;
- width: 100%;
- height: 100%;
+ position: relative;
+ width: 100%;
+ height: 100%;
}
.reveal img {
- filter: blur(20px);
- opacity: 0.4;
+ filter: blur(20px);
+ opacity: 0.4;
}
.reveal-text {
- color: white;
- position: absolute;
- top: 15px;
- left: 10px;
- right: 10px;
- text-align: center;
- font-weight: bold;
+ color: white;
+ position: absolute;
+ top: 15px;
+ left: 10px;
+ right: 10px;
+ text-align: center;
+ font-weight: bold;
}
.reveal-interaction {
- opacity: 0.8;
+ opacity: 0.8;
}
.reveal.revealed img {
- filter: none;
- opacity: 1;
+ filter: none;
+ opacity: 1;
}
.reveal.revealed .reveal-text {
- display: none;
+ display: none;
}
#content.top-index h1,
#content.flash-index h1 {
- text-align: center;
- font-size: 2em;
+ text-align: center;
+ font-size: 2em;
}
#content.flash-index h2 {
- text-align: center;
- font-size: 2.5em;
- font-variant: small-caps;
- font-style: oblique;
- margin-bottom: 0;
- text-align: center;
- width: 100%;
+ text-align: center;
+ font-size: 2.5em;
+ font-variant: small-caps;
+ font-style: oblique;
+ margin-bottom: 0;
+ text-align: center;
+ width: 100%;
}
#content.top-index h2 {
- text-align: center;
- font-size: 2em;
- font-weight: normal;
- margin-bottom: 0.25em;
+ text-align: center;
+ font-size: 2em;
+ font-weight: normal;
+ margin-bottom: 0.25em;
}
.quick-info {
- text-align: center;
+ text-align: center;
}
ul.quick-info {
- list-style: none;
- padding-left: 0;
+ list-style: none;
+ padding-left: 0;
}
ul.quick-info li {
- display: inline-block;
+ display: inline-block;
}
ul.quick-info li:not(:last-child)::after {
- content: " \00b7 ";
- font-weight: 800;
+ content: " \00b7 ";
+ font-weight: 800;
}
#intro-menu {
- margin: 24px 0;
- padding: 10px;
- background-color: #222222;
- text-align: center;
- border: 1px dotted var(--primary-color);
- border-radius: 2px;
+ margin: 24px 0;
+ padding: 10px;
+ background-color: #222222;
+ text-align: center;
+ border: 1px dotted var(--primary-color);
+ border-radius: 2px;
}
#intro-menu p {
- margin: 12px 0;
+ margin: 12px 0;
}
#intro-menu a {
- margin: 0 6px;
+ margin: 0 6px;
}
li .by {
- display: inline-block;
- font-style: oblique;
+ display: inline-block;
+ font-style: oblique;
}
li .by a {
- display: inline-block;
+ display: inline-block;
}
p code {
- font-size: 1em;
- font-family: 'courier new';
- font-weight: 800;
+ font-size: 1em;
+ font-family: "courier new";
+ font-weight: 800;
}
blockquote {
- margin-left: 40px;
- max-width: 600px;
- margin-right: 0;
+ margin-left: 40px;
+ max-width: 600px;
+ margin-right: 0;
}
.long-content {
- margin-left: 12%;
- margin-right: 12%;
+ margin-left: 12%;
+ margin-right: 12%;
}
p img {
- max-width: 100%;
- height: auto;
+ max-width: 100%;
+ height: auto;
}
dl dt {
- padding-left: 40px;
- max-width: 600px;
+ padding-left: 40px;
+ max-width: 600px;
}
dl dt {
- margin-bottom: 2px;
+ margin-bottom: 2px;
}
dl dd {
- margin-bottom: 1em;
+ margin-bottom: 1em;
}
-dl ul, dl ol {
- margin-top: 0;
- margin-bottom: 0;
+dl ul,
+dl ol {
+ margin-top: 0;
+ margin-bottom: 0;
}
.album-group-list dt {
- font-style: oblique;
- padding-left: 0;
+ font-style: oblique;
+ padding-left: 0;
}
.album-group-list dd {
- margin-left: 0;
+ margin-left: 0;
}
.group-chronology-link {
- font-style: oblique;
+ font-style: oblique;
}
hr.split::before {
- content: "(split)";
- color: #808080;
+ content: "(split)";
+ color: #808080;
}
hr.split {
- position: relative;
- overflow: hidden;
- border: none;
+ position: relative;
+ overflow: hidden;
+ border: none;
}
hr.split::after {
- display: inline-block;
- content: "";
- border: 1px inset #808080;
- width: 100%;
- position: absolute;
- top: 50%;
- margin-top: -2px;
- margin-left: 10px;
+ display: inline-block;
+ content: "";
+ border: 1px inset #808080;
+ width: 100%;
+ position: absolute;
+ top: 50%;
+ margin-top: -2px;
+ margin-left: 10px;
}
li > ul {
- margin-top: 5px;
+ margin-top: 5px;
}
#info-card-container {
- position: absolute;
+ position: absolute;
- left: 0;
- right: 10px;
+ left: 0;
+ right: 10px;
- pointer-events: none; /* Padding area shouldn't 8e interactive. */
- display: none;
+ pointer-events: none; /* Padding area shouldn't 8e interactive. */
+ display: none;
}
#info-card-container.show,
#info-card-container.hide {
- display: flex;
+ display: flex;
}
#info-card-container > * {
- flex-basis: 400px;
+ flex-basis: 400px;
}
#info-card-container.show {
- animation: 0.2s linear forwards info-card-show;
- transition: top 0.1s, left 0.1s;
+ animation: 0.2s linear forwards info-card-show;
+ transition: top 0.1s, left 0.1s;
}
#info-card-container.hide {
- animation: 0.2s linear forwards info-card-hide;
+ animation: 0.2s linear forwards info-card-hide;
}
@keyframes info-card-show {
- 0% {
- opacity: 0;
- margin-top: -5px;
- }
+ 0% {
+ opacity: 0;
+ margin-top: -5px;
+ }
- 100% {
- opacity: 1;
- margin-top: 0;
- }
+ 100% {
+ opacity: 1;
+ margin-top: 0;
+ }
}
@keyframes info-card-hide {
- 0% {
- opacity: 1;
- margin-top: 0;
- }
+ 0% {
+ opacity: 1;
+ margin-top: 0;
+ }
- 100% {
- opacity: 0;
- margin-top: 5px;
- display: none !important;
- }
+ 100% {
+ opacity: 0;
+ margin-top: 5px;
+ display: none !important;
+ }
}
.info-card-decor {
- padding-left: 3ch;
- border-top: 1px solid white;
+ padding-left: 3ch;
+ border-top: 1px solid white;
}
.info-card {
- background-color: black;
- color: white;
+ background-color: black;
+ color: white;
- border: 1px dotted var(--primary-color);
- border-radius: 3px;
- box-shadow: 0 5px 5px black;
+ border: 1px dotted var(--primary-color);
+ border-radius: 3px;
+ box-shadow: 0 5px 5px black;
- padding: 5px;
- font-size: 0.9em;
+ padding: 5px;
+ font-size: 0.9em;
- pointer-events: none;
+ pointer-events: none;
}
.info-card::after {
- content: "";
- display: block;
- clear: both;
+ content: "";
+ display: block;
+ clear: both;
}
#info-card-container.show .info-card {
- animation: 0.01s linear 0.2s forwards info-card-become-interactive;
+ animation: 0.01s linear 0.2s forwards info-card-become-interactive;
}
@keyframes info-card-become-interactive {
- to {
- pointer-events: auto;
- }
+ to {
+ pointer-events: auto;
+ }
}
.info-card-art-container {
- float: right;
+ float: right;
- width: 40%;
- margin: 5px;
- font-size: 0.8em;
+ width: 40%;
+ margin: 5px;
+ font-size: 0.8em;
- /* Dynamically shown. */
- display: none;
+ /* Dynamically shown. */
+ display: none;
}
.info-card-art-container .image-container {
- padding: 2px;
+ padding: 2px;
}
.info-card-art {
- display: block;
- width: 100%;
- height: 100%;
+ display: block;
+ width: 100%;
+ height: 100%;
}
.info-card-name {
- font-size: 1em;
- border-bottom: 1px dotted;
- margin: 0;
+ font-size: 1em;
+ border-bottom: 1px dotted;
+ margin: 0;
}
.info-card p {
- margin-top: 0.25em;
- margin-bottom: 0.25em;
+ margin-top: 0.25em;
+ margin-bottom: 0.25em;
}
.info-card p:last-child {
- margin-bottom: 0;
+ margin-bottom: 0;
}
@media (max-width: 900px) {
- .sidebar-column:not(.no-hide) {
- display: none;
- }
+ .sidebar-column:not(.no-hide) {
+ display: none;
+ }
- #secondary-nav:not(.no-hide) {
- display: block;
- }
+ #secondary-nav:not(.no-hide) {
+ display: block;
+ }
- .layout-columns.vertical-when-thin {
- flex-direction: column;
- }
+ .layout-columns.vertical-when-thin {
+ flex-direction: column;
+ }
- .layout-columns.vertical-when-thin > *:not(:last-child) {
- margin-bottom: 10px;
- }
+ .layout-columns.vertical-when-thin > *:not(:last-child) {
+ margin-bottom: 10px;
+ }
- .sidebar-column.no-hide {
- max-width: unset !important;
- flex-basis: unset !important;
- margin-right: 0 !important;
- margin-left: 0 !important;
- }
+ .sidebar-column.no-hide {
+ max-width: unset !important;
+ flex-basis: unset !important;
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
- .sidebar .news-entry:not(.first-news-entry) {
- display: none;
- }
+ .sidebar .news-entry:not(.first-news-entry) {
+ display: none;
+ }
}
@media (max-width: 600px) {
- .content-columns {
- columns: 1;
- }
+ .content-columns {
+ columns: 1;
+ }
- #cover-art-container {
- float: none;
- margin: 0 10px 10px 10px;
- margin: 0;
- width: 100%;
- max-width: unset;
- }
+ #cover-art-container {
+ float: none;
+ margin: 0 10px 10px 10px;
+ margin: 0;
+ width: 100%;
+ max-width: unset;
+ }
- #header {
- display: block;
- }
+ #header {
+ display: block;
+ }
- #header > div:not(:first-child) {
- margin-top: 0.5em;
- }
+ #header > div:not(:first-child) {
+ margin-top: 0.5em;
+ }
}
/* important easter egg mode */
-html[data-language-code=preview-en][data-url-key="localized.home"] #content h1::after {
- font-family: cursive;
- display: block;
- content: "(Preview Build)";
+html[data-language-code="preview-en"][data-url-key="localized.home"]
+ #content
+ h1::after {
+ font-family: cursive;
+ display: block;
+ content: "(Preview Build)";
}
-html[data-language-code=preview-en] #header h2 > :first-child::before {
- content: "(Preview Build! ✨) ";
- animation: preview-notice 4s infinite;
+html[data-language-code="preview-en"] #header h2 > :first-child::before {
+ content: "(Preview Build! ✨) ";
+ animation: preview-notice 4s infinite;
}
@keyframes preview-notice {
- 0% {
- color: #cc5500;
- }
+ 0% {
+ color: #cc5500;
+ }
- 50% {
- color: #ffaa00;
- }
+ 50% {
+ color: #ffaa00;
+ }
- 100% {
- color: #cc5500;
- }
+ 100% {
+ color: #cc5500;
+ }
}
diff --git a/src/strings-default.json b/src/strings-default.json
index fb2e333c..d94f6deb 100644
--- a/src/strings-default.json
+++ b/src/strings-default.json
@@ -1,379 +1,379 @@
{
- "meta.languageCode": "en",
- "meta.languageName": "English",
- "count.tracks": "{TRACKS}",
- "count.tracks.withUnit.zero": "",
- "count.tracks.withUnit.one": "{TRACKS} track",
- "count.tracks.withUnit.two": "",
- "count.tracks.withUnit.few": "",
- "count.tracks.withUnit.many": "",
- "count.tracks.withUnit.other": "{TRACKS} tracks",
- "count.additionalFiles": "{FILES}",
- "count.additionalFiles.withUnit.zero": "",
- "count.additionalFiles.withUnit.one": "{FILES} additional file",
- "count.additionalFiles.withUnit.two": "",
- "count.additionalFiles.withUnit.few": "",
- "count.additionalFiles.withUnit.many": "",
- "count.additionalFiles.withUnit.other": "{FILES} additional files",
- "count.albums": "{ALBUMS}",
- "count.albums.withUnit.zero": "",
- "count.albums.withUnit.one": "{ALBUMS} album",
- "count.albums.withUnit.two": "",
- "count.albums.withUnit.two": "",
- "count.albums.withUnit.few": "",
- "count.albums.withUnit.many": "",
- "count.albums.withUnit.other": "{ALBUMS} albums",
- "count.commentaryEntries": "{ENTRIES}",
- "count.commentaryEntries.withUnit.zero": "",
- "count.commentaryEntries.withUnit.one": "{ENTRIES} entry",
- "count.commentaryEntries.withUnit.two": "",
- "count.commentaryEntries.withUnit.few": "",
- "count.commentaryEntries.withUnit.many": "",
- "count.commentaryEntries.withUnit.other": "{ENTRIES} entries",
- "count.contributions": "{CONTRIBUTIONS}",
- "count.contributions.withUnit.zero": "",
- "count.contributions.withUnit.one": "{CONTRIBUTIONS} contribution",
- "count.contributions.withUnit.two": "",
- "count.contributions.withUnit.few": "",
- "count.contributions.withUnit.many": "",
- "count.contributions.withUnit.other": "{CONTRIBUTIONS} contributions",
- "count.coverArts": "{COVER_ARTS}",
- "count.coverArts.withUnit.zero": "",
- "count.coverArts.withUnit.one": "{COVER_ARTS} cover art",
- "count.coverArts.withUnit.two": "",
- "count.coverArts.withUnit.few": "",
- "count.coverArts.withUnit.many": "",
- "count.coverArts.withUnit.other": "{COVER_ARTS} cover arts",
- "count.timesReferenced": "{TIMES_REFERENCED}",
- "count.timesReferenced.withUnit.zero": "",
- "count.timesReferenced.withUnit.one": "{TIMES_REFERENCED} time referenced",
- "count.timesReferenced.withUnit.two": "",
- "count.timesReferenced.withUnit.few": "",
- "count.timesReferenced.withUnit.many": "",
- "count.timesReferenced.withUnit.other": "{TIMES_REFERENCED} times referenced",
- "count.words": "{WORDS}",
- "count.words.thousand": "{WORDS}k",
- "count.words.withUnit.zero": "",
- "count.words.withUnit.one": "{WORDS} word",
- "count.words.withUnit.two": "",
- "count.words.withUnit.few": "",
- "count.words.withUnit.many": "",
- "count.words.withUnit.other": "{WORDS} words",
- "count.timesUsed": "{TIMES_USED}",
- "count.timesUsed.withUnit.zero": "",
- "count.timesUsed.withUnit.one": "used {TIMES_USED} time",
- "count.timesUsed.withUnit.two": "",
- "count.timesUsed.withUnit.few": "",
- "count.timesUsed.withUnit.many": "",
- "count.timesUsed.withUnit.other": "used {TIMES_USED} times",
- "count.index.zero": "",
- "count.index.one": "{INDEX}st",
- "count.index.two": "{INDEX}nd",
- "count.index.few": "{INDEX}rd",
- "count.index.many": "",
- "count.index.other": "{INDEX}th",
- "count.duration.hours": "{HOURS}:{MINUTES}:{SECONDS}",
- "count.duration.hours.withUnit": "{HOURS}:{MINUTES}:{SECONDS} hours",
- "count.duration.minutes": "{MINUTES}:{SECONDS}",
- "count.duration.minutes.withUnit": "{MINUTES}:{SECONDS} minutes",
- "count.duration.approximate": "~{DURATION}",
- "count.duration.missing": "_:__",
- "count.fileSize.terabytes": "{TERABYTES} TB",
- "count.fileSize.gigabytes": "{GIGABYTES} GB",
- "count.fileSize.megabytes": "{MEGABYTES} MB",
- "count.fileSize.kilobytes": "{KILOBYTES} kB",
- "count.fileSize.bytes": "{BYTES} bytes",
- "releaseInfo.by": "By {ARTISTS}.",
- "releaseInfo.from": "From {ALBUM}.",
- "releaseInfo.coverArtBy": "Cover art by {ARTISTS}.",
- "releaseInfo.wallpaperArtBy": "Wallpaper art by {ARTISTS}.",
- "releaseInfo.bannerArtBy": "Banner art by {ARTISTS}.",
- "releaseInfo.released": "Released {DATE}.",
- "releaseInfo.artReleased": "Art released {DATE}.",
- "releaseInfo.addedToWiki": "Added to wiki {DATE}.",
- "releaseInfo.duration": "Duration: {DURATION}.",
- "releaseInfo.viewCommentary": "View {LINK}!",
- "releaseInfo.viewCommentary.link": "commentary page",
- "releaseInfo.listenOn": "Listen on {LINKS}.",
- "releaseInfo.listenOn.noLinks": "This track has no URLs at which it can be listened.",
- "releaseInfo.visitOn": "Visit on {LINKS}.",
- "releaseInfo.playOn": "Play on {LINKS}.",
- "releaseInfo.alsoReleasedAs": "Also released as:",
- "releaseInfo.alsoReleasedAs.item": "{TRACK} (on {ALBUM})",
- "releaseInfo.contributors": "Contributors:",
- "releaseInfo.tracksReferenced": "Tracks that {TRACK} references:",
- "releaseInfo.tracksThatReference": "Tracks that reference {TRACK}:",
- "releaseInfo.flashesThatFeature": "Flashes & games that feature {TRACK}:",
- "releaseInfo.flashesThatFeature.item": "{FLASH}",
- "releaseInfo.flashesThatFeature.item.asDifferentRelease": "{FLASH} (as {TRACK})",
- "releaseInfo.lyrics": "Lyrics:",
- "releaseInfo.artistCommentary": "Artist commentary:",
- "releaseInfo.artistCommentary.seeOriginalRelease": "See {ORIGINAL}!",
- "releaseInfo.artTags": "Tags:",
- "releaseInfo.additionalFiles.shortcut": "{ANCHOR_LINK} {TITLES}",
- "releaseInfo.additionalFiles.shortcut.anchorLink": "Additional files:",
- "releaseInfo.additionalFiles.heading": "Has {ADDITIONAL_FILES}:",
- "releaseInfo.additionalFiles.entry": "{TITLE}",
- "releaseInfo.additionalFiles.entry.withDescription": "{TITLE}: {DESCRIPTION}",
- "releaseInfo.additionalFiles.file": "{FILE}",
- "releaseInfo.additionalFiles.file.withSize": "{FILE} ({SIZE})",
- "releaseInfo.note": "Note:",
- "trackList.section.withDuration": "{SECTION} ({DURATION}):",
- "trackList.group": "{GROUP}:",
- "trackList.group.other": "Other",
- "trackList.item.withDuration": "({DURATION}) {TRACK}",
- "trackList.item.withDuration.withArtists": "({DURATION}) {TRACK} {BY}",
- "trackList.item.withArtists": "{TRACK} {BY}",
- "trackList.item.withArtists.by": "by {ARTISTS}",
- "trackList.item.rerelease": "{TRACK} (re-release)",
- "misc.alt.albumCover": "album cover",
- "misc.alt.albumBanner": "album banner",
- "misc.alt.trackCover": "track cover",
- "misc.alt.artistAvatar": "artist avatar",
- "misc.alt.flashArt": "flash art",
- "misc.chronology.seeArtistPages": "(See artist pages for chronology info!)",
- "misc.chronology.heading.coverArt": "{INDEX} cover art by {ARTIST}",
- "misc.chronology.heading.flash": "{INDEX} flash/game by {ARTIST}",
- "misc.chronology.heading.track": "{INDEX} track by {ARTIST}",
- "misc.external.domain": "External ({DOMAIN})",
- "misc.external.local": "Wiki Archive (local upload)",
- "misc.external.bandcamp": "Bandcamp",
- "misc.external.bandcamp.domain": "Bandcamp ({DOMAIN})",
- "misc.external.deviantart": "DeviantArt",
- "misc.external.instagram": "Instagram",
- "misc.external.mastodon": "Mastodon",
- "misc.external.mastodon.domain": "Mastodon ({DOMAIN})",
- "misc.external.patreon": "Patreon",
- "misc.external.poetryFoundation": "Poetry Foundation",
- "misc.external.soundcloud": "SoundCloud",
- "misc.external.tumblr": "Tumblr",
- "misc.external.twitter": "Twitter",
- "misc.external.wikipedia": "Wikipedia",
- "misc.external.youtube": "YouTube",
- "misc.external.youtube.playlist": "YouTube (playlist)",
- "misc.external.youtube.fullAlbum": "YouTube (full album)",
- "misc.external.flash.bgreco": "{LINK} (HQ Audio)",
- "misc.external.flash.homestuck.page": "{LINK} (page {PAGE})",
- "misc.external.flash.homestuck.secret": "{LINK} (secret page)",
- "misc.external.flash.youtube": "{LINK} (on any device)",
- "misc.nav.previous": "Previous",
- "misc.nav.next": "Next",
- "misc.nav.info": "Info",
- "misc.nav.gallery": "Gallery",
- "misc.pageTitle": "{TITLE}",
- "misc.pageTitle.withWikiName": "{TITLE} | {WIKI_NAME}",
- "misc.skippers.skipToContent": "Skip to content",
- "misc.skippers.skipToSidebar": "Skip to sidebar",
- "misc.skippers.skipToSidebar.left": "Skip to sidebar (left)",
- "misc.skippers.skipToSidebar.right": "Skip to sidebar (right)",
- "misc.skippers.skipToFooter": "Skip to footer",
- "misc.socialEmbed.heading": "{WIKI_NAME} | {HEADING}",
- "misc.jumpTo": "Jump to:",
- "misc.jumpTo.withLinks": "Jump to: {LINKS}.",
- "misc.contentWarnings": "cw: {WARNINGS}",
- "misc.contentWarnings.reveal": "click to show",
- "misc.albumGrid.details": "({TRACKS}, {TIME})",
- "misc.albumGrid.noCoverArt": "{ALBUM}",
- "misc.uiLanguage": "UI Language: {LANGUAGES}",
- "homepage.title": "{TITLE}",
- "homepage.news.title": "News",
- "homepage.news.entry.viewRest": "(View rest of entry!)",
- "albumSidebar.trackList.fallbackGroupName": "Track list",
- "albumSidebar.trackList.group": "{GROUP}",
- "albumSidebar.trackList.group.withRange": "{GROUP} ({RANGE})",
- "albumSidebar.trackList.item": "{TRACK}",
- "albumSidebar.groupBox.title": "{GROUP}",
- "albumSidebar.groupBox.next": "Next: {ALBUM}",
- "albumSidebar.groupBox.previous": "Previous: {ALBUM}",
- "albumPage.title": "{ALBUM}",
- "albumPage.nav.album": "{ALBUM}",
- "albumPage.nav.randomTrack": "Random Track",
- "albumCommentaryPage.title": "{ALBUM} - Commentary",
- "albumCommentaryPage.infoLine": "{WORDS} across {ENTRIES}.",
- "albumCommentaryPage.nav.album": "Album: {ALBUM}",
- "albumCommentaryPage.entry.title.albumCommentary": "Album commentary",
- "albumCommentaryPage.entry.title.trackCommentary": "{TRACK}",
- "artistPage.title": "{ARTIST}",
- "artistPage.creditList.album": "{ALBUM}",
- "artistPage.creditList.album.withDate": "{ALBUM} ({DATE})",
- "artistPage.creditList.album.withDuration": "{ALBUM} ({DURATION})",
- "artistPage.creditList.album.withDate.withDuration": "{ALBUM} ({DATE}; {DURATION})",
- "artistPage.creditList.flashAct": "{ACT}",
- "artistPage.creditList.flashAct.withDateRange": "{ACT} ({DATE_RANGE})",
- "artistPage.creditList.entry.track": "{TRACK}",
- "artistPage.creditList.entry.track.withDuration": "({DURATION}) {TRACK}",
- "artistPage.creditList.entry.album.coverArt": "(cover art)",
- "artistPage.creditList.entry.album.wallpaperArt": "(wallpaper art)",
- "artistPage.creditList.entry.album.bannerArt": "(banner art)",
- "artistPage.creditList.entry.album.commentary": "(album commentary)",
- "artistPage.creditList.entry.flash": "{FLASH}",
- "artistPage.creditList.entry.rerelease": "{ENTRY} (re-release)",
- "artistPage.creditList.entry.withContribution": "{ENTRY} ({CONTRIBUTION})",
- "artistPage.creditList.entry.withArtists": "{ENTRY} (with {ARTISTS})",
- "artistPage.creditList.entry.withArtists.withContribution": "{ENTRY} ({CONTRIBUTION}; with {ARTISTS})",
- "artistPage.contributedDurationLine": "{ARTIST} has contributed {DURATION} of music shared on this wiki.",
- "artistPage.musicGroupsLine": "Contributed music to groups: {GROUPS}",
- "artistPage.artGroupsLine": "Contributed art to groups: {GROUPS}",
- "artistPage.groupsLine.item": "{GROUP} ({CONTRIBUTIONS})",
- "artistPage.trackList.title": "Tracks",
- "artistPage.artList.title": "Art",
- "artistPage.flashList.title": "Flashes & Games",
- "artistPage.commentaryList.title": "Commentary",
- "artistPage.viewArtGallery": "View {LINK}!",
- "artistPage.viewArtGallery.orBrowseList": "View {LINK}! Or browse the list:",
- "artistPage.viewArtGallery.link": "art gallery",
- "artistPage.nav.artist": "Artist: {ARTIST}",
- "artistGalleryPage.title": "{ARTIST} - Gallery",
- "artistGalleryPage.infoLine": "Contributed to {COVER_ARTS}.",
- "commentaryIndex.title": "Commentary",
- "commentaryIndex.infoLine": "{WORDS} across {ENTRIES}, in all.",
- "commentaryIndex.albumList.title": "Choose an album:",
- "commentaryIndex.albumList.item": "{ALBUM} ({WORDS} across {ENTRIES})",
- "flashIndex.title": "Flashes & Games",
- "flashPage.title": "{FLASH}",
- "flashPage.nav.flash": "{FLASH}",
- "groupSidebar.title": "Groups",
- "groupSidebar.groupList.category": "{CATEGORY}",
- "groupSidebar.groupList.item": "{GROUP}",
- "groupPage.nav.group": "Group: {GROUP}",
- "groupInfoPage.title": "{GROUP}",
- "groupInfoPage.viewAlbumGallery": "View {LINK}! Or browse the list:",
- "groupInfoPage.viewAlbumGallery.link": "album gallery",
- "groupInfoPage.albumList.title": "Albums",
- "groupInfoPage.albumList.item": "({YEAR}) {ALBUM}",
- "groupInfoPage.albumList.item.withoutYear": "{ALBUM}",
- "groupInfoPage.albumList.item.withAccent": "{ITEM} {ACCENT}",
- "groupInfoPage.albumList.item.otherGroupAccent": "(from {GROUP})",
- "groupGalleryPage.title": "{GROUP} - Gallery",
- "groupGalleryPage.infoLine": "{TRACKS} across {ALBUMS}, totaling {TIME}.",
- "groupGalleryPage.anotherGroupLine": "({LINK})",
- "groupGalleryPage.anotherGroupLine.link": "Choose another group to filter by!",
- "listingIndex.title": "Listings",
- "listingIndex.infoLine": "{WIKI}: {TRACKS} across {ALBUMS}, totaling {DURATION}.",
- "listingIndex.exploreList": "Feel free to explore any of the listings linked below and in the sidebar!",
- "listingPage.target.album": "Albums",
- "listingPage.target.artist": "Artists",
- "listingPage.target.group": "Groups",
- "listingPage.target.track": "Tracks",
- "listingPage.target.tag": "Tags",
- "listingPage.target.other": "Other",
- "listingPage.listAlbums.byName.title": "Albums - by Name",
- "listingPage.listAlbums.byName.title.short": "...by Name",
- "listingPage.listAlbums.byName.item": "{ALBUM} ({TRACKS})",
- "listingPage.listAlbums.byTracks.title": "Albums - by Tracks",
- "listingPage.listAlbums.byTracks.title.short": "...by Tracks",
- "listingPage.listAlbums.byTracks.item": "{ALBUM} ({TRACKS})",
- "listingPage.listAlbums.byDuration.title": "Albums - by Duration",
- "listingPage.listAlbums.byDuration.title.short": "...by Duration",
- "listingPage.listAlbums.byDuration.item": "{ALBUM} ({DURATION})",
- "listingPage.listAlbums.byDate.title": "Albums - by Date",
- "listingPage.listAlbums.byDate.title.short": "...by Date",
- "listingPage.listAlbums.byDate.item": "{ALBUM} ({DATE})",
- "listingPage.listAlbums.byDateAdded.title.short": "...by Date Added to Wiki",
- "listingPage.listAlbums.byDateAdded.title": "Albums - by Date Added to Wiki",
- "listingPage.listAlbums.byDateAdded.date": "{DATE}",
- "listingPage.listAlbums.byDateAdded.album": "{ALBUM}",
- "listingPage.listArtists.byName.title": "Artists - by Name",
- "listingPage.listArtists.byName.title.short": "...by Name",
- "listingPage.listArtists.byName.item": "{ARTIST} ({CONTRIBUTIONS})",
- "listingPage.listArtists.byContribs.title": "Artists - by Contributions",
- "listingPage.listArtists.byContribs.title.short": "...by Contributions",
- "listingPage.listArtists.byContribs.item": "{ARTIST} ({CONTRIBUTIONS})",
- "listingPage.listArtists.byCommentary.title": "Artists - by Commentary Entries",
- "listingPage.listArtists.byCommentary.title.short": "...by Commentary Entries",
- "listingPage.listArtists.byCommentary.item": "{ARTIST} ({ENTRIES})",
- "listingPage.listArtists.byDuration.title": "Artists - by Duration",
- "listingPage.listArtists.byDuration.title.short": "...by Duration",
- "listingPage.listArtists.byDuration.item": "{ARTIST} ({DURATION})",
- "listingPage.listArtists.byLatest.title": "Artists - by Latest Contribution",
- "listingPage.listArtists.byLatest.title.short": "...by Latest Contribution",
- "listingPage.listArtists.byLatest.item": "{ARTIST} ({DATE})",
- "listingPage.listGroups.byName.title": "Groups - by Name",
- "listingPage.listGroups.byName.title.short": "...by Name",
- "listingPage.listGroups.byName.item": "{GROUP} ({GALLERY})",
- "listingPage.listGroups.byName.item.gallery": "Gallery",
- "listingPage.listGroups.byCategory.title": "Groups - by Category",
- "listingPage.listGroups.byCategory.title.short": "...by Category",
- "listingPage.listGroups.byCategory.category": "{CATEGORY}",
- "listingPage.listGroups.byCategory.group": "{GROUP} ({GALLERY})",
- "listingPage.listGroups.byCategory.group.gallery": "Gallery",
- "listingPage.listGroups.byAlbums.title": "Groups - by Albums",
- "listingPage.listGroups.byAlbums.title.short": "...by Albums",
- "listingPage.listGroups.byAlbums.item": "{GROUP} ({ALBUMS})",
- "listingPage.listGroups.byTracks.title": "Groups - by Tracks",
- "listingPage.listGroups.byTracks.title.short": "...by Tracks",
- "listingPage.listGroups.byTracks.item": "{GROUP} ({TRACKS})",
- "listingPage.listGroups.byDuration.title": "Groups - by Duration",
- "listingPage.listGroups.byDuration.title.short": "...by Duration",
- "listingPage.listGroups.byDuration.item": "{GROUP} ({DURATION})",
- "listingPage.listGroups.byLatest.title": "Groups - by Latest Album",
- "listingPage.listGroups.byLatest.title.short": "...by Latest Album",
- "listingPage.listGroups.byLatest.item": "{GROUP} ({DATE})",
- "listingPage.listTracks.byName.title": "Tracks - by Name",
- "listingPage.listTracks.byName.title.short": "...by Name",
- "listingPage.listTracks.byName.item": "{TRACK}",
- "listingPage.listTracks.byAlbum.title": "Tracks - by Album",
- "listingPage.listTracks.byAlbum.title.short": "...by Album",
- "listingPage.listTracks.byAlbum.album": "{ALBUM}",
- "listingPage.listTracks.byAlbum.track": "{TRACK}",
- "listingPage.listTracks.byDate.title": "Tracks - by Date",
- "listingPage.listTracks.byDate.title.short": "...by Date",
- "listingPage.listTracks.byDate.album": "{ALBUM} ({DATE})",
- "listingPage.listTracks.byDate.track": "{TRACK}",
- "listingPage.listTracks.byDate.track.rerelease": "{TRACK} (re-release)",
- "listingPage.listTracks.byDuration.title": "Tracks - by Duration",
- "listingPage.listTracks.byDuration.title.short": "...by Duration",
- "listingPage.listTracks.byDuration.item": "{TRACK} ({DURATION})",
- "listingPage.listTracks.byDurationInAlbum.title": "Tracks - by Duration (in Album)",
- "listingPage.listTracks.byDurationInAlbum.title.short": "...by Duration (in Album)",
- "listingPage.listTracks.byDurationInAlbum.album": "{ALBUM}",
- "listingPage.listTracks.byDurationInAlbum.track": "{TRACK} ({DURATION})",
- "listingPage.listTracks.byTimesReferenced.title": "Tracks - by Times Referenced",
- "listingPage.listTracks.byTimesReferenced.title.short": "...by Times Referenced",
- "listingPage.listTracks.byTimesReferenced.item": "{TRACK} ({TIMES_REFERENCED})",
- "listingPage.listTracks.inFlashes.byAlbum.title": "Tracks - in Flashes & Games (by Album)",
- "listingPage.listTracks.inFlashes.byAlbum.title.short": "...in Flashes & Games (by Album)",
- "listingPage.listTracks.inFlashes.byAlbum.album": "{ALBUM} ({DATE})",
- "listingPage.listTracks.inFlashes.byAlbum.track": "{TRACK} (in {FLASHES})",
- "listingPage.listTracks.inFlashes.byFlash.title": "Tracks - in Flashes & Games (by Flash)",
- "listingPage.listTracks.inFlashes.byFlash.title.short": "...in Flashes & Games (by Flash)",
- "listingPage.listTracks.inFlashes.byFlash.flash": "{FLASH} ({DATE})",
- "listingPage.listTracks.inFlashes.byFlash.track": "{TRACK} (from {ALBUM})",
- "listingPage.listTracks.withLyrics.title": "Tracks - with Lyrics",
- "listingPage.listTracks.withLyrics.title.short": "...with Lyrics",
- "listingPage.listTracks.withLyrics.album": "{ALBUM} ({DATE})",
- "listingPage.listTracks.withLyrics.track": "{TRACK}",
- "listingPage.listTags.byName.title": "Tags - by Name",
- "listingPage.listTags.byName.title.short": "...by Name",
- "listingPage.listTags.byName.item": "{TAG} ({TIMES_USED})",
- "listingPage.listTags.byUses.title": "Tags - by Uses",
- "listingPage.listTags.byUses.title.short": "...by Uses",
- "listingPage.listTags.byUses.item": "{TAG} ({TIMES_USED})",
- "listingPage.other.randomPages.title": "Random Pages",
- "listingPage.other.randomPages.title.short": "Random Pages",
- "listingPage.misc.trackContributors": "Track Contributors",
- "listingPage.misc.artContributors": "Art Contributors",
- "listingPage.misc.artAndFlashContributors": "Art & Flash Contributors",
- "newsIndex.title": "News",
- "newsIndex.entry.viewRest": "(View rest of entry!)",
- "newsEntryPage.title": "{ENTRY}",
- "newsEntryPage.published": "(Published {DATE}.)",
- "newsEntryPage.nav.news": "News",
- "newsEntryPage.nav.entry": "{DATE}: {ENTRY}",
- "redirectPage.title": "Moved to {TITLE}",
- "redirectPage.infoLine": "This page has been moved to {TARGET}.",
- "tagPage.title": "{TAG}",
- "tagPage.infoLine": "Appears in {COVER_ARTS}.",
- "tagPage.nav.tag": "Tag: {TAG}",
- "trackPage.title": "{TRACK}",
- "trackPage.referenceList.fandom": "Fandom:",
- "trackPage.referenceList.official": "Official:",
- "trackPage.nav.track": "{TRACK}",
- "trackPage.nav.track.withNumber": "{NUMBER}. {TRACK}",
- "trackPage.nav.random": "Random",
- "trackPage.socialEmbed.heading": "{ALBUM}",
- "trackPage.socialEmbed.title": "{TRACK}",
- "trackPage.socialEmbed.body.withArtists.withCoverArtists": "By {ARTISTS}; art by {COVER_ARTISTS}.",
- "trackPage.socialEmbed.body.withArtists": "By {ARTISTS}.",
- "trackPage.socialEmbed.body.withCoverArtists": "Art by {COVER_ARTISTS}."
+ "meta.languageCode": "en",
+ "meta.languageName": "English",
+ "count.tracks": "{TRACKS}",
+ "count.tracks.withUnit.zero": "",
+ "count.tracks.withUnit.one": "{TRACKS} track",
+ "count.tracks.withUnit.two": "",
+ "count.tracks.withUnit.few": "",
+ "count.tracks.withUnit.many": "",
+ "count.tracks.withUnit.other": "{TRACKS} tracks",
+ "count.additionalFiles": "{FILES}",
+ "count.additionalFiles.withUnit.zero": "",
+ "count.additionalFiles.withUnit.one": "{FILES} additional file",
+ "count.additionalFiles.withUnit.two": "",
+ "count.additionalFiles.withUnit.few": "",
+ "count.additionalFiles.withUnit.many": "",
+ "count.additionalFiles.withUnit.other": "{FILES} additional files",
+ "count.albums": "{ALBUMS}",
+ "count.albums.withUnit.zero": "",
+ "count.albums.withUnit.one": "{ALBUMS} album",
+ "count.albums.withUnit.two": "",
+ "count.albums.withUnit.two": "",
+ "count.albums.withUnit.few": "",
+ "count.albums.withUnit.many": "",
+ "count.albums.withUnit.other": "{ALBUMS} albums",
+ "count.commentaryEntries": "{ENTRIES}",
+ "count.commentaryEntries.withUnit.zero": "",
+ "count.commentaryEntries.withUnit.one": "{ENTRIES} entry",
+ "count.commentaryEntries.withUnit.two": "",
+ "count.commentaryEntries.withUnit.few": "",
+ "count.commentaryEntries.withUnit.many": "",
+ "count.commentaryEntries.withUnit.other": "{ENTRIES} entries",
+ "count.contributions": "{CONTRIBUTIONS}",
+ "count.contributions.withUnit.zero": "",
+ "count.contributions.withUnit.one": "{CONTRIBUTIONS} contribution",
+ "count.contributions.withUnit.two": "",
+ "count.contributions.withUnit.few": "",
+ "count.contributions.withUnit.many": "",
+ "count.contributions.withUnit.other": "{CONTRIBUTIONS} contributions",
+ "count.coverArts": "{COVER_ARTS}",
+ "count.coverArts.withUnit.zero": "",
+ "count.coverArts.withUnit.one": "{COVER_ARTS} cover art",
+ "count.coverArts.withUnit.two": "",
+ "count.coverArts.withUnit.few": "",
+ "count.coverArts.withUnit.many": "",
+ "count.coverArts.withUnit.other": "{COVER_ARTS} cover arts",
+ "count.timesReferenced": "{TIMES_REFERENCED}",
+ "count.timesReferenced.withUnit.zero": "",
+ "count.timesReferenced.withUnit.one": "{TIMES_REFERENCED} time referenced",
+ "count.timesReferenced.withUnit.two": "",
+ "count.timesReferenced.withUnit.few": "",
+ "count.timesReferenced.withUnit.many": "",
+ "count.timesReferenced.withUnit.other": "{TIMES_REFERENCED} times referenced",
+ "count.words": "{WORDS}",
+ "count.words.thousand": "{WORDS}k",
+ "count.words.withUnit.zero": "",
+ "count.words.withUnit.one": "{WORDS} word",
+ "count.words.withUnit.two": "",
+ "count.words.withUnit.few": "",
+ "count.words.withUnit.many": "",
+ "count.words.withUnit.other": "{WORDS} words",
+ "count.timesUsed": "{TIMES_USED}",
+ "count.timesUsed.withUnit.zero": "",
+ "count.timesUsed.withUnit.one": "used {TIMES_USED} time",
+ "count.timesUsed.withUnit.two": "",
+ "count.timesUsed.withUnit.few": "",
+ "count.timesUsed.withUnit.many": "",
+ "count.timesUsed.withUnit.other": "used {TIMES_USED} times",
+ "count.index.zero": "",
+ "count.index.one": "{INDEX}st",
+ "count.index.two": "{INDEX}nd",
+ "count.index.few": "{INDEX}rd",
+ "count.index.many": "",
+ "count.index.other": "{INDEX}th",
+ "count.duration.hours": "{HOURS}:{MINUTES}:{SECONDS}",
+ "count.duration.hours.withUnit": "{HOURS}:{MINUTES}:{SECONDS} hours",
+ "count.duration.minutes": "{MINUTES}:{SECONDS}",
+ "count.duration.minutes.withUnit": "{MINUTES}:{SECONDS} minutes",
+ "count.duration.approximate": "~{DURATION}",
+ "count.duration.missing": "_:__",
+ "count.fileSize.terabytes": "{TERABYTES} TB",
+ "count.fileSize.gigabytes": "{GIGABYTES} GB",
+ "count.fileSize.megabytes": "{MEGABYTES} MB",
+ "count.fileSize.kilobytes": "{KILOBYTES} kB",
+ "count.fileSize.bytes": "{BYTES} bytes",
+ "releaseInfo.by": "By {ARTISTS}.",
+ "releaseInfo.from": "From {ALBUM}.",
+ "releaseInfo.coverArtBy": "Cover art by {ARTISTS}.",
+ "releaseInfo.wallpaperArtBy": "Wallpaper art by {ARTISTS}.",
+ "releaseInfo.bannerArtBy": "Banner art by {ARTISTS}.",
+ "releaseInfo.released": "Released {DATE}.",
+ "releaseInfo.artReleased": "Art released {DATE}.",
+ "releaseInfo.addedToWiki": "Added to wiki {DATE}.",
+ "releaseInfo.duration": "Duration: {DURATION}.",
+ "releaseInfo.viewCommentary": "View {LINK}!",
+ "releaseInfo.viewCommentary.link": "commentary page",
+ "releaseInfo.listenOn": "Listen on {LINKS}.",
+ "releaseInfo.listenOn.noLinks": "This track has no URLs at which it can be listened.",
+ "releaseInfo.visitOn": "Visit on {LINKS}.",
+ "releaseInfo.playOn": "Play on {LINKS}.",
+ "releaseInfo.alsoReleasedAs": "Also released as:",
+ "releaseInfo.alsoReleasedAs.item": "{TRACK} (on {ALBUM})",
+ "releaseInfo.contributors": "Contributors:",
+ "releaseInfo.tracksReferenced": "Tracks that {TRACK} references:",
+ "releaseInfo.tracksThatReference": "Tracks that reference {TRACK}:",
+ "releaseInfo.flashesThatFeature": "Flashes & games that feature {TRACK}:",
+ "releaseInfo.flashesThatFeature.item": "{FLASH}",
+ "releaseInfo.flashesThatFeature.item.asDifferentRelease": "{FLASH} (as {TRACK})",
+ "releaseInfo.lyrics": "Lyrics:",
+ "releaseInfo.artistCommentary": "Artist commentary:",
+ "releaseInfo.artistCommentary.seeOriginalRelease": "See {ORIGINAL}!",
+ "releaseInfo.artTags": "Tags:",
+ "releaseInfo.additionalFiles.shortcut": "{ANCHOR_LINK} {TITLES}",
+ "releaseInfo.additionalFiles.shortcut.anchorLink": "Additional files:",
+ "releaseInfo.additionalFiles.heading": "Has {ADDITIONAL_FILES}:",
+ "releaseInfo.additionalFiles.entry": "{TITLE}",
+ "releaseInfo.additionalFiles.entry.withDescription": "{TITLE}: {DESCRIPTION}",
+ "releaseInfo.additionalFiles.file": "{FILE}",
+ "releaseInfo.additionalFiles.file.withSize": "{FILE} ({SIZE})",
+ "releaseInfo.note": "Note:",
+ "trackList.section.withDuration": "{SECTION} ({DURATION}):",
+ "trackList.group": "{GROUP}:",
+ "trackList.group.other": "Other",
+ "trackList.item.withDuration": "({DURATION}) {TRACK}",
+ "trackList.item.withDuration.withArtists": "({DURATION}) {TRACK} {BY}",
+ "trackList.item.withArtists": "{TRACK} {BY}",
+ "trackList.item.withArtists.by": "by {ARTISTS}",
+ "trackList.item.rerelease": "{TRACK} (re-release)",
+ "misc.alt.albumCover": "album cover",
+ "misc.alt.albumBanner": "album banner",
+ "misc.alt.trackCover": "track cover",
+ "misc.alt.artistAvatar": "artist avatar",
+ "misc.alt.flashArt": "flash art",
+ "misc.chronology.seeArtistPages": "(See artist pages for chronology info!)",
+ "misc.chronology.heading.coverArt": "{INDEX} cover art by {ARTIST}",
+ "misc.chronology.heading.flash": "{INDEX} flash/game by {ARTIST}",
+ "misc.chronology.heading.track": "{INDEX} track by {ARTIST}",
+ "misc.external.domain": "External ({DOMAIN})",
+ "misc.external.local": "Wiki Archive (local upload)",
+ "misc.external.bandcamp": "Bandcamp",
+ "misc.external.bandcamp.domain": "Bandcamp ({DOMAIN})",
+ "misc.external.deviantart": "DeviantArt",
+ "misc.external.instagram": "Instagram",
+ "misc.external.mastodon": "Mastodon",
+ "misc.external.mastodon.domain": "Mastodon ({DOMAIN})",
+ "misc.external.patreon": "Patreon",
+ "misc.external.poetryFoundation": "Poetry Foundation",
+ "misc.external.soundcloud": "SoundCloud",
+ "misc.external.tumblr": "Tumblr",
+ "misc.external.twitter": "Twitter",
+ "misc.external.wikipedia": "Wikipedia",
+ "misc.external.youtube": "YouTube",
+ "misc.external.youtube.playlist": "YouTube (playlist)",
+ "misc.external.youtube.fullAlbum": "YouTube (full album)",
+ "misc.external.flash.bgreco": "{LINK} (HQ Audio)",
+ "misc.external.flash.homestuck.page": "{LINK} (page {PAGE})",
+ "misc.external.flash.homestuck.secret": "{LINK} (secret page)",
+ "misc.external.flash.youtube": "{LINK} (on any device)",
+ "misc.nav.previous": "Previous",
+ "misc.nav.next": "Next",
+ "misc.nav.info": "Info",
+ "misc.nav.gallery": "Gallery",
+ "misc.pageTitle": "{TITLE}",
+ "misc.pageTitle.withWikiName": "{TITLE} | {WIKI_NAME}",
+ "misc.skippers.skipToContent": "Skip to content",
+ "misc.skippers.skipToSidebar": "Skip to sidebar",
+ "misc.skippers.skipToSidebar.left": "Skip to sidebar (left)",
+ "misc.skippers.skipToSidebar.right": "Skip to sidebar (right)",
+ "misc.skippers.skipToFooter": "Skip to footer",
+ "misc.socialEmbed.heading": "{WIKI_NAME} | {HEADING}",
+ "misc.jumpTo": "Jump to:",
+ "misc.jumpTo.withLinks": "Jump to: {LINKS}.",
+ "misc.contentWarnings": "cw: {WARNINGS}",
+ "misc.contentWarnings.reveal": "click to show",
+ "misc.albumGrid.details": "({TRACKS}, {TIME})",
+ "misc.albumGrid.noCoverArt": "{ALBUM}",
+ "misc.uiLanguage": "UI Language: {LANGUAGES}",
+ "homepage.title": "{TITLE}",
+ "homepage.news.title": "News",
+ "homepage.news.entry.viewRest": "(View rest of entry!)",
+ "albumSidebar.trackList.fallbackGroupName": "Track list",
+ "albumSidebar.trackList.group": "{GROUP}",
+ "albumSidebar.trackList.group.withRange": "{GROUP} ({RANGE})",
+ "albumSidebar.trackList.item": "{TRACK}",
+ "albumSidebar.groupBox.title": "{GROUP}",
+ "albumSidebar.groupBox.next": "Next: {ALBUM}",
+ "albumSidebar.groupBox.previous": "Previous: {ALBUM}",
+ "albumPage.title": "{ALBUM}",
+ "albumPage.nav.album": "{ALBUM}",
+ "albumPage.nav.randomTrack": "Random Track",
+ "albumCommentaryPage.title": "{ALBUM} - Commentary",
+ "albumCommentaryPage.infoLine": "{WORDS} across {ENTRIES}.",
+ "albumCommentaryPage.nav.album": "Album: {ALBUM}",
+ "albumCommentaryPage.entry.title.albumCommentary": "Album commentary",
+ "albumCommentaryPage.entry.title.trackCommentary": "{TRACK}",
+ "artistPage.title": "{ARTIST}",
+ "artistPage.creditList.album": "{ALBUM}",
+ "artistPage.creditList.album.withDate": "{ALBUM} ({DATE})",
+ "artistPage.creditList.album.withDuration": "{ALBUM} ({DURATION})",
+ "artistPage.creditList.album.withDate.withDuration": "{ALBUM} ({DATE}; {DURATION})",
+ "artistPage.creditList.flashAct": "{ACT}",
+ "artistPage.creditList.flashAct.withDateRange": "{ACT} ({DATE_RANGE})",
+ "artistPage.creditList.entry.track": "{TRACK}",
+ "artistPage.creditList.entry.track.withDuration": "({DURATION}) {TRACK}",
+ "artistPage.creditList.entry.album.coverArt": "(cover art)",
+ "artistPage.creditList.entry.album.wallpaperArt": "(wallpaper art)",
+ "artistPage.creditList.entry.album.bannerArt": "(banner art)",
+ "artistPage.creditList.entry.album.commentary": "(album commentary)",
+ "artistPage.creditList.entry.flash": "{FLASH}",
+ "artistPage.creditList.entry.rerelease": "{ENTRY} (re-release)",
+ "artistPage.creditList.entry.withContribution": "{ENTRY} ({CONTRIBUTION})",
+ "artistPage.creditList.entry.withArtists": "{ENTRY} (with {ARTISTS})",
+ "artistPage.creditList.entry.withArtists.withContribution": "{ENTRY} ({CONTRIBUTION}; with {ARTISTS})",
+ "artistPage.contributedDurationLine": "{ARTIST} has contributed {DURATION} of music shared on this wiki.",
+ "artistPage.musicGroupsLine": "Contributed music to groups: {GROUPS}",
+ "artistPage.artGroupsLine": "Contributed art to groups: {GROUPS}",
+ "artistPage.groupsLine.item": "{GROUP} ({CONTRIBUTIONS})",
+ "artistPage.trackList.title": "Tracks",
+ "artistPage.artList.title": "Art",
+ "artistPage.flashList.title": "Flashes & Games",
+ "artistPage.commentaryList.title": "Commentary",
+ "artistPage.viewArtGallery": "View {LINK}!",
+ "artistPage.viewArtGallery.orBrowseList": "View {LINK}! Or browse the list:",
+ "artistPage.viewArtGallery.link": "art gallery",
+ "artistPage.nav.artist": "Artist: {ARTIST}",
+ "artistGalleryPage.title": "{ARTIST} - Gallery",
+ "artistGalleryPage.infoLine": "Contributed to {COVER_ARTS}.",
+ "commentaryIndex.title": "Commentary",
+ "commentaryIndex.infoLine": "{WORDS} across {ENTRIES}, in all.",
+ "commentaryIndex.albumList.title": "Choose an album:",
+ "commentaryIndex.albumList.item": "{ALBUM} ({WORDS} across {ENTRIES})",
+ "flashIndex.title": "Flashes & Games",
+ "flashPage.title": "{FLASH}",
+ "flashPage.nav.flash": "{FLASH}",
+ "groupSidebar.title": "Groups",
+ "groupSidebar.groupList.category": "{CATEGORY}",
+ "groupSidebar.groupList.item": "{GROUP}",
+ "groupPage.nav.group": "Group: {GROUP}",
+ "groupInfoPage.title": "{GROUP}",
+ "groupInfoPage.viewAlbumGallery": "View {LINK}! Or browse the list:",
+ "groupInfoPage.viewAlbumGallery.link": "album gallery",
+ "groupInfoPage.albumList.title": "Albums",
+ "groupInfoPage.albumList.item": "({YEAR}) {ALBUM}",
+ "groupInfoPage.albumList.item.withoutYear": "{ALBUM}",
+ "groupInfoPage.albumList.item.withAccent": "{ITEM} {ACCENT}",
+ "groupInfoPage.albumList.item.otherGroupAccent": "(from {GROUP})",
+ "groupGalleryPage.title": "{GROUP} - Gallery",
+ "groupGalleryPage.infoLine": "{TRACKS} across {ALBUMS}, totaling {TIME}.",
+ "groupGalleryPage.anotherGroupLine": "({LINK})",
+ "groupGalleryPage.anotherGroupLine.link": "Choose another group to filter by!",
+ "listingIndex.title": "Listings",
+ "listingIndex.infoLine": "{WIKI}: {TRACKS} across {ALBUMS}, totaling {DURATION}.",
+ "listingIndex.exploreList": "Feel free to explore any of the listings linked below and in the sidebar!",
+ "listingPage.target.album": "Albums",
+ "listingPage.target.artist": "Artists",
+ "listingPage.target.group": "Groups",
+ "listingPage.target.track": "Tracks",
+ "listingPage.target.tag": "Tags",
+ "listingPage.target.other": "Other",
+ "listingPage.listAlbums.byName.title": "Albums - by Name",
+ "listingPage.listAlbums.byName.title.short": "...by Name",
+ "listingPage.listAlbums.byName.item": "{ALBUM} ({TRACKS})",
+ "listingPage.listAlbums.byTracks.title": "Albums - by Tracks",
+ "listingPage.listAlbums.byTracks.title.short": "...by Tracks",
+ "listingPage.listAlbums.byTracks.item": "{ALBUM} ({TRACKS})",
+ "listingPage.listAlbums.byDuration.title": "Albums - by Duration",
+ "listingPage.listAlbums.byDuration.title.short": "...by Duration",
+ "listingPage.listAlbums.byDuration.item": "{ALBUM} ({DURATION})",
+ "listingPage.listAlbums.byDate.title": "Albums - by Date",
+ "listingPage.listAlbums.byDate.title.short": "...by Date",
+ "listingPage.listAlbums.byDate.item": "{ALBUM} ({DATE})",
+ "listingPage.listAlbums.byDateAdded.title.short": "...by Date Added to Wiki",
+ "listingPage.listAlbums.byDateAdded.title": "Albums - by Date Added to Wiki",
+ "listingPage.listAlbums.byDateAdded.date": "{DATE}",
+ "listingPage.listAlbums.byDateAdded.album": "{ALBUM}",
+ "listingPage.listArtists.byName.title": "Artists - by Name",
+ "listingPage.listArtists.byName.title.short": "...by Name",
+ "listingPage.listArtists.byName.item": "{ARTIST} ({CONTRIBUTIONS})",
+ "listingPage.listArtists.byContribs.title": "Artists - by Contributions",
+ "listingPage.listArtists.byContribs.title.short": "...by Contributions",
+ "listingPage.listArtists.byContribs.item": "{ARTIST} ({CONTRIBUTIONS})",
+ "listingPage.listArtists.byCommentary.title": "Artists - by Commentary Entries",
+ "listingPage.listArtists.byCommentary.title.short": "...by Commentary Entries",
+ "listingPage.listArtists.byCommentary.item": "{ARTIST} ({ENTRIES})",
+ "listingPage.listArtists.byDuration.title": "Artists - by Duration",
+ "listingPage.listArtists.byDuration.title.short": "...by Duration",
+ "listingPage.listArtists.byDuration.item": "{ARTIST} ({DURATION})",
+ "listingPage.listArtists.byLatest.title": "Artists - by Latest Contribution",
+ "listingPage.listArtists.byLatest.title.short": "...by Latest Contribution",
+ "listingPage.listArtists.byLatest.item": "{ARTIST} ({DATE})",
+ "listingPage.listGroups.byName.title": "Groups - by Name",
+ "listingPage.listGroups.byName.title.short": "...by Name",
+ "listingPage.listGroups.byName.item": "{GROUP} ({GALLERY})",
+ "listingPage.listGroups.byName.item.gallery": "Gallery",
+ "listingPage.listGroups.byCategory.title": "Groups - by Category",
+ "listingPage.listGroups.byCategory.title.short": "...by Category",
+ "listingPage.listGroups.byCategory.category": "{CATEGORY}",
+ "listingPage.listGroups.byCategory.group": "{GROUP} ({GALLERY})",
+ "listingPage.listGroups.byCategory.group.gallery": "Gallery",
+ "listingPage.listGroups.byAlbums.title": "Groups - by Albums",
+ "listingPage.listGroups.byAlbums.title.short": "...by Albums",
+ "listingPage.listGroups.byAlbums.item": "{GROUP} ({ALBUMS})",
+ "listingPage.listGroups.byTracks.title": "Groups - by Tracks",
+ "listingPage.listGroups.byTracks.title.short": "...by Tracks",
+ "listingPage.listGroups.byTracks.item": "{GROUP} ({TRACKS})",
+ "listingPage.listGroups.byDuration.title": "Groups - by Duration",
+ "listingPage.listGroups.byDuration.title.short": "...by Duration",
+ "listingPage.listGroups.byDuration.item": "{GROUP} ({DURATION})",
+ "listingPage.listGroups.byLatest.title": "Groups - by Latest Album",
+ "listingPage.listGroups.byLatest.title.short": "...by Latest Album",
+ "listingPage.listGroups.byLatest.item": "{GROUP} ({DATE})",
+ "listingPage.listTracks.byName.title": "Tracks - by Name",
+ "listingPage.listTracks.byName.title.short": "...by Name",
+ "listingPage.listTracks.byName.item": "{TRACK}",
+ "listingPage.listTracks.byAlbum.title": "Tracks - by Album",
+ "listingPage.listTracks.byAlbum.title.short": "...by Album",
+ "listingPage.listTracks.byAlbum.album": "{ALBUM}",
+ "listingPage.listTracks.byAlbum.track": "{TRACK}",
+ "listingPage.listTracks.byDate.title": "Tracks - by Date",
+ "listingPage.listTracks.byDate.title.short": "...by Date",
+ "listingPage.listTracks.byDate.album": "{ALBUM} ({DATE})",
+ "listingPage.listTracks.byDate.track": "{TRACK}",
+ "listingPage.listTracks.byDate.track.rerelease": "{TRACK} (re-release)",
+ "listingPage.listTracks.byDuration.title": "Tracks - by Duration",
+ "listingPage.listTracks.byDuration.title.short": "...by Duration",
+ "listingPage.listTracks.byDuration.item": "{TRACK} ({DURATION})",
+ "listingPage.listTracks.byDurationInAlbum.title": "Tracks - by Duration (in Album)",
+ "listingPage.listTracks.byDurationInAlbum.title.short": "...by Duration (in Album)",
+ "listingPage.listTracks.byDurationInAlbum.album": "{ALBUM}",
+ "listingPage.listTracks.byDurationInAlbum.track": "{TRACK} ({DURATION})",
+ "listingPage.listTracks.byTimesReferenced.title": "Tracks - by Times Referenced",
+ "listingPage.listTracks.byTimesReferenced.title.short": "...by Times Referenced",
+ "listingPage.listTracks.byTimesReferenced.item": "{TRACK} ({TIMES_REFERENCED})",
+ "listingPage.listTracks.inFlashes.byAlbum.title": "Tracks - in Flashes & Games (by Album)",
+ "listingPage.listTracks.inFlashes.byAlbum.title.short": "...in Flashes & Games (by Album)",
+ "listingPage.listTracks.inFlashes.byAlbum.album": "{ALBUM} ({DATE})",
+ "listingPage.listTracks.inFlashes.byAlbum.track": "{TRACK} (in {FLASHES})",
+ "listingPage.listTracks.inFlashes.byFlash.title": "Tracks - in Flashes & Games (by Flash)",
+ "listingPage.listTracks.inFlashes.byFlash.title.short": "...in Flashes & Games (by Flash)",
+ "listingPage.listTracks.inFlashes.byFlash.flash": "{FLASH} ({DATE})",
+ "listingPage.listTracks.inFlashes.byFlash.track": "{TRACK} (from {ALBUM})",
+ "listingPage.listTracks.withLyrics.title": "Tracks - with Lyrics",
+ "listingPage.listTracks.withLyrics.title.short": "...with Lyrics",
+ "listingPage.listTracks.withLyrics.album": "{ALBUM} ({DATE})",
+ "listingPage.listTracks.withLyrics.track": "{TRACK}",
+ "listingPage.listTags.byName.title": "Tags - by Name",
+ "listingPage.listTags.byName.title.short": "...by Name",
+ "listingPage.listTags.byName.item": "{TAG} ({TIMES_USED})",
+ "listingPage.listTags.byUses.title": "Tags - by Uses",
+ "listingPage.listTags.byUses.title.short": "...by Uses",
+ "listingPage.listTags.byUses.item": "{TAG} ({TIMES_USED})",
+ "listingPage.other.randomPages.title": "Random Pages",
+ "listingPage.other.randomPages.title.short": "Random Pages",
+ "listingPage.misc.trackContributors": "Track Contributors",
+ "listingPage.misc.artContributors": "Art Contributors",
+ "listingPage.misc.artAndFlashContributors": "Art & Flash Contributors",
+ "newsIndex.title": "News",
+ "newsIndex.entry.viewRest": "(View rest of entry!)",
+ "newsEntryPage.title": "{ENTRY}",
+ "newsEntryPage.published": "(Published {DATE}.)",
+ "newsEntryPage.nav.news": "News",
+ "newsEntryPage.nav.entry": "{DATE}: {ENTRY}",
+ "redirectPage.title": "Moved to {TITLE}",
+ "redirectPage.infoLine": "This page has been moved to {TARGET}.",
+ "tagPage.title": "{TAG}",
+ "tagPage.infoLine": "Appears in {COVER_ARTS}.",
+ "tagPage.nav.tag": "Tag: {TAG}",
+ "trackPage.title": "{TRACK}",
+ "trackPage.referenceList.fandom": "Fandom:",
+ "trackPage.referenceList.official": "Official:",
+ "trackPage.nav.track": "{TRACK}",
+ "trackPage.nav.track.withNumber": "{NUMBER}. {TRACK}",
+ "trackPage.nav.random": "Random",
+ "trackPage.socialEmbed.heading": "{ALBUM}",
+ "trackPage.socialEmbed.title": "{TRACK}",
+ "trackPage.socialEmbed.body.withArtists.withCoverArtists": "By {ARTISTS}; art by {COVER_ARTISTS}.",
+ "trackPage.socialEmbed.body.withArtists": "By {ARTISTS}.",
+ "trackPage.socialEmbed.body.withCoverArtists": "Art by {COVER_ARTISTS}."
}
diff --git a/src/upd8.js b/src/upd8.js
index d9bca28f..576166aa 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -31,153 +31,145 @@
// Oh yeah, like. Just run this through some relatively recent version of
// node.js and you'll 8e fine. ...Within the project root. O8viously.
-import * as path from 'path';
-import { promisify } from 'util';
-import { fileURLToPath } from 'url';
+import * as path from "path";
+import { promisify } from "util";
+import { fileURLToPath } from "url";
// I made this dependency myself! A long, long time ago. It is pro8a8ly my
// most useful li8rary ever. I'm not sure 8esides me actually uses it, though.
-import fixWS from 'fix-whitespace';
+import fixWS from "fix-whitespace";
// Wait nevermind, I forgot a8out why-do-kids-love-the-taste-of-cinnamon-toast-
// crunch. THAT is my 8est li8rary.
// It stands for "HTML Entities", apparently. Cursed.
-import he from 'he';
+import he from "he";
import {
- copyFile,
- mkdir,
- readFile,
- stat,
- symlink,
- writeFile,
- unlink,
-} from 'fs/promises';
+ copyFile,
+ mkdir,
+ readFile,
+ stat,
+ symlink,
+ writeFile,
+ unlink,
+} from "fs/promises";
-import { inspect as nodeInspect } from 'util';
+import { inspect as nodeInspect } from "util";
-import genThumbs from './gen-thumbs.js';
-import { listingSpec, listingTargetSpec } from './listing-spec.js';
-import urlSpec from './url-spec.js';
-import * as pageSpecs from './page/index.js';
+import genThumbs from "./gen-thumbs.js";
+import { listingSpec, listingTargetSpec } from "./listing-spec.js";
+import urlSpec from "./url-spec.js";
+import * as pageSpecs from "./page/index.js";
-import find, { bindFind } from './util/find.js';
-import * as html from './util/html.js';
-import unbound_link, {getLinkThemeString} from './util/link.js';
-import { findFiles } from './util/io.js';
+import find, { bindFind } from "./util/find.js";
+import * as html from "./util/html.js";
+import unbound_link, { getLinkThemeString } from "./util/link.js";
+import { findFiles } from "./util/io.js";
-import CacheableObject from './data/cacheable-object.js';
+import CacheableObject from "./data/cacheable-object.js";
-import { serializeThings } from './data/serialize.js';
+import { serializeThings } from "./data/serialize.js";
-import {
- Language,
-} from './data/things.js';
-
-import {
- filterDuplicateDirectories,
- filterReferenceErrors,
- linkWikiDataArrays,
- loadAndProcessDataDocuments,
- sortWikiDataArrays,
- WIKI_INFO_FILE,
-} from './data/yaml.js';
+import { Language } from "./data/things.js";
import {
- fancifyFlashURL,
- fancifyURL,
- generateAdditionalFilesShortcut,
- generateAdditionalFilesList,
- generateChronologyLinks,
- generateCoverLink,
- generateInfoGalleryLinks,
- generatePreviousNextLinks,
- generateTrackListDividedByGroups,
- getAlbumGridHTML,
- getAlbumStylesheet,
- getArtistString,
- getFlashGridHTML,
- getFooterLocalizationLinks,
- getGridHTML,
- getRevealStringFromTags,
- getRevealStringFromWarnings,
- getThemeString,
- iconifyURL
-} from './misc-templates.js';
+ filterDuplicateDirectories,
+ filterReferenceErrors,
+ linkWikiDataArrays,
+ loadAndProcessDataDocuments,
+ sortWikiDataArrays,
+ WIKI_INFO_FILE,
+} from "./data/yaml.js";
import {
- color,
- decorateTime,
- logWarn,
- logInfo,
- logError,
- parseOptions,
- progressPromiseAll,
- ENABLE_COLOR
-} from './util/cli.js';
+ fancifyFlashURL,
+ fancifyURL,
+ generateAdditionalFilesShortcut,
+ generateAdditionalFilesList,
+ generateChronologyLinks,
+ generateCoverLink,
+ generateInfoGalleryLinks,
+ generatePreviousNextLinks,
+ generateTrackListDividedByGroups,
+ getAlbumGridHTML,
+ getAlbumStylesheet,
+ getArtistString,
+ getFlashGridHTML,
+ getFooterLocalizationLinks,
+ getGridHTML,
+ getRevealStringFromTags,
+ getRevealStringFromWarnings,
+ getThemeString,
+ iconifyURL,
+} from "./misc-templates.js";
import {
- validateReplacerSpec,
- transformInline
-} from './util/replacer.js';
+ color,
+ decorateTime,
+ logWarn,
+ logInfo,
+ logError,
+ parseOptions,
+ progressPromiseAll,
+ ENABLE_COLOR,
+} from "./util/cli.js";
+
+import { validateReplacerSpec, transformInline } from "./util/replacer.js";
import {
- chunkByConditions,
- chunkByProperties,
- getAlbumCover,
- getAlbumListTag,
- getAllTracks,
- getArtistAvatar,
- getArtistNumContributions,
- getFlashCover,
- getKebabCase,
- getTotalDuration,
- getTrackCover,
-} from './util/wiki-data.js';
+ chunkByConditions,
+ chunkByProperties,
+ getAlbumCover,
+ getAlbumListTag,
+ getAllTracks,
+ getArtistAvatar,
+ getArtistNumContributions,
+ getFlashCover,
+ getKebabCase,
+ getTotalDuration,
+ getTrackCover,
+} from "./util/wiki-data.js";
import {
- serializeContribs,
- serializeCover,
- serializeGroupsForAlbum,
- serializeGroupsForTrack,
- serializeImagePaths,
- serializeLink
-} from './util/serialize.js';
+ serializeContribs,
+ serializeCover,
+ serializeGroupsForAlbum,
+ serializeGroupsForTrack,
+ serializeImagePaths,
+ serializeLink,
+} from "./util/serialize.js";
import {
- bindOpts,
- decorateErrorWithIndex,
- filterAggregateAsync,
- filterEmptyLines,
- mapAggregate,
- mapAggregateAsync,
- openAggregate,
- queue,
- showAggregate,
- splitArray,
- unique,
- withAggregate,
- withEntries
-} from './util/sugar.js';
-
-import {
- generateURLs,
- thumb
-} from './util/urls.js';
+ bindOpts,
+ decorateErrorWithIndex,
+ filterAggregateAsync,
+ filterEmptyLines,
+ mapAggregate,
+ mapAggregateAsync,
+ openAggregate,
+ queue,
+ showAggregate,
+ splitArray,
+ unique,
+ withAggregate,
+ withEntries,
+} from "./util/sugar.js";
+
+import { generateURLs, thumb } from "./util/urls.js";
// Pensive emoji!
import {
- FANDOM_GROUP_DIRECTORY,
- OFFICIAL_GROUP_DIRECTORY
-} from './util/magic-constants.js';
+ FANDOM_GROUP_DIRECTORY,
+ OFFICIAL_GROUP_DIRECTORY,
+} from "./util/magic-constants.js";
-import FileSizePreloader from './file-size-preloader.js';
+import FileSizePreloader from "./file-size-preloader.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CACHEBUST = 10;
-const DEFAULT_STRINGS_FILE = 'strings-default.json';
+const DEFAULT_STRINGS_FILE = "strings-default.json";
// Code that's common 8etween the 8uild code (i.e. upd8.js) and gener8ted
// site code should 8e put here. Which, uh, ~~only really means this one
@@ -186,20 +178,20 @@ const DEFAULT_STRINGS_FILE = 'strings-default.json';
// Rather than hard code it, anything in this directory can 8e shared across
// 8oth ends of the code8ase.
// (This gets symlinked into the --data-path directory.)
-const UTILITY_DIRECTORY = 'util';
+const UTILITY_DIRECTORY = "util";
// Code that's used only in the static site! CSS, cilent JS, etc.
// (This gets symlinked into the --data-path directory.)
-const STATIC_DIRECTORY = 'static';
+const STATIC_DIRECTORY = "static";
// This exists adjacent to index.html for any page with oEmbed metadata.
-const OEMBED_JSON_FILE = 'oembed.json';
+const OEMBED_JSON_FILE = "oembed.json";
// Automatically copied (if present) from media directory to site root.
-const FAVICON_FILE = 'favicon.ico';
+const FAVICON_FILE = "favicon.ico";
function inspect(value) {
- return nodeInspect(value, {colors: ENABLE_COLOR});
+ return nodeInspect(value, { colors: ENABLE_COLOR });
}
// Shared varia8les! These are more efficient to access than a shared varia8le
@@ -223,556 +215,610 @@ let queueSize;
const urls = generateURLs(urlSpec);
function splitLines(text) {
- return text.split(/\r\n|\r|\n/);
+ return text.split(/\r\n|\r|\n/);
}
const replacerSpec = {
- 'album': {
- find: 'album',
- link: 'album'
- },
- 'album-commentary': {
- find: 'album',
- link: 'albumCommentary'
- },
- 'artist': {
- find: 'artist',
- link: 'artist'
- },
- 'artist-gallery': {
- find: 'artist',
- link: 'artistGallery'
- },
- 'commentary-index': {
- find: null,
- link: 'commentaryIndex'
- },
- 'date': {
- find: null,
- value: ref => new Date(ref),
- html: (date, {language}) => `
${language.formatDate(date)} `
- },
- 'flash': {
- find: 'flash',
- link: 'flash',
- transformName(name, node, input) {
- const nextCharacter = input[node.iEnd];
- const lastCharacter = name[name.length - 1];
- if (
- ![' ', '\n', '<'].includes(nextCharacter) &&
- lastCharacter === '.'
- ) {
- return name.slice(0, -1);
- } else {
- return name;
- }
- }
- },
- 'group': {
- find: 'group',
- link: 'groupInfo'
- },
- 'group-gallery': {
- find: 'group',
- link: 'groupGallery'
- },
- 'home': {
- find: null,
- link: 'home'
- },
- 'listing-index': {
- find: null,
- link: 'listingIndex'
- },
- 'listing': {
- find: 'listing',
- link: 'listing'
- },
- 'media': {
- find: null,
- link: 'media'
- },
- 'news-index': {
- find: null,
- link: 'newsIndex'
- },
- 'news-entry': {
- find: 'newsEntry',
- link: 'newsEntry'
- },
- 'root': {
- find: null,
- link: 'root'
- },
- 'site': {
- find: null,
- link: 'site'
- },
- 'static': {
- find: 'staticPage',
- link: 'staticPage'
- },
- 'string': {
- find: null,
- value: ref => ref,
- html: (ref, {language, args}) => language.$(ref, args)
+ album: {
+ find: "album",
+ link: "album",
+ },
+ "album-commentary": {
+ find: "album",
+ link: "albumCommentary",
+ },
+ artist: {
+ find: "artist",
+ link: "artist",
+ },
+ "artist-gallery": {
+ find: "artist",
+ link: "artistGallery",
+ },
+ "commentary-index": {
+ find: null,
+ link: "commentaryIndex",
+ },
+ date: {
+ find: null,
+ value: (ref) => new Date(ref),
+ html: (date, { language }) =>
+ `
${language.formatDate(date)} `,
+ },
+ flash: {
+ find: "flash",
+ link: "flash",
+ transformName(name, node, input) {
+ const nextCharacter = input[node.iEnd];
+ const lastCharacter = name[name.length - 1];
+ if (![" ", "\n", "<"].includes(nextCharacter) && lastCharacter === ".") {
+ return name.slice(0, -1);
+ } else {
+ return name;
+ }
},
- 'tag': {
- find: 'artTag',
- link: 'tag'
- },
- 'track': {
- find: 'track',
- link: 'track'
- }
+ },
+ group: {
+ find: "group",
+ link: "groupInfo",
+ },
+ "group-gallery": {
+ find: "group",
+ link: "groupGallery",
+ },
+ home: {
+ find: null,
+ link: "home",
+ },
+ "listing-index": {
+ find: null,
+ link: "listingIndex",
+ },
+ listing: {
+ find: "listing",
+ link: "listing",
+ },
+ media: {
+ find: null,
+ link: "media",
+ },
+ "news-index": {
+ find: null,
+ link: "newsIndex",
+ },
+ "news-entry": {
+ find: "newsEntry",
+ link: "newsEntry",
+ },
+ root: {
+ find: null,
+ link: "root",
+ },
+ site: {
+ find: null,
+ link: "site",
+ },
+ static: {
+ find: "staticPage",
+ link: "staticPage",
+ },
+ string: {
+ find: null,
+ value: (ref) => ref,
+ html: (ref, { language, args }) => language.$(ref, args),
+ },
+ tag: {
+ find: "artTag",
+ link: "tag",
+ },
+ track: {
+ find: "track",
+ link: "track",
+ },
};
-if (!validateReplacerSpec(replacerSpec, {find, link: unbound_link})) {
- process.exit();
+if (!validateReplacerSpec(replacerSpec, { find, link: unbound_link })) {
+ process.exit();
}
-function parseAttributes(string, {to}) {
- const attributes = Object.create(null);
- const skipWhitespace = i => {
- const ws = /\s/;
- if (ws.test(string[i])) {
- const match = string.slice(i).match(/[^\s]/);
- if (match) {
- return i + match.index;
- } else {
- return string.length;
- }
- } else {
- return i;
- }
- };
-
- for (let i = 0; i < string.length;) {
- i = skipWhitespace(i);
- const aStart = i;
- const aEnd = i + string.slice(i).match(/[\s=]|$/).index;
- const attribute = string.slice(aStart, aEnd);
- i = skipWhitespace(aEnd);
- if (string[i] === '=') {
- i = skipWhitespace(i + 1);
- let end, endOffset;
- if (string[i] === '"' || string[i] === "'") {
- end = string[i];
- endOffset = 1;
- i++;
- } else {
- end = '\\s';
- endOffset = 0;
- }
- const vStart = i;
- const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index;
- const value = string.slice(vStart, vEnd);
- i = vEnd + endOffset;
- if (attribute === 'src' && value.startsWith('media/')) {
- attributes[attribute] = to('media.path', value.slice('media/'.length));
- } else {
- attributes[attribute] = value;
- }
- } else {
- attributes[attribute] = attribute;
- }
+function parseAttributes(string, { to }) {
+ const attributes = Object.create(null);
+ const skipWhitespace = (i) => {
+ const ws = /\s/;
+ if (ws.test(string[i])) {
+ const match = string.slice(i).match(/[^\s]/);
+ if (match) {
+ return i + match.index;
+ } else {
+ return string.length;
+ }
+ } else {
+ return i;
}
- return Object.fromEntries(Object.entries(attributes).map(([ key, val ]) => [
- key,
- val === 'true' ? true :
- val === 'false' ? false :
- val === key ? true :
- val
- ]));
+ };
+
+ for (let i = 0; i < string.length; ) {
+ i = skipWhitespace(i);
+ const aStart = i;
+ const aEnd = i + string.slice(i).match(/[\s=]|$/).index;
+ const attribute = string.slice(aStart, aEnd);
+ i = skipWhitespace(aEnd);
+ if (string[i] === "=") {
+ i = skipWhitespace(i + 1);
+ let end, endOffset;
+ if (string[i] === '"' || string[i] === "'") {
+ end = string[i];
+ endOffset = 1;
+ i++;
+ } else {
+ end = "\\s";
+ endOffset = 0;
+ }
+ const vStart = i;
+ const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index;
+ const value = string.slice(vStart, vEnd);
+ i = vEnd + endOffset;
+ if (attribute === "src" && value.startsWith("media/")) {
+ attributes[attribute] = to("media.path", value.slice("media/".length));
+ } else {
+ attributes[attribute] = value;
+ }
+ } else {
+ attributes[attribute] = attribute;
+ }
+ }
+ return Object.fromEntries(
+ Object.entries(attributes).map(([key, val]) => [
+ key,
+ val === "true"
+ ? true
+ : val === "false"
+ ? false
+ : val === key
+ ? true
+ : val,
+ ])
+ );
}
function joinLineBreaks(sourceLines) {
- const outLines = [];
-
- let lineSoFar = '';
- for (let i = 0; i < sourceLines.length; i++) {
- const line = sourceLines[i];
- lineSoFar += line;
- if (!line.endsWith('
')) {
- outLines.push(lineSoFar);
- lineSoFar = '';
- }
+ const outLines = [];
+
+ let lineSoFar = "";
+ for (let i = 0; i < sourceLines.length; i++) {
+ const line = sourceLines[i];
+ lineSoFar += line;
+ if (!line.endsWith("
")) {
+ outLines.push(lineSoFar);
+ lineSoFar = "";
}
+ }
- if (lineSoFar) {
- outLines.push(lineSoFar);
- }
+ if (lineSoFar) {
+ outLines.push(lineSoFar);
+ }
- return outLines;
+ return outLines;
}
-function transformMultiline(text, {
- parseAttributes,
- transformInline
-}) {
- // Heck yes, HTML magics.
-
- text = transformInline(text.trim());
-
- const outLines = [];
-
- const indentString = ' '.repeat(4);
-
- let levelIndents = [];
- const openLevel = indent => {
- // opening a sublist is a pain: to be semantically *and* visually
- // correct, we have to append the
at the end of the existing
- // previous
- const previousLine = outLines[outLines.length - 1];
- if (previousLine?.endsWith(' ')) {
- // we will re-close the later
- outLines[outLines.length - 1] = previousLine.slice(0, -5) + ' ';
- } else {
- // if the previous line isn't a list item, this is the opening of
- // the first list level, so no need for indent
- outLines.push('');
- }
- levelIndents.push(indent);
- };
- const closeLevel = () => {
- levelIndents.pop();
- if (levelIndents.length) {
- // closing a sublist, so close the list item containing it too
- outLines.push(indentString.repeat(levelIndents.length) + ' ');
- } else {
- // closing the final list level! no need for indent here
- outLines.push(' ');
- }
- };
+function transformMultiline(text, { parseAttributes, transformInline }) {
+ // Heck yes, HTML magics.
- // okay yes we should support nested formatting, more than one blockquote
- // layer, etc, but hear me out here: making all that work would basically
- // be the same as implementing an entire markdown converter, which im not
- // interested in doing lol. sorry!!!
- let inBlockquote = false;
-
- let lines = splitLines(text);
- lines = joinLineBreaks(lines);
- for (let line of lines) {
- const imageLine = line.startsWith('
/g, (match, attributes) => img({
- lazy: true,
- link: true,
- thumb: 'medium',
- ...parseAttributes(attributes)
- }));
-
- let indentThisLine = 0;
- let lineContent = line;
- let lineTag = 'p';
-
- const listMatch = line.match(/^( *)- *(.*)$/);
- if (listMatch) {
- // is a list item!
- if (!levelIndents.length) {
- // first level is always indent = 0, regardless of actual line
- // content (this is to avoid going to a lesser indent than the
- // initial level)
- openLevel(0);
- } else {
- // find level corresponding to indent
- const indent = listMatch[1].length;
- let i;
- for (i = levelIndents.length - 1; i >= 0; i--) {
- if (levelIndents[i] <= indent) break;
- }
- // note: i cannot equal -1 because the first indentation level
- // is always 0, and the minimum indentation is also 0
- if (levelIndents[i] === indent) {
- // same indent! return to that level
- while (levelIndents.length - 1 > i) closeLevel();
- // (if this is already the current level, the above loop
- // will do nothing)
- } else if (levelIndents[i] < indent) {
- // lesser indent! branch based on index
- if (i === levelIndents.length - 1) {
- // top level is lesser: add a new level
- openLevel(indent);
- } else {
- // lower level is lesser: return to that level
- while (levelIndents.length - 1 > i) closeLevel();
- }
- }
- }
- // finally, set variables for appending content line
- indentThisLine = levelIndents.length;
- lineContent = listMatch[2];
- lineTag = 'li';
- } else {
- // not a list item! close any existing list levels
- while (levelIndents.length) closeLevel();
-
- // like i said, no nested shenanigans - quotes only appear outside
- // of lists. sorry!
- const quoteMatch = line.match(/^> *(.*)$/);
- if (quoteMatch) {
- // is a quote! open a blockquote tag if it doesnt already exist
- if (!inBlockquote) {
- inBlockquote = true;
- outLines.push('
');
- }
- indentThisLine = 1;
- lineContent = quoteMatch[1];
- } else if (inBlockquote) {
- // not a quote! close a blockquote tag if it exists
- inBlockquote = false;
- outLines.push(' ');
- }
+ text = transformInline(text.trim());
- // let some escaped symbols display as the normal symbol, since the
- // point of escaping them is just to avoid having them be treated as
- // syntax markers!
- if (lineContent.match(/( *)\\-/)) {
- lineContent = lineContent.replace('\\-', '-');
- } else if (lineContent.match(/( *)\\>/)) {
- lineContent = lineContent.replace('\\>', '>');
- }
- }
+ const outLines = [];
- if (lineTag === 'p') {
- // certain inline element tags should still be postioned within a
- // paragraph; other elements (e.g. headings) should be added as-is
- const elementMatch = line.match(/^<(.*?)[ >]/);
- if (elementMatch && !imageLine && !['a', 'abbr', 'b', 'bdo', 'br', 'cite', 'code', 'data', 'datalist', 'del', 'dfn', 'em', 'i', 'img', 'ins', 'kbd', 'mark', 'output', 'picture', 'q', 'ruby', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'svg', 'time', 'var', 'wbr'].includes(elementMatch[1])) {
- lineTag = '';
- }
- }
+ const indentString = " ".repeat(4);
- let pushString = indentString.repeat(indentThisLine);
- if (lineTag) {
- pushString += `<${lineTag}>${lineContent}${lineTag}>`;
- } else {
- pushString += lineContent;
- }
- outLines.push(pushString);
+ let levelIndents = [];
+ const openLevel = (indent) => {
+ // opening a sublist is a pain: to be semantically *and* visually
+ // correct, we have to append the
at the end of the existing
+ // previous
+ const previousLine = outLines[outLines.length - 1];
+ if (previousLine?.endsWith(" ")) {
+ // we will re-close the later
+ outLines[outLines.length - 1] = previousLine.slice(0, -5) + " ";
+ } else {
+ // if the previous line isn't a list item, this is the opening of
+ // the first list level, so no need for indent
+ outLines.push("");
}
+ levelIndents.push(indent);
+ };
+ const closeLevel = () => {
+ levelIndents.pop();
+ if (levelIndents.length) {
+ // closing a sublist, so close the list item containing it too
+ outLines.push(indentString.repeat(levelIndents.length) + " ");
+ } else {
+ // closing the final list level! no need for indent here
+ outLines.push(" ");
+ }
+ };
+
+ // okay yes we should support nested formatting, more than one blockquote
+ // layer, etc, but hear me out here: making all that work would basically
+ // be the same as implementing an entire markdown converter, which im not
+ // interested in doing lol. sorry!!!
+ let inBlockquote = false;
+
+ let lines = splitLines(text);
+ lines = joinLineBreaks(lines);
+ for (let line of lines) {
+ const imageLine = line.startsWith("
/g, (match, attributes) =>
+ img({
+ lazy: true,
+ link: true,
+ thumb: "medium",
+ ...parseAttributes(attributes),
+ })
+ );
- // after processing all lines...
-
- // if still in a list, close all levels
- while (levelIndents.length) closeLevel();
-
- // if still in a blockquote, close its tag
- if (inBlockquote) {
+ let indentThisLine = 0;
+ let lineContent = line;
+ let lineTag = "p";
+
+ const listMatch = line.match(/^( *)- *(.*)$/);
+ if (listMatch) {
+ // is a list item!
+ if (!levelIndents.length) {
+ // first level is always indent = 0, regardless of actual line
+ // content (this is to avoid going to a lesser indent than the
+ // initial level)
+ openLevel(0);
+ } else {
+ // find level corresponding to indent
+ const indent = listMatch[1].length;
+ let i;
+ for (i = levelIndents.length - 1; i >= 0; i--) {
+ if (levelIndents[i] <= indent) break;
+ }
+ // note: i cannot equal -1 because the first indentation level
+ // is always 0, and the minimum indentation is also 0
+ if (levelIndents[i] === indent) {
+ // same indent! return to that level
+ while (levelIndents.length - 1 > i) closeLevel();
+ // (if this is already the current level, the above loop
+ // will do nothing)
+ } else if (levelIndents[i] < indent) {
+ // lesser indent! branch based on index
+ if (i === levelIndents.length - 1) {
+ // top level is lesser: add a new level
+ openLevel(indent);
+ } else {
+ // lower level is lesser: return to that level
+ while (levelIndents.length - 1 > i) closeLevel();
+ }
+ }
+ }
+ // finally, set variables for appending content line
+ indentThisLine = levelIndents.length;
+ lineContent = listMatch[2];
+ lineTag = "li";
+ } else {
+ // not a list item! close any existing list levels
+ while (levelIndents.length) closeLevel();
+
+ // like i said, no nested shenanigans - quotes only appear outside
+ // of lists. sorry!
+ const quoteMatch = line.match(/^> *(.*)$/);
+ if (quoteMatch) {
+ // is a quote! open a blockquote tag if it doesnt already exist
+ if (!inBlockquote) {
+ inBlockquote = true;
+ outLines.push("
");
+ }
+ indentThisLine = 1;
+ lineContent = quoteMatch[1];
+ } else if (inBlockquote) {
+ // not a quote! close a blockquote tag if it exists
inBlockquote = false;
- outLines.push(' ');
+ outLines.push("");
+ }
+
+ // let some escaped symbols display as the normal symbol, since the
+ // point of escaping them is just to avoid having them be treated as
+ // syntax markers!
+ if (lineContent.match(/( *)\\-/)) {
+ lineContent = lineContent.replace("\\-", "-");
+ } else if (lineContent.match(/( *)\\>/)) {
+ lineContent = lineContent.replace("\\>", ">");
+ }
}
- return outLines.join('\n');
-}
+ if (lineTag === "p") {
+ // certain inline element tags should still be postioned within a
+ // paragraph; other elements (e.g. headings) should be added as-is
+ const elementMatch = line.match(/^<(.*?)[ >]/);
+ if (
+ elementMatch &&
+ !imageLine &&
+ ![
+ "a",
+ "abbr",
+ "b",
+ "bdo",
+ "br",
+ "cite",
+ "code",
+ "data",
+ "datalist",
+ "del",
+ "dfn",
+ "em",
+ "i",
+ "img",
+ "ins",
+ "kbd",
+ "mark",
+ "output",
+ "picture",
+ "q",
+ "ruby",
+ "samp",
+ "small",
+ "span",
+ "strong",
+ "sub",
+ "sup",
+ "svg",
+ "time",
+ "var",
+ "wbr",
+ ].includes(elementMatch[1])
+ ) {
+ lineTag = "";
+ }
+ }
-function transformLyrics(text, {
- transformInline,
- transformMultiline
-}) {
- // Different from transformMultiline 'cuz it joins multiple lines together
- // with line 8reaks (
); transformMultiline treats each line as its own
- // complete paragraph (or list, etc).
-
- // If it looks like old data, then like, oh god.
- // Use the normal transformMultiline tool.
- if (text.includes('
${lineContent}${lineTag}>`;
+ } else {
+ pushString += lineContent;
}
+ outLines.push(pushString);
+ }
- text = transformInline(text.trim());
+ // after processing all lines...
- let buildLine = '';
- const addLine = () => outLines.push(`
${buildLine}
`);
- const outLines = [];
- for (const line of text.split('\n')) {
- if (line.length) {
- if (buildLine.length) {
- buildLine += '
';
- }
- buildLine += line;
- } else if (buildLine.length) {
- addLine();
- buildLine = '';
- }
- }
- if (buildLine.length) {
- addLine();
+ // if still in a list, close all levels
+ while (levelIndents.length) closeLevel();
+
+ // if still in a blockquote, close its tag
+ if (inBlockquote) {
+ inBlockquote = false;
+ outLines.push("");
+ }
+
+ return outLines.join("\n");
+}
+
+function transformLyrics(text, { transformInline, transformMultiline }) {
+ // Different from transformMultiline 'cuz it joins multiple lines together
+ // with line 8reaks (
); transformMultiline treats each line as its own
+ // complete paragraph (or list, etc).
+
+ // If it looks like old data, then like, oh god.
+ // Use the normal transformMultiline tool.
+ if (text.includes("
outLines.push(`
${buildLine}
`);
+ const outLines = [];
+ for (const line of text.split("\n")) {
+ if (line.length) {
+ if (buildLine.length) {
+ buildLine += "
";
+ }
+ buildLine += line;
+ } else if (buildLine.length) {
+ addLine();
+ buildLine = "";
}
- return outLines.join('\n');
+ }
+ if (buildLine.length) {
+ addLine();
+ }
+ return outLines.join("\n");
}
function stringifyThings(thingData) {
- return JSON.stringify(serializeThings(thingData));
+ return JSON.stringify(serializeThings(thingData));
}
function img({
- src,
- alt,
- noSrcText = '',
- thumb: thumbKey,
- reveal,
- id,
+ src,
+ alt,
+ noSrcText = "",
+ thumb: thumbKey,
+ reveal,
+ id,
+ class: className,
+ width,
+ height,
+ link = false,
+ lazy = false,
+ square = false,
+}) {
+ const willSquare = square;
+ const willLink = typeof link === "string" || link;
+
+ const originalSrc = src;
+ const thumbSrc = src && (thumbKey ? thumb[thumbKey](src) : src);
+
+ const imgAttributes = html.attributes({
+ id: link ? "" : id,
class: className,
+ alt,
width,
height,
- link = false,
- lazy = false,
- square = false
-}) {
- const willSquare = square;
- const willLink = typeof link === 'string' || link;
-
- const originalSrc = src;
- const thumbSrc = src && (thumbKey ? thumb[thumbKey](src) : src);
-
- const imgAttributes = html.attributes({
- id: link ? '' : id,
- class: className,
- alt,
- width,
- height
- });
-
- const noSrcHTML = !src && wrap(`
${noSrcText}
`);
- const nonlazyHTML = src && wrap(`
`);
- const lazyHTML = src && lazy && wrap(`
`, true);
+ });
+
+ const noSrcHTML =
+ !src && wrap(`
${noSrcText}
`);
+ const nonlazyHTML = src && wrap(`
`);
+ const lazyHTML =
+ src &&
+ lazy &&
+ wrap(
+ `
`,
+ true
+ );
- if (!src) {
- return noSrcHTML;
- } else if (lazy) {
- return fixWS`
+ if (!src) {
+ return noSrcHTML;
+ } else if (lazy) {
+ return fixWS`
${nonlazyHTML}
${lazyHTML}
`;
- } else {
- return nonlazyHTML;
- }
+ } else {
+ return nonlazyHTML;
+ }
- function wrap(input, hide = false) {
- let wrapped = input;
+ function wrap(input, hide = false) {
+ let wrapped = input;
- wrapped = `
${wrapped}
`;
- wrapped = `
${wrapped}
`;
+ wrapped = `
${wrapped}
`;
+ wrapped = `
${wrapped}
`;
- if (reveal) {
- wrapped = fixWS`
+ if (reveal) {
+ wrapped = fixWS`
${wrapped}
${reveal}
`;
- }
-
- if (willSquare) {
- wrapped = html.tag('div', {class: 'square-content'}, wrapped);
- wrapped = html.tag('div', {class: ['square', hide && !willLink && 'js-hide']}, wrapped);
- }
+ }
- if (willLink) {
- wrapped = html.tag('a', {
- id,
- class: ['box', hide && 'js-hide'],
- href: typeof link === 'string' ? link : originalSrc
- }, wrapped);
- }
+ if (willSquare) {
+ wrapped = html.tag("div", { class: "square-content" }, wrapped);
+ wrapped = html.tag(
+ "div",
+ { class: ["square", hide && !willLink && "js-hide"] },
+ wrapped
+ );
+ }
- return wrapped;
+ if (willLink) {
+ wrapped = html.tag(
+ "a",
+ {
+ id,
+ class: ["box", hide && "js-hide"],
+ href: typeof link === "string" ? link : originalSrc,
+ },
+ wrapped
+ );
}
+
+ return wrapped;
+ }
}
function validateWritePath(path, urlGroup) {
- if (!Array.isArray(path)) {
- return {error: `Expected array, got ${path}`};
- }
+ if (!Array.isArray(path)) {
+ return { error: `Expected array, got ${path}` };
+ }
- const { paths } = urlGroup;
+ const { paths } = urlGroup;
- const definedKeys = Object.keys(paths);
- const specifiedKey = path[0];
+ const definedKeys = Object.keys(paths);
+ const specifiedKey = path[0];
- if (!definedKeys.includes(specifiedKey)) {
- return {error: `Specified key ${specifiedKey} isn't defined`};
- }
+ if (!definedKeys.includes(specifiedKey)) {
+ return { error: `Specified key ${specifiedKey} isn't defined` };
+ }
- const expectedArgs = paths[specifiedKey].match(/<>/g)?.length ?? 0;
- const specifiedArgs = path.length - 1;
+ const expectedArgs = paths[specifiedKey].match(/<>/g)?.length ?? 0;
+ const specifiedArgs = path.length - 1;
- if (specifiedArgs !== expectedArgs) {
- return {error: `Expected ${expectedArgs} arguments, got ${specifiedArgs}`};
- }
+ if (specifiedArgs !== expectedArgs) {
+ return {
+ error: `Expected ${expectedArgs} arguments, got ${specifiedArgs}`,
+ };
+ }
- return {success: true};
+ return { success: true };
}
function validateWriteObject(obj) {
- if (typeof obj !== 'object') {
- return {error: `Expected object, got ${typeof obj}`};
- }
+ if (typeof obj !== "object") {
+ return { error: `Expected object, got ${typeof obj}` };
+ }
- if (typeof obj.type !== 'string') {
- return {error: `Expected type to be string, got ${obj.type}`};
- }
+ if (typeof obj.type !== "string") {
+ return { error: `Expected type to be string, got ${obj.type}` };
+ }
- switch (obj.type) {
- case 'legacy': {
- if (typeof obj.write !== 'function') {
- return {error: `Expected write to be string, got ${obj.write}`};
- }
+ switch (obj.type) {
+ case "legacy": {
+ if (typeof obj.write !== "function") {
+ return { error: `Expected write to be string, got ${obj.write}` };
+ }
- break;
- }
+ break;
+ }
- case 'page': {
- const path = validateWritePath(obj.path, urlSpec.localized);
- if (path.error) {
- return {error: `Path validation failed: ${path.error}`};
- }
+ case "page": {
+ const path = validateWritePath(obj.path, urlSpec.localized);
+ if (path.error) {
+ return { error: `Path validation failed: ${path.error}` };
+ }
- if (typeof obj.page !== 'function') {
- return {error: `Expected page to be function, got ${obj.content}`};
- }
+ if (typeof obj.page !== "function") {
+ return { error: `Expected page to be function, got ${obj.content}` };
+ }
- break;
- }
+ break;
+ }
- case 'data': {
- const path = validateWritePath(obj.path, urlSpec.data);
- if (path.error) {
- return {error: `Path validation failed: ${path.error}`};
- }
+ case "data": {
+ const path = validateWritePath(obj.path, urlSpec.data);
+ if (path.error) {
+ return { error: `Path validation failed: ${path.error}` };
+ }
- if (typeof obj.data !== 'function') {
- return {error: `Expected data to be function, got ${obj.data}`};
- }
+ if (typeof obj.data !== "function") {
+ return { error: `Expected data to be function, got ${obj.data}` };
+ }
- break;
- }
+ break;
+ }
- case 'redirect': {
- const fromPath = validateWritePath(obj.fromPath, urlSpec.localized);
- if (fromPath.error) {
- return {error: `Path (fromPath) validation failed: ${fromPath.error}`};
- }
+ case "redirect": {
+ const fromPath = validateWritePath(obj.fromPath, urlSpec.localized);
+ if (fromPath.error) {
+ return {
+ error: `Path (fromPath) validation failed: ${fromPath.error}`,
+ };
+ }
- const toPath = validateWritePath(obj.toPath, urlSpec.localized);
- if (toPath.error) {
- return {error: `Path (toPath) validation failed: ${toPath.error}`};
- }
+ const toPath = validateWritePath(obj.toPath, urlSpec.localized);
+ if (toPath.error) {
+ return { error: `Path (toPath) validation failed: ${toPath.error}` };
+ }
- if (typeof obj.title !== 'function') {
- return {error: `Expected title to be function, got ${obj.title}`};
- }
+ if (typeof obj.title !== "function") {
+ return { error: `Expected title to be function, got ${obj.title}` };
+ }
- break;
- }
+ break;
+ }
- default: {
- return {error: `Unknown type: ${obj.type}`};
- }
+ default: {
+ return { error: `Unknown type: ${obj.type}` };
}
+ }
- return {success: true};
+ return { success: true };
}
/*
@@ -787,12 +833,10 @@ async function writeData(subKey, directory, data) {
// touching the original one (which had contained everything).
const writePage = {};
-writePage.to = ({
- baseDirectory,
- pageSubKey,
- paths
-}) => (targetFullKey, ...args) => {
- const [ groupKey, subKey ] = targetFullKey.split('.');
+writePage.to =
+ ({ baseDirectory, pageSubKey, paths }) =>
+ (targetFullKey, ...args) => {
+ const [groupKey, subKey] = targetFullKey.split(".");
let path = paths.subdirectoryPrefix;
let from;
@@ -800,33 +844,39 @@ writePage.to = ({
// When linking to *outside* the localized area of the site, we need to
// make sure the result is correctly relative to the 8ase directory.
- if (groupKey !== 'localized' && groupKey !== 'localizedDefaultLanguage' && baseDirectory) {
- from = 'localizedWithBaseDirectory.' + pageSubKey;
- to = targetFullKey;
- } else if (groupKey === 'localizedDefaultLanguage' && baseDirectory) {
- // Special case for specifically linking *from* a page with base
- // directory *to* a page without! Used for the language switcher and
- // hopefully nothing else oh god.
- from = 'localizedWithBaseDirectory.' + pageSubKey;
- to = 'localized.' + subKey;
- } else if (groupKey === 'localizedDefaultLanguage') {
- // Linking to the default, except surprise, we're already IN the default
- // (no baseDirectory set).
- from = 'localized.' + pageSubKey;
- to = 'localized.' + subKey;
+ if (
+ groupKey !== "localized" &&
+ groupKey !== "localizedDefaultLanguage" &&
+ baseDirectory
+ ) {
+ from = "localizedWithBaseDirectory." + pageSubKey;
+ to = targetFullKey;
+ } else if (groupKey === "localizedDefaultLanguage" && baseDirectory) {
+ // Special case for specifically linking *from* a page with base
+ // directory *to* a page without! Used for the language switcher and
+ // hopefully nothing else oh god.
+ from = "localizedWithBaseDirectory." + pageSubKey;
+ to = "localized." + subKey;
+ } else if (groupKey === "localizedDefaultLanguage") {
+ // Linking to the default, except surprise, we're already IN the default
+ // (no baseDirectory set).
+ from = "localized." + pageSubKey;
+ to = "localized." + subKey;
} else {
- // If we're linking inside the localized area (or there just is no
- // 8ase directory), the 8ase directory doesn't matter.
- from = 'localized.' + pageSubKey;
- to = targetFullKey;
+ // If we're linking inside the localized area (or there just is no
+ // 8ase directory), the 8ase directory doesn't matter.
+ from = "localized." + pageSubKey;
+ to = targetFullKey;
}
path += urls.from(from).to(to, ...args);
return path;
-};
+ };
-writePage.html = (pageInfo, {
+writePage.html = (
+ pageInfo,
+ {
defaultLanguage,
language,
languages,
@@ -835,486 +885,653 @@ writePage.html = (pageInfo, {
oEmbedJSONHref,
to,
transformMultiline,
- wikiData
-}) => {
- const { wikiInfo } = wikiData;
-
- let {
- title = '',
- meta = {},
- theme = '',
- stylesheet = '',
-
- showWikiNameInTitle = true,
-
- // missing properties are auto-filled, see below!
- body = {},
- banner = {},
- main = {},
- sidebarLeft = {},
- sidebarRight = {},
- nav = {},
- secondaryNav = {},
- footer = {},
- socialEmbed = {},
- } = pageInfo;
-
- body.style ??= '';
-
- theme = theme || getThemeString(wikiInfo.color);
-
- banner ||= {};
- banner.classes ??= [];
- banner.src ??= '';
- banner.position ??= '';
- banner.dimensions ??= [0, 0];
-
- main.classes ??= [];
- main.content ??= '';
-
- sidebarLeft ??= {};
- sidebarRight ??= {};
-
- for (const sidebar of [sidebarLeft, sidebarRight]) {
- sidebar.classes ??= [];
- sidebar.content ??= '';
- sidebar.collapse ??= true;
- }
-
- nav.classes ??= [];
- nav.content ??= '';
- nav.bottomRowContent ??= '';
- nav.links ??= [];
- nav.linkContainerClasses ??= [];
-
- secondaryNav ??= {};
- secondaryNav.content ??= '';
- secondaryNav.content ??= '';
-
- footer.classes ??= [];
- footer.content ??= (wikiInfo.footerContent ? transformMultiline(wikiInfo.footerContent) : '');
-
- footer.content += '\n' + getFooterLocalizationLinks(paths.pathname, {
- defaultLanguage, languages, paths, language, to
+ wikiData,
+ }
+) => {
+ const { wikiInfo } = wikiData;
+
+ let {
+ title = "",
+ meta = {},
+ theme = "",
+ stylesheet = "",
+
+ showWikiNameInTitle = true,
+
+ // missing properties are auto-filled, see below!
+ body = {},
+ banner = {},
+ main = {},
+ sidebarLeft = {},
+ sidebarRight = {},
+ nav = {},
+ secondaryNav = {},
+ footer = {},
+ socialEmbed = {},
+ } = pageInfo;
+
+ body.style ??= "";
+
+ theme = theme || getThemeString(wikiInfo.color);
+
+ banner ||= {};
+ banner.classes ??= [];
+ banner.src ??= "";
+ banner.position ??= "";
+ banner.dimensions ??= [0, 0];
+
+ main.classes ??= [];
+ main.content ??= "";
+
+ sidebarLeft ??= {};
+ sidebarRight ??= {};
+
+ for (const sidebar of [sidebarLeft, sidebarRight]) {
+ sidebar.classes ??= [];
+ sidebar.content ??= "";
+ sidebar.collapse ??= true;
+ }
+
+ nav.classes ??= [];
+ nav.content ??= "";
+ nav.bottomRowContent ??= "";
+ nav.links ??= [];
+ nav.linkContainerClasses ??= [];
+
+ secondaryNav ??= {};
+ secondaryNav.content ??= "";
+ secondaryNav.content ??= "";
+
+ footer.classes ??= [];
+ footer.content ??= wikiInfo.footerContent
+ ? transformMultiline(wikiInfo.footerContent)
+ : "";
+
+ footer.content +=
+ "\n" +
+ getFooterLocalizationLinks(paths.pathname, {
+ defaultLanguage,
+ languages,
+ paths,
+ language,
+ to,
});
- const canonical = (wikiInfo.canonicalBase
- ? wikiInfo.canonicalBase + (paths.pathname === '/' ? '' : paths.pathname)
- : '');
-
- const localizedCanonical = (wikiInfo.canonicalBase
- ? Object.entries(localizedPaths).map(([ code, { pathname } ]) => ({
- lang: code,
- href: wikiInfo.canonicalBase + (pathname === '/' ? '' : pathname)
- }))
- : []);
-
- const collapseSidebars = (sidebarLeft.collapse !== false) && (sidebarRight.collapse !== false);
-
- const mainHTML = main.content && html.tag('main', {
- id: 'content',
- class: main.classes
- }, main.content);
-
- const footerHTML = footer.content && html.tag('footer', {
- id: 'footer',
- class: footer.classes
- }, footer.content);
-
- const generateSidebarHTML = (id, {
- content,
- multiple,
- classes,
- collapse = true,
- wide = false
- }) => (content
- ? html.tag('div',
- {id, class: [
- 'sidebar-column',
- 'sidebar',
- wide && 'wide',
- !collapse && 'no-hide',
- ...classes
- ]},
- content)
- : multiple ? html.tag('div',
- {id, class: [
- 'sidebar-column',
- 'sidebar-multiple',
- wide && 'wide',
- !collapse && 'no-hide'
- ]},
- multiple.map(content => html.tag('div',
- {class: ['sidebar', ...classes]},
- content)))
- : '');
-
- const sidebarLeftHTML = generateSidebarHTML('sidebar-left', sidebarLeft);
- const sidebarRightHTML = generateSidebarHTML('sidebar-right', sidebarRight);
-
- if (nav.simple) {
- nav.linkContainerClasses = ['nav-links-hierarchy'];
- nav.links = [
- {toHome: true},
- {toCurrentPage: true}
- ];
- }
-
- const links = (nav.links || []).filter(Boolean);
-
- const navLinkParts = [];
- for (let i = 0; i < links.length; i++) {
- let cur = links[i];
- const prev = links[i - 1];
- const next = links[i + 1];
+ const canonical = wikiInfo.canonicalBase
+ ? wikiInfo.canonicalBase + (paths.pathname === "/" ? "" : paths.pathname)
+ : "";
+
+ const localizedCanonical = wikiInfo.canonicalBase
+ ? Object.entries(localizedPaths).map(([code, { pathname }]) => ({
+ lang: code,
+ href: wikiInfo.canonicalBase + (pathname === "/" ? "" : pathname),
+ }))
+ : [];
+
+ const collapseSidebars =
+ sidebarLeft.collapse !== false && sidebarRight.collapse !== false;
+
+ const mainHTML =
+ main.content &&
+ html.tag(
+ "main",
+ {
+ id: "content",
+ class: main.classes,
+ },
+ main.content
+ );
- let { title: linkTitle } = cur;
+ const footerHTML =
+ footer.content &&
+ html.tag(
+ "footer",
+ {
+ id: "footer",
+ class: footer.classes,
+ },
+ footer.content
+ );
- if (cur.toHome) {
- linkTitle ??= wikiInfo.nameShort;
- } else if (cur.toCurrentPage) {
- linkTitle ??= title;
- }
+ const generateSidebarHTML = (
+ id,
+ { content, multiple, classes, collapse = true, wide = false }
+ ) =>
+ content
+ ? html.tag(
+ "div",
+ {
+ id,
+ class: [
+ "sidebar-column",
+ "sidebar",
+ wide && "wide",
+ !collapse && "no-hide",
+ ...classes,
+ ],
+ },
+ content
+ )
+ : multiple
+ ? html.tag(
+ "div",
+ {
+ id,
+ class: [
+ "sidebar-column",
+ "sidebar-multiple",
+ wide && "wide",
+ !collapse && "no-hide",
+ ],
+ },
+ multiple.map((content) =>
+ html.tag("div", { class: ["sidebar", ...classes] }, content)
+ )
+ )
+ : "";
+
+ const sidebarLeftHTML = generateSidebarHTML("sidebar-left", sidebarLeft);
+ const sidebarRightHTML = generateSidebarHTML("sidebar-right", sidebarRight);
+
+ if (nav.simple) {
+ nav.linkContainerClasses = ["nav-links-hierarchy"];
+ nav.links = [{ toHome: true }, { toCurrentPage: true }];
+ }
+
+ const links = (nav.links || []).filter(Boolean);
+
+ const navLinkParts = [];
+ for (let i = 0; i < links.length; i++) {
+ let cur = links[i];
+ const prev = links[i - 1];
+ const next = links[i + 1];
+
+ let { title: linkTitle } = cur;
+
+ if (cur.toHome) {
+ linkTitle ??= wikiInfo.nameShort;
+ } else if (cur.toCurrentPage) {
+ linkTitle ??= title;
+ }
- let partContent;
+ let partContent;
- if (typeof cur.html === 'string') {
- if (!cur.html) {
- logWarn`Empty HTML in nav link ${JSON.stringify(cur)}`;
- console.trace();
- }
- partContent = cur.html;
- } else {
- const attributes = {
- class: (cur.toCurrentPage || i === links.length - 1) && 'current',
- href: (
- cur.toCurrentPage ? '' :
- cur.toHome ? to('localized.home') :
- cur.path ? to(...cur.path) :
- cur.href ? (() => {
- logWarn`Using legacy href format nav link in ${paths.pathname}`;
- return cur.href;
- })() :
- null)
- };
- if (attributes.href === null) {
- throw new Error(`Expected some href specifier for link to ${linkTitle} (${JSON.stringify(cur)})`);
- }
- partContent = html.tag('a', attributes, linkTitle);
- }
+ if (typeof cur.html === "string") {
+ if (!cur.html) {
+ logWarn`Empty HTML in nav link ${JSON.stringify(cur)}`;
+ console.trace();
+ }
+ partContent = cur.html;
+ } else {
+ const attributes = {
+ class: (cur.toCurrentPage || i === links.length - 1) && "current",
+ href: cur.toCurrentPage
+ ? ""
+ : cur.toHome
+ ? to("localized.home")
+ : cur.path
+ ? to(...cur.path)
+ : cur.href
+ ? (() => {
+ logWarn`Using legacy href format nav link in ${paths.pathname}`;
+ return cur.href;
+ })()
+ : null,
+ };
+ if (attributes.href === null) {
+ throw new Error(
+ `Expected some href specifier for link to ${linkTitle} (${JSON.stringify(
+ cur
+ )})`
+ );
+ }
+ partContent = html.tag("a", attributes, linkTitle);
+ }
- const part = html.tag('span',
- {class: cur.divider === false && 'no-divider'},
- partContent);
+ const part = html.tag(
+ "span",
+ { class: cur.divider === false && "no-divider" },
+ partContent
+ );
- navLinkParts.push(part);
- }
+ navLinkParts.push(part);
+ }
- const navHTML = html.tag('nav', {
- [html.onlyIfContent]: true,
- id: 'header',
- class: [
- ...nav.classes,
- links.length && 'nav-has-main-links',
- nav.content && 'nav-has-content',
- nav.bottomRowContent && 'nav-has-bottom-row',
- ],
- }, [
- links.length && html.tag('div',
- {class: ['nav-main-links', ...nav.linkContainerClasses]},
- navLinkParts),
- nav.content && html.tag('div', {class: 'nav-content'}, nav.content),
- nav.bottomRowContent && html.tag('div', {class: 'nav-bottom-row'}, nav.bottomRowContent),
- ]);
-
- const secondaryNavHTML = html.tag('nav', {
- [html.onlyIfContent]: true,
- id: 'secondary-nav',
- class: secondaryNav.classes
- }, [
- secondaryNav.content
- ]);
-
- const bannerSrc = (
- banner.src ? banner.src :
- banner.path ? to(...banner.path) :
- null);
-
- const bannerHTML = banner.position && bannerSrc && html.tag('div',
- {
- id: 'banner',
- class: banner.classes
- },
- html.tag('img', {
- src: bannerSrc,
- alt: banner.alt,
- width: banner.dimensions[0] || 1100,
- height: banner.dimensions[1] || 200
- })
+ const navHTML = html.tag(
+ "nav",
+ {
+ [html.onlyIfContent]: true,
+ id: "header",
+ class: [
+ ...nav.classes,
+ links.length && "nav-has-main-links",
+ nav.content && "nav-has-content",
+ nav.bottomRowContent && "nav-has-bottom-row",
+ ],
+ },
+ [
+ links.length &&
+ html.tag(
+ "div",
+ { class: ["nav-main-links", ...nav.linkContainerClasses] },
+ navLinkParts
+ ),
+ nav.content && html.tag("div", { class: "nav-content" }, nav.content),
+ nav.bottomRowContent &&
+ html.tag("div", { class: "nav-bottom-row" }, nav.bottomRowContent),
+ ]
+ );
+
+ const secondaryNavHTML = html.tag(
+ "nav",
+ {
+ [html.onlyIfContent]: true,
+ id: "secondary-nav",
+ class: secondaryNav.classes,
+ },
+ [secondaryNav.content]
+ );
+
+ const bannerSrc = banner.src
+ ? banner.src
+ : banner.path
+ ? to(...banner.path)
+ : null;
+
+ const bannerHTML =
+ banner.position &&
+ bannerSrc &&
+ html.tag(
+ "div",
+ {
+ id: "banner",
+ class: banner.classes,
+ },
+ html.tag("img", {
+ src: bannerSrc,
+ alt: banner.alt,
+ width: banner.dimensions[0] || 1100,
+ height: banner.dimensions[1] || 200,
+ })
);
- const layoutHTML = [
- navHTML,
- banner.position === 'top' && bannerHTML,
- secondaryNavHTML,
- html.tag('div',
- {class: ['layout-columns', !collapseSidebars && 'vertical-when-thin']},
- [
- sidebarLeftHTML,
- mainHTML,
- sidebarRightHTML
- ]),
- banner.position === 'bottom' && bannerHTML,
- footerHTML
- ].filter(Boolean).join('\n');
-
- const infoCardHTML = fixWS`
+ const layoutHTML = [
+ navHTML,
+ banner.position === "top" && bannerHTML,
+ secondaryNavHTML,
+ html.tag(
+ "div",
+ { class: ["layout-columns", !collapseSidebars && "vertical-when-thin"] },
+ [sidebarLeftHTML, mainHTML, sidebarRightHTML]
+ ),
+ banner.position === "bottom" && bannerHTML,
+ footerHTML,
+ ]
+ .filter(Boolean)
+ .join("\n");
+
+ const infoCardHTML = fixWS`
${img({
- class: 'info-card-art',
- src: '',
- link: true,
- square: true
+ class: "info-card-art",
+ src: "",
+ link: true,
+ square: true,
})}
${img({
- class: 'info-card-art',
- src: '',
- link: true,
- square: true,
- reveal: getRevealStringFromWarnings(' ', {language})
+ class: "info-card-art",
+ src: "",
+ link: true,
+ square: true,
+ reveal: getRevealStringFromWarnings(
+ ' ',
+ { language }
+ ),
})}
-
${language.$('releaseInfo.from', {album: ' '})}
-
${language.$('releaseInfo.by', {artists: ' '})}
-
${language.$('releaseInfo.coverArtBy', {artists: ' '})}
+
${language.$(
+ "releaseInfo.from",
+ { album: " " }
+ )}
+
${language.$(
+ "releaseInfo.by",
+ { artists: " " }
+ )}
+
${language.$(
+ "releaseInfo.coverArtBy",
+ { artists: " " }
+ )}
`;
- const socialEmbedHTML = [
- socialEmbed.title && html.tag('meta', {property: 'og:title', content: socialEmbed.title}),
- socialEmbed.description && html.tag('meta', {property: 'og:description', content: socialEmbed.description}),
- socialEmbed.image && html.tag('meta', {property: 'og:image', content: socialEmbed.image}),
- socialEmbed.color && html.tag('meta', {name: 'theme-color', content: socialEmbed.color}),
- oEmbedJSONHref && html.tag('link', {type: 'application/json+oembed', href: oEmbedJSONHref}),
- ].filter(Boolean).join('\n');
-
- return filterEmptyLines(fixWS`
+ const socialEmbedHTML = [
+ socialEmbed.title &&
+ html.tag("meta", { property: "og:title", content: socialEmbed.title }),
+ socialEmbed.description &&
+ html.tag("meta", {
+ property: "og:description",
+ content: socialEmbed.description,
+ }),
+ socialEmbed.image &&
+ html.tag("meta", { property: "og:image", content: socialEmbed.image }),
+ socialEmbed.color &&
+ html.tag("meta", { name: "theme-color", content: socialEmbed.color }),
+ oEmbedJSONHref &&
+ html.tag("link", {
+ type: "application/json+oembed",
+ href: oEmbedJSONHref,
+ }),
+ ]
+ .filter(Boolean)
+ .join("\n");
+
+ return filterEmptyLines(fixWS`
[['data-url-value' + i], v])),
- 'data-rebase-localized': to('localized.root'),
- 'data-rebase-shared': to('shared.root'),
- 'data-rebase-media': to('media.root'),
- 'data-rebase-data': to('data.root')
+ lang: language.intlCode,
+ "data-language-code": language.code,
+ "data-url-key": paths.toPath[0],
+ ...Object.fromEntries(
+ paths.toPath.slice(1).map((v, i) => [["data-url-value" + i], v])
+ ),
+ "data-rebase-localized": to("localized.root"),
+ "data-rebase-shared": to("shared.root"),
+ "data-rebase-media": to("media.root"),
+ "data-rebase-data": to("data.root"),
})}>
-
${(showWikiNameInTitle
- ? language.formatString('misc.pageTitle.withWikiName', {
+ ${
+ showWikiNameInTitle
+ ? language.formatString("misc.pageTitle.withWikiName", {
title,
- wikiName: wikiInfo.nameShort
- })
- : language.formatString('misc.pageTitle', {title}))}
+ wikiName: wikiInfo.nameShort,
+ })
+ : language.formatString("misc.pageTitle", { title })
+ }
- ${Object.entries(meta).filter(([ key, value ]) => value).map(([ key, value ]) => `
`).join('\n')}
+ ${Object.entries(meta)
+ .filter(([key, value]) => value)
+ .map(
+ ([key, value]) =>
+ `
`
+ )
+ .join("\n")}
${canonical && `
`}
- ${localizedCanonical.map(({ lang, href }) => `
`).join('\n')}
+ ${localizedCanonical
+ .map(
+ ({ lang, href }) =>
+ `
`
+ )
+ .join("\n")}
${socialEmbedHTML}
-
- ${(theme || stylesheet) && fixWS`
+
+ ${
+ (theme || stylesheet) &&
+ fixWS`
- `}
-
+ `
+ }
+
-
+
- ${mainHTML && fixWS`
+ ${
+ mainHTML &&
+ fixWS`
${[
- ['#content', language.$('misc.skippers.skipToContent')],
- sidebarLeftHTML && ['#sidebar-left', (sidebarRightHTML
- ? language.$('misc.skippers.skipToSidebar.left')
- : language.$('misc.skippers.skipToSidebar'))],
- sidebarRightHTML && ['#sidebar-right', (sidebarLeftHTML
- ? language.$('misc.skippers.skipToSidebar.right')
- : language.$('misc.skippers.skipToSidebar'))],
- footerHTML && ['#footer', language.$('misc.skippers.skipToFooter')]
- ].filter(Boolean).map(([ href, title ]) => fixWS`
+ [
+ "#content",
+ language.$("misc.skippers.skipToContent"),
+ ],
+ sidebarLeftHTML && [
+ "#sidebar-left",
+ sidebarRightHTML
+ ? language.$(
+ "misc.skippers.skipToSidebar.left"
+ )
+ : language.$("misc.skippers.skipToSidebar"),
+ ],
+ sidebarRightHTML && [
+ "#sidebar-right",
+ sidebarLeftHTML
+ ? language.$(
+ "misc.skippers.skipToSidebar.right"
+ )
+ : language.$("misc.skippers.skipToSidebar"),
+ ],
+ footerHTML && [
+ "#footer",
+ language.$("misc.skippers.skipToFooter"),
+ ],
+ ]
+ .filter(Boolean)
+ .map(
+ ([href, title]) => fixWS`
${title}
- `).join('\n')}
+ `
+ )
+ .join("\n")}
- `}
+ `
+ }
${layoutHTML}
${infoCardHTML}
-
+
`);
};
-writePage.oEmbedJSON = (pageInfo, {
- language,
- wikiData,
-}) => {
- const { socialEmbed } = pageInfo;
- const { wikiInfo } = wikiData;
- const { canonicalBase, nameShort } = wikiInfo;
-
- if (!socialEmbed) return '';
-
- const entries = [
- socialEmbed.heading && ['author_name',
- language.$('misc.socialEmbed.heading', {
- wikiName: nameShort,
- heading: socialEmbed.heading
- })],
- socialEmbed.headingLink && canonicalBase && ['author_url',
- canonicalBase.replace(/\/$/, '') + '/' +
- socialEmbed.headingLink.replace(/^\//, '')],
- ].filter(Boolean);
-
- if (!entries.length) return '';
-
- return JSON.stringify(Object.fromEntries(entries));
+writePage.oEmbedJSON = (pageInfo, { language, wikiData }) => {
+ const { socialEmbed } = pageInfo;
+ const { wikiInfo } = wikiData;
+ const { canonicalBase, nameShort } = wikiInfo;
+
+ if (!socialEmbed) return "";
+
+ const entries = [
+ socialEmbed.heading && [
+ "author_name",
+ language.$("misc.socialEmbed.heading", {
+ wikiName: nameShort,
+ heading: socialEmbed.heading,
+ }),
+ ],
+ socialEmbed.headingLink &&
+ canonicalBase && [
+ "author_url",
+ canonicalBase.replace(/\/$/, "") +
+ "/" +
+ socialEmbed.headingLink.replace(/^\//, ""),
+ ],
+ ].filter(Boolean);
+
+ if (!entries.length) return "";
+
+ return JSON.stringify(Object.fromEntries(entries));
};
-writePage.write = async ({
- html,
- oEmbedJSON = '',
- paths,
-}) => {
- await mkdir(paths.outputDirectory, {recursive: true});
- await Promise.all([
- writeFile(paths.outputFile, html),
- oEmbedJSON && writeFile(paths.oEmbedJSONFile, oEmbedJSON)
- ].filter(Boolean));
+writePage.write = async ({ html, oEmbedJSON = "", paths }) => {
+ await mkdir(paths.outputDirectory, { recursive: true });
+ await Promise.all(
+ [
+ writeFile(paths.outputFile, html),
+ oEmbedJSON && writeFile(paths.oEmbedJSONFile, oEmbedJSON),
+ ].filter(Boolean)
+ );
};
// TODO: This only supports one <>-style argument.
-writePage.paths = (baseDirectory, fullKey, directory = '', {
- file = 'index.html'
-} = {}) => {
- const [ groupKey, subKey ] = fullKey.split('.');
-
- const pathname = (groupKey === 'localized' && baseDirectory
- ? urls.from('shared.root').toDevice('localizedWithBaseDirectory.' + subKey, baseDirectory, directory)
- : urls.from('shared.root').toDevice(fullKey, directory));
-
- // Needed for the rare directory which itself contains a slash, e.g. for
- // listings, with directories like 'albums/by-name'.
- const subdirectoryPrefix = '../'.repeat(directory.split('/').length - 1);
-
- const outputDirectory = path.join(outputPath, pathname);
- const outputFile = path.join(outputDirectory, file);
- const oEmbedJSONFile = path.join(outputDirectory, OEMBED_JSON_FILE);
-
- return {
- toPath: [fullKey, directory],
- pathname,
- subdirectoryPrefix,
- outputDirectory, outputFile,
- oEmbedJSONFile,
- };
+writePage.paths = (
+ baseDirectory,
+ fullKey,
+ directory = "",
+ { file = "index.html" } = {}
+) => {
+ const [groupKey, subKey] = fullKey.split(".");
+
+ const pathname =
+ groupKey === "localized" && baseDirectory
+ ? urls
+ .from("shared.root")
+ .toDevice(
+ "localizedWithBaseDirectory." + subKey,
+ baseDirectory,
+ directory
+ )
+ : urls.from("shared.root").toDevice(fullKey, directory);
+
+ // Needed for the rare directory which itself contains a slash, e.g. for
+ // listings, with directories like 'albums/by-name'.
+ const subdirectoryPrefix = "../".repeat(directory.split("/").length - 1);
+
+ const outputDirectory = path.join(outputPath, pathname);
+ const outputFile = path.join(outputDirectory, file);
+ const oEmbedJSONFile = path.join(outputDirectory, OEMBED_JSON_FILE);
+
+ return {
+ toPath: [fullKey, directory],
+ pathname,
+ subdirectoryPrefix,
+ outputDirectory,
+ outputFile,
+ oEmbedJSONFile,
+ };
};
async function writeFavicon() {
+ try {
+ await stat(path.join(mediaPath, FAVICON_FILE));
+ } catch (error) {
+ return;
+ }
+
+ try {
+ await copyFile(
+ path.join(mediaPath, FAVICON_FILE),
+ path.join(outputPath, FAVICON_FILE)
+ );
+ } catch (error) {
+ logWarn`Failed to copy favicon! ${error.message}`;
+ return;
+ }
+
+ logInfo`Copied favicon to site root.`;
+}
+
+function writeSymlinks() {
+ return progressPromiseAll("Writing site symlinks.", [
+ link(path.join(__dirname, UTILITY_DIRECTORY), "shared.utilityRoot"),
+ link(path.join(__dirname, STATIC_DIRECTORY), "shared.staticRoot"),
+ link(mediaPath, "media.root"),
+ ]);
+
+ async function link(directory, urlKey) {
+ const pathname = urls.from("shared.root").toDevice(urlKey);
+ const file = path.join(outputPath, pathname);
try {
- await stat(path.join(mediaPath, FAVICON_FILE));
+ await unlink(file);
} catch (error) {
- return;
+ if (error.code !== "ENOENT") {
+ throw error;
+ }
}
-
try {
- await copyFile(
- path.join(mediaPath, FAVICON_FILE),
- path.join(outputPath, FAVICON_FILE)
- );
+ await symlink(path.resolve(directory), file);
} catch (error) {
- logWarn`Failed to copy favicon! ${error.message}`;
- return;
+ if (error.code === "EPERM") {
+ await symlink(path.resolve(directory), file, "junction");
+ }
}
-
- logInfo`Copied favicon to site root.`;
+ }
}
-function writeSymlinks() {
- return progressPromiseAll('Writing site symlinks.', [
- link(path.join(__dirname, UTILITY_DIRECTORY), 'shared.utilityRoot'),
- link(path.join(__dirname, STATIC_DIRECTORY), 'shared.staticRoot'),
- link(mediaPath, 'media.root')
- ]);
-
- async function link(directory, urlKey) {
- const pathname = urls.from('shared.root').toDevice(urlKey);
- const file = path.join(outputPath, pathname);
- try {
- await unlink(file);
- } catch (error) {
- if (error.code !== 'ENOENT') {
- throw error;
- }
- }
- try {
- await symlink(path.resolve(directory), file);
- } catch (error) {
- if (error.code === 'EPERM') {
- await symlink(path.resolve(directory), file, 'junction');
- }
- }
- }
-}
-
-function writeSharedFilesAndPages({language, wikiData}) {
- const { groupData, wikiInfo } = wikiData;
-
- const redirect = async (title, from, urlKey, directory) => {
- const target = path.relative(from, urls.from('shared.root').to(urlKey, directory));
- const content = generateRedirectPage(title, target, {language});
- await mkdir(path.join(outputPath, from), {recursive: true});
- await writeFile(path.join(outputPath, from, 'index.html'), content);
- };
-
- return progressPromiseAll(`Writing files & pages shared across languages.`, [
- groupData?.some(group => group.directory === 'fandom') &&
- redirect('Fandom - Gallery', 'albums/fandom', 'localized.groupGallery', 'fandom'),
-
- groupData?.some(group => group.directory === 'official') &&
- redirect('Official - Gallery', 'albums/official', 'localized.groupGallery', 'official'),
+function writeSharedFilesAndPages({ language, wikiData }) {
+ const { groupData, wikiInfo } = wikiData;
- wikiInfo.enableListings &&
- redirect('Album Commentary', 'list/all-commentary', 'localized.commentaryIndex', ''),
-
- writeFile(path.join(outputPath, 'data.json'), fixWS`
+ const redirect = async (title, from, urlKey, directory) => {
+ const target = path.relative(
+ from,
+ urls.from("shared.root").to(urlKey, directory)
+ );
+ const content = generateRedirectPage(title, target, { language });
+ await mkdir(path.join(outputPath, from), { recursive: true });
+ await writeFile(path.join(outputPath, from, "index.html"), content);
+ };
+
+ return progressPromiseAll(
+ `Writing files & pages shared across languages.`,
+ [
+ groupData?.some((group) => group.directory === "fandom") &&
+ redirect(
+ "Fandom - Gallery",
+ "albums/fandom",
+ "localized.groupGallery",
+ "fandom"
+ ),
+
+ groupData?.some((group) => group.directory === "official") &&
+ redirect(
+ "Official - Gallery",
+ "albums/official",
+ "localized.groupGallery",
+ "official"
+ ),
+
+ wikiInfo.enableListings &&
+ redirect(
+ "Album Commentary",
+ "list/all-commentary",
+ "localized.commentaryIndex",
+ ""
+ ),
+
+ writeFile(
+ path.join(outputPath, "data.json"),
+ fixWS`
{
"albumData": ${stringifyThings(wikiData.albumData)},
- ${wikiInfo.enableFlashesAndGames && `"flashData": ${stringifyThings(wikiData.flashData)},`}
+ ${
+ wikiInfo.enableFlashesAndGames &&
+ `"flashData": ${stringifyThings(wikiData.flashData)},`
+ }
"artistData": ${stringifyThings(wikiData.artistData)}
}
- `)
- ].filter(Boolean));
+ `
+ ),
+ ].filter(Boolean)
+ );
}
-function generateRedirectPage(title, target, {language}) {
- return fixWS`
+function generateRedirectPage(title, target, { language }) {
+ return fixWS`
-
${language.$('redirectPage.title', {title})}
+
${language.$("redirectPage.title", { title })}
@@ -1322,9 +1539,9 @@ function generateRedirectPage(title, target, {language}) {
- ${language.$('redirectPage.title', {title})}
- ${language.$('redirectPage.infoLine', {
- target: `${target} `
+
${language.$("redirectPage.title", { title })}
+ ${language.$("redirectPage.infoLine", {
+ target: `${target} `,
})}
@@ -1334,622 +1551,663 @@ function generateRedirectPage(title, target, {language}) {
// RIP toAnythingMan (previously getHrefOfAnythingMan), 2020-05-25<>2021-05-14.
// ........Yet the function 8reathes life anew as linkAnythingMan! ::::)
-function linkAnythingMan(anythingMan, {link, wikiData, ...opts}) {
- return (
- wikiData.albumData.includes(anythingMan) ? link.album(anythingMan, opts) :
- wikiData.trackData.includes(anythingMan) ? link.track(anythingMan, opts) :
- wikiData.flashData?.includes(anythingMan) ? link.flash(anythingMan, opts) :
- 'idk bud'
- )
+function linkAnythingMan(anythingMan, { link, wikiData, ...opts }) {
+ return wikiData.albumData.includes(anythingMan)
+ ? link.album(anythingMan, opts)
+ : wikiData.trackData.includes(anythingMan)
+ ? link.track(anythingMan, opts)
+ : wikiData.flashData?.includes(anythingMan)
+ ? link.flash(anythingMan, opts)
+ : "idk bud";
}
async function processLanguageFile(file) {
- const contents = await readFile(file, 'utf-8');
- const json = JSON.parse(contents);
-
- const code = json['meta.languageCode'];
- if (!code) {
- throw new Error(`Missing language code (file: ${file})`);
- }
- delete json['meta.languageCode'];
-
- const intlCode = json['meta.languageIntlCode'] ?? null;
- delete json['meta.languageIntlCode'];
-
- const name = json['meta.languageName'];
- if (!name) {
- throw new Error(`Missing language name (${code})`);
- }
- delete json['meta.languageName'];
-
- const hidden = json['meta.hidden'] ?? false;
- delete json['meta.hidden'];
-
- if (json['meta.baseDirectory']) {
- logWarn`(${code}) Language JSON still has unused meta.baseDirectory`;
- delete json['meta.baseDirectory'];
- }
-
- const language = new Language();
- language.code = code;
- language.intlCode = intlCode;
- language.name = name;
- language.hidden = hidden;
- language.escapeHTML = string => he.encode(string, {useNamedReferences: true});
- language.strings = json;
- return language;
+ const contents = await readFile(file, "utf-8");
+ const json = JSON.parse(contents);
+
+ const code = json["meta.languageCode"];
+ if (!code) {
+ throw new Error(`Missing language code (file: ${file})`);
+ }
+ delete json["meta.languageCode"];
+
+ const intlCode = json["meta.languageIntlCode"] ?? null;
+ delete json["meta.languageIntlCode"];
+
+ const name = json["meta.languageName"];
+ if (!name) {
+ throw new Error(`Missing language name (${code})`);
+ }
+ delete json["meta.languageName"];
+
+ const hidden = json["meta.hidden"] ?? false;
+ delete json["meta.hidden"];
+
+ if (json["meta.baseDirectory"]) {
+ logWarn`(${code}) Language JSON still has unused meta.baseDirectory`;
+ delete json["meta.baseDirectory"];
+ }
+
+ const language = new Language();
+ language.code = code;
+ language.intlCode = intlCode;
+ language.name = name;
+ language.hidden = hidden;
+ language.escapeHTML = (string) =>
+ he.encode(string, { useNamedReferences: true });
+ language.strings = json;
+ return language;
}
// Wrapper function for running a function once for all languages.
-async function wrapLanguages(fn, {languages, writeOneLanguage = null}) {
- const k = writeOneLanguage;
- const languagesToRun = (k
- ? {[k]: languages[k]}
- : languages);
+async function wrapLanguages(fn, { languages, writeOneLanguage = null }) {
+ const k = writeOneLanguage;
+ const languagesToRun = k ? { [k]: languages[k] } : languages;
- const entries = Object.entries(languagesToRun)
- .filter(([ key ]) => key !== 'default');
+ const entries = Object.entries(languagesToRun).filter(
+ ([key]) => key !== "default"
+ );
- for (let i = 0; i < entries.length; i++) {
- const [ key, language ] = entries[i];
+ for (let i = 0; i < entries.length; i++) {
+ const [key, language] = entries[i];
- await fn(language, i, entries);
- }
+ await fn(language, i, entries);
+ }
}
async function main() {
- Error.stackTraceLimit = Infinity;
+ Error.stackTraceLimit = Infinity;
- const WD = wikiData;
+ const WD = wikiData;
- WD.listingSpec = listingSpec;
- WD.listingTargetSpec = listingTargetSpec;
-
- const miscOptions = await parseOptions(process.argv.slice(2), {
- // Data files for the site, including flash, artist, and al8um data,
- // and like a jillion other things too. Pretty much everything which
- // makes an individual wiki what it is goes here!
- 'data-path': {
- type: 'value'
- },
+ WD.listingSpec = listingSpec;
+ WD.listingTargetSpec = listingTargetSpec;
- // Static media will 8e referenced in the site here! The contents are
- // categorized; check out MEDIA_ALBUM_ART_DIRECTORY and other constants
- // near the top of this file (upd8.js).
- 'media-path': {
- type: 'value'
- },
-
- // String files! For the most part, this is used for translating the
- // site to different languages, though you can also customize strings
- // for your own 8uild of the site if you'd like. Files here should all
- // match the format in strings-default.json in this repository. (If a
- // language file is missing any strings, the site code will fall 8ack
- // to what's specified in strings-default.json.)
- //
- // Unlike the other options here, this one's optional - the site will
- // 8uild with the default (English) strings if this path is left
- // unspecified.
- 'lang-path': {
- type: 'value'
- },
+ const miscOptions = await parseOptions(process.argv.slice(2), {
+ // Data files for the site, including flash, artist, and al8um data,
+ // and like a jillion other things too. Pretty much everything which
+ // makes an individual wiki what it is goes here!
+ "data-path": {
+ type: "value",
+ },
- // This is the output directory. It's the one you'll upload online with
- // rsync or whatever when you're pushing an upd8, and also the one
- // you'd archive if you wanted to make a 8ackup of the whole dang
- // site. Just keep in mind that the gener8ted result will contain a
- // couple symlinked directories, so if you're uploading, you're pro8a8ly
- // gonna want to resolve those yourself.
- 'out-path': {
- type: 'value'
- },
+ // Static media will 8e referenced in the site here! The contents are
+ // categorized; check out MEDIA_ALBUM_ART_DIRECTORY and other constants
+ // near the top of this file (upd8.js).
+ "media-path": {
+ type: "value",
+ },
- // Thum8nail gener8tion is *usually* something you want, 8ut it can 8e
- // kinda a pain to run every time, since it does necessit8te reading
- // every media file at run time. Pass this to skip it.
- 'skip-thumbs': {
- type: 'flag'
- },
+ // String files! For the most part, this is used for translating the
+ // site to different languages, though you can also customize strings
+ // for your own 8uild of the site if you'd like. Files here should all
+ // match the format in strings-default.json in this repository. (If a
+ // language file is missing any strings, the site code will fall 8ack
+ // to what's specified in strings-default.json.)
+ //
+ // Unlike the other options here, this one's optional - the site will
+ // 8uild with the default (English) strings if this path is left
+ // unspecified.
+ "lang-path": {
+ type: "value",
+ },
- // Or, if you *only* want to gener8te newly upd8ted thum8nails, you can
- // pass this flag! It exits 8efore 8uilding the rest of the site.
- 'thumbs-only': {
- type: 'flag'
- },
+ // This is the output directory. It's the one you'll upload online with
+ // rsync or whatever when you're pushing an upd8, and also the one
+ // you'd archive if you wanted to make a 8ackup of the whole dang
+ // site. Just keep in mind that the gener8ted result will contain a
+ // couple symlinked directories, so if you're uploading, you're pro8a8ly
+ // gonna want to resolve those yourself.
+ "out-path": {
+ type: "value",
+ },
- // Just working on data entries and not interested in actually
- // generating site HTML yet? This flag will cut execution off right
- // 8efore any site 8uilding actually happens.
- 'no-build': {
- type: 'flag'
- },
+ // Thum8nail gener8tion is *usually* something you want, 8ut it can 8e
+ // kinda a pain to run every time, since it does necessit8te reading
+ // every media file at run time. Pass this to skip it.
+ "skip-thumbs": {
+ type: "flag",
+ },
- // Only want to 8uild one language during testing? This can chop down
- // 8uild times a pretty 8ig chunk! Just pass a single language code.
- 'lang': {
- type: 'value'
- },
+ // Or, if you *only* want to gener8te newly upd8ted thum8nails, you can
+ // pass this flag! It exits 8efore 8uilding the rest of the site.
+ "thumbs-only": {
+ type: "flag",
+ },
- // Working without a dev server and just using file:// URLs in your we8
- // 8rowser? This will automatically append index.html to links across
- // the site. Not recommended for production, since it isn't guaranteed
- // 100% error-free (and index.html-style links are less pretty anyway).
- 'append-index-html': {
- type: 'flag'
- },
+ // Just working on data entries and not interested in actually
+ // generating site HTML yet? This flag will cut execution off right
+ // 8efore any site 8uilding actually happens.
+ "no-build": {
+ type: "flag",
+ },
- // Want sweet, sweet trace8ack info in aggreg8te error messages? This
- // will print all the juicy details (or at least the first relevant
- // line) right to your output, 8ut also pro8a8ly give you a headache
- // 8ecause wow that is a lot of visual noise.
- 'show-traces': {
- type: 'flag'
- },
+ // Only want to 8uild one language during testing? This can chop down
+ // 8uild times a pretty 8ig chunk! Just pass a single language code.
+ lang: {
+ type: "value",
+ },
- 'queue-size': {
- type: 'value',
- validate(size) {
- if (parseInt(size) !== parseFloat(size)) return 'an integer';
- if (parseInt(size) < 0) return 'a counting number or zero';
- return true;
- }
- },
- queue: {alias: 'queue-size'},
+ // Working without a dev server and just using file:// URLs in your we8
+ // 8rowser? This will automatically append index.html to links across
+ // the site. Not recommended for production, since it isn't guaranteed
+ // 100% error-free (and index.html-style links are less pretty anyway).
+ "append-index-html": {
+ type: "flag",
+ },
- // This option is super slow and has the potential for bugs! It puts
- // CacheableObject in a mode where every instance is a Proxy which will
- // keep track of invalid property accesses.
- 'show-invalid-property-accesses': {
- type: 'flag'
- },
+ // Want sweet, sweet trace8ack info in aggreg8te error messages? This
+ // will print all the juicy details (or at least the first relevant
+ // line) right to your output, 8ut also pro8a8ly give you a headache
+ // 8ecause wow that is a lot of visual noise.
+ "show-traces": {
+ type: "flag",
+ },
- [parseOptions.handleUnknown]: () => {}
- });
+ "queue-size": {
+ type: "value",
+ validate(size) {
+ if (parseInt(size) !== parseFloat(size)) return "an integer";
+ if (parseInt(size) < 0) return "a counting number or zero";
+ return true;
+ },
+ },
+ queue: { alias: "queue-size" },
- dataPath = miscOptions['data-path'] || process.env.HSMUSIC_DATA;
- mediaPath = miscOptions['media-path'] || process.env.HSMUSIC_MEDIA;
- langPath = miscOptions['lang-path'] || process.env.HSMUSIC_LANG; // Can 8e left unset!
- outputPath = miscOptions['out-path'] || process.env.HSMUSIC_OUT;
+ // This option is super slow and has the potential for bugs! It puts
+ // CacheableObject in a mode where every instance is a Proxy which will
+ // keep track of invalid property accesses.
+ "show-invalid-property-accesses": {
+ type: "flag",
+ },
- const writeOneLanguage = miscOptions['lang'];
+ [parseOptions.handleUnknown]: () => {},
+ });
- {
- let errored = false;
- const error = (cond, msg) => {
- if (cond) {
- console.error(`\x1b[31;1m${msg}\x1b[0m`);
- errored = true;
- }
- };
- error(!dataPath, `Expected --data-path option or HSMUSIC_DATA to be set`);
- error(!mediaPath, `Expected --media-path option or HSMUSIC_MEDIA to be set`);
- error(!outputPath, `Expected --out-path option or HSMUSIC_OUT to be set`);
- if (errored) {
- return;
- }
- }
+ dataPath = miscOptions["data-path"] || process.env.HSMUSIC_DATA;
+ mediaPath = miscOptions["media-path"] || process.env.HSMUSIC_MEDIA;
+ langPath = miscOptions["lang-path"] || process.env.HSMUSIC_LANG; // Can 8e left unset!
+ outputPath = miscOptions["out-path"] || process.env.HSMUSIC_OUT;
- const appendIndexHTML = miscOptions['append-index-html'] ?? false;
- if (appendIndexHTML) {
- logWarn`Appending index.html to link hrefs. (Note: not recommended for production release!)`;
- unbound_link.globalOptions.appendIndexHTML = true;
- }
+ const writeOneLanguage = miscOptions["lang"];
- const skipThumbs = miscOptions['skip-thumbs'] ?? false;
- const thumbsOnly = miscOptions['thumbs-only'] ?? false;
- const noBuild = miscOptions['no-build'] ?? false;
- const showAggregateTraces = miscOptions['show-traces'] ?? false;
-
- const niceShowAggregate = (error, ...opts) => {
- showAggregate(error, {
- showTraces: showAggregateTraces,
- pathToFile: f => path.relative(__dirname, f),
- ...opts
- });
+ {
+ let errored = false;
+ const error = (cond, msg) => {
+ if (cond) {
+ console.error(`\x1b[31;1m${msg}\x1b[0m`);
+ errored = true;
+ }
};
-
- if (skipThumbs && thumbsOnly) {
- logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`;
- return;
+ error(!dataPath, `Expected --data-path option or HSMUSIC_DATA to be set`);
+ error(
+ !mediaPath,
+ `Expected --media-path option or HSMUSIC_MEDIA to be set`
+ );
+ error(!outputPath, `Expected --out-path option or HSMUSIC_OUT to be set`);
+ if (errored) {
+ return;
}
-
- if (skipThumbs) {
- logInfo`Skipping thumbnail generation.`;
- } else {
- logInfo`Begin thumbnail generation... -----+`;
- const result = await genThumbs(mediaPath, {queueSize, quiet: true});
- logInfo`Done thumbnail generation! --------+`;
- if (!result) return;
- if (thumbsOnly) return;
+ }
+
+ const appendIndexHTML = miscOptions["append-index-html"] ?? false;
+ if (appendIndexHTML) {
+ logWarn`Appending index.html to link hrefs. (Note: not recommended for production release!)`;
+ unbound_link.globalOptions.appendIndexHTML = true;
+ }
+
+ const skipThumbs = miscOptions["skip-thumbs"] ?? false;
+ const thumbsOnly = miscOptions["thumbs-only"] ?? false;
+ const noBuild = miscOptions["no-build"] ?? false;
+ const showAggregateTraces = miscOptions["show-traces"] ?? false;
+
+ const niceShowAggregate = (error, ...opts) => {
+ showAggregate(error, {
+ showTraces: showAggregateTraces,
+ pathToFile: (f) => path.relative(__dirname, f),
+ ...opts,
+ });
+ };
+
+ if (skipThumbs && thumbsOnly) {
+ logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`;
+ return;
+ }
+
+ if (skipThumbs) {
+ logInfo`Skipping thumbnail generation.`;
+ } else {
+ logInfo`Begin thumbnail generation... -----+`;
+ const result = await genThumbs(mediaPath, { queueSize, quiet: true });
+ logInfo`Done thumbnail generation! --------+`;
+ if (!result) return;
+ if (thumbsOnly) return;
+ }
+
+ const showInvalidPropertyAccesses =
+ miscOptions["show-invalid-property-accesses"] ?? false;
+
+ if (showInvalidPropertyAccesses) {
+ CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true;
+ }
+
+ const { aggregate: processDataAggregate, result: wikiDataResult } =
+ await loadAndProcessDataDocuments({ dataPath });
+
+ Object.assign(wikiData, wikiDataResult);
+
+ {
+ const logThings = (thingDataProp, label) =>
+ logInfo` - ${
+ wikiData[thingDataProp]?.length ?? color.red("(Missing!)")
+ } ${color.normal(color.dim(label))}`;
+ try {
+ logInfo`Loaded data and processed objects:`;
+ logThings("albumData", "albums");
+ logThings("trackData", "tracks");
+ logThings("artistData", "artists");
+ if (wikiData.flashData) {
+ logThings("flashData", "flashes");
+ logThings("flashActData", "flash acts");
+ }
+ logThings("groupData", "groups");
+ logThings("groupCategoryData", "group categories");
+ logThings("artTagData", "art tags");
+ if (wikiData.newsData) {
+ logThings("newsData", "news entries");
+ }
+ logThings("staticPageData", "static pages");
+ if (wikiData.homepageLayout) {
+ logInfo` - ${1} homepage layout (${
+ wikiData.homepageLayout.rows.length
+ } rows)`;
+ }
+ if (wikiData.wikiInfo) {
+ logInfo` - ${1} wiki config file`;
+ }
+ } catch (error) {
+ console.error(`Error showing data summary:`, error);
}
- const showInvalidPropertyAccesses = miscOptions['show-invalid-property-accesses'] ?? false;
+ let errorless = true;
+ try {
+ processDataAggregate.close();
+ } catch (error) {
+ niceShowAggregate(error);
+ logWarn`The above errors were detected while processing data files.`;
+ logWarn`If the remaining valid data is complete enough, the wiki will`;
+ logWarn`still build - but all errored data will be skipped.`;
+ logWarn`(Resolve errors for more complete output!)`;
+ errorless = false;
+ }
- if (showInvalidPropertyAccesses) {
- CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true;
+ if (errorless) {
+ logInfo`All data processed without any errors - nice!`;
+ logInfo`(This means all source files will be fully accounted for during page generation.)`;
}
+ }
- const {
- aggregate: processDataAggregate,
- result: wikiDataResult
- } = await loadAndProcessDataDocuments({dataPath});
+ if (!WD.wikiInfo) {
+ logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`;
+ return;
+ }
- Object.assign(wikiData, wikiDataResult);
+ let duplicateDirectoriesErrored = false;
- {
- const logThings = (thingDataProp, label) => logInfo` - ${wikiData[thingDataProp]?.length ?? color.red('(Missing!)')} ${color.normal(color.dim(label))}`;
- try {
- logInfo`Loaded data and processed objects:`;
- logThings('albumData', 'albums');
- logThings('trackData', 'tracks');
- logThings('artistData', 'artists');
- if (wikiData.flashData) {
- logThings('flashData', 'flashes');
- logThings('flashActData', 'flash acts');
- }
- logThings('groupData', 'groups');
- logThings('groupCategoryData', 'group categories');
- logThings('artTagData', 'art tags');
- if (wikiData.newsData) {
- logThings('newsData', 'news entries');
- }
- logThings('staticPageData', 'static pages');
- if (wikiData.homepageLayout) {
- logInfo` - ${1} homepage layout (${wikiData.homepageLayout.rows.length} rows)`;
- }
- if (wikiData.wikiInfo) {
- logInfo` - ${1} wiki config file`;
- }
- } catch (error) {
- console.error(`Error showing data summary:`, error);
- }
-
- let errorless = true;
- try {
- processDataAggregate.close();
- } catch (error) {
- niceShowAggregate(error);
- logWarn`The above errors were detected while processing data files.`;
- logWarn`If the remaining valid data is complete enough, the wiki will`;
- logWarn`still build - but all errored data will be skipped.`;
- logWarn`(Resolve errors for more complete output!)`;
- errorless = false;
- }
-
- if (errorless) {
- logInfo`All data processed without any errors - nice!`;
- logInfo`(This means all source files will be fully accounted for during page generation.)`;
- }
+ function filterAndShowDuplicateDirectories() {
+ const aggregate = filterDuplicateDirectories(wikiData);
+ let errorless = true;
+ try {
+ aggregate.close();
+ } catch (aggregate) {
+ niceShowAggregate(aggregate);
+ logWarn`The above duplicate directories were detected while reviewing data files.`;
+ logWarn`Each thing listed above will been totally excempt from this build of the site!`;
+ logWarn`Specify unique 'Directory' fields in data entries to resolve these.`;
+ logWarn`${`Note:`} This will probably result in reference errors below.`;
+ logWarn`${`. . .`} You should fix duplicate directories first!`;
+ logWarn`(Resolve errors for more complete output!)`;
+ duplicateDirectoriesErrored = true;
+ errorless = false;
}
-
- if (!WD.wikiInfo) {
- logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`;
- return;
+ if (errorless) {
+ logInfo`No duplicate directories found - nice!`;
}
+ }
- let duplicateDirectoriesErrored = false;
-
- function filterAndShowDuplicateDirectories() {
- const aggregate = filterDuplicateDirectories(wikiData);
- let errorless = true;
- try {
- aggregate.close();
- } catch (aggregate) {
- niceShowAggregate(aggregate);
- logWarn`The above duplicate directories were detected while reviewing data files.`;
- logWarn`Each thing listed above will been totally excempt from this build of the site!`;
- logWarn`Specify unique 'Directory' fields in data entries to resolve these.`;
- logWarn`${`Note:`} This will probably result in reference errors below.`;
- logWarn`${`. . .`} You should fix duplicate directories first!`;
- logWarn`(Resolve errors for more complete output!)`;
- duplicateDirectoriesErrored = true;
- errorless = false;
- }
- if (errorless) {
- logInfo`No duplicate directories found - nice!`;
- }
+ function filterAndShowReferenceErrors() {
+ const aggregate = filterReferenceErrors(wikiData);
+ let errorless = true;
+ try {
+ aggregate.close();
+ } catch (error) {
+ niceShowAggregate(error);
+ logWarn`The above errors were detected while validating references in data files.`;
+ logWarn`If the remaining valid data is complete enough, the wiki will still build -`;
+ logWarn`but all errored references will be skipped.`;
+ if (duplicateDirectoriesErrored) {
+ logWarn`${`Note:`} Duplicate directories were found as well. Review those first,`;
+ logWarn`${`. . .`} as they may have caused some of the errors detected above.`;
+ }
+ logWarn`(Resolve errors for more complete output!)`;
+ errorless = false;
}
-
- function filterAndShowReferenceErrors() {
- const aggregate = filterReferenceErrors(wikiData);
- let errorless = true;
- try {
- aggregate.close();
- } catch (error) {
- niceShowAggregate(error);
- logWarn`The above errors were detected while validating references in data files.`;
- logWarn`If the remaining valid data is complete enough, the wiki will still build -`;
- logWarn`but all errored references will be skipped.`;
- if (duplicateDirectoriesErrored) {
- logWarn`${`Note:`} Duplicate directories were found as well. Review those first,`;
- logWarn`${`. . .`} as they may have caused some of the errors detected above.`;
- }
- logWarn`(Resolve errors for more complete output!)`;
- errorless = false;
- }
- if (errorless) {
- logInfo`All references validated without any errors - nice!`;
- logInfo`(This means all references between things, such as leitmotif references`
- logInfo` and artist credits, will be fully accounted for during page generation.)`;
- }
+ if (errorless) {
+ logInfo`All references validated without any errors - nice!`;
+ logInfo`(This means all references between things, such as leitmotif references`;
+ logInfo` and artist credits, will be fully accounted for during page generation.)`;
}
+ }
- // Link data arrays so that all essential references between objects are
- // complete, so properties (like dates!) are inherited where that's
- // appropriate.
- linkWikiDataArrays(wikiData);
+ // Link data arrays so that all essential references between objects are
+ // complete, so properties (like dates!) are inherited where that's
+ // appropriate.
+ linkWikiDataArrays(wikiData);
- // Filter out any things with duplicate directories throughout the data,
- // warning about them too.
- filterAndShowDuplicateDirectories();
+ // Filter out any things with duplicate directories throughout the data,
+ // warning about them too.
+ filterAndShowDuplicateDirectories();
- // Filter out any reference errors throughout the data, warning about them
- // too.
- filterAndShowReferenceErrors();
+ // Filter out any reference errors throughout the data, warning about them
+ // too.
+ filterAndShowReferenceErrors();
- // Sort data arrays so that they're all in order! This may use properties
- // which are only available after the initial linking.
- sortWikiDataArrays(wikiData);
+ // Sort data arrays so that they're all in order! This may use properties
+ // which are only available after the initial linking.
+ sortWikiDataArrays(wikiData);
- const internalDefaultLanguage = await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE));
+ const internalDefaultLanguage = await processLanguageFile(
+ path.join(__dirname, DEFAULT_STRINGS_FILE)
+ );
- let languages;
- if (langPath) {
- const languageDataFiles = await findFiles(langPath, {
- filter: f => path.extname(f) === '.json'
- });
+ let languages;
+ if (langPath) {
+ const languageDataFiles = await findFiles(langPath, {
+ filter: (f) => path.extname(f) === ".json",
+ });
- const results = await progressPromiseAll(`Reading & processing language files.`, languageDataFiles
- .map(file => processLanguageFile(file)));
+ const results = await progressPromiseAll(
+ `Reading & processing language files.`,
+ languageDataFiles.map((file) => processLanguageFile(file))
+ );
- languages = Object.fromEntries(results.map(language => [language.code, language]));
+ languages = Object.fromEntries(
+ results.map((language) => [language.code, language])
+ );
+ } else {
+ languages = {};
+ }
+
+ const customDefaultLanguage =
+ languages[WD.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code];
+ let finalDefaultLanguage;
+
+ if (customDefaultLanguage) {
+ logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`;
+ customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings;
+ finalDefaultLanguage = customDefaultLanguage;
+ } else if (WD.wikiInfo.defaultLanguage) {
+ logError`Wiki info file specified default language is ${WD.wikiInfo.defaultLanguage}, but no such language file exists!`;
+ if (langPath) {
+ logError`Check if an appropriate file exists in ${langPath}?`;
} else {
- languages = {};
+ logError`Be sure to specify ${"--lang"} or ${"HSMUSIC_LANG"} with the path to language files.`;
}
-
- const customDefaultLanguage = languages[WD.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code];
- let finalDefaultLanguage;
-
- if (customDefaultLanguage) {
- logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`;
- customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings;
- finalDefaultLanguage = customDefaultLanguage;
- } else if (WD.wikiInfo.defaultLanguage) {
- logError`Wiki info file specified default language is ${WD.wikiInfo.defaultLanguage}, but no such language file exists!`;
- if (langPath) {
- logError`Check if an appropriate file exists in ${langPath}?`;
- } else {
- logError`Be sure to specify ${'--lang'} or ${'HSMUSIC_LANG'} with the path to language files.`;
- }
- return;
- } else {
- languages[internalDefaultLanguage.code] = internalDefaultLanguage;
- finalDefaultLanguage = internalDefaultLanguage;
+ return;
+ } else {
+ languages[internalDefaultLanguage.code] = internalDefaultLanguage;
+ finalDefaultLanguage = internalDefaultLanguage;
+ }
+
+ for (const language of Object.values(languages)) {
+ if (language === finalDefaultLanguage) {
+ continue;
}
- for (const language of Object.values(languages)) {
- if (language === finalDefaultLanguage) {
- continue;
- }
+ language.inheritedStrings = finalDefaultLanguage.strings;
+ }
+
+ logInfo`Loaded language strings: ${Object.keys(languages).join(", ")}`;
+
+ if (noBuild) {
+ logInfo`Not generating any site or page files this run (--no-build passed).`;
+ } else if (writeOneLanguage && !(writeOneLanguage in languages)) {
+ logError`Specified to write only ${writeOneLanguage}, but there is no strings file with this language code!`;
+ return;
+ } else if (writeOneLanguage) {
+ logInfo`Writing only language ${writeOneLanguage} this run.`;
+ } else {
+ logInfo`Writing all languages.`;
+ }
+
+ {
+ const tagRefs = new Set(
+ [...WD.trackData, ...WD.albumData].flatMap(
+ (thing) => thing.artTagsByRef ?? []
+ )
+ );
- language.inheritedStrings = finalDefaultLanguage.strings;
+ for (const ref of tagRefs) {
+ if (find.artTag(ref, WD.artTagData)) {
+ tagRefs.delete(ref);
+ }
}
- logInfo`Loaded language strings: ${Object.keys(languages).join(', ')}`;
-
- if (noBuild) {
- logInfo`Not generating any site or page files this run (--no-build passed).`;
- } else if (writeOneLanguage && !(writeOneLanguage in languages)) {
- logError`Specified to write only ${writeOneLanguage}, but there is no strings file with this language code!`;
- return;
- } else if (writeOneLanguage) {
- logInfo`Writing only language ${writeOneLanguage} this run.`;
- } else {
- logInfo`Writing all languages.`;
+ if (tagRefs.size) {
+ for (const ref of Array.from(tagRefs).sort()) {
+ console.log(`\x1b[33;1m- Missing tag: "${ref}"\x1b[0m`);
+ }
+ return;
}
-
- {
- const tagRefs = new Set([...WD.trackData, ...WD.albumData].flatMap(thing => thing.artTagsByRef ?? []));
-
- for (const ref of tagRefs) {
- if (find.artTag(ref, WD.artTagData)) {
- tagRefs.delete(ref);
- }
+ }
+
+ WD.officialAlbumData = WD.albumData.filter((album) =>
+ album.groups.some((group) => group.directory === OFFICIAL_GROUP_DIRECTORY)
+ );
+ WD.fandomAlbumData = WD.albumData.filter((album) =>
+ album.groups.every((group) => group.directory !== OFFICIAL_GROUP_DIRECTORY)
+ );
+
+ const fileSizePreloader = new FileSizePreloader();
+
+ // File sizes of additional files need to be precalculated before we can
+ // actually reference 'em in site building, so get those loading right
+ // away. We actually need to keep track of two things here - the on-device
+ // file paths we're actually reading, and the corresponding on-site media
+ // paths that will be exposed in site build code. We'll build a mapping
+ // function between them so that when site code requests a site path,
+ // it'll get the size of the file at the corresponding device path.
+ const additionalFilePaths = [
+ ...WD.albumData.flatMap((album) =>
+ [
+ ...(album.additionalFiles ?? []),
+ ...album.tracks.flatMap((track) => track.additionalFiles ?? []),
+ ]
+ .flatMap((fileGroup) => fileGroup.files)
+ .map((file) => ({
+ device: path.join(
+ mediaPath,
+ urls
+ .from("media.root")
+ .toDevice("media.albumAdditionalFile", album.directory, file)
+ ),
+ media: urls
+ .from("media.root")
+ .to("media.albumAdditionalFile", album.directory, file),
+ }))
+ ),
+ ];
+
+ const getSizeOfAdditionalFile = (mediaPath) => {
+ const { device = null } =
+ additionalFilePaths.find(({ media }) => media === mediaPath) || {};
+ if (!device) return null;
+ return fileSizePreloader.getSizeOfPath(device);
+ };
+
+ logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`;
+
+ fileSizePreloader.loadPaths(
+ ...additionalFilePaths.map((path) => path.device)
+ );
+ await fileSizePreloader.waitUntilDoneLoading();
+
+ logInfo`Done preloading filesizes!`;
+
+ if (noBuild) return;
+
+ // Makes writing a little nicer on CPU theoretically, 8ut also costs in
+ // performance right now 'cuz it'll w8 for file writes to 8e completed
+ // 8efore moving on to more data processing. So, defaults to zero, which
+ // disa8les the queue feature altogether.
+ queueSize = +(miscOptions["queue-size"] ?? 0);
+
+ const buildDictionary = pageSpecs;
+
+ // NOT for ena8ling or disa8ling specific features of the site!
+ // This is only in charge of what general groups of files to 8uild.
+ // They're here to make development quicker when you're only working
+ // on some particular area(s) of the site rather than making changes
+ // across all of them.
+ const writeFlags = await parseOptions(process.argv.slice(2), {
+ all: { type: "flag" }, // Defaults to true if none 8elow specified.
+
+ // Kinda a hack t8h!
+ ...Object.fromEntries(
+ Object.keys(buildDictionary).map((key) => [key, { type: "flag" }])
+ ),
+
+ [parseOptions.handleUnknown]: () => {},
+ });
+
+ const writeAll = !Object.keys(writeFlags).length || writeFlags.all;
+
+ logInfo`Writing site pages: ${
+ writeAll ? "all" : Object.keys(writeFlags).join(", ")
+ }`;
+
+ await writeFavicon();
+ await writeSymlinks();
+ await writeSharedFilesAndPages({ language: finalDefaultLanguage, wikiData });
+
+ const buildSteps = writeAll
+ ? Object.entries(buildDictionary)
+ : Object.entries(buildDictionary).filter(([flag]) => writeFlags[flag]);
+
+ let writes;
+ {
+ let error = false;
+
+ const buildStepsWithTargets = buildSteps
+ .map(([flag, pageSpec]) => {
+ // Condition not met: skip this build step altogether.
+ if (pageSpec.condition && !pageSpec.condition({ wikiData })) {
+ return null;
}
- if (tagRefs.size) {
- for (const ref of Array.from(tagRefs).sort()) {
- console.log(`\x1b[33;1m- Missing tag: "${ref}"\x1b[0m`);
- }
- return;
+ // May still call writeTargetless if present.
+ if (!pageSpec.targets) {
+ return { flag, pageSpec, targets: [] };
}
- }
-
- WD.officialAlbumData = WD.albumData.filter(album => album.groups.some(group => group.directory === OFFICIAL_GROUP_DIRECTORY));
- WD.fandomAlbumData = WD.albumData.filter(album => album.groups.every(group => group.directory !== OFFICIAL_GROUP_DIRECTORY));
-
- const fileSizePreloader = new FileSizePreloader();
-
- // File sizes of additional files need to be precalculated before we can
- // actually reference 'em in site building, so get those loading right
- // away. We actually need to keep track of two things here - the on-device
- // file paths we're actually reading, and the corresponding on-site media
- // paths that will be exposed in site build code. We'll build a mapping
- // function between them so that when site code requests a site path,
- // it'll get the size of the file at the corresponding device path.
- const additionalFilePaths = [
- ...WD.albumData.flatMap(album => (
- [
- ...album.additionalFiles ?? [],
- ...album.tracks.flatMap(track => track.additionalFiles ?? [])
- ]
- .flatMap(fileGroup => fileGroup.files)
- .map(file => ({
- device: (path.join(mediaPath, urls
- .from('media.root')
- .toDevice('media.albumAdditionalFile', album.directory, file))),
- media: (urls
- .from('media.root')
- .to('media.albumAdditionalFile', album.directory, file))
- })))),
- ];
-
- const getSizeOfAdditionalFile = mediaPath => {
- const { device = null } = additionalFilePaths.find(({ media }) => media === mediaPath) || {};
- if (!device) return null;
- return fileSizePreloader.getSizeOfPath(device);
- };
-
- logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`;
-
- fileSizePreloader.loadPaths(...additionalFilePaths.map(path => path.device));
- await fileSizePreloader.waitUntilDoneLoading();
-
- logInfo`Done preloading filesizes!`;
-
- if (noBuild) return;
-
- // Makes writing a little nicer on CPU theoretically, 8ut also costs in
- // performance right now 'cuz it'll w8 for file writes to 8e completed
- // 8efore moving on to more data processing. So, defaults to zero, which
- // disa8les the queue feature altogether.
- queueSize = +(miscOptions['queue-size'] ?? 0);
-
- const buildDictionary = pageSpecs;
-
- // NOT for ena8ling or disa8ling specific features of the site!
- // This is only in charge of what general groups of files to 8uild.
- // They're here to make development quicker when you're only working
- // on some particular area(s) of the site rather than making changes
- // across all of them.
- const writeFlags = await parseOptions(process.argv.slice(2), {
- all: {type: 'flag'}, // Defaults to true if none 8elow specified.
-
- // Kinda a hack t8h!
- ...Object.fromEntries(Object.keys(buildDictionary)
- .map(key => [key, {type: 'flag'}])),
-
- [parseOptions.handleUnknown]: () => {}
- });
-
- const writeAll = !Object.keys(writeFlags).length || writeFlags.all;
-
- logInfo`Writing site pages: ${writeAll ? 'all' : Object.keys(writeFlags).join(', ')}`;
-
- await writeFavicon();
- await writeSymlinks();
- await writeSharedFilesAndPages({language: finalDefaultLanguage, wikiData});
-
- const buildSteps = (writeAll
- ? Object.entries(buildDictionary)
- : (Object.entries(buildDictionary)
- .filter(([ flag ]) => writeFlags[flag])));
-
- let writes;
- {
- let error = false;
- const buildStepsWithTargets = buildSteps.map(([ flag, pageSpec ]) => {
- // Condition not met: skip this build step altogether.
- if (pageSpec.condition && !pageSpec.condition({wikiData})) {
- return null;
- }
-
- // May still call writeTargetless if present.
- if (!pageSpec.targets) {
- return {flag, pageSpec, targets: []};
- }
-
- if (!pageSpec.write) {
- logError`${flag + '.targets'} is specified, but ${flag + '.write'} is missing!`;
- error = true;
- return null;
- }
-
- const targets = pageSpec.targets({wikiData});
- if (!Array.isArray(targets)) {
- logError`${flag + '.targets'} was called, but it didn't return an array! (${typeof targets})`;
- error = true;
- return null;
- }
-
- return {flag, pageSpec, targets};
- }).filter(Boolean);
+ if (!pageSpec.write) {
+ logError`${flag + ".targets"} is specified, but ${
+ flag + ".write"
+ } is missing!`;
+ error = true;
+ return null;
+ }
- if (error) {
- return;
+ const targets = pageSpec.targets({ wikiData });
+ if (!Array.isArray(targets)) {
+ logError`${
+ flag + ".targets"
+ } was called, but it didn't return an array! (${typeof targets})`;
+ error = true;
+ return null;
}
- const validateWrites = (writes, fnName) => {
- // Do a quick valid8tion! If one of the writeThingPages functions go
- // wrong, this will stall out early and tell us which did.
+ return { flag, pageSpec, targets };
+ })
+ .filter(Boolean);
- if (!Array.isArray(writes)) {
- logError`${fnName} didn't return an array!`;
- error = true;
- return false;
- }
+ if (error) {
+ return;
+ }
- if (!(
- writes.every(obj => typeof obj === 'object') &&
- writes.every(obj => {
- const result = validateWriteObject(obj);
- if (result.error) {
- logError`Validating write object failed: ${result.error}`;
- return false;
- } else {
- return true;
- }
- })
- )) {
- logError`${fnName} returned invalid entries!`;
- error = true;
- return false;
+ const validateWrites = (writes, fnName) => {
+ // Do a quick valid8tion! If one of the writeThingPages functions go
+ // wrong, this will stall out early and tell us which did.
+
+ if (!Array.isArray(writes)) {
+ logError`${fnName} didn't return an array!`;
+ error = true;
+ return false;
+ }
+
+ if (
+ !(
+ writes.every((obj) => typeof obj === "object") &&
+ writes.every((obj) => {
+ const result = validateWriteObject(obj);
+ if (result.error) {
+ logError`Validating write object failed: ${result.error}`;
+ return false;
+ } else {
+ return true;
}
+ })
+ )
+ ) {
+ logError`${fnName} returned invalid entries!`;
+ error = true;
+ return false;
+ }
+
+ return true;
+ };
- return true;
- };
-
- // return;
+ // return;
- writes = buildStepsWithTargets.flatMap(({ flag, pageSpec, targets }) => {
- const writes = targets.flatMap(target =>
- pageSpec.write(target, {wikiData})?.slice() || []);
+ writes = buildStepsWithTargets.flatMap(({ flag, pageSpec, targets }) => {
+ const writes = targets.flatMap(
+ (target) => pageSpec.write(target, { wikiData })?.slice() || []
+ );
- if (!validateWrites(writes, flag + '.write')) {
- return [];
- }
+ if (!validateWrites(writes, flag + ".write")) {
+ return [];
+ }
- if (pageSpec.writeTargetless) {
- const writes2 = pageSpec.writeTargetless({wikiData});
+ if (pageSpec.writeTargetless) {
+ const writes2 = pageSpec.writeTargetless({ wikiData });
- if (!validateWrites(writes2, flag + '.writeTargetless')) {
- return [];
- }
+ if (!validateWrites(writes2, flag + ".writeTargetless")) {
+ return [];
+ }
- writes.push(...writes2);
- }
+ writes.push(...writes2);
+ }
- return writes;
- });
+ return writes;
+ });
- if (error) {
- return;
- }
+ if (error) {
+ return;
}
+ }
- const pageWrites = writes.filter(({ type }) => type === 'page');
- const dataWrites = writes.filter(({ type }) => type === 'data');
- const redirectWrites = writes.filter(({ type }) => type === 'redirect');
+ const pageWrites = writes.filter(({ type }) => type === "page");
+ const dataWrites = writes.filter(({ type }) => type === "data");
+ const redirectWrites = writes.filter(({ type }) => type === "redirect");
- if (writes.length) {
- logInfo`Total of ${writes.length} writes returned. (${pageWrites.length} page, ${dataWrites.length} data [currently skipped], ${redirectWrites.length} redirect)`;
- } else {
- logWarn`No writes returned at all, so exiting early. This is probably a bug!`;
- return;
- }
+ if (writes.length) {
+ logInfo`Total of ${writes.length} writes returned. (${pageWrites.length} page, ${dataWrites.length} data [currently skipped], ${redirectWrites.length} redirect)`;
+ } else {
+ logWarn`No writes returned at all, so exiting early. This is probably a bug!`;
+ return;
+ }
- /*
+ /*
await progressPromiseAll(`Writing data files shared across languages.`, queue(
dataWrites.map(({path, data}) => () => {
const bound = {};
@@ -1985,272 +2243,331 @@ async function main() {
));
*/
- const perLanguageFn = async (language, i, entries) => {
- const baseDirectory = (language === finalDefaultLanguage ? '' : language.code);
+ const perLanguageFn = async (language, i, entries) => {
+ const baseDirectory =
+ language === finalDefaultLanguage ? "" : language.code;
- console.log(`\x1b[34;1m${
- (`[${i + 1}/${entries.length}] ${language.code} (-> /${baseDirectory}) `
- .padEnd(60, '-'))
- }\x1b[0m`);
+ console.log(
+ `\x1b[34;1m${`[${i + 1}/${entries.length}] ${
+ language.code
+ } (-> /${baseDirectory}) `.padEnd(60, "-")}\x1b[0m`
+ );
- await progressPromiseAll(`Writing ${language.code}`, queue([
- ...pageWrites.map(({type, ...props}) => () => {
- const { path, page } = props;
+ await progressPromiseAll(
+ `Writing ${language.code}`,
+ queue(
+ [
+ ...pageWrites.map(({ type, ...props }) => () => {
+ const { path, page } = props;
- // TODO: This only supports one <>-style argument.
- const pageSubKey = path[0];
- const directory = path[1];
-
- const localizedPaths = Object.fromEntries(Object.entries(languages)
- .filter(([ key, language ]) => key !== 'default' && !language.hidden)
- .map(([ key, language ]) => [language.code, writePage.paths(
- (language === finalDefaultLanguage ? '' : language.code),
- 'localized.' + pageSubKey,
- directory
- )]));
-
- const paths = writePage.paths(
- baseDirectory,
- 'localized.' + pageSubKey,
+ // TODO: This only supports one <>-style argument.
+ const pageSubKey = path[0];
+ const directory = path[1];
+
+ const localizedPaths = Object.fromEntries(
+ Object.entries(languages)
+ .filter(
+ ([key, language]) => key !== "default" && !language.hidden
+ )
+ .map(([key, language]) => [
+ language.code,
+ writePage.paths(
+ language === finalDefaultLanguage ? "" : language.code,
+ "localized." + pageSubKey,
directory
- );
-
- const to = writePage.to({
- baseDirectory,
- pageSubKey,
- paths
- });
-
- const absoluteTo = (targetFullKey, ...args) => {
- const [ groupKey, subKey ] = targetFullKey.split('.');
- const from = urls.from('shared.root');
- return '/' + (groupKey === 'localized' && baseDirectory
- ? from.to('localizedWithBaseDirectory.' + subKey, baseDirectory, ...args)
- : from.to(targetFullKey, ...args));
- };
-
- // TODO: Is there some nicer way to define these,
- // may8e without totally re-8inding everything for
- // each page?
- const bound = {};
-
- bound.link = withEntries(unbound_link, entries => entries
- .map(([ key, fn ]) => [key, bindOpts(fn, {to})]));
-
- bound.linkAnythingMan = bindOpts(linkAnythingMan, {
- link: bound.link,
- wikiData
- });
+ ),
+ ])
+ );
+
+ const paths = writePage.paths(
+ baseDirectory,
+ "localized." + pageSubKey,
+ directory
+ );
+
+ const to = writePage.to({
+ baseDirectory,
+ pageSubKey,
+ paths,
+ });
- bound.parseAttributes = bindOpts(parseAttributes, {
- to
- });
+ const absoluteTo = (targetFullKey, ...args) => {
+ const [groupKey, subKey] = targetFullKey.split(".");
+ const from = urls.from("shared.root");
+ return (
+ "/" +
+ (groupKey === "localized" && baseDirectory
+ ? from.to(
+ "localizedWithBaseDirectory." + subKey,
+ baseDirectory,
+ ...args
+ )
+ : from.to(targetFullKey, ...args))
+ );
+ };
- bound.find = bindFind(wikiData, {mode: 'warn'});
+ // TODO: Is there some nicer way to define these,
+ // may8e without totally re-8inding everything for
+ // each page?
+ const bound = {};
- bound.transformInline = bindOpts(transformInline, {
- find: bound.find,
- link: bound.link,
- replacerSpec,
- language,
- to,
- wikiData
- });
+ bound.link = withEntries(unbound_link, (entries) =>
+ entries.map(([key, fn]) => [key, bindOpts(fn, { to })])
+ );
- bound.transformMultiline = bindOpts(transformMultiline, {
- transformInline: bound.transformInline,
- parseAttributes: bound.parseAttributes
- });
-
- bound.transformLyrics = bindOpts(transformLyrics, {
- transformInline: bound.transformInline,
- transformMultiline: bound.transformMultiline
- });
+ bound.linkAnythingMan = bindOpts(linkAnythingMan, {
+ link: bound.link,
+ wikiData,
+ });
- bound.iconifyURL = bindOpts(iconifyURL, {
- language,
- to
- });
+ bound.parseAttributes = bindOpts(parseAttributes, {
+ to,
+ });
- bound.fancifyURL = bindOpts(fancifyURL, {
- language
- });
+ bound.find = bindFind(wikiData, { mode: "warn" });
- bound.fancifyFlashURL = bindOpts(fancifyFlashURL, {
- [bindOpts.bindIndex]: 2,
- language
- });
+ bound.transformInline = bindOpts(transformInline, {
+ find: bound.find,
+ link: bound.link,
+ replacerSpec,
+ language,
+ to,
+ wikiData,
+ });
- bound.getLinkThemeString = getLinkThemeString;
+ bound.transformMultiline = bindOpts(transformMultiline, {
+ transformInline: bound.transformInline,
+ parseAttributes: bound.parseAttributes,
+ });
- bound.getThemeString = getThemeString;
+ bound.transformLyrics = bindOpts(transformLyrics, {
+ transformInline: bound.transformInline,
+ transformMultiline: bound.transformMultiline,
+ });
- bound.getArtistString = bindOpts(getArtistString, {
- iconifyURL: bound.iconifyURL,
- link: bound.link,
- language
- });
+ bound.iconifyURL = bindOpts(iconifyURL, {
+ language,
+ to,
+ });
- bound.getAlbumCover = bindOpts(getAlbumCover, {
- to
- });
+ bound.fancifyURL = bindOpts(fancifyURL, {
+ language,
+ });
- bound.getTrackCover = bindOpts(getTrackCover, {
- to
- });
+ bound.fancifyFlashURL = bindOpts(fancifyFlashURL, {
+ [bindOpts.bindIndex]: 2,
+ language,
+ });
- bound.getFlashCover = bindOpts(getFlashCover, {
- to
- });
+ bound.getLinkThemeString = getLinkThemeString;
- bound.getArtistAvatar = bindOpts(getArtistAvatar, {
- to
- });
+ bound.getThemeString = getThemeString;
- bound.generateAdditionalFilesShortcut = bindOpts(generateAdditionalFilesShortcut, {
- language
- });
+ bound.getArtistString = bindOpts(getArtistString, {
+ iconifyURL: bound.iconifyURL,
+ link: bound.link,
+ language,
+ });
- bound.generateAdditionalFilesList = bindOpts(generateAdditionalFilesList, {
- language
- });
+ bound.getAlbumCover = bindOpts(getAlbumCover, {
+ to,
+ });
- bound.generateChronologyLinks = bindOpts(generateChronologyLinks, {
- link: bound.link,
- linkAnythingMan: bound.linkAnythingMan,
- language,
- wikiData
- });
+ bound.getTrackCover = bindOpts(getTrackCover, {
+ to,
+ });
- bound.generateCoverLink = bindOpts(generateCoverLink, {
- [bindOpts.bindIndex]: 0,
- img,
- link: bound.link,
- language,
- to,
- wikiData
- });
+ bound.getFlashCover = bindOpts(getFlashCover, {
+ to,
+ });
- bound.generateInfoGalleryLinks = bindOpts(generateInfoGalleryLinks, {
- [bindOpts.bindIndex]: 2,
- link: bound.link,
- language
- });
+ bound.getArtistAvatar = bindOpts(getArtistAvatar, {
+ to,
+ });
- bound.generatePreviousNextLinks = bindOpts(generatePreviousNextLinks, {
- link: bound.link,
- language
- });
+ bound.generateAdditionalFilesShortcut = bindOpts(
+ generateAdditionalFilesShortcut,
+ {
+ language,
+ }
+ );
+
+ bound.generateAdditionalFilesList = bindOpts(
+ generateAdditionalFilesList,
+ {
+ language,
+ }
+ );
+
+ bound.generateChronologyLinks = bindOpts(generateChronologyLinks, {
+ link: bound.link,
+ linkAnythingMan: bound.linkAnythingMan,
+ language,
+ wikiData,
+ });
- bound.generateTrackListDividedByGroups = bindOpts(generateTrackListDividedByGroups, {
- language,
- wikiData,
- });
+ bound.generateCoverLink = bindOpts(generateCoverLink, {
+ [bindOpts.bindIndex]: 0,
+ img,
+ link: bound.link,
+ language,
+ to,
+ wikiData,
+ });
- bound.getGridHTML = bindOpts(getGridHTML, {
- [bindOpts.bindIndex]: 0,
- img,
- language
- });
+ bound.generateInfoGalleryLinks = bindOpts(
+ generateInfoGalleryLinks,
+ {
+ [bindOpts.bindIndex]: 2,
+ link: bound.link,
+ language,
+ }
+ );
+
+ bound.generatePreviousNextLinks = bindOpts(
+ generatePreviousNextLinks,
+ {
+ link: bound.link,
+ language,
+ }
+ );
+
+ bound.generateTrackListDividedByGroups = bindOpts(
+ generateTrackListDividedByGroups,
+ {
+ language,
+ wikiData,
+ }
+ );
+
+ bound.getGridHTML = bindOpts(getGridHTML, {
+ [bindOpts.bindIndex]: 0,
+ img,
+ language,
+ });
- bound.getAlbumGridHTML = bindOpts(getAlbumGridHTML, {
- [bindOpts.bindIndex]: 0,
- getAlbumCover: bound.getAlbumCover,
- getGridHTML: bound.getGridHTML,
- link: bound.link,
- language
- });
+ bound.getAlbumGridHTML = bindOpts(getAlbumGridHTML, {
+ [bindOpts.bindIndex]: 0,
+ getAlbumCover: bound.getAlbumCover,
+ getGridHTML: bound.getGridHTML,
+ link: bound.link,
+ language,
+ });
- bound.getFlashGridHTML = bindOpts(getFlashGridHTML, {
- [bindOpts.bindIndex]: 0,
- getFlashCover: bound.getFlashCover,
- getGridHTML: bound.getGridHTML,
- link: bound.link
- });
+ bound.getFlashGridHTML = bindOpts(getFlashGridHTML, {
+ [bindOpts.bindIndex]: 0,
+ getFlashCover: bound.getFlashCover,
+ getGridHTML: bound.getGridHTML,
+ link: bound.link,
+ });
- bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, {
- language
- });
+ bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, {
+ language,
+ });
- bound.getRevealStringFromWarnings = bindOpts(getRevealStringFromWarnings, {
- language
- });
+ bound.getRevealStringFromWarnings = bindOpts(
+ getRevealStringFromWarnings,
+ {
+ language,
+ }
+ );
- bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, {
- to
- });
+ bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, {
+ to,
+ });
- const pageInfo = page({
- ...bound,
+ const pageInfo = page({
+ ...bound,
- language,
+ language,
- absoluteTo,
- relativeTo: to,
- to,
- urls,
+ absoluteTo,
+ relativeTo: to,
+ to,
+ urls,
- getSizeOfAdditionalFile,
- });
+ getSizeOfAdditionalFile,
+ });
- const oEmbedJSON = writePage.oEmbedJSON(pageInfo, {
- language,
- wikiData,
- });
+ const oEmbedJSON = writePage.oEmbedJSON(pageInfo, {
+ language,
+ wikiData,
+ });
- const oEmbedJSONHref = (oEmbedJSON && wikiData.wikiInfo.canonicalBase) && (
- wikiData.wikiInfo.canonicalBase + urls.from('shared.root').to('shared.path', paths.pathname + OEMBED_JSON_FILE));
-
- const html = writePage.html(pageInfo, {
- defaultLanguage: finalDefaultLanguage,
- language,
- languages,
- localizedPaths,
- oEmbedJSONHref,
- paths,
- to,
- transformMultiline: bound.transformMultiline,
- wikiData
- });
+ const oEmbedJSONHref =
+ oEmbedJSON &&
+ wikiData.wikiInfo.canonicalBase &&
+ wikiData.wikiInfo.canonicalBase +
+ urls
+ .from("shared.root")
+ .to("shared.path", paths.pathname + OEMBED_JSON_FILE);
+
+ const html = writePage.html(pageInfo, {
+ defaultLanguage: finalDefaultLanguage,
+ language,
+ languages,
+ localizedPaths,
+ oEmbedJSONHref,
+ paths,
+ to,
+ transformMultiline: bound.transformMultiline,
+ wikiData,
+ });
- return writePage.write({
- html,
- oEmbedJSON,
- paths,
- });
- }),
- ...redirectWrites.map(({fromPath, toPath, title: titleFn}) => () => {
+ return writePage.write({
+ html,
+ oEmbedJSON,
+ paths,
+ });
+ }),
+ ...redirectWrites.map(
+ ({ fromPath, toPath, title: titleFn }) =>
+ () => {
const title = titleFn({
- language
+ language,
});
// TODO: This only supports one <>-style argument.
- const fromPaths = writePage.paths(baseDirectory, 'localized.' + fromPath[0], fromPath[1]);
- const to = writePage.to({baseDirectory, pageSubKey: fromPath[0], paths: fromPaths});
-
- const target = to('localized.' + toPath[0], ...toPath.slice(1));
- const html = generateRedirectPage(title, target, {language});
- return writePage.write({html, paths: fromPaths});
- })
- ], queueSize));
- };
+ const fromPaths = writePage.paths(
+ baseDirectory,
+ "localized." + fromPath[0],
+ fromPath[1]
+ );
+ const to = writePage.to({
+ baseDirectory,
+ pageSubKey: fromPath[0],
+ paths: fromPaths,
+ });
- await wrapLanguages(perLanguageFn, {
- languages,
- writeOneLanguage,
- });
+ const target = to("localized." + toPath[0], ...toPath.slice(1));
+ const html = generateRedirectPage(title, target, { language });
+ return writePage.write({ html, paths: fromPaths });
+ }
+ ),
+ ],
+ queueSize
+ )
+ );
+ };
+
+ await wrapLanguages(perLanguageFn, {
+ languages,
+ writeOneLanguage,
+ });
- // The single most important step.
- logInfo`Written!`;
+ // The single most important step.
+ logInfo`Written!`;
}
-main().catch(error => {
+main()
+ .catch((error) => {
if (error instanceof AggregateError) {
- showAggregate(error);
+ showAggregate(error);
} else {
- console.error(error);
+ console.error(error);
}
-}).then(() => {
+ })
+ .then(() => {
decorateTime.displayTime();
CacheableObject.showInvalidAccesses();
-});
+ });
diff --git a/src/url-spec.js b/src/url-spec.js
index 5c599416..cd35abed 100644
--- a/src/url-spec.js
+++ b/src/url-spec.js
@@ -1,93 +1,92 @@
-import {withEntries} from './util/sugar.js';
+import { withEntries } from "./util/sugar.js";
const urlSpec = {
- data: {
- prefix: 'data/',
+ data: {
+ prefix: "data/",
- paths: {
- root: '',
- path: '<>',
+ paths: {
+ root: "",
+ path: "<>",
- album: 'album/<>',
- artist: 'artist/<>',
- track: 'track/<>'
- }
+ album: "album/<>",
+ artist: "artist/<>",
+ track: "track/<>",
},
+ },
- localized: {
- // TODO: Implement this.
- // prefix: '_languageCode',
+ localized: {
+ // TODO: Implement this.
+ // prefix: '_languageCode',
- paths: {
- root: '',
- path: '<>',
+ paths: {
+ root: "",
+ path: "<>",
- home: '',
+ home: "",
- album: 'album/<>/',
- albumCommentary: 'commentary/album/<>/',
+ album: "album/<>/",
+ albumCommentary: "commentary/album/<>/",
- artist: 'artist/<>/',
- artistGallery: 'artist/<>/gallery/',
+ artist: "artist/<>/",
+ artistGallery: "artist/<>/gallery/",
- commentaryIndex: 'commentary/',
+ commentaryIndex: "commentary/",
- flashIndex: 'flash/',
- flash: 'flash/<>/',
+ flashIndex: "flash/",
+ flash: "flash/<>/",
- groupInfo: 'group/<>/',
- groupGallery: 'group/<>/gallery/',
+ groupInfo: "group/<>/",
+ groupGallery: "group/<>/gallery/",
- listingIndex: 'list/',
- listing: 'list/<>/',
+ listingIndex: "list/",
+ listing: "list/<>/",
- newsIndex: 'news/',
- newsEntry: 'news/<>/',
+ newsIndex: "news/",
+ newsEntry: "news/<>/",
- staticPage: '<>/',
- tag: 'tag/<>/',
- track: 'track/<>/'
- }
+ staticPage: "<>/",
+ tag: "tag/<>/",
+ track: "track/<>/",
},
+ },
- shared: {
- paths: {
- root: '',
- path: '<>',
+ shared: {
+ paths: {
+ root: "",
+ path: "<>",
- utilityRoot: 'util',
- staticRoot: 'static',
+ utilityRoot: "util",
+ staticRoot: "static",
- utilityFile: 'util/<>',
- staticFile: 'static/<>'
- }
+ utilityFile: "util/<>",
+ staticFile: "static/<>",
},
-
- media: {
- prefix: 'media/',
-
- paths: {
- root: '',
- path: '<>',
-
- albumCover: 'album-art/<>/cover.<>',
- albumWallpaper: 'album-art/<>/bg.<>',
- albumBanner: 'album-art/<>/banner.<>',
- trackCover: 'album-art/<>/<>.<>',
- artistAvatar: 'artist-avatar/<>.<>',
- flashArt: 'flash-art/<>.<>',
- albumAdditionalFile: 'album-additional/<>/<>',
- }
- }
+ },
+
+ media: {
+ prefix: "media/",
+
+ paths: {
+ root: "",
+ path: "<>",
+
+ albumCover: "album-art/<>/cover.<>",
+ albumWallpaper: "album-art/<>/bg.<>",
+ albumBanner: "album-art/<>/banner.<>",
+ trackCover: "album-art/<>/<>.<>",
+ artistAvatar: "artist-avatar/<>.<>",
+ flashArt: "flash-art/<>.<>",
+ albumAdditionalFile: "album-additional/<>/<>",
+ },
+ },
};
// This gets automatically switched in place when working from a baseDirectory,
// so it should never be referenced manually.
urlSpec.localizedWithBaseDirectory = {
- paths: withEntries(
- urlSpec.localized.paths,
- entries => entries.map(([key, path]) => [key, '<>/' + path])
- )
+ paths: withEntries(urlSpec.localized.paths, (entries) =>
+ entries.map(([key, path]) => [key, "<>/" + path])
+ ),
};
export default urlSpec;
diff --git a/src/util/cli.js b/src/util/cli.js
index 0bbf3af4..e073bed8 100644
--- a/src/util/cli.js
+++ b/src/util/cli.js
@@ -5,47 +5,52 @@
const { process } = globalThis;
-export const ENABLE_COLOR = process && (
- (process.env.CLICOLOR_FORCE && process.env.CLICOLOR_FORCE === '1')
- ?? (process.env.CLICOLOR && process.env.CLICOLOR === '1' && process.stdout.hasColors && process.stdout.hasColors())
- ?? (process.stdout.hasColors ? process.stdout.hasColors() : true));
+export const ENABLE_COLOR =
+ process &&
+ ((process.env.CLICOLOR_FORCE && process.env.CLICOLOR_FORCE === "1") ??
+ (process.env.CLICOLOR &&
+ process.env.CLICOLOR === "1" &&
+ process.stdout.hasColors &&
+ process.stdout.hasColors()) ??
+ (process.stdout.hasColors ? process.stdout.hasColors() : true));
-const C = n => (ENABLE_COLOR
- ? text => `\x1b[${n}m${text}\x1b[0m`
- : text => text);
+const C = (n) =>
+ ENABLE_COLOR ? (text) => `\x1b[${n}m${text}\x1b[0m` : (text) => text;
export const color = {
- bright: C('1'),
- dim: C('2'),
- normal: C('22'),
- black: C('30'),
- red: C('31'),
- green: C('32'),
- yellow: C('33'),
- blue: C('34'),
- magenta: C('35'),
- cyan: C('36'),
- white: C('37')
+ bright: C("1"),
+ dim: C("2"),
+ normal: C("22"),
+ black: C("30"),
+ red: C("31"),
+ green: C("32"),
+ yellow: C("33"),
+ blue: C("34"),
+ magenta: C("35"),
+ cyan: C("36"),
+ white: C("37"),
};
-const logColor = color => (literals, ...values) => {
- const w = s => process.stdout.write(s);
- const wc = text => {
- if (ENABLE_COLOR) w(text);
+const logColor =
+ (color) =>
+ (literals, ...values) => {
+ const w = (s) => process.stdout.write(s);
+ const wc = (text) => {
+ if (ENABLE_COLOR) w(text);
};
wc(`\x1b[${color}m`);
for (let i = 0; i < literals.length; i++) {
- w(literals[i]);
- if (values[i] !== undefined) {
- wc(`\x1b[1m`);
- w(String(values[i]));
- wc(`\x1b[0;${color}m`);
- }
+ w(literals[i]);
+ if (values[i] !== undefined) {
+ wc(`\x1b[1m`);
+ w(String(values[i]));
+ wc(`\x1b[0;${color}m`);
+ }
}
wc(`\x1b[0m`);
- w('\n');
-};
+ w("\n");
+ };
export const logInfo = logColor(2);
export const logWarn = logColor(33);
@@ -53,205 +58,220 @@ export const logError = logColor(31);
// Stolen as #@CK from mtui!
export async function parseOptions(options, optionDescriptorMap) {
- // This function is sorely lacking in comments, but the basic usage is
- // as such:
- //
- // options is the array of options you want to process;
- // optionDescriptorMap is a mapping of option names to objects that describe
- // the expected value for their corresponding options.
- // Returned is a mapping of any specified option names to their values, or
- // a process.exit(1) and error message if there were any issues.
- //
- // Here are examples of optionDescriptorMap to cover all the things you can
- // do with it:
- //
- // optionDescriptorMap: {
- // 'telnet-server': {type: 'flag'},
- // 't': {alias: 'telnet-server'}
- // }
- //
- // options: ['t'] -> result: {'telnet-server': true}
- //
- // optionDescriptorMap: {
- // 'directory': {
- // type: 'value',
- // validate(name) {
- // // const whitelistedDirectories = ['apple', 'banana']
- // if (whitelistedDirectories.includes(name)) {
- // return true
- // } else {
- // return 'a whitelisted directory'
- // }
- // }
- // },
- // 'files': {type: 'series'}
- // }
- //
- // ['--directory', 'apple'] -> {'directory': 'apple'}
- // ['--directory', 'artichoke'] -> (error)
- // ['--files', 'a', 'b', 'c', ';'] -> {'files': ['a', 'b', 'c']}
- //
- // TODO: Be able to validate the values in a series option.
+ // This function is sorely lacking in comments, but the basic usage is
+ // as such:
+ //
+ // options is the array of options you want to process;
+ // optionDescriptorMap is a mapping of option names to objects that describe
+ // the expected value for their corresponding options.
+ // Returned is a mapping of any specified option names to their values, or
+ // a process.exit(1) and error message if there were any issues.
+ //
+ // Here are examples of optionDescriptorMap to cover all the things you can
+ // do with it:
+ //
+ // optionDescriptorMap: {
+ // 'telnet-server': {type: 'flag'},
+ // 't': {alias: 'telnet-server'}
+ // }
+ //
+ // options: ['t'] -> result: {'telnet-server': true}
+ //
+ // optionDescriptorMap: {
+ // 'directory': {
+ // type: 'value',
+ // validate(name) {
+ // // const whitelistedDirectories = ['apple', 'banana']
+ // if (whitelistedDirectories.includes(name)) {
+ // return true
+ // } else {
+ // return 'a whitelisted directory'
+ // }
+ // }
+ // },
+ // 'files': {type: 'series'}
+ // }
+ //
+ // ['--directory', 'apple'] -> {'directory': 'apple'}
+ // ['--directory', 'artichoke'] -> (error)
+ // ['--files', 'a', 'b', 'c', ';'] -> {'files': ['a', 'b', 'c']}
+ //
+ // TODO: Be able to validate the values in a series option.
- const handleDashless = optionDescriptorMap[parseOptions.handleDashless];
- const handleUnknown = optionDescriptorMap[parseOptions.handleUnknown];
- const result = Object.create(null);
- for (let i = 0; i < options.length; i++) {
- const option = options[i];
- if (option.startsWith('--')) {
- // --x can be a flag or expect a value or series of values
- let name = option.slice(2).split('=')[0]; // '--x'.split('=') = ['--x']
- let descriptor = optionDescriptorMap[name];
- if (!descriptor) {
- if (handleUnknown) {
- handleUnknown(option);
- } else {
- console.error(`Unknown option name: ${name}`);
- process.exit(1);
- }
- continue;
- }
- if (descriptor.alias) {
- name = descriptor.alias;
- descriptor = optionDescriptorMap[name];
- }
- if (descriptor.type === 'flag') {
- result[name] = true;
- } else if (descriptor.type === 'value') {
- let value = option.slice(2).split('=')[1];
- if (!value) {
- value = options[++i];
- if (!value || value.startsWith('-')) {
- value = null;
- }
- }
- if (!value) {
- console.error(`Expected a value for --${name}`);
- process.exit(1);
- }
- result[name] = value;
- } else if (descriptor.type === 'series') {
- if (!options.slice(i).includes(';')) {
- console.error(`Expected a series of values concluding with ; (\\;) for --${name}`);
- process.exit(1);
- }
- const endIndex = i + options.slice(i).indexOf(';');
- result[name] = options.slice(i + 1, endIndex);
- i = endIndex;
- }
- if (descriptor.validate) {
- const validation = await descriptor.validate(result[name]);
- if (validation !== true) {
- console.error(`Expected ${validation} for --${name}`);
- process.exit(1);
- }
- }
- } else if (option.startsWith('-')) {
- // mtui doesn't use any -x=y or -x y format optionuments
- // -x will always just be a flag
- let name = option.slice(1);
- let descriptor = optionDescriptorMap[name];
- if (!descriptor) {
- if (handleUnknown) {
- handleUnknown(option);
- } else {
- console.error(`Unknown option name: ${name}`);
- process.exit(1);
- }
- continue;
- }
- if (descriptor.alias) {
- name = descriptor.alias;
- descriptor = optionDescriptorMap[name];
- }
- if (descriptor.type === 'flag') {
- result[name] = true;
- } else {
- console.error(`Use --${name} (value) to specify ${name}`);
- process.exit(1);
- }
- } else if (handleDashless) {
- handleDashless(option);
+ const handleDashless = optionDescriptorMap[parseOptions.handleDashless];
+ const handleUnknown = optionDescriptorMap[parseOptions.handleUnknown];
+ const result = Object.create(null);
+ for (let i = 0; i < options.length; i++) {
+ const option = options[i];
+ if (option.startsWith("--")) {
+ // --x can be a flag or expect a value or series of values
+ let name = option.slice(2).split("=")[0]; // '--x'.split('=') = ['--x']
+ let descriptor = optionDescriptorMap[name];
+ if (!descriptor) {
+ if (handleUnknown) {
+ handleUnknown(option);
+ } else {
+ console.error(`Unknown option name: ${name}`);
+ process.exit(1);
+ }
+ continue;
+ }
+ if (descriptor.alias) {
+ name = descriptor.alias;
+ descriptor = optionDescriptorMap[name];
+ }
+ if (descriptor.type === "flag") {
+ result[name] = true;
+ } else if (descriptor.type === "value") {
+ let value = option.slice(2).split("=")[1];
+ if (!value) {
+ value = options[++i];
+ if (!value || value.startsWith("-")) {
+ value = null;
+ }
+ }
+ if (!value) {
+ console.error(`Expected a value for --${name}`);
+ process.exit(1);
+ }
+ result[name] = value;
+ } else if (descriptor.type === "series") {
+ if (!options.slice(i).includes(";")) {
+ console.error(
+ `Expected a series of values concluding with ; (\\;) for --${name}`
+ );
+ process.exit(1);
+ }
+ const endIndex = i + options.slice(i).indexOf(";");
+ result[name] = options.slice(i + 1, endIndex);
+ i = endIndex;
+ }
+ if (descriptor.validate) {
+ const validation = await descriptor.validate(result[name]);
+ if (validation !== true) {
+ console.error(`Expected ${validation} for --${name}`);
+ process.exit(1);
+ }
+ }
+ } else if (option.startsWith("-")) {
+ // mtui doesn't use any -x=y or -x y format optionuments
+ // -x will always just be a flag
+ let name = option.slice(1);
+ let descriptor = optionDescriptorMap[name];
+ if (!descriptor) {
+ if (handleUnknown) {
+ handleUnknown(option);
+ } else {
+ console.error(`Unknown option name: ${name}`);
+ process.exit(1);
}
+ continue;
+ }
+ if (descriptor.alias) {
+ name = descriptor.alias;
+ descriptor = optionDescriptorMap[name];
+ }
+ if (descriptor.type === "flag") {
+ result[name] = true;
+ } else {
+ console.error(`Use --${name} (value) to specify ${name}`);
+ process.exit(1);
+ }
+ } else if (handleDashless) {
+ handleDashless(option);
}
- return result;
+ }
+ return result;
}
export const handleDashless = Symbol();
export const handleUnknown = Symbol();
export function decorateTime(arg1, arg2) {
- const [ id, functionToBeWrapped ] =
- ((typeof arg1 === 'string' || typeof arg1 === 'symbol')
- ? [arg1, arg2]
- : [Symbol(arg1.name), arg1]);
+ const [id, functionToBeWrapped] =
+ typeof arg1 === "string" || typeof arg1 === "symbol"
+ ? [arg1, arg2]
+ : [Symbol(arg1.name), arg1];
- const meta = decorateTime.idMetaMap[id] ?? {
- wrappedName: functionToBeWrapped.name,
- timeSpent: 0,
- timesCalled: 0,
- displayTime() {
- const averageTime = meta.timeSpent / meta.timesCalled;
- console.log(`\x1b[1m${typeof id === 'symbol' ? id.description : id}(...):\x1b[0m ${meta.timeSpent} ms / ${meta.timesCalled} calls \x1b[2m(avg: ${averageTime} ms)\x1b[0m`);
- }
- };
+ const meta = decorateTime.idMetaMap[id] ?? {
+ wrappedName: functionToBeWrapped.name,
+ timeSpent: 0,
+ timesCalled: 0,
+ displayTime() {
+ const averageTime = meta.timeSpent / meta.timesCalled;
+ console.log(
+ `\x1b[1m${typeof id === "symbol" ? id.description : id}(...):\x1b[0m ${
+ meta.timeSpent
+ } ms / ${meta.timesCalled} calls \x1b[2m(avg: ${averageTime} ms)\x1b[0m`
+ );
+ },
+ };
- decorateTime.idMetaMap[id] = meta;
+ decorateTime.idMetaMap[id] = meta;
- const fn = function(...args) {
- const start = Date.now();
- const ret = functionToBeWrapped(...args);
- const end = Date.now();
- meta.timeSpent += end - start;
- meta.timesCalled++;
- return ret;
- };
+ const fn = function (...args) {
+ const start = Date.now();
+ const ret = functionToBeWrapped(...args);
+ const end = Date.now();
+ meta.timeSpent += end - start;
+ meta.timesCalled++;
+ return ret;
+ };
- fn.displayTime = meta.displayTime;
+ fn.displayTime = meta.displayTime;
- return fn;
+ return fn;
}
decorateTime.idMetaMap = Object.create(null);
-decorateTime.displayTime = function() {
- const map = decorateTime.idMetaMap;
+decorateTime.displayTime = function () {
+ const map = decorateTime.idMetaMap;
- const keys = [
- ...Object.getOwnPropertySymbols(map),
- ...Object.getOwnPropertyNames(map)
- ];
+ const keys = [
+ ...Object.getOwnPropertySymbols(map),
+ ...Object.getOwnPropertyNames(map),
+ ];
- if (keys.length) {
- console.log(`\x1b[1mdecorateTime results: ` + '-'.repeat(40) + '\x1b[0m');
- for (const key of keys) {
- map[key].displayTime();
- }
+ if (keys.length) {
+ console.log(`\x1b[1mdecorateTime results: ` + "-".repeat(40) + "\x1b[0m");
+ for (const key of keys) {
+ map[key].displayTime();
}
+ }
};
export function progressPromiseAll(msgOrMsgFn, array) {
- if (!array.length) {
- return Promise.resolve([]);
- }
+ if (!array.length) {
+ return Promise.resolve([]);
+ }
- const msgFn = (typeof msgOrMsgFn === 'function'
- ? msgOrMsgFn
- : () => msgOrMsgFn);
+ const msgFn =
+ typeof msgOrMsgFn === "function" ? msgOrMsgFn : () => msgOrMsgFn;
- let done = 0, total = array.length;
- process.stdout.write(`\r${msgFn()} [0/${total}]`);
- const start = Date.now();
- return Promise.all(array.map(promise => Promise.resolve(promise).then(val => {
+ let done = 0,
+ total = array.length;
+ process.stdout.write(`\r${msgFn()} [0/${total}]`);
+ const start = Date.now();
+ return Promise.all(
+ array.map((promise) =>
+ Promise.resolve(promise).then((val) => {
done++;
// const pc = `${done}/${total}`;
- const pc = (Math.round(done / total * 1000) / 10 + '%').padEnd('99.9%'.length, ' ');
+ const pc = (Math.round((done / total) * 1000) / 10 + "%").padEnd(
+ "99.9%".length,
+ " "
+ );
if (done === total) {
- const time = Date.now() - start;
- process.stdout.write(`\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n`)
+ const time = Date.now() - start;
+ process.stdout.write(
+ `\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n`
+ );
} else {
- process.stdout.write(`\r${msgFn()} [${pc}] `);
+ process.stdout.write(`\r${msgFn()} [${pc}] `);
}
return val;
- })));
+ })
+ )
+ );
}
diff --git a/src/util/colors.js b/src/util/colors.js
index f568557a..4450a49f 100644
--- a/src/util/colors.js
+++ b/src/util/colors.js
@@ -3,23 +3,31 @@
// Graciously stolen from https://stackoverflow.com/a/54071699! ::::)
// in: r,g,b in [0,1], out: h in [0,360) and s,l in [0,1]
export function rgb2hsl(r, g, b) {
- let a=Math.max(r,g,b), n=a-Math.min(r,g,b), f=(1-Math.abs(a+a-n-1));
- let h= n && ((a==r) ? (g-b)/n : ((a==g) ? 2+(b-r)/n : 4+(r-g)/n));
- return [60*(h<0?h+6:h), f ? n/f : 0, (a+a-n)/2];
+ let a = Math.max(r, g, b),
+ n = a - Math.min(r, g, b),
+ f = 1 - Math.abs(a + a - n - 1);
+ let h =
+ n && (a == r ? (g - b) / n : a == g ? 2 + (b - r) / n : 4 + (r - g) / n);
+ return [60 * (h < 0 ? h + 6 : h), f ? n / f : 0, (a + a - n) / 2];
}
export function getColors(primary) {
- const [ r, g, b ] = primary.slice(1)
- .match(/[0-9a-fA-F]{2,2}/g)
- .slice(0, 3)
- .map(val => parseInt(val, 16) / 255);
- const [ h, s, l ] = rgb2hsl(r, g, b);
- const dim = `hsl(${Math.round(h)}deg, ${Math.round(s * 50)}%, ${Math.round(l * 80)}%)`;
- const bg = `hsla(${Math.round(h)}deg, ${Math.round(s * 15)}%, 12%, 0.80)`;
+ const [r, g, b] = primary
+ .slice(1)
+ .match(/[0-9a-fA-F]{2,2}/g)
+ .slice(0, 3)
+ .map((val) => parseInt(val, 16) / 255);
+ const [h, s, l] = rgb2hsl(r, g, b);
+ const dim = `hsl(${Math.round(h)}deg, ${Math.round(s * 50)}%, ${Math.round(
+ l * 80
+ )}%)`;
+ const bg = `hsla(${Math.round(h)}deg, ${Math.round(s * 15)}%, 12%, 0.80)`;
- return {
- primary, dim, bg,
- rgb: [r, g, b],
- hsl: [h, s, l],
- };
+ return {
+ primary,
+ dim,
+ bg,
+ rgb: [r, g, b],
+ hsl: [h, s, l],
+ };
}
diff --git a/src/util/find.js b/src/util/find.js
index 7cedb3d2..49a3a19a 100644
--- a/src/util/find.js
+++ b/src/util/find.js
@@ -1,126 +1,131 @@
-import {
- color,
- logError,
- logWarn
-} from './cli.js';
+import { color, logError, logWarn } from "./cli.js";
-import { inspect } from 'util';
+import { inspect } from "util";
function warnOrThrow(mode, message) {
- switch (mode) {
- case 'error':
- throw new Error(message);
- case 'warn':
- logWarn(message);
- default:
- return null;
- }
+ switch (mode) {
+ case "error":
+ throw new Error(message);
+ case "warn":
+ logWarn(message);
+ default:
+ return null;
+ }
}
function findHelper(keys, findFns = {}) {
- // Note: This cache explicitly *doesn't* support mutable data arrays. If the
- // data array is modified, make sure it's actually a new array object, not
- // the original, or the cache here will break and act as though the data
- // hasn't changed!
- const cache = new WeakMap();
-
- const byDirectory = findFns.byDirectory || matchDirectory;
- const byName = findFns.byName || matchName;
-
- const keyRefRegex = new RegExp(String.raw`^(?:(${keys.join('|')}):(?=\S))?(.*)$`);
-
- // The mode argument here may be 'warn', 'error', or 'quiet'. 'error' throws
- // errors for null matches (with details about the error), while 'warn' and
- // 'quiet' both return null, with 'warn' logging details directly to the
- // console.
- return (fullRef, data, {mode = 'warn'} = {}) => {
- if (!fullRef) return null;
- if (typeof fullRef !== 'string') {
- throw new Error(`Got a reference that is ${typeof fullRef}, not string: ${fullRef}`);
- }
-
- if (!data) {
- throw new Error(`Expected data to be present`);
- }
-
- if (!Array.isArray(data) && data.wikiData) {
- throw new Error(`Old {wikiData: {...}} format provided`);
- }
-
- let cacheForThisData = cache.get(data);
- const cachedValue = cacheForThisData?.[fullRef];
- if (cachedValue) {
- globalThis.NUM_CACHE = (globalThis.NUM_CACHE || 0) + 1;
- return cachedValue;
- }
- if (!cacheForThisData) {
- cacheForThisData = Object.create(null);
- cache.set(data, cacheForThisData);
- }
-
- const match = fullRef.match(keyRefRegex);
- if (!match) {
- return warnOrThrow(mode, `Malformed link reference: "${fullRef}"`);
- }
-
- const key = match[1];
- const ref = match[2];
-
- const found = (key
- ? byDirectory(ref, data, mode)
- : byName(ref, data, mode));
-
- if (!found) {
- warnOrThrow(mode, `Didn't match anything for ${color.bright(fullRef)}`);
- }
-
- cacheForThisData[fullRef] = found;
-
- return found;
- };
-}
+ // Note: This cache explicitly *doesn't* support mutable data arrays. If the
+ // data array is modified, make sure it's actually a new array object, not
+ // the original, or the cache here will break and act as though the data
+ // hasn't changed!
+ const cache = new WeakMap();
+
+ const byDirectory = findFns.byDirectory || matchDirectory;
+ const byName = findFns.byName || matchName;
+
+ const keyRefRegex = new RegExp(
+ String.raw`^(?:(${keys.join("|")}):(?=\S))?(.*)$`
+ );
+
+ // The mode argument here may be 'warn', 'error', or 'quiet'. 'error' throws
+ // errors for null matches (with details about the error), while 'warn' and
+ // 'quiet' both return null, with 'warn' logging details directly to the
+ // console.
+ return (fullRef, data, { mode = "warn" } = {}) => {
+ if (!fullRef) return null;
+ if (typeof fullRef !== "string") {
+ throw new Error(
+ `Got a reference that is ${typeof fullRef}, not string: ${fullRef}`
+ );
+ }
-function matchDirectory(ref, data, mode) {
- return data.find(({ directory }) => directory === ref);
-}
+ if (!data) {
+ throw new Error(`Expected data to be present`);
+ }
-function matchName(ref, data, mode) {
- const matches = data.filter(({ name }) => name.toLowerCase() === ref.toLowerCase());
+ if (!Array.isArray(data) && data.wikiData) {
+ throw new Error(`Old {wikiData: {...}} format provided`);
+ }
- if (matches.length > 1) {
- return warnOrThrow(mode,
- `Multiple matches for reference "${ref}". Please resolve:\n` +
- matches.map(match => `- ${inspect(match)}\n`).join('') +
- `Returning null for this reference.`);
+ let cacheForThisData = cache.get(data);
+ const cachedValue = cacheForThisData?.[fullRef];
+ if (cachedValue) {
+ globalThis.NUM_CACHE = (globalThis.NUM_CACHE || 0) + 1;
+ return cachedValue;
+ }
+ if (!cacheForThisData) {
+ cacheForThisData = Object.create(null);
+ cache.set(data, cacheForThisData);
}
- if (matches.length === 0) {
- return null;
+ const match = fullRef.match(keyRefRegex);
+ if (!match) {
+ return warnOrThrow(mode, `Malformed link reference: "${fullRef}"`);
}
- const thing = matches[0];
+ const key = match[1];
+ const ref = match[2];
- if (ref !== thing.name) {
- warnOrThrow(mode, `Bad capitalization: ${color.red(ref)} -> ${color.green(thing.name)}`);
+ const found = key ? byDirectory(ref, data, mode) : byName(ref, data, mode);
+
+ if (!found) {
+ warnOrThrow(mode, `Didn't match anything for ${color.bright(fullRef)}`);
}
- return thing;
+ cacheForThisData[fullRef] = found;
+
+ return found;
+ };
+}
+
+function matchDirectory(ref, data, mode) {
+ return data.find(({ directory }) => directory === ref);
+}
+
+function matchName(ref, data, mode) {
+ const matches = data.filter(
+ ({ name }) => name.toLowerCase() === ref.toLowerCase()
+ );
+
+ if (matches.length > 1) {
+ return warnOrThrow(
+ mode,
+ `Multiple matches for reference "${ref}". Please resolve:\n` +
+ matches.map((match) => `- ${inspect(match)}\n`).join("") +
+ `Returning null for this reference.`
+ );
+ }
+
+ if (matches.length === 0) {
+ return null;
+ }
+
+ const thing = matches[0];
+
+ if (ref !== thing.name) {
+ warnOrThrow(
+ mode,
+ `Bad capitalization: ${color.red(ref)} -> ${color.green(thing.name)}`
+ );
+ }
+
+ return thing;
}
function matchTagName(ref, data, quiet) {
- return matchName(ref.startsWith('cw: ') ? ref.slice(4) : ref, data, quiet);
+ return matchName(ref.startsWith("cw: ") ? ref.slice(4) : ref, data, quiet);
}
const find = {
- album: findHelper(['album', 'album-commentary']),
- artist: findHelper(['artist', 'artist-gallery']),
- artTag: findHelper(['tag'], {byName: matchTagName}),
- flash: findHelper(['flash']),
- group: findHelper(['group', 'group-gallery']),
- listing: findHelper(['listing']),
- newsEntry: findHelper(['news-entry']),
- staticPage: findHelper(['static']),
- track: findHelper(['track'])
+ album: findHelper(["album", "album-commentary"]),
+ artist: findHelper(["artist", "artist-gallery"]),
+ artTag: findHelper(["tag"], { byName: matchTagName }),
+ flash: findHelper(["flash"]),
+ group: findHelper(["group", "group-gallery"]),
+ listing: findHelper(["listing"]),
+ newsEntry: findHelper(["news-entry"]),
+ staticPage: findHelper(["static"]),
+ track: findHelper(["track"]),
};
export default find;
@@ -131,25 +136,30 @@ export default find;
// called, so if their values change, you'll have to continue with a fresh call
// to bindFind.
export function bindFind(wikiData, opts1) {
- return Object.fromEntries(Object.entries({
- album: 'albumData',
- artist: 'artistData',
- artTag: 'artTagData',
- flash: 'flashData',
- group: 'groupData',
- listing: 'listingSpec',
- newsEntry: 'newsData',
- staticPage: 'staticPageData',
- track: 'trackData',
- }).map(([ key, value ]) => {
- const findFn = find[key];
- const thingData = wikiData[value];
- return [key, (opts1
- ? (ref, opts2) => (opts2
- ? findFn(ref, thingData, {...opts1, ...opts2})
- : findFn(ref, thingData, opts1))
- : (ref, opts2) => (opts2
- ? findFn(ref, thingData, opts2)
- : findFn(ref, thingData)))];
- }));
+ return Object.fromEntries(
+ Object.entries({
+ album: "albumData",
+ artist: "artistData",
+ artTag: "artTagData",
+ flash: "flashData",
+ group: "groupData",
+ listing: "listingSpec",
+ newsEntry: "newsData",
+ staticPage: "staticPageData",
+ track: "trackData",
+ }).map(([key, value]) => {
+ const findFn = find[key];
+ const thingData = wikiData[value];
+ return [
+ key,
+ opts1
+ ? (ref, opts2) =>
+ opts2
+ ? findFn(ref, thingData, { ...opts1, ...opts2 })
+ : findFn(ref, thingData, opts1)
+ : (ref, opts2) =>
+ opts2 ? findFn(ref, thingData, opts2) : findFn(ref, thingData),
+ ];
+ })
+ );
}
diff --git a/src/util/html.js b/src/util/html.js
index a9b4bb9b..ceca5966 100644
--- a/src/util/html.js
+++ b/src/util/html.js
@@ -3,19 +3,19 @@
// COMPREHENSIVE!
// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
export const selfClosingTags = [
- 'area',
- 'base',
- 'br',
- 'col',
- 'embed',
- 'hr',
- 'img',
- 'input',
- 'link',
- 'meta',
- 'source',
- 'track',
- 'wbr',
+ "area",
+ "base",
+ "br",
+ "col",
+ "embed",
+ "hr",
+ "img",
+ "input",
+ "link",
+ "meta",
+ "source",
+ "track",
+ "wbr",
];
// Pass to tag() as an attri8utes key to make tag() return a 8lank string
@@ -24,86 +24,87 @@ export const selfClosingTags = [
export const onlyIfContent = Symbol();
export function tag(tagName, ...args) {
- const selfClosing = selfClosingTags.includes(tagName);
+ const selfClosing = selfClosingTags.includes(tagName);
- let openTag;
- let content;
- let attrs;
+ let openTag;
+ let content;
+ let attrs;
- if (typeof args[0] === 'object' && !Array.isArray(args[0])) {
- attrs = args[0];
- content = args[1];
- } else {
- content = args[0];
- }
+ if (typeof args[0] === "object" && !Array.isArray(args[0])) {
+ attrs = args[0];
+ content = args[1];
+ } else {
+ content = args[0];
+ }
- if (selfClosing && content) {
- throw new Error(`Tag <${tagName}> is self-closing but got content!`);
- }
+ if (selfClosing && content) {
+ throw new Error(`Tag <${tagName}> is self-closing but got content!`);
+ }
- if (attrs?.[onlyIfContent] && !content) {
- return '';
- }
+ if (attrs?.[onlyIfContent] && !content) {
+ return "";
+ }
- if (attrs) {
- const attrString = attributes(args[0]);
- if (attrString) {
- openTag = `${tagName} ${attrString}`;
- }
+ if (attrs) {
+ const attrString = attributes(args[0]);
+ if (attrString) {
+ openTag = `${tagName} ${attrString}`;
}
+ }
- if (!openTag) {
- openTag = tagName;
- }
+ if (!openTag) {
+ openTag = tagName;
+ }
- if (Array.isArray(content)) {
- content = content.filter(Boolean).join('\n');
- }
+ if (Array.isArray(content)) {
+ content = content.filter(Boolean).join("\n");
+ }
- if (content) {
- if (content.includes('\n')) {
- return (
- `<${openTag}>\n` +
- content.split('\n').map(line => ' ' + line + '\n').join('') +
- `${tagName}>`
- );
- } else {
- return `<${openTag}>${content}${tagName}>`;
- }
+ if (content) {
+ if (content.includes("\n")) {
+ return (
+ `<${openTag}>\n` +
+ content
+ .split("\n")
+ .map((line) => " " + line + "\n")
+ .join("") +
+ `${tagName}>`
+ );
+ } else {
+ return `<${openTag}>${content}${tagName}>`;
+ }
+ } else {
+ if (selfClosing) {
+ return `<${openTag}>`;
} else {
- if (selfClosing) {
- return `<${openTag}>`;
- } else {
- return `<${openTag}>${tagName}>`;
- }
+ return `<${openTag}>${tagName}>`;
}
+ }
}
export function escapeAttributeValue(value) {
- return value
- .replaceAll('"', '"')
- .replaceAll("'", ''');
+ return value.replaceAll('"', """).replaceAll("'", "'");
}
export function attributes(attribs) {
- return Object.entries(attribs)
- .map(([ key, val ]) => {
- if (typeof val === 'undefined' || val === null)
- return [key, val, false];
- else if (typeof val === 'string')
- return [key, val, true];
- else if (typeof val === 'boolean')
- return [key, val, val];
- else if (typeof val === 'number')
- return [key, val.toString(), true];
- else if (Array.isArray(val))
- return [key, val.filter(Boolean).join(' '), val.length > 0];
- else
- throw new Error(`Attribute value for ${key} should be primitive or array, got ${typeof val}`);
- })
- .filter(([ key, val, keep ]) => keep)
- .map(([ key, val ]) => (typeof val === 'boolean'
- ? `${key}`
- : `${key}="${escapeAttributeValue(val)}"`))
- .join(' ');
+ return Object.entries(attribs)
+ .map(([key, val]) => {
+ if (typeof val === "undefined" || val === null) return [key, val, false];
+ else if (typeof val === "string") return [key, val, true];
+ else if (typeof val === "boolean") return [key, val, val];
+ else if (typeof val === "number") return [key, val.toString(), true];
+ else if (Array.isArray(val))
+ return [key, val.filter(Boolean).join(" "), val.length > 0];
+ else
+ throw new Error(
+ `Attribute value for ${key} should be primitive or array, got ${typeof val}`
+ );
+ })
+ .filter(([key, val, keep]) => keep)
+ .map(([key, val]) =>
+ typeof val === "boolean"
+ ? `${key}`
+ : `${key}="${escapeAttributeValue(val)}"`
+ )
+ .join(" ");
}
diff --git a/src/util/io.js b/src/util/io.js
index 1d74399f..c17e2633 100644
--- a/src/util/io.js
+++ b/src/util/io.js
@@ -1,14 +1,14 @@
// Utility functions for interacting with files and other external data
// interfacey constructs.
-import { readdir } from 'fs/promises';
-import * as path from 'path';
+import { readdir } from "fs/promises";
+import * as path from "path";
-export async function findFiles(dataPath, {
- filter = f => true,
- joinParentDirectory = true,
-} = {}) {
- return (await readdir(dataPath))
- .filter(file => filter(file))
- .map(file => joinParentDirectory ? path.join(dataPath, file) : file);
+export async function findFiles(
+ dataPath,
+ { filter = (f) => true, joinParentDirectory = true } = {}
+) {
+ return (await readdir(dataPath))
+ .filter((file) => filter(file))
+ .map((file) => (joinParentDirectory ? path.join(dataPath, file) : file));
}
diff --git a/src/util/link.js b/src/util/link.js
index 68539621..0e3be3e5 100644
--- a/src/util/link.js
+++ b/src/util/link.js
@@ -9,108 +9,129 @@
// options availa8le in all the functions, making a common interface for
// gener8ting just a8out any link on the site.
-import * as html from './html.js'
-import { getColors } from './colors.js'
+import * as html from "./html.js";
+import { getColors } from "./colors.js";
export function getLinkThemeString(color) {
- if (!color) return '';
+ if (!color) return "";
- const { primary, dim } = getColors(color);
- return `--primary-color: ${primary}; --dim-color: ${dim}`;
+ const { primary, dim } = getColors(color);
+ return `--primary-color: ${primary}; --dim-color: ${dim}`;
}
const appendIndexHTMLRegex = /^(?!https?:\/\/).+\/$/;
-const linkHelper = (hrefFn, {color = true, attr = null} = {}) =>
- (thing, {
- to,
- text = '',
- attributes = null,
- class: className = '',
- color: color2 = true,
- hash = ''
- }) => {
- let href = hrefFn(thing, {to});
+const linkHelper =
+ (hrefFn, { color = true, attr = null } = {}) =>
+ (
+ thing,
+ {
+ to,
+ text = "",
+ attributes = null,
+ class: className = "",
+ color: color2 = true,
+ hash = "",
+ }
+ ) => {
+ let href = hrefFn(thing, { to });
- if (link.globalOptions.appendIndexHTML) {
- if (appendIndexHTMLRegex.test(href)) {
- href += 'index.html';
- }
- }
+ if (link.globalOptions.appendIndexHTML) {
+ if (appendIndexHTMLRegex.test(href)) {
+ href += "index.html";
+ }
+ }
- if (hash) {
- href += (hash.startsWith('#') ? '' : '#') + hash;
- }
+ if (hash) {
+ href += (hash.startsWith("#") ? "" : "#") + hash;
+ }
- return html.tag('a', {
- ...attr ? attr(thing) : {},
- ...attributes ? attributes : {},
- href,
- style: (
- typeof color2 === 'string' ? getLinkThemeString(color2) :
- color2 && color ? getLinkThemeString(thing.color) :
- ''),
- class: className
- }, text || thing.name)
- };
+ return html.tag(
+ "a",
+ {
+ ...(attr ? attr(thing) : {}),
+ ...(attributes ? attributes : {}),
+ href,
+ style:
+ typeof color2 === "string"
+ ? getLinkThemeString(color2)
+ : color2 && color
+ ? getLinkThemeString(thing.color)
+ : "",
+ class: className,
+ },
+ text || thing.name
+ );
+ };
-const linkDirectory = (key, {expose = null, attr = null, ...conf} = {}) =>
- linkHelper((thing, {to}) => to('localized.' + key, thing.directory), {
- attr: thing => ({
- ...attr ? attr(thing) : {},
- ...expose ? {[expose]: thing.directory} : {}
- }),
- ...conf
- });
+const linkDirectory = (key, { expose = null, attr = null, ...conf } = {}) =>
+ linkHelper((thing, { to }) => to("localized." + key, thing.directory), {
+ attr: (thing) => ({
+ ...(attr ? attr(thing) : {}),
+ ...(expose ? { [expose]: thing.directory } : {}),
+ }),
+ ...conf,
+ });
-const linkPathname = (key, conf) => linkHelper(({directory: pathname}, {to}) => to(key, pathname), conf);
-const linkIndex = (key, conf) => linkHelper((_, {to}) => to('localized.' + key), conf);
+const linkPathname = (key, conf) =>
+ linkHelper(({ directory: pathname }, { to }) => to(key, pathname), conf);
+const linkIndex = (key, conf) =>
+ linkHelper((_, { to }) => to("localized." + key), conf);
const link = {
- globalOptions: {
- // This should usually only 8e used during development! It'll take any
- // href that ends with `/` and append `index.html` to the returned
- // value (for to.thing() functions). This is handy when developing
- // without a local server (i.e. using file:// protocol URLs in your
- // 8rowser), 8ut isn't guaranteed to 8e 100% 8ug-free.
- appendIndexHTML: false
- },
+ globalOptions: {
+ // This should usually only 8e used during development! It'll take any
+ // href that ends with `/` and append `index.html` to the returned
+ // value (for to.thing() functions). This is handy when developing
+ // without a local server (i.e. using file:// protocol URLs in your
+ // 8rowser), 8ut isn't guaranteed to 8e 100% 8ug-free.
+ appendIndexHTML: false,
+ },
- album: linkDirectory('album'),
- albumCommentary: linkDirectory('albumCommentary'),
- artist: linkDirectory('artist', {color: false}),
- artistGallery: linkDirectory('artistGallery', {color: false}),
- commentaryIndex: linkIndex('commentaryIndex', {color: false}),
- flashIndex: linkIndex('flashIndex', {color: false}),
- flash: linkDirectory('flash'),
- groupInfo: linkDirectory('groupInfo'),
- groupGallery: linkDirectory('groupGallery'),
- home: linkIndex('home', {color: false}),
- listingIndex: linkIndex('listingIndex'),
- listing: linkDirectory('listing'),
- newsIndex: linkIndex('newsIndex', {color: false}),
- newsEntry: linkDirectory('newsEntry', {color: false}),
- staticPage: linkDirectory('staticPage', {color: false}),
- tag: linkDirectory('tag'),
- track: linkDirectory('track', {expose: 'data-track'}),
+ album: linkDirectory("album"),
+ albumCommentary: linkDirectory("albumCommentary"),
+ artist: linkDirectory("artist", { color: false }),
+ artistGallery: linkDirectory("artistGallery", { color: false }),
+ commentaryIndex: linkIndex("commentaryIndex", { color: false }),
+ flashIndex: linkIndex("flashIndex", { color: false }),
+ flash: linkDirectory("flash"),
+ groupInfo: linkDirectory("groupInfo"),
+ groupGallery: linkDirectory("groupGallery"),
+ home: linkIndex("home", { color: false }),
+ listingIndex: linkIndex("listingIndex"),
+ listing: linkDirectory("listing"),
+ newsIndex: linkIndex("newsIndex", { color: false }),
+ newsEntry: linkDirectory("newsEntry", { color: false }),
+ staticPage: linkDirectory("staticPage", { color: false }),
+ tag: linkDirectory("tag"),
+ track: linkDirectory("track", { expose: "data-track" }),
- // TODO: This is a bit hacky. Files are just strings (not objects), so we
- // have to manually provide the album alongside the file. They also don't
- // follow the usual {name: whatever} type shape, so we have to provide that
- // ourselves.
- _albumAdditionalFileHelper: linkHelper(
- ((fakeFileObject, { to }) =>
- to('media.albumAdditionalFile', fakeFileObject.album.directory, fakeFileObject.name)),
- {color: false}),
- albumAdditionalFile: ({ file, album }, { to }) => link._albumAdditionalFileHelper({
+ // TODO: This is a bit hacky. Files are just strings (not objects), so we
+ // have to manually provide the album alongside the file. They also don't
+ // follow the usual {name: whatever} type shape, so we have to provide that
+ // ourselves.
+ _albumAdditionalFileHelper: linkHelper(
+ (fakeFileObject, { to }) =>
+ to(
+ "media.albumAdditionalFile",
+ fakeFileObject.album.directory,
+ fakeFileObject.name
+ ),
+ { color: false }
+ ),
+ albumAdditionalFile: ({ file, album }, { to }) =>
+ link._albumAdditionalFileHelper(
+ {
name: file,
- album
- }, {to}),
+ album,
+ },
+ { to }
+ ),
- media: linkPathname('media.path', {color: false}),
- root: linkPathname('shared.path', {color: false}),
- data: linkPathname('data.path', {color: false}),
- site: linkPathname('localized.path', {color: false})
+ media: linkPathname("media.path", { color: false }),
+ root: linkPathname("shared.path", { color: false }),
+ data: linkPathname("data.path", { color: false }),
+ site: linkPathname("localized.path", { color: false }),
};
export default link;
diff --git a/src/util/magic-constants.js b/src/util/magic-constants.js
index 73fdbc6d..c59e14aa 100644
--- a/src/util/magic-constants.js
+++ b/src/util/magic-constants.js
@@ -6,5 +6,5 @@
// All such uses should eventually be replaced with better code in due time
// (TM).
-export const OFFICIAL_GROUP_DIRECTORY = 'official';
-export const FANDOM_GROUP_DIRECTORY = 'fandom';
+export const OFFICIAL_GROUP_DIRECTORY = "official";
+export const FANDOM_GROUP_DIRECTORY = "fandom";
diff --git a/src/util/node-utils.js b/src/util/node-utils.js
index ad87cae3..889a276c 100644
--- a/src/util/node-utils.js
+++ b/src/util/node-utils.js
@@ -1,40 +1,43 @@
// Utility functions which are only relevant to particular Node.js constructs.
-import { fileURLToPath } from 'url';
+import { fileURLToPath } from "url";
-import _commandExists from 'command-exists';
+import _commandExists from "command-exists";
// This package throws an error instead of returning false when the command
// doesn't exist, for some reason. Yay for making logic more difficult!
// Here's a straightforward workaround.
export function commandExists(command) {
- return _commandExists(command).then(() => true, () => false);
+ return _commandExists(command).then(
+ () => true,
+ () => false
+ );
}
// Very cool function origin8ting in... http-music pro8a8ly!
// Sorry if we happen to 8e violating past-us's copyright, lmao.
export function promisifyProcess(proc, showLogging = true) {
- // Takes a process (from the child_process module) and returns a promise
- // that resolves when the process exits (or rejects, if the exit code is
- // non-zero).
- //
- // Ayy look, no alpha8etical second letter! Couldn't tell this was written
- // like three years ago 8efore I was me. 8888)
+ // Takes a process (from the child_process module) and returns a promise
+ // that resolves when the process exits (or rejects, if the exit code is
+ // non-zero).
+ //
+ // Ayy look, no alpha8etical second letter! Couldn't tell this was written
+ // like three years ago 8efore I was me. 8888)
- return new Promise((resolve, reject) => {
- if (showLogging) {
- proc.stdout.pipe(process.stdout);
- proc.stderr.pipe(process.stderr);
- }
+ return new Promise((resolve, reject) => {
+ if (showLogging) {
+ proc.stdout.pipe(process.stdout);
+ proc.stderr.pipe(process.stderr);
+ }
- proc.on('exit', code => {
- if (code === 0) {
- resolve();
- } else {
- reject(code);
- }
- })
- })
+ proc.on("exit", (code) => {
+ if (code === 0) {
+ resolve();
+ } else {
+ reject(code);
+ }
+ });
+ });
}
// Handy-dandy utility function for detecting whether the passed URL is the
@@ -42,5 +45,5 @@ export function promisifyProcess(proc, showLogging = true) {
// is great 'cuz (module === require.main) doesn't work without CommonJS
// modules.
export function isMain(importMetaURL) {
- return (process.argv[1] === fileURLToPath(importMetaURL));
+ return process.argv[1] === fileURLToPath(importMetaURL);
}
diff --git a/src/util/replacer.js b/src/util/replacer.js
index b29044f2..311f7633 100644
--- a/src/util/replacer.js
+++ b/src/util/replacer.js
@@ -1,429 +1,460 @@
-import {logError, logWarn} from './cli.js';
-import {escapeRegex} from './sugar.js';
-
-export function validateReplacerSpec(replacerSpec, {find, link}) {
- let success = true;
-
- for (const [key, {link: linkKey, find: findKey, value, html}] of Object.entries(replacerSpec)) {
- if (!html && !link[linkKey]) {
- logError`The replacer spec ${key} has invalid link key ${linkKey}! Specify it in link specs or fix typo.`;
- success = false;
- }
- if (findKey && !find[findKey]) {
- logError`The replacer spec ${key} has invalid find key ${findKey}! Specify it in find specs or fix typo.`;
- success = false;
- }
+import { logError, logWarn } from "./cli.js";
+import { escapeRegex } from "./sugar.js";
+
+export function validateReplacerSpec(replacerSpec, { find, link }) {
+ let success = true;
+
+ for (const [
+ key,
+ { link: linkKey, find: findKey, value, html },
+ ] of Object.entries(replacerSpec)) {
+ if (!html && !link[linkKey]) {
+ logError`The replacer spec ${key} has invalid link key ${linkKey}! Specify it in link specs or fix typo.`;
+ success = false;
}
+ if (findKey && !find[findKey]) {
+ logError`The replacer spec ${key} has invalid find key ${findKey}! Specify it in find specs or fix typo.`;
+ success = false;
+ }
+ }
- return success;
+ return success;
}
// Syntax literals.
-const tagBeginning = '[[';
-const tagEnding = ']]';
-const tagReplacerValue = ':';
-const tagHash = '#';
-const tagArgument = '*';
-const tagArgumentValue = '=';
-const tagLabel = '|';
+const tagBeginning = "[[";
+const tagEnding = "]]";
+const tagReplacerValue = ":";
+const tagHash = "#";
+const tagArgument = "*";
+const tagArgumentValue = "=";
+const tagLabel = "|";
-const noPrecedingWhitespace = '(? ({i, type: 'error', data: {message}});
-const endOfInput = (i, comment) => makeError(i, `Unexpected end of input (${comment}).`);
+const makeError = (i, message) => ({ i, type: "error", data: { message } });
+const endOfInput = (i, comment) =>
+ makeError(i, `Unexpected end of input (${comment}).`);
// These are 8asically stored on the glo8al scope, which might seem odd
// for a recursive function, 8ut the values are only ever used immediately
// after they're set.
-let stopped,
- stop_iMatch,
- stop_iParse,
- stop_literal;
+let stopped, stop_iMatch, stop_iParse, stop_literal;
function parseOneTextNode(input, i, stopAt) {
- return parseNodes(input, i, stopAt, true)[0];
+ return parseNodes(input, i, stopAt, true)[0];
}
function parseNodes(input, i, stopAt, textOnly) {
- let nodes = [];
- let escapeNext = false;
- let string = '';
- let iString = 0;
-
- stopped = false;
+ let nodes = [];
+ let escapeNext = false;
+ let string = "";
+ let iString = 0;
- const pushTextNode = (isLast) => {
- string = input.slice(iString, i);
+ stopped = false;
- // If this is the last text node 8efore stopping (at a stopAt match
- // or the end of the input), trim off whitespace at the end.
- if (isLast) {
- string = string.trimEnd();
- }
+ const pushTextNode = (isLast) => {
+ string = input.slice(iString, i);
- if (string.length) {
- nodes.push({i: iString, iEnd: i, type: 'text', data: string});
- string = '';
- }
- };
-
- const literalsToMatch = stopAt ? stopAt.concat([R_tagBeginning]) : [R_tagBeginning];
-
- // The 8ackslash stuff here is to only match an even (or zero) num8er
- // of sequential 'slashes. Even amounts always cancel out! Odd amounts
- // don't, which would mean the following literal is 8eing escaped and
- // should 8e counted only as part of the current string/text.
- //
- // Inspired 8y this: https://stackoverflow.com/a/41470813
- const regexpSource = `(?= 0) {
- lineStart += 1;
- } else {
- lineStart = 0;
- }
+ let lineStart = input.slice(0, i).lastIndexOf("\n");
+ if (lineStart >= 0) {
+ lineStart += 1;
+ } else {
+ lineStart = 0;
+ }
- let lineEnd = input.slice(i).indexOf('\n');
- if (lineEnd >= 0) {
- lineEnd += i;
- } else {
- lineEnd = input.length;
- }
+ let lineEnd = input.slice(i).indexOf("\n");
+ if (lineEnd >= 0) {
+ lineEnd += i;
+ } else {
+ lineEnd = input.length;
+ }
- const line = input.slice(lineStart, lineEnd);
+ const line = input.slice(lineStart, lineEnd);
- const cursor = i - lineStart;
+ const cursor = i - lineStart;
- throw new SyntaxError(fixWS`
+ throw new SyntaxError(fixWS`
Parse error (at pos ${i}): ${message}
${line}
- ${'-'.repeat(cursor) + '^'}
+ ${"-".repeat(cursor) + "^"}
`);
- }
+ }
}
function evaluateTag(node, opts) {
- const { find, input, language, link, replacerSpec, to, wikiData } = opts;
-
- const source = input.slice(node.i, node.iEnd);
-
- const replacerKeyImplied = !node.data.replacerKey;
- const replacerKey = (replacerKeyImplied
- ? 'track'
- : node.data.replacerKey.data);
-
- if (!replacerSpec[replacerKey]) {
- logWarn`The link ${source} has an invalid replacer key!`;
- return source;
- }
-
- const {
- find: findKey,
- link: linkKey,
- value: valueFn,
- html: htmlFn,
- transformName
- } = replacerSpec[replacerKey];
-
- const replacerValue = transformNodes(node.data.replacerValue, opts);
-
- const value = (
- valueFn ? valueFn(replacerValue) :
- findKey ? find[findKey]((replacerKeyImplied
- ? replacerValue
- : replacerKey + `:` + replacerValue)) :
- {
- directory: replacerValue,
- name: null
- });
-
- if (!value) {
- logWarn`The link ${source} does not match anything!`;
- return source;
- }
-
- const enteredLabel = node.data.label && transformNode(node.data.label, opts);
-
- const label = (enteredLabel
- || transformName && transformName(value.name, node, input)
- || value.name);
-
- if (!valueFn && !label) {
- logWarn`The link ${source} requires a label be entered!`;
- return source;
- }
-
- const hash = node.data.hash && transformNodes(node.data.hash, opts);
-
- const args = node.data.args && Object.fromEntries(node.data.args.map(
- ({ key, value }) => [
- transformNode(key, opts),
- transformNodes(value, opts)
- ]));
-
- const fn = (htmlFn
- ? htmlFn
- : link[linkKey]);
-
- try {
- return fn(value, {text: label, hash, args, language, to});
- } catch (error) {
- logError`The link ${source} failed to be processed: ${error}`;
- return source;
- }
+ const { find, input, language, link, replacerSpec, to, wikiData } = opts;
+
+ const source = input.slice(node.i, node.iEnd);
+
+ const replacerKeyImplied = !node.data.replacerKey;
+ const replacerKey = replacerKeyImplied ? "track" : node.data.replacerKey.data;
+
+ if (!replacerSpec[replacerKey]) {
+ logWarn`The link ${source} has an invalid replacer key!`;
+ return source;
+ }
+
+ const {
+ find: findKey,
+ link: linkKey,
+ value: valueFn,
+ html: htmlFn,
+ transformName,
+ } = replacerSpec[replacerKey];
+
+ const replacerValue = transformNodes(node.data.replacerValue, opts);
+
+ const value = valueFn
+ ? valueFn(replacerValue)
+ : findKey
+ ? find[findKey](
+ replacerKeyImplied ? replacerValue : replacerKey + `:` + replacerValue
+ )
+ : {
+ directory: replacerValue,
+ name: null,
+ };
+
+ if (!value) {
+ logWarn`The link ${source} does not match anything!`;
+ return source;
+ }
+
+ const enteredLabel = node.data.label && transformNode(node.data.label, opts);
+
+ const label =
+ enteredLabel ||
+ (transformName && transformName(value.name, node, input)) ||
+ value.name;
+
+ if (!valueFn && !label) {
+ logWarn`The link ${source} requires a label be entered!`;
+ return source;
+ }
+
+ const hash = node.data.hash && transformNodes(node.data.hash, opts);
+
+ const args =
+ node.data.args &&
+ Object.fromEntries(
+ node.data.args.map(({ key, value }) => [
+ transformNode(key, opts),
+ transformNodes(value, opts),
+ ])
+ );
+
+ const fn = htmlFn ? htmlFn : link[linkKey];
+
+ try {
+ return fn(value, { text: label, hash, args, language, to });
+ } catch (error) {
+ logError`The link ${source} failed to be processed: ${error}`;
+ return source;
+ }
}
function transformNode(node, opts) {
- if (!node) {
- throw new Error('Expected a node!');
- }
-
- if (Array.isArray(node)) {
- throw new Error('Got an array - use transformNodes here!');
- }
-
- switch (node.type) {
- case 'text':
- return node.data;
- case 'tag':
- return evaluateTag(node, opts);
- default:
- throw new Error(`Unknown node type ${node.type}`);
- }
+ if (!node) {
+ throw new Error("Expected a node!");
+ }
+
+ if (Array.isArray(node)) {
+ throw new Error("Got an array - use transformNodes here!");
+ }
+
+ switch (node.type) {
+ case "text":
+ return node.data;
+ case "tag":
+ return evaluateTag(node, opts);
+ default:
+ throw new Error(`Unknown node type ${node.type}`);
+ }
}
function transformNodes(nodes, opts) {
- if (!nodes || !Array.isArray(nodes)) {
- throw new Error(`Expected an array of nodes! Got: ${nodes}`);
- }
+ if (!nodes || !Array.isArray(nodes)) {
+ throw new Error(`Expected an array of nodes! Got: ${nodes}`);
+ }
- return nodes.map(node => transformNode(node, opts)).join('');
+ return nodes.map((node) => transformNode(node, opts)).join("");
}
-export function transformInline(input, {replacerSpec, find, link, language, to, wikiData}) {
- if (!replacerSpec) throw new Error('Expected replacerSpec');
- if (!find) throw new Error('Expected find');
- if (!link) throw new Error('Expected link');
- if (!language) throw new Error('Expected language');
- if (!to) throw new Error('Expected to');
- if (!wikiData) throw new Error('Expected wikiData');
-
- const nodes = parseInput(input);
- return transformNodes(nodes, {input, find, link, replacerSpec, language, to, wikiData});
+export function transformInline(
+ input,
+ { replacerSpec, find, link, language, to, wikiData }
+) {
+ if (!replacerSpec) throw new Error("Expected replacerSpec");
+ if (!find) throw new Error("Expected find");
+ if (!link) throw new Error("Expected link");
+ if (!language) throw new Error("Expected language");
+ if (!to) throw new Error("Expected to");
+ if (!wikiData) throw new Error("Expected wikiData");
+
+ const nodes = parseInput(input);
+ return transformNodes(nodes, {
+ input,
+ find,
+ link,
+ replacerSpec,
+ language,
+ to,
+ wikiData,
+ });
}
diff --git a/src/util/serialize.js b/src/util/serialize.js
index e30951f6..57736cf4 100644
--- a/src/util/serialize.js
+++ b/src/util/serialize.js
@@ -1,71 +1,70 @@
export function serializeLink(thing) {
- const ret = {};
- ret.name = thing.name;
- ret.directory = thing.directory;
- if (thing.color) ret.color = thing.color;
- return ret;
+ const ret = {};
+ ret.name = thing.name;
+ ret.directory = thing.directory;
+ if (thing.color) ret.color = thing.color;
+ return ret;
}
export function serializeContribs(contribs) {
- return contribs.map(({ who, what }) => {
- const ret = {};
- ret.artist = serializeLink(who);
- if (what) ret.contribution = what;
- return ret;
- });
+ return contribs.map(({ who, what }) => {
+ const ret = {};
+ ret.artist = serializeLink(who);
+ if (what) ret.contribution = what;
+ return ret;
+ });
}
-export function serializeImagePaths(original, {thumb}) {
- return {
- original,
- medium: thumb.medium(original),
- small: thumb.small(original)
- };
+export function serializeImagePaths(original, { thumb }) {
+ return {
+ original,
+ medium: thumb.medium(original),
+ small: thumb.small(original),
+ };
}
-export function serializeCover(thing, pathFunction, {
- serializeImagePaths,
- urls
-}) {
- const coverPath = pathFunction(thing, {
- to: urls.from('media.root').to
- });
+export function serializeCover(
+ thing,
+ pathFunction,
+ { serializeImagePaths, urls }
+) {
+ const coverPath = pathFunction(thing, {
+ to: urls.from("media.root").to,
+ });
- const { artTags } = thing;
+ const { artTags } = thing;
- const cwTags = artTags.filter(tag => tag.isContentWarning);
- const linkTags = artTags.filter(tag => !tag.isContentWarning);
+ const cwTags = artTags.filter((tag) => tag.isContentWarning);
+ const linkTags = artTags.filter((tag) => !tag.isContentWarning);
- return {
- paths: serializeImagePaths(coverPath),
- tags: linkTags.map(serializeLink),
- warnings: cwTags.map(tag => tag.name)
- };
+ return {
+ paths: serializeImagePaths(coverPath),
+ tags: linkTags.map(serializeLink),
+ warnings: cwTags.map((tag) => tag.name),
+ };
}
-export function serializeGroupsForAlbum(album, {
- serializeLink
-}) {
- return album.groups.map(group => {
- const index = group.albums.indexOf(album);
- const next = group.albums[index + 1] || null;
- const previous = group.albums[index - 1] || null;
- return {group, index, next, previous};
- }).map(({group, index, next, previous}) => ({
- link: serializeLink(group),
- descriptionShort: group.descriptionShort,
- albumIndex: index,
- nextAlbum: next && serializeLink(next),
- previousAlbum: previous && serializeLink(previous),
- urls: group.urls
+export function serializeGroupsForAlbum(album, { serializeLink }) {
+ return album.groups
+ .map((group) => {
+ const index = group.albums.indexOf(album);
+ const next = group.albums[index + 1] || null;
+ const previous = group.albums[index - 1] || null;
+ return { group, index, next, previous };
+ })
+ .map(({ group, index, next, previous }) => ({
+ link: serializeLink(group),
+ descriptionShort: group.descriptionShort,
+ albumIndex: index,
+ nextAlbum: next && serializeLink(next),
+ previousAlbum: previous && serializeLink(previous),
+ urls: group.urls,
}));
}
-export function serializeGroupsForTrack(track, {
- serializeLink
-}) {
- return track.album.groups.map(group => ({
- link: serializeLink(group),
- urls: group.urls,
- }));
+export function serializeGroupsForTrack(track, { serializeLink }) {
+ return track.album.groups.map((group) => ({
+ link: serializeLink(group),
+ urls: group.urls,
+ }));
}
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 99f706f1..70672bfd 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -6,69 +6,81 @@
// It will likely only do exactly what I want it to, and only in the cases I
// decided were relevant enough to 8other handling.
-import { color } from './cli.js';
+import { color } from "./cli.js";
// Apparently JavaScript doesn't come with a function to split an array into
// chunks! Weird. Anyway, this is an awesome place to use a generator, even
// though we don't really make use of the 8enefits of generators any time we
// actually use this. 8ut it's still awesome, 8ecause I say so.
export function* splitArray(array, fn) {
- let lastIndex = 0;
- while (lastIndex < array.length) {
- let nextIndex = array.findIndex((item, index) => index >= lastIndex && fn(item));
- if (nextIndex === -1) {
- nextIndex = array.length;
- }
- yield array.slice(lastIndex, nextIndex);
- // Plus one because we don't want to include the dividing line in the
- // next array we yield.
- lastIndex = nextIndex + 1;
+ let lastIndex = 0;
+ while (lastIndex < array.length) {
+ let nextIndex = array.findIndex(
+ (item, index) => index >= lastIndex && fn(item)
+ );
+ if (nextIndex === -1) {
+ nextIndex = array.length;
}
-};
+ yield array.slice(lastIndex, nextIndex);
+ // Plus one because we don't want to include the dividing line in the
+ // next array we yield.
+ lastIndex = nextIndex + 1;
+ }
+}
-export const mapInPlace = (array, fn) => array.splice(0, array.length, ...array.map(fn));
+export const mapInPlace = (array, fn) =>
+ array.splice(0, array.length, ...array.map(fn));
-export const filterEmptyLines = string => string.split('\n').filter(line => line.trim()).join('\n');
+export const filterEmptyLines = (string) =>
+ string
+ .split("\n")
+ .filter((line) => line.trim())
+ .join("\n");
-export const unique = arr => Array.from(new Set(arr));
+export const unique = (arr) => Array.from(new Set(arr));
-export const compareArrays = (arr1, arr2, {checkOrder = true} = {}) => (
- arr1.length === arr2.length && (checkOrder
- ? (arr1.every((x, i) => arr2[i] === x))
- : (arr1.every(x => arr2.includes(x)))));
+export const compareArrays = (arr1, arr2, { checkOrder = true } = {}) =>
+ arr1.length === arr2.length &&
+ (checkOrder
+ ? arr1.every((x, i) => arr2[i] === x)
+ : arr1.every((x) => arr2.includes(x)));
// Stolen from jq! Which pro8a8ly stole the concept from other places. Nice.
-export const withEntries = (obj, fn) => Object.fromEntries(fn(Object.entries(obj)));
+export const withEntries = (obj, fn) =>
+ Object.fromEntries(fn(Object.entries(obj)));
export function queue(array, max = 50) {
- if (max === 0) {
- return array.map(fn => fn());
- }
-
- const begin = [];
- let current = 0;
- const ret = array.map(fn => new Promise((resolve, reject) => {
+ if (max === 0) {
+ return array.map((fn) => fn());
+ }
+
+ const begin = [];
+ let current = 0;
+ const ret = array.map(
+ (fn) =>
+ new Promise((resolve, reject) => {
begin.push(() => {
- current++;
- Promise.resolve(fn()).then(value => {
- current--;
- if (current < max && begin.length) {
- begin.shift()();
- }
- resolve(value);
- }, reject);
+ current++;
+ Promise.resolve(fn()).then((value) => {
+ current--;
+ if (current < max && begin.length) {
+ begin.shift()();
+ }
+ resolve(value);
+ }, reject);
});
- }));
+ })
+ );
- for (let i = 0; i < max && begin.length; i++) {
- begin.shift()();
- }
+ for (let i = 0; i < max && begin.length; i++) {
+ begin.shift()();
+ }
- return ret;
+ return ret;
}
export function delay(ms) {
- return new Promise(res => setTimeout(res, ms));
+ return new Promise((res) => setTimeout(res, ms));
}
// Stolen from here: https://stackoverflow.com/a/3561711
@@ -76,22 +88,22 @@ export function delay(ms) {
// There's a proposal for a native JS function like this, 8ut it's not even
// past stage 1 yet: https://github.com/tc39/proposal-regex-escaping
export function escapeRegex(string) {
- return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+ return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
}
export function bindOpts(fn, bind) {
- const bindIndex = bind[bindOpts.bindIndex] ?? 1;
+ const bindIndex = bind[bindOpts.bindIndex] ?? 1;
- const bound = function(...args) {
- const opts = args[bindIndex] ?? {};
- return fn(...args.slice(0, bindIndex), {...bind, ...opts});
- };
+ const bound = function (...args) {
+ const opts = args[bindIndex] ?? {};
+ return fn(...args.slice(0, bindIndex), { ...bind, ...opts });
+ };
- Object.defineProperty(bound, 'name', {
- value: (fn.name ? `(options-bound) ${fn.name}` : `(options-bound)`)
- });
+ Object.defineProperty(bound, "name", {
+ value: fn.name ? `(options-bound) ${fn.name}` : `(options-bound)`,
+ });
- return bound;
+ return bound;
}
bindOpts.bindIndex = Symbol();
@@ -108,103 +120,108 @@ bindOpts.bindIndex = Symbol();
// object containing all caught errors (or doesn't throw anything if there were
// no errors).
export function openAggregate({
- // Constructor to use, defaulting to the builtin AggregateError class.
- // Anything passed here should probably extend from that! May be used for
- // letting callers programatically distinguish between multiple aggregate
- // errors.
- //
- // This should be provided using the aggregateThrows utility function.
- [openAggregate.errorClassSymbol]: errorClass = AggregateError,
-
- // Optional human-readable message to describe the aggregate error, if
- // constructed.
- message = '',
-
- // Value to return when a provided function throws an error. If this is a
- // function, it will be called with the arguments given to the function.
- // (This is primarily useful when wrapping a function and then providing it
- // to another utility, e.g. array.map().)
- returnOnFail = null
+ // Constructor to use, defaulting to the builtin AggregateError class.
+ // Anything passed here should probably extend from that! May be used for
+ // letting callers programatically distinguish between multiple aggregate
+ // errors.
+ //
+ // This should be provided using the aggregateThrows utility function.
+ [openAggregate.errorClassSymbol]: errorClass = AggregateError,
+
+ // Optional human-readable message to describe the aggregate error, if
+ // constructed.
+ message = "",
+
+ // Value to return when a provided function throws an error. If this is a
+ // function, it will be called with the arguments given to the function.
+ // (This is primarily useful when wrapping a function and then providing it
+ // to another utility, e.g. array.map().)
+ returnOnFail = null,
} = {}) {
- const errors = [];
-
- const aggregate = {};
-
- aggregate.wrap = fn => (...args) => {
- try {
- return fn(...args);
- } catch (error) {
- errors.push(error);
- return (typeof returnOnFail === 'function'
- ? returnOnFail(...args)
- : returnOnFail);
- }
- };
-
- aggregate.wrapAsync = fn => (...args) => {
- return fn(...args).then(
- value => value,
- error => {
- errors.push(error);
- return (typeof returnOnFail === 'function'
- ? returnOnFail(...args)
- : returnOnFail);
- });
- };
-
- aggregate.call = (fn, ...args) => {
- return aggregate.wrap(fn)(...args);
- };
-
- aggregate.callAsync = (fn, ...args) => {
- return aggregate.wrapAsync(fn)(...args);
- };
-
- aggregate.nest = (...args) => {
- return aggregate.call(() => withAggregate(...args));
+ const errors = [];
+
+ const aggregate = {};
+
+ aggregate.wrap =
+ (fn) =>
+ (...args) => {
+ try {
+ return fn(...args);
+ } catch (error) {
+ errors.push(error);
+ return typeof returnOnFail === "function"
+ ? returnOnFail(...args)
+ : returnOnFail;
+ }
};
- aggregate.nestAsync = (...args) => {
- return aggregate.callAsync(() => withAggregateAsync(...args));
- };
-
- aggregate.map = (...args) => {
- const parent = aggregate;
- const { result, aggregate: child } = mapAggregate(...args);
- parent.call(child.close);
- return result;
- };
-
- aggregate.mapAsync = async (...args) => {
- const parent = aggregate;
- const { result, aggregate: child } = await mapAggregateAsync(...args);
- parent.call(child.close);
- return result;
- };
-
- aggregate.filter = (...args) => {
- const parent = aggregate;
- const { result, aggregate: child } = filterAggregate(...args);
- parent.call(child.close);
- return result;
- };
-
- aggregate.throws = aggregateThrows;
-
- aggregate.close = () => {
- if (errors.length) {
- throw Reflect.construct(errorClass, [errors, message]);
+ aggregate.wrapAsync =
+ (fn) =>
+ (...args) => {
+ return fn(...args).then(
+ (value) => value,
+ (error) => {
+ errors.push(error);
+ return typeof returnOnFail === "function"
+ ? returnOnFail(...args)
+ : returnOnFail;
}
+ );
};
- return aggregate;
+ aggregate.call = (fn, ...args) => {
+ return aggregate.wrap(fn)(...args);
+ };
+
+ aggregate.callAsync = (fn, ...args) => {
+ return aggregate.wrapAsync(fn)(...args);
+ };
+
+ aggregate.nest = (...args) => {
+ return aggregate.call(() => withAggregate(...args));
+ };
+
+ aggregate.nestAsync = (...args) => {
+ return aggregate.callAsync(() => withAggregateAsync(...args));
+ };
+
+ aggregate.map = (...args) => {
+ const parent = aggregate;
+ const { result, aggregate: child } = mapAggregate(...args);
+ parent.call(child.close);
+ return result;
+ };
+
+ aggregate.mapAsync = async (...args) => {
+ const parent = aggregate;
+ const { result, aggregate: child } = await mapAggregateAsync(...args);
+ parent.call(child.close);
+ return result;
+ };
+
+ aggregate.filter = (...args) => {
+ const parent = aggregate;
+ const { result, aggregate: child } = filterAggregate(...args);
+ parent.call(child.close);
+ return result;
+ };
+
+ aggregate.throws = aggregateThrows;
+
+ aggregate.close = () => {
+ if (errors.length) {
+ throw Reflect.construct(errorClass, [errors, message]);
+ }
+ };
+
+ return aggregate;
}
-openAggregate.errorClassSymbol = Symbol('error class');
+openAggregate.errorClassSymbol = Symbol("error class");
// Utility function for providing {errorClass} parameter to aggregate functions.
export function aggregateThrows(errorClass) {
- return {[openAggregate.errorClassSymbol]: errorClass};
+ return { [openAggregate.errorClassSymbol]: errorClass };
}
// Performs an ordinary array map with the given function, collating into a
@@ -217,36 +234,38 @@ export function aggregateThrows(errorClass) {
// use aggregate.close() to throw the error. (This aggregate may be passed to a
// parent aggregate: `parent.call(aggregate.close)`!)
export function mapAggregate(array, fn, aggregateOpts) {
- return _mapAggregate('sync', null, array, fn, aggregateOpts);
+ return _mapAggregate("sync", null, array, fn, aggregateOpts);
}
-export function mapAggregateAsync(array, fn, {
- promiseAll = Promise.all.bind(Promise),
- ...aggregateOpts
-} = {}) {
- return _mapAggregate('async', promiseAll, array, fn, aggregateOpts);
+export function mapAggregateAsync(
+ array,
+ fn,
+ { promiseAll = Promise.all.bind(Promise), ...aggregateOpts } = {}
+) {
+ return _mapAggregate("async", promiseAll, array, fn, aggregateOpts);
}
// Helper function for mapAggregate which holds code common between sync and
// async versions.
export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) {
- const failureSymbol = Symbol();
-
- const aggregate = openAggregate({
- returnOnFail: failureSymbol,
- ...aggregateOpts
+ const failureSymbol = Symbol();
+
+ const aggregate = openAggregate({
+ returnOnFail: failureSymbol,
+ ...aggregateOpts,
+ });
+
+ if (mode === "sync") {
+ const result = array
+ .map(aggregate.wrap(fn))
+ .filter((value) => value !== failureSymbol);
+ return { result, aggregate };
+ } else {
+ return promiseAll(array.map(aggregate.wrapAsync(fn))).then((values) => {
+ const result = values.filter((value) => value !== failureSymbol);
+ return { result, aggregate };
});
-
- if (mode === 'sync') {
- const result = array.map(aggregate.wrap(fn))
- .filter(value => value !== failureSymbol);
- return {result, aggregate};
- } else {
- return promiseAll(array.map(aggregate.wrapAsync(fn))).then(values => {
- const result = values.filter(value => value !== failureSymbol);
- return {result, aggregate};
- });
- }
+ }
}
// Performs an ordinary array filter with the given function, collating into a
@@ -257,162 +276,174 @@ export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) {
//
// As with mapAggregate, the returned aggregate property is not yet closed.
export function filterAggregate(array, fn, aggregateOpts) {
- return _filterAggregate('sync', null, array, fn, aggregateOpts);
+ return _filterAggregate("sync", null, array, fn, aggregateOpts);
}
-export async function filterAggregateAsync(array, fn, {
- promiseAll = Promise.all.bind(Promise),
- ...aggregateOpts
-} = {}) {
- return _filterAggregate('async', promiseAll, array, fn, aggregateOpts);
+export async function filterAggregateAsync(
+ array,
+ fn,
+ { promiseAll = Promise.all.bind(Promise), ...aggregateOpts } = {}
+) {
+ return _filterAggregate("async", promiseAll, array, fn, aggregateOpts);
}
// Helper function for filterAggregate which holds code common between sync and
// async versions.
function _filterAggregate(mode, promiseAll, array, fn, aggregateOpts) {
- const failureSymbol = Symbol();
-
- const aggregate = openAggregate({
- returnOnFail: failureSymbol,
- ...aggregateOpts
+ const failureSymbol = Symbol();
+
+ const aggregate = openAggregate({
+ returnOnFail: failureSymbol,
+ ...aggregateOpts,
+ });
+
+ function filterFunction(value) {
+ // Filter out results which match the failureSymbol, i.e. errored
+ // inputs.
+ if (value === failureSymbol) return false;
+
+ // Always keep results which match the overridden returnOnFail
+ // value, if provided.
+ if (value === aggregateOpts.returnOnFail) return true;
+
+ // Otherwise, filter according to the returned value of the wrapped
+ // function.
+ return value.output;
+ }
+
+ function mapFunction(value) {
+ // Then turn the results back into their corresponding input, or, if
+ // provided, the overridden returnOnFail value.
+ return value === aggregateOpts.returnOnFail ? value : value.input;
+ }
+
+ function wrapperFunction(x, ...rest) {
+ return {
+ input: x,
+ output: fn(x, ...rest),
+ };
+ }
+
+ if (mode === "sync") {
+ const result = array
+ .map(
+ aggregate.wrap((input, index, array) => {
+ const output = fn(input, index, array);
+ return { input, output };
+ })
+ )
+ .filter(filterFunction)
+ .map(mapFunction);
+
+ return { result, aggregate };
+ } else {
+ return promiseAll(
+ array.map(
+ aggregate.wrapAsync(async (input, index, array) => {
+ const output = await fn(input, index, array);
+ return { input, output };
+ })
+ )
+ ).then((values) => {
+ const result = values.filter(filterFunction).map(mapFunction);
+
+ return { result, aggregate };
});
-
- function filterFunction(value) {
- // Filter out results which match the failureSymbol, i.e. errored
- // inputs.
- if (value === failureSymbol) return false;
-
- // Always keep results which match the overridden returnOnFail
- // value, if provided.
- if (value === aggregateOpts.returnOnFail) return true;
-
- // Otherwise, filter according to the returned value of the wrapped
- // function.
- return value.output;
- }
-
- function mapFunction(value) {
- // Then turn the results back into their corresponding input, or, if
- // provided, the overridden returnOnFail value.
- return (value === aggregateOpts.returnOnFail
- ? value
- : value.input);
- }
-
- function wrapperFunction(x, ...rest) {
- return {
- input: x,
- output: fn(x, ...rest)
- };
- }
-
- if (mode === 'sync') {
- const result = array
- .map(aggregate.wrap((input, index, array) => {
- const output = fn(input, index, array);
- return {input, output};
- }))
- .filter(filterFunction)
- .map(mapFunction);
-
- return {result, aggregate};
- } else {
- return promiseAll(array.map(aggregate.wrapAsync(async (input, index, array) => {
- const output = await fn(input, index, array);
- return {input, output};
- }))).then(values => {
- const result = values
- .filter(filterFunction)
- .map(mapFunction);
-
- return {result, aggregate};
- });
- }
+ }
}
// Totally sugar function for opening an aggregate, running the provided
// function with it, then closing the function and returning the result (if
// there's no throw).
export function withAggregate(aggregateOpts, fn) {
- return _withAggregate('sync', aggregateOpts, fn);
+ return _withAggregate("sync", aggregateOpts, fn);
}
export function withAggregateAsync(aggregateOpts, fn) {
- return _withAggregate('async', aggregateOpts, fn);
+ return _withAggregate("async", aggregateOpts, fn);
}
export function _withAggregate(mode, aggregateOpts, fn) {
- if (typeof aggregateOpts === 'function') {
- fn = aggregateOpts;
- aggregateOpts = {};
- }
-
- const aggregate = openAggregate(aggregateOpts);
+ if (typeof aggregateOpts === "function") {
+ fn = aggregateOpts;
+ aggregateOpts = {};
+ }
+
+ const aggregate = openAggregate(aggregateOpts);
+
+ if (mode === "sync") {
+ const result = fn(aggregate);
+ aggregate.close();
+ return result;
+ } else {
+ return fn(aggregate).then((result) => {
+ aggregate.close();
+ return result;
+ });
+ }
+}
- if (mode === 'sync') {
- const result = fn(aggregate);
- aggregate.close();
- return result;
+export function showAggregate(
+ topError,
+ { pathToFile = (p) => p, showTraces = true } = {}
+) {
+ const recursive = (error, { level }) => {
+ let header = showTraces
+ ? `[${error.constructor.name || "unnamed"}] ${
+ error.message || "(no message)"
+ }`
+ : error instanceof AggregateError
+ ? `[${error.message || "(no message)"}]`
+ : error.message || "(no message)";
+ if (showTraces) {
+ const stackLines = error.stack?.split("\n");
+ const stackLine = stackLines?.find(
+ (line) =>
+ line.trim().startsWith("at") &&
+ !line.includes("sugar") &&
+ !line.includes("node:") &&
+ !line.includes("
")
+ );
+ const tracePart = stackLine
+ ? "- " +
+ stackLine
+ .trim()
+ .replace(/file:\/\/(.*\.js)/, (match, pathname) =>
+ pathToFile(pathname)
+ )
+ : "(no stack trace)";
+ header += ` ${color.dim(tracePart)}`;
+ }
+ const bar = level % 2 === 0 ? "\u2502" : color.dim("\u254e");
+ const head = level % 2 === 0 ? "\u257f" : color.dim("\u257f");
+
+ if (error instanceof AggregateError) {
+ return (
+ header +
+ "\n" +
+ error.errors
+ .map((error) => recursive(error, { level: level + 1 }))
+ .flatMap((str) => str.split("\n"))
+ .map((line, i, lines) =>
+ i === 0 ? ` ${head} ${line}` : ` ${bar} ${line}`
+ )
+ .join("\n")
+ );
} else {
- return fn(aggregate).then(result => {
- aggregate.close();
- return result;
- });
+ return header;
}
-}
+ };
-export function showAggregate(topError, {
- pathToFile = p => p,
- showTraces = true
-} = {}) {
- const recursive = (error, {level}) => {
- let header = (showTraces
- ? `[${error.constructor.name || 'unnamed'}] ${error.message || '(no message)'}`
- : (error instanceof AggregateError
- ? `[${error.message || '(no message)'}]`
- : error.message || '(no message)'));
- if (showTraces) {
- const stackLines = error.stack?.split('\n');
- const stackLine = stackLines?.find(line =>
- line.trim().startsWith('at')
- && !line.includes('sugar')
- && !line.includes('node:')
- && !line.includes(''));
- const tracePart = (stackLine
- ? '- ' + stackLine.trim().replace(/file:\/\/(.*\.js)/, (match, pathname) => pathToFile(pathname))
- : '(no stack trace)');
- header += ` ${color.dim(tracePart)}`;
- }
- const bar = (level % 2 === 0
- ? '\u2502'
- : color.dim('\u254e'));
- const head = (level % 2 === 0
- ? '\u257f'
- : color.dim('\u257f'));
-
- if (error instanceof AggregateError) {
- return header + '\n' + (error.errors
- .map(error => recursive(error, {level: level + 1}))
- .flatMap(str => str.split('\n'))
- .map((line, i, lines) => (i === 0
- ? ` ${head} ${line}`
- : ` ${bar} ${line}`))
- .join('\n'));
- } else {
- return header;
- }
- };
-
- console.error(recursive(topError, {level: 0}));
+ console.error(recursive(topError, { level: 0 }));
}
export function decorateErrorWithIndex(fn) {
- return (x, index, array) => {
- try {
- return fn(x, index, array);
- } catch (error) {
- error.message = `(${color.yellow(`#${index + 1}`)}) ${error.message}`;
- throw error;
- }
+ return (x, index, array) => {
+ try {
+ return fn(x, index, array);
+ } catch (error) {
+ error.message = `(${color.yellow(`#${index + 1}`)}) ${error.message}`;
+ throw error;
}
+ };
}
diff --git a/src/util/urls.js b/src/util/urls.js
index e15c018b..8fc2aba7 100644
--- a/src/util/urls.js
+++ b/src/util/urls.js
@@ -8,117 +8,133 @@
// actual path strings. More a8stract operations using wiki data o8jects is
// the domain of link.js.
-import * as path from 'path';
-import { withEntries } from './sugar.js';
+import * as path from "path";
+import { withEntries } from "./sugar.js";
export function generateURLs(urlSpec) {
- const getValueForFullKey = (obj, fullKey, prop = null) => {
- const [ groupKey, subKey ] = fullKey.split('.');
- if (!groupKey || !subKey) {
- throw new Error(`Expected group key and subkey (got ${fullKey})`);
- }
-
- if (!obj.hasOwnProperty(groupKey)) {
- throw new Error(`Expected valid group key (got ${groupKey})`);
- }
-
- const group = obj[groupKey];
-
- if (!group.hasOwnProperty(subKey)) {
- throw new Error(`Expected valid subkey (got ${subKey} for group ${groupKey})`);
- }
-
- return {
- value: group[subKey],
- group
- };
+ const getValueForFullKey = (obj, fullKey, prop = null) => {
+ const [groupKey, subKey] = fullKey.split(".");
+ if (!groupKey || !subKey) {
+ throw new Error(`Expected group key and subkey (got ${fullKey})`);
+ }
+
+ if (!obj.hasOwnProperty(groupKey)) {
+ throw new Error(`Expected valid group key (got ${groupKey})`);
+ }
+
+ const group = obj[groupKey];
+
+ if (!group.hasOwnProperty(subKey)) {
+ throw new Error(
+ `Expected valid subkey (got ${subKey} for group ${groupKey})`
+ );
+ }
+
+ return {
+ value: group[subKey],
+ group,
};
+ };
- // This should be called on values which are going to be passed to
- // path.relative, because relative will resolve a leading slash as the root
- // directory of the working device, which we aren't looking for here.
- const trimLeadingSlash = P => P.startsWith('/') ? P.slice(1) : P;
+ // This should be called on values which are going to be passed to
+ // path.relative, because relative will resolve a leading slash as the root
+ // directory of the working device, which we aren't looking for here.
+ const trimLeadingSlash = (P) => (P.startsWith("/") ? P.slice(1) : P);
- const generateTo = (fromPath, fromGroup) => {
- const A = trimLeadingSlash(fromPath);
+ const generateTo = (fromPath, fromGroup) => {
+ const A = trimLeadingSlash(fromPath);
- const rebasePrefix = '../'.repeat((fromGroup.prefix || '').split('/').filter(Boolean).length);
+ const rebasePrefix = "../".repeat(
+ (fromGroup.prefix || "").split("/").filter(Boolean).length
+ );
- const pathHelper = (toPath, toGroup) => {
- let B = trimLeadingSlash(toPath);
+ const pathHelper = (toPath, toGroup) => {
+ let B = trimLeadingSlash(toPath);
- let argIndex = 0;
- B = B.replaceAll('<>', () => `<${argIndex++}>`);
+ let argIndex = 0;
+ B = B.replaceAll("<>", () => `<${argIndex++}>`);
- if (toGroup.prefix !== fromGroup.prefix) {
- // TODO: Handle differing domains in prefixes.
- B = rebasePrefix + (toGroup.prefix || '') + B;
- }
+ if (toGroup.prefix !== fromGroup.prefix) {
+ // TODO: Handle differing domains in prefixes.
+ B = rebasePrefix + (toGroup.prefix || "") + B;
+ }
- const suffix = (toPath.endsWith('/') ? '/' : '');
+ const suffix = toPath.endsWith("/") ? "/" : "";
- return {
- posix: path.posix.relative(A, B) + suffix,
- device: path.relative(A, B) + suffix
- };
- };
-
- const groupSymbol = Symbol();
+ return {
+ posix: path.posix.relative(A, B) + suffix,
+ device: path.relative(A, B) + suffix,
+ };
+ };
- const groupHelper = urlGroup => ({
- [groupSymbol]: urlGroup,
- ...withEntries(urlGroup.paths, entries => entries
- .map(([key, path]) => [key, pathHelper(path, urlGroup)]))
+ const groupSymbol = Symbol();
+
+ const groupHelper = (urlGroup) => ({
+ [groupSymbol]: urlGroup,
+ ...withEntries(urlGroup.paths, (entries) =>
+ entries.map(([key, path]) => [key, pathHelper(path, urlGroup)])
+ ),
+ });
+
+ const relative = withEntries(urlSpec, (entries) =>
+ entries.map(([key, urlGroup]) => [key, groupHelper(urlGroup)])
+ );
+
+ const toHelper =
+ (delimiterMode) =>
+ (key, ...args) => {
+ const {
+ value: { [delimiterMode]: template },
+ } = getValueForFullKey(relative, key);
+
+ let missing = 0;
+ let result = template.replaceAll(/<([0-9]+)>/g, (match, n) => {
+ if (n < args.length) {
+ return args[n];
+ } else {
+ missing++;
+ }
});
- const relative = withEntries(urlSpec, entries => entries
- .map(([key, urlGroup]) => [key, groupHelper(urlGroup)]));
-
- const toHelper = (delimiterMode) => (key, ...args) => {
- const {
- value: {[delimiterMode]: template}
- } = getValueForFullKey(relative, key);
-
- let missing = 0;
- let result = template.replaceAll(/<([0-9]+)>/g, (match, n) => {
- if (n < args.length) {
- return args[n];
- } else {
- missing++;
- }
- });
-
- if (missing) {
- throw new Error(`Expected ${missing + args.length} arguments, got ${args.length} (key ${key}, args [${args}])`);
- }
-
- return result;
- };
-
- return {
- to: toHelper('posix'),
- toDevice: toHelper('device')
- };
+ if (missing) {
+ throw new Error(
+ `Expected ${missing + args.length} arguments, got ${
+ args.length
+ } (key ${key}, args [${args}])`
+ );
+ }
+
+ return result;
+ };
+
+ return {
+ to: toHelper("posix"),
+ toDevice: toHelper("device"),
};
+ };
- const generateFrom = () => {
- const map = withEntries(urlSpec, entries => entries
- .map(([key, group]) => [key, withEntries(group.paths, entries => entries
- .map(([key, path]) => [key, generateTo(path, group)])
- )]));
+ const generateFrom = () => {
+ const map = withEntries(urlSpec, (entries) =>
+ entries.map(([key, group]) => [
+ key,
+ withEntries(group.paths, (entries) =>
+ entries.map(([key, path]) => [key, generateTo(path, group)])
+ ),
+ ])
+ );
- const from = key => getValueForFullKey(map, key).value;
+ const from = (key) => getValueForFullKey(map, key).value;
- return {from, map};
- };
+ return { from, map };
+ };
- return generateFrom();
+ return generateFrom();
}
-const thumbnailHelper = name => file =>
- file.replace(/\.(jpg|png)$/, name + '.jpg');
+const thumbnailHelper = (name) => (file) =>
+ file.replace(/\.(jpg|png)$/, name + ".jpg");
export const thumb = {
- medium: thumbnailHelper('.medium'),
- small: thumbnailHelper('.small')
+ medium: thumbnailHelper(".medium"),
+ small: thumbnailHelper(".small"),
};
diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js
index 5aef812d..f7610fdb 100644
--- a/src/util/wiki-data.js
+++ b/src/util/wiki-data.js
@@ -3,63 +3,64 @@
// Generic value operations
export function getKebabCase(name) {
- return name
- .split(' ')
- .join('-')
- .replace(/&/g, 'and')
- .replace(/[^a-zA-Z0-9\-]/g, '')
- .replace(/-{2,}/g, '-')
- .replace(/^-+|-+$/g, '')
- .toLowerCase();
+ return name
+ .split(" ")
+ .join("-")
+ .replace(/&/g, "and")
+ .replace(/[^a-zA-Z0-9\-]/g, "")
+ .replace(/-{2,}/g, "-")
+ .replace(/^-+|-+$/g, "")
+ .toLowerCase();
}
export function chunkByConditions(array, conditions) {
- if (array.length === 0) {
- return [];
- } else if (conditions.length === 0) {
- return [array];
+ if (array.length === 0) {
+ return [];
+ } else if (conditions.length === 0) {
+ 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;
+ }
}
-
- 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);
- }
+ if (chunk) {
+ out.push(cur);
+ cur = [item];
+ } else {
+ cur.push(item);
}
- out.push(cur);
- return out;
+ }
+ 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
- }));
+ 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,
+ }));
}
// Sorting functions - all utils here are mutating, so make sure to initially
@@ -71,37 +72,42 @@ export function chunkByProperties(array, properties) {
// 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.
+ // Compare two strings without considering capitalization... unless they
+ // happen to be the same that way.
- const al = a.toLowerCase();
- const bl = b.toLowerCase();
+ const al = a.toLowerCase();
+ const bl = b.toLowerCase();
- return (al === bl
- ? a.localeCompare(b, undefined, {numeric: true})
- : al.localeCompare(bl, undefined, {numeric: true}));
+ 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, '');
-
- // Remove common English (only, for now) prefixes.
- s = s.replace(/^(?:an?|the) /i, '');
-
- return 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, "");
+
+ // 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
@@ -132,106 +138,103 @@ export function normalizeName(s) {
// ...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 = o => o.directory
-} = {}) {
- return data.sort((a, b) => {
- const ad = getDirectory(a);
- const bd = getDirectory(b);
- return compareCaseLessSensitive(ad, bd)
- });
+export function sortByDirectory(
+ data,
+ { getDirectory = (o) => o.directory } = {}
+) {
+ return data.sort((a, b) => {
+ const ad = getDirectory(a);
+ const bd = getDirectory(b);
+ return compareCaseLessSensitive(ad, bd);
+ });
}
-export function sortByName(data, {
- getName = o => o.name
-} = {}) {
- return data.sort((a, b) => {
- const an = getName(a);
- const bn = getName(b);
- const ann = normalizeName(an);
- const bnn = normalizeName(bn);
- return (
- compareCaseLessSensitive(ann, bnn) ||
- compareCaseLessSensitive(an, bn));
- });
+export function sortByName(data, { getName = (o) => o.name } = {}) {
+ return data.sort((a, b) => {
+ const an = getName(a);
+ const bn = getName(b);
+ const ann = normalizeName(an);
+ const bnn = normalizeName(bn);
+ return (
+ compareCaseLessSensitive(ann, bnn) || compareCaseLessSensitive(an, bn)
+ );
+ });
}
-export function sortByDate(data, {
- getDate = o => o.date
-} = {}) {
- return data.sort((a, b) => {
- const ad = getDate(a);
- const bd = getDate(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 (ad && bd) {
- return ad - bd;
- } else if (ad) {
- return -1;
- } else if (bd) {
- return 1;
- } else {
- // 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 sortByDate(data, { getDate = (o) => o.date } = {}) {
+ return data.sort((a, b) => {
+ const ad = getDate(a);
+ const bd = getDate(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 (ad && bd) {
+ return ad - bd;
+ } else if (ad) {
+ return -1;
+ } else if (bd) {
+ return 1;
+ } else {
+ // 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 sortByPositionInAlbum(data) {
- return data.sort((a, b) => {
- const aa = a.album;
- const ba = b.album;
-
- // Don't change the sort when the two tracks are from separate albums.
- // This function doesn't change the order of albums or try to "merge"
- // two separated chunks of tracks from the same album together.
- if (aa !== ba) {
- return 0;
- }
+ return data.sort((a, b) => {
+ const aa = a.album;
+ const ba = b.album;
+
+ // Don't change the sort when the two tracks are from separate albums.
+ // This function doesn't change the order of albums or try to "merge"
+ // two separated chunks of tracks from the same album together.
+ if (aa !== ba) {
+ return 0;
+ }
- // Don't change the sort when only one (or neither) item is actually
- // a track (i.e. has an album).
- if (!aa || !ba) {
- return 0;
- }
+ // Don't change the sort when only one (or neither) item is actually
+ // a track (i.e. has an album).
+ if (!aa || !ba) {
+ return 0;
+ }
- const ai = aa.tracks.indexOf(a);
- const bi = ba.tracks.indexOf(b);
+ const ai = aa.tracks.indexOf(a);
+ const bi = ba.tracks.indexOf(b);
- // There's no reason this two-way reference (a track's album and the
- // album's track list) should be broken, but if for any reason it is,
- // don't change the sort.
- if (ai === -1 || bi === -1) {
- return 0;
- }
+ // There's no reason this two-way reference (a track's album and the
+ // album's track list) should be broken, but if for any reason it is,
+ // don't change the sort.
+ if (ai === -1 || bi === -1) {
+ return 0;
+ }
- return ai - bi;
- });
+ return ai - bi;
+ });
}
// 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) {
- 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;
- }
- });
+ 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
@@ -249,20 +252,23 @@ export function sortByConditions(data, conditions) {
// 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;
+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, {getDirectory, getName, getDate} = {}) {
- sortAlphabetically(data, {getDirectory, getName});
- sortByDate(data, {getDate});
- return data;
+export function sortChronologically(
+ data,
+ { getDirectory, getName, getDate } = {}
+) {
+ sortAlphabetically(data, { getDirectory, getName });
+ sortByDate(data, { getDate });
+ return data;
}
// Highly contextual sort functions - these are only for very specific types
@@ -273,44 +279,46 @@ export function sortChronologically(data, {getDirectory, getName, getDate} = {})
// release date but can be overridden) above all else.
//
// This function also works for data lists which contain only tracks.
-export function sortAlbumsTracksChronologically(data, {getDate} = {}) {
- // Sort albums before tracks...
- sortByConditions(data, [t => t.album === undefined]);
+export function sortAlbumsTracksChronologically(data, { 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)
- });
+ // Group tracks by album...
+ sortByDirectory(data, {
+ getDirectory: (t) => (t.album ? t.album.directory : t.directory),
+ });
- // Sort tracks by position in album...
- sortByPositionInAlbum(data);
+ // 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, {getDate});
+ // ...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, { getDate });
- return data;
+ return data;
}
// Specific data utilities
export function filterAlbumsByCommentary(albums) {
- return albums.filter(album => [album, ...album.tracks].some(x => x.commentary));
+ return albums.filter((album) =>
+ [album, ...album.tracks].some((x) => x.commentary)
+ );
}
-export function getAlbumCover(album, {to}) {
- // Some albums don't have art! This function returns null in that case.
- if (album.hasCoverArt) {
- return to('media.albumCover', album.directory, album.coverArtFileExtension);
- } else {
- return null;
- }
+export function getAlbumCover(album, { to }) {
+ // Some albums don't have art! This function returns null in that case.
+ if (album.hasCoverArt) {
+ return to("media.albumCover", album.directory, album.coverArtFileExtension);
+ } else {
+ return null;
+ }
}
export function getAlbumListTag(album) {
- return (album.hasTrackNumbers ? 'ol' : 'ul');
+ return album.hasTrackNumbers ? "ol" : "ul";
}
// This gets all the track o8jects defined in every al8um, and sorts them 8y
@@ -331,157 +339,169 @@ export function getAlbumListTag(album) {
// d8s, 8ut still keep the al8um listing in a specific order, since that isn't
// sorted 8y date.
export function getAllTracks(albumData) {
- return sortByDate(albumData.flatMap(album => album.tracks));
+ return sortByDate(albumData.flatMap((album) => album.tracks));
}
export function getArtistNumContributions(artist) {
- return (
- (artist.tracksAsAny?.length ?? 0) +
- (artist.albumsAsCoverArtist?.length ?? 0) +
- (artist.flashesAsContributor?.length ?? 0)
- );
+ return (
+ (artist.tracksAsAny?.length ?? 0) +
+ (artist.albumsAsCoverArtist?.length ?? 0) +
+ (artist.flashesAsContributor?.length ?? 0)
+ );
}
-export function getFlashCover(flash, {to}) {
- return to('media.flashArt', flash.directory, flash.coverArtFileExtension);
+export function getFlashCover(flash, { to }) {
+ return to("media.flashArt", flash.directory, flash.coverArtFileExtension);
}
export function getFlashLink(flash) {
- return `https://homestuck.com/story/${flash.page}`;
+ return `https://homestuck.com/story/${flash.page}`;
}
export function getTotalDuration(tracks) {
- return tracks.reduce((duration, track) => duration + track.duration, 0);
+ return tracks.reduce((duration, track) => duration + track.duration, 0);
}
-export function getTrackCover(track, {to}) {
- // Some albums don't have any track art at all, and in those, every track
- // just inherits the album's own cover art. Note that since cover art isn't
- // guaranteed on albums either, it's possible that this function returns
- // null!
- if (!track.hasCoverArt) {
- return getAlbumCover(track.album, {to});
- } else {
- return to('media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension);
- }
+export function getTrackCover(track, { to }) {
+ // Some albums don't have any track art at all, and in those, every track
+ // just inherits the album's own cover art. Note that since cover art isn't
+ // guaranteed on albums either, it's possible that this function returns
+ // null!
+ if (!track.hasCoverArt) {
+ return getAlbumCover(track.album, { to });
+ } else {
+ return to(
+ "media.trackCover",
+ track.album.directory,
+ track.directory,
+ track.coverArtFileExtension
+ );
+ }
}
-export function getArtistAvatar(artist, {to}) {
- return to('media.artistAvatar', artist.directory, artist.avatarFileExtension);
+export function getArtistAvatar(artist, { to }) {
+ return to("media.artistAvatar", artist.directory, artist.avatarFileExtension);
}
// Big-ass homepage row functions
-export function getNewAdditions(numAlbums, {wikiData}) {
- const { albumData } = wikiData;
-
- // Sort al8ums, in descending order of priority, 8y...
- //
- // * D8te of addition to the wiki (descending).
- // * Major releases first.
- // * D8te of release (descending).
- //
- // Major releases go first to 8etter ensure they show up in the list (and
- // are usually at the start of the final output for a given d8 of release
- // too).
- const sortedAlbums = albumData.filter(album => album.isListedOnHomepage).sort((a, b) => {
- if (a.dateAddedToWiki > b.dateAddedToWiki) return -1;
- if (a.dateAddedToWiki < b.dateAddedToWiki) return 1;
- if (a.isMajorRelease && !b.isMajorRelease) return -1;
- if (!a.isMajorRelease && b.isMajorRelease) return 1;
- if (a.date > b.date) return -1;
- if (a.date < b.date) return 1;
+export function getNewAdditions(numAlbums, { wikiData }) {
+ const { albumData } = wikiData;
+
+ // Sort al8ums, in descending order of priority, 8y...
+ //
+ // * D8te of addition to the wiki (descending).
+ // * Major releases first.
+ // * D8te of release (descending).
+ //
+ // Major releases go first to 8etter ensure they show up in the list (and
+ // are usually at the start of the final output for a given d8 of release
+ // too).
+ const sortedAlbums = albumData
+ .filter((album) => album.isListedOnHomepage)
+ .sort((a, b) => {
+ if (a.dateAddedToWiki > b.dateAddedToWiki) return -1;
+ if (a.dateAddedToWiki < b.dateAddedToWiki) return 1;
+ if (a.isMajorRelease && !b.isMajorRelease) return -1;
+ if (!a.isMajorRelease && b.isMajorRelease) return 1;
+ if (a.date > b.date) return -1;
+ if (a.date < b.date) return 1;
});
- // When multiple al8ums are added to the wiki at a time, we want to show
- // all of them 8efore pulling al8ums from the next (earlier) date. We also
- // want to show a diverse selection of al8ums - with limited space, we'd
- // rather not show only the latest al8ums, if those happen to all 8e
- // closely rel8ted!
- //
- // Specifically, we're concerned with avoiding too much overlap amongst
- // the primary (first/top-most) group. We do this 8y collecting every
- // primary group present amongst the al8ums for a given d8 into one
- // (ordered) array, initially sorted (inherently) 8y latest al8um from
- // the group. Then we cycle over the array, adding one al8um from each
- // group until all the al8ums from that release d8 have 8een added (or
- // we've met the total target num8er of al8ums). Once we've added all the
- // al8ums for a given group, it's struck from the array (so the groups
- // with the most additions on one d8 will have their oldest releases
- // collected more towards the end of the list).
-
- const albums = [];
-
- let i = 0;
- outerLoop: while (i < sortedAlbums.length) {
- // 8uild up a list of groups and their al8ums 8y order of decending
- // release, iter8ting until we're on a different d8. (We use a map for
- // indexing so we don't have to iter8te through the entire array each
- // time we access one of its entries. This is 8asically unnecessary
- // since this will never 8e an expensive enough task for that to
- // matter.... 8ut it's nicer code. BBBB) )
- const currentDate = sortedAlbums[i].dateAddedToWiki;
- const groupMap = new Map();
- const groupArray = [];
- for (let album; (album = sortedAlbums[i]) && +album.dateAddedToWiki === +currentDate; i++) {
- const primaryGroup = album.groups[0];
- if (groupMap.has(primaryGroup)) {
- groupMap.get(primaryGroup).push(album);
- } else {
- const entry = [album]
- groupMap.set(primaryGroup, entry);
- groupArray.push(entry);
- }
+ // When multiple al8ums are added to the wiki at a time, we want to show
+ // all of them 8efore pulling al8ums from the next (earlier) date. We also
+ // want to show a diverse selection of al8ums - with limited space, we'd
+ // rather not show only the latest al8ums, if those happen to all 8e
+ // closely rel8ted!
+ //
+ // Specifically, we're concerned with avoiding too much overlap amongst
+ // the primary (first/top-most) group. We do this 8y collecting every
+ // primary group present amongst the al8ums for a given d8 into one
+ // (ordered) array, initially sorted (inherently) 8y latest al8um from
+ // the group. Then we cycle over the array, adding one al8um from each
+ // group until all the al8ums from that release d8 have 8een added (or
+ // we've met the total target num8er of al8ums). Once we've added all the
+ // al8ums for a given group, it's struck from the array (so the groups
+ // with the most additions on one d8 will have their oldest releases
+ // collected more towards the end of the list).
+
+ const albums = [];
+
+ let i = 0;
+ outerLoop: while (i < sortedAlbums.length) {
+ // 8uild up a list of groups and their al8ums 8y order of decending
+ // release, iter8ting until we're on a different d8. (We use a map for
+ // indexing so we don't have to iter8te through the entire array each
+ // time we access one of its entries. This is 8asically unnecessary
+ // since this will never 8e an expensive enough task for that to
+ // matter.... 8ut it's nicer code. BBBB) )
+ const currentDate = sortedAlbums[i].dateAddedToWiki;
+ const groupMap = new Map();
+ const groupArray = [];
+ for (
+ let album;
+ (album = sortedAlbums[i]) && +album.dateAddedToWiki === +currentDate;
+ i++
+ ) {
+ const primaryGroup = album.groups[0];
+ if (groupMap.has(primaryGroup)) {
+ groupMap.get(primaryGroup).push(album);
+ } else {
+ const entry = [album];
+ groupMap.set(primaryGroup, entry);
+ groupArray.push(entry);
+ }
+ }
+
+ // Then cycle over that sorted array, adding one al8um from each to
+ // the main array until we've run out or have met the target num8er
+ // of al8ums.
+ while (groupArray.length) {
+ let j = 0;
+ while (j < groupArray.length) {
+ const entry = groupArray[j];
+ const album = entry.shift();
+ albums.push(album);
+
+ // This is the only time we ever add anything to the main al8um
+ // list, so it's also the only place we need to check if we've
+ // met the target length.
+ if (albums.length === numAlbums) {
+ // If we've met it, 8r8k out of the outer loop - we're done
+ // here!
+ break outerLoop;
}
- // Then cycle over that sorted array, adding one al8um from each to
- // the main array until we've run out or have met the target num8er
- // of al8ums.
- while (groupArray.length) {
- let j = 0;
- while (j < groupArray.length) {
- const entry = groupArray[j];
- const album = entry.shift();
- albums.push(album);
-
-
- // This is the only time we ever add anything to the main al8um
- // list, so it's also the only place we need to check if we've
- // met the target length.
- if (albums.length === numAlbums) {
- // If we've met it, 8r8k out of the outer loop - we're done
- // here!
- break outerLoop;
- }
-
- if (entry.length) {
- j++;
- } else {
- groupArray.splice(j, 1);
- }
- }
+ if (entry.length) {
+ j++;
+ } else {
+ groupArray.splice(j, 1);
}
+ }
}
+ }
- // Finally, do some quick mapping shenanigans to 8etter display the result
- // in a grid. (This should pro8a8ly 8e a separ8te, shared function, 8ut
- // whatevs.)
- return albums.map(album => ({large: album.isMajorRelease, item: album}));
+ // Finally, do some quick mapping shenanigans to 8etter display the result
+ // in a grid. (This should pro8a8ly 8e a separ8te, shared function, 8ut
+ // whatevs.)
+ return albums.map((album) => ({ large: album.isMajorRelease, item: album }));
}
-export function getNewReleases(numReleases, {wikiData}) {
- const { albumData } = wikiData;
+export function getNewReleases(numReleases, { wikiData }) {
+ const { albumData } = wikiData;
- const latestFirst = albumData.filter(album => album.isListedOnHomepage).reverse();
- const majorReleases = latestFirst.filter(album => album.isMajorRelease);
- majorReleases.splice(1);
+ const latestFirst = albumData
+ .filter((album) => album.isListedOnHomepage)
+ .reverse();
+ const majorReleases = latestFirst.filter((album) => album.isMajorRelease);
+ majorReleases.splice(1);
- const otherReleases = latestFirst
- .filter(album => !majorReleases.includes(album))
- .slice(0, numReleases - majorReleases.length);
+ const otherReleases = latestFirst
+ .filter((album) => !majorReleases.includes(album))
+ .slice(0, numReleases - majorReleases.length);
- return [
- ...majorReleases.map(album => ({large: true, item: album})),
- ...otherReleases.map(album => ({large: false, item: album}))
- ];
+ return [
+ ...majorReleases.map((album) => ({ large: true, item: album })),
+ ...otherReleases.map((album) => ({ large: false, item: album })),
+ ];
}
--
cgit 1.3.0-6-gf8a5