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 --- .eslintrc.json | 14 + .prettierrc.json | 1 + package-lock.json | 1387 ++++++++++++++- package.json | 2 + 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 +++---- 48 files changed, 14699 insertions(+), 11213 deletions(-) create mode 100644 .eslintrc.json create mode 100644 .prettierrc.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..066effe --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + } +} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/package-lock.json b/package-lock.json index e8a4281..2eb1495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,112 @@ "hsmusic": "src/upd8.js" }, "devDependencies": { + "eslint": "^8.18.0", + "prettier": "2.7.1", "tape": "^5.4.1" } }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -84,12 +187,86 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/deep-equal": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", @@ -116,6 +293,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -134,6 +317,18 @@ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", "dev": true }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotignore": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", @@ -216,11 +411,229 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz", + "integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "dependencies": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/fix-whitespace": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fix-whitespace/-/fix-whitespace-1.0.4.tgz", "integrity": "sha512-TYJpw4orIgDpaINRkw1BVJQF8rPTNSUbW/s4mLYSApUt0MquGfI+iripYHibg9l9fe795VauuVCLTpDvy8KFWQ==" }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -248,6 +661,12 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -307,6 +726,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -341,6 +787,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", @@ -376,6 +831,40 @@ "he": "bin/he" } }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -489,6 +978,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -648,6 +1158,12 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -659,6 +1175,37 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -677,6 +1224,18 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -738,6 +1297,35 @@ "wrappy": "1" } }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -747,12 +1335,54 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz", @@ -769,6 +1399,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, "node_modules/resolve": { "version": "2.0.0-next.3", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", @@ -782,6 +1424,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/resumer": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", @@ -791,6 +1442,42 @@ "through": "~2.3.4" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -832,20 +1519,56 @@ "define-properties": "^1.1.3" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "has-flag": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, "node_modules/tape": { @@ -880,12 +1603,42 @@ "tape": "bin/tape" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -901,6 +1654,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -952,6 +1735,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -960,6 +1752,80 @@ } }, "dependencies": { + "@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1009,12 +1875,63 @@ "get-intrinsic": "^1.0.2" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, "deep-equal": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", @@ -1038,6 +1955,12 @@ "which-typed-array": "^1.1.2" } }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1053,6 +1976,15 @@ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", "dev": true }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "dotignore": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", @@ -1117,11 +2049,177 @@ "is-symbol": "^1.0.2" } }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz", + "integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "requires": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, "fix-whitespace": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fix-whitespace/-/fix-whitespace-1.0.4.tgz", "integrity": "sha512-TYJpw4orIgDpaINRkw1BVJQF8rPTNSUbW/s4mLYSApUt0MquGfI+iripYHibg9l9fe795VauuVCLTpDvy8KFWQ==" }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -1149,6 +2247,12 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -1190,6 +2294,24 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -1215,6 +2337,12 @@ "get-intrinsic": "^1.1.1" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", @@ -1235,6 +2363,28 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1315,6 +2465,21 @@ "has-tostringtag": "^1.0.0" } }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -1420,6 +2585,12 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1428,6 +2599,34 @@ "argparse": "^2.0.1" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1443,6 +2642,18 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -1486,18 +2697,65 @@ "wrappy": "1" } }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, "regexp.prototype.flags": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz", @@ -1508,6 +2766,12 @@ "define-properties": "^1.1.3" } }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, "resolve": { "version": "2.0.0-next.3", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", @@ -1518,6 +2782,12 @@ "path-parse": "^1.0.6" } }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, "resumer": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", @@ -1527,6 +2797,30 @@ "through": "~2.3.4" } }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -1569,6 +2863,30 @@ "define-properties": "^1.1.3" } }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, "tape": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/tape/-/tape-5.4.1.tgz", @@ -1598,12 +2916,33 @@ "through": "^2.3.8" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -1616,6 +2955,30 @@ "which-boxed-primitive": "^1.0.2" } }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, "which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -1655,6 +3018,12 @@ "is-typed-array": "^1.1.7" } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index c48d868..938ec6a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ }, "license": "GPL-3.0", "devDependencies": { + "eslint": "^8.18.0", + "prettier": "2.7.1", "tape": "^5.4.1" } } diff --git a/src/data/cacheable-object.js b/src/data/cacheable-object.js index 4afb036..76efbd8 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 3ed4fad..0ff56ad 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 9d4e888..fc84d1e 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 6a5cdb5..62c0141 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 0d325ae..714dc3a 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 763dfd2..cfbb985 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 d0807cc..d179e56 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 839c1d4..9e78d38 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 df2b038..92b9d9d 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 => `
  1. ${row}
  2. `) - .join('\n'))} + ${album.tracks + .map((track) => + language.$( + "listingPage.listTracks.byAlbum.track", + { + track: link.track(track), + } + ) + ) + .map((row) => `
  3. ${row}
  4. `) + .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 61afa71..4f3a75d 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 57135a4..3c19723 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 c265fdc..48747d7 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")}
    - `).join('\n')} + ` + ) + .join("\n")}
    - ` : fixWS` + ` + : fixWS` <${listTag}> - ${album.tracks.map(trackToListItem).join('\n')} + ${album.tracks.map(trackToListItem).join("\n")} - `} - ${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")} - ` - ])).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 && ``} - ${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 && + `` + } + ${ + 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 ac23e90..4fc129e 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 6c31a01..8be2010 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` -

    ${language.$('artistPage.commentaryList.title')}

    + ` + } + ${ + commentaryThings.length && + fixWS` +

    ${language.$( + "artistPage.commentaryList.title" + )}

    - ${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 21a22b9..4d8b9f1 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 b83244a..bea9ecc 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 a19df6c..ebe3a8d 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`
    - ${row.actionLinks.map(action => transformInline(action) - .replace(' + transformInline(action).replace( + " - `} + ` + }
    - `} + ` + }
    - `).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` -

    ${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` +

    ${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 f580cbe..50fbd7a 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 447a0c8..886c8a9 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 9336506..2fc5d7b 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`
    -

    ${link.newsEntry(entry)}

    +

    ${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 e9b6a04..39acd64 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 471439d..98b552b 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 c4ec6c5..15316e8 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")}

    - `} - ${track.contributorContribs.length && fixWS` -

    ${language.$('releaseInfo.contributors')}

    + ` + } + ${ + track.contributorContribs.length && + fixWS` +

    ${language.$("releaseInfo.contributors")}

    - `} - ${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}`, + })}

    - `} - ${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 cd4c321..1a694d7 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 7397735..72fa9cc 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 a403d7c..230dad2 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 d26584a..586f37b 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 e003135..d80c57c 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 fb2e333..d94f6de 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 d9bca28..576166a 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}) => `` - }, - '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 }) => + ``, + }, + 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