diff options
| -rw-r--r-- | package-lock.json | 136 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | src/cli.js | 21 | ||||
| -rw-r--r-- | src/content/dependencies/generateDividedFeaturedInFlashesList.js | 92 | ||||
| -rw-r--r-- | src/content/dependencies/generateDividedTrackList.js | 4 | ||||
| -rw-r--r-- | src/data/composite/wiki-properties/urls.js | 4 | ||||
| -rw-r--r-- | src/data/things/MusicVideo.js | 6 | ||||
| -rw-r--r-- | src/reformat-urls.js | 218 | ||||
| -rw-r--r-- | src/strings-default.yaml | 1 | ||||
| -rwxr-xr-x | src/upd8.js | 322 | ||||
| -rw-r--r-- | src/validators.js | 104 | ||||
| -rw-r--r-- | src/write/build-modes/index.js | 1 | ||||
| -rw-r--r-- | src/write/tidy-modes/format-urls.js | 34 | ||||
| -rw-r--r-- | src/write/tidy-modes/index.js | 2 | ||||
| -rw-r--r-- | src/write/tidy-modes/sort.js (renamed from src/write/build-modes/sort.js) | 69 |
15 files changed, 799 insertions, 217 deletions
diff --git a/package-lock.json b/package-lock.json index 65e12bd7..14996d6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "js-yaml": "4.1.1", "marked": "17.0.5", "msgpackr": "1.11.9", + "replace-in-file": "8.4.0", "rimraf": "6.1.3", "striptags": "4.0.0-alpha.4", "word-count": "0.3.1", @@ -1585,7 +1586,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1598,7 +1598,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1747,7 +1746,6 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -2073,7 +2071,6 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, "license": "MIT" }, "node_modules/env-paths": { @@ -2114,7 +2111,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2440,7 +2436,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -2450,7 +2445,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3788,6 +3782,63 @@ "dev": true, "license": "MIT" }, + "node_modules/replace-in-file": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-8.4.0.tgz", + "integrity": "sha512-D28k8jy2LtUGbCzCnR3znajaTWIjJ/Uee3UdodzcHRxE7zn6NmYW/dcSqyivnsYU3W+MxdX6SbF28NvJ0GRoLA==", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "glob": "^13.0.0", + "yargs": "^18.0.0" + }, + "bin": { + "replace-in-file": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/replace-in-file/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/replace-in-file/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/replace-in-file/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4094,7 +4145,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -4168,7 +4218,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -4797,7 +4846,6 @@ "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -4927,7 +4975,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -6046,14 +6093,12 @@ "ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==" }, "ansi-styles": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==" }, "arg": { "version": "4.1.3", @@ -6151,8 +6196,7 @@ "chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==" }, "chokidar": { "version": "5.0.0", @@ -6365,8 +6409,7 @@ "emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" }, "env-paths": { "version": "2.2.1", @@ -6389,8 +6432,7 @@ "escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" }, "escape-string-regexp": { "version": "4.0.0", @@ -6589,14 +6631,12 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-east-asian-width": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "dev": true + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==" }, "glob": { "version": "13.0.6", @@ -7481,6 +7521,46 @@ "integrity": "sha512-Zb9DJ5u6GhgqRSBnxV2QSnLqEwcKxHWFA1N2yUa4ZUAO1P8jlWKYtWZ6/ooV6yylspGXJX0O/uNzEv0xrCtwaA==", "dev": true }, + "replace-in-file": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-8.4.0.tgz", + "integrity": "sha512-D28k8jy2LtUGbCzCnR3znajaTWIjJ/Uee3UdodzcHRxE7zn6NmYW/dcSqyivnsYU3W+MxdX6SbF28NvJ0GRoLA==", + "requires": { + "chalk": "^5.6.2", + "glob": "^13.0.0", + "yargs": "^18.0.0" + }, + "dependencies": { + "cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "requires": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + } + }, + "yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "requires": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + } + }, + "yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==" + } + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7688,7 +7768,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, "requires": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -7739,7 +7818,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, "requires": { "ansi-regex": "^6.2.2" } @@ -8166,7 +8244,6 @@ "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, "requires": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -8243,8 +8320,7 @@ "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { "version": "5.0.0", diff --git a/package.json b/package.json index 7aef341a..79fe4b53 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "#page-specs": "./src/page/index.js", "#quickstat": "./src/quickstat.js", "#node-utils": "./src/node-utils.js", + "#reformat-urls": "./src/reformat-urls.js", "#repl": "./src/write/build-modes/repl.js", "#replacer": "./src/replacer.js", "#reverse": "./src/reverse.js", @@ -72,6 +73,7 @@ "js-yaml": "4.1.1", "marked": "17.0.5", "msgpackr": "1.11.9", + "replace-in-file": "8.4.0", "rimraf": "6.1.3", "striptags": "4.0.0-alpha.4", "word-count": "0.3.1", diff --git a/src/cli.js b/src/cli.js index ec72a625..52ac9f9c 100644 --- a/src/cli.js +++ b/src/cli.js @@ -231,18 +231,29 @@ export function showHelpForOptions({ options, indentWrap, sort = entries => entries, + silentIfNoOptions = false, }) { - if (heading) { - console.log(colors.bright(heading)); - } - const sortedOptions = sort( Object.entries(options) .map(([name, descriptor]) => ({name, descriptor}))); + if (!sortedOptions.length && silentIfNoOptions) return; + + if (heading) { + console.log(colors.bright(heading)); + } + if (!sortedOptions.length) { - console.log(`(No options available)`) + if (heading) { + console.log(``); + console.log(` (No options available)`); + console.log(``); + } else { + console.log(`(No options available)`); + } + + return; } let justInsertedPaddingLine = false; diff --git a/src/content/dependencies/generateDividedFeaturedInFlashesList.js b/src/content/dependencies/generateDividedFeaturedInFlashesList.js index 93e29991..c4f5e445 100644 --- a/src/content/dependencies/generateDividedFeaturedInFlashesList.js +++ b/src/content/dependencies/generateDividedFeaturedInFlashesList.js @@ -14,13 +14,26 @@ export default { query(sprawl, features, _contextTrack) { if (!sprawl.enableFlashesAndGames) { - return {dividingSides: [], dividedFeatures: []}; + return { + dividingSides: [], dividingLabels: [], + dividedFeatures: [], + undividedFeatures: [], + }; + } + + if (empty(sprawl.divideFlashListsBySides)) { + return { + dividingSides: [], dividingLabels: [], + dividedFeatures: [], + undividedFeatures: features, + }; } const {allSides} = sprawl; const divisions = new Map(); const dividingSideIndices = []; + const undividedFeatures = []; for (const {side, label} of sprawl.divideFlashListsBySides) { divisions.set(side, {label, features: []}); dividingSideIndices.push(allSides.indexOf(side)); @@ -33,11 +46,13 @@ export default { const closestDividingSideIndex = dividingSideIndices.findLast(i => i <= sideIndex); - const closestDividingSide = - allSides.at(closestDividingSideIndex); + if (typeof closestDividingSideIndex === 'number') { + const closestDividingSide = + allSides.at(closestDividingSideIndex); - if (closestDividingSide) { divisions.get(closestDividingSide).features.push(feature); + } else { + undividedFeatures.push(feature); } } @@ -51,12 +66,16 @@ export default { dividedFeatures, (_side, _label, dividedFeatures) => !empty(dividedFeatures)); - return {dividingSides, dividingLabels, dividedFeatures}; + return { + dividingSides, dividingLabels, + dividedFeatures, + undividedFeatures, + }; }, - relations: (relation, query, sprawl, features, contextTrack) => ({ + relations: (relation, query, _sprawl, features, contextTrack) => ({ flatList: - (empty(sprawl.divideFlashListsBySides) + (empty(query.dividedFeatures) ? relation('generateTrackFeaturedInFlashesList', features, contextTrack) : null), @@ -70,24 +89,19 @@ export default { dividedFlashLists: query.dividedFeatures .map(features => relation('generateTrackFeaturedInFlashesList', features, contextTrack)), + + undividedFlashList: + (empty(query.undividedFeatures) + ? null + : relation('generateTrackFeaturedInFlashesList', query.undividedFeatures, contextTrack)), }), data: (query, _sprawl, _tracks) => ({ - dividingSideNames: - query.dividingSides - .map(side => side.name), - dividingLabels: query.dividingLabels, }), - slots: { - headingString: { - type: 'string', - }, - }, - - generate(data, relations, slots, {html, language}) { + generate(data, relations, {html, language}) { if (relations.flatList) { return relations.flatList; } @@ -107,36 +121,26 @@ export default { html.tag('dl', {class: 'division-list'}, {[html.onlyIfContent]: true}, - language.encapsulate('flashList', listCapsule => + language.encapsulate('flashList', listCapsule => [ stitchArrays({ sideLink: relations.dividingSideLinks, flashList: relations.dividedFlashLists, - sideName: data.dividingSideNames, - label: data.dividingLabels, - }).map(({sideLink, flashList, sideName, label}) => [ - language.encapsulate(listCapsule, 'underSide', capsule => - (slots.headingString - ? relations.contentHeading.clone().slots({ - tag: 'dt', - - title: - language.$(capsule, {side: sideLink}), - - stickyTitle: - language.$(slots.headingString, 'sticky', 'fromGroup', { - side: - (label - ? language.sanitize(label) - : language.sanitize(sideName)), - }), - }) - - : html.tag('dt', - language.$(capsule, { - side: sideLink, - })))), + }).map(({sideLink, flashList}) => [ + html.tag('dt', + language.$(listCapsule, 'underSide', {side: sideLink})), html.tag('dd', flashList), - ])))); + ]), + + html.tags([ + html.tag('dt', + {[html.onlyIfSiblings]: true}, + language.$(listCapsule, 'underOther')), + + html.tag('dd', + {[html.onlyIfContent]: true}, + relations.undividedFlashList), + ]), + ]))); }, }; diff --git a/src/content/dependencies/generateDividedTrackList.js b/src/content/dependencies/generateDividedTrackList.js index e89f08db..870c8156 100644 --- a/src/content/dependencies/generateDividedTrackList.js +++ b/src/content/dependencies/generateDividedTrackList.js @@ -85,9 +85,9 @@ export default { return {groupingGroups, groupedTracks, ungroupedTracks}; }, - relations: (relation, query, sprawl, tracks, contextTrack) => ({ + relations: (relation, query, _sprawl, tracks, contextTrack) => ({ flatList: - (empty(sprawl.divideTrackListsByGroups) + (empty(query.groupedTracks) ? relation('generateNearbyTrackList', tracks, contextTrack, []) : null), diff --git a/src/data/composite/wiki-properties/urls.js b/src/data/composite/wiki-properties/urls.js index 3160a0bf..04ccf689 100644 --- a/src/data/composite/wiki-properties/urls.js +++ b/src/data/composite/wiki-properties/urls.js @@ -1,14 +1,14 @@ // A list of URLs! This will always be present on the data object, even if set // to an empty array or null. -import {isURL, validateArrayItems} from '#validators'; +import {isCuratedURL, validateArrayItems} from '#validators'; // TODO: Not templateCompositeFrom. export default function() { return { flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)}, + update: {validate: validateArrayItems(isCuratedURL)}, expose: {transform: value => value ?? []}, }; } diff --git a/src/data/things/MusicVideo.js b/src/data/things/MusicVideo.js index 7ebbba37..16dffa3b 100644 --- a/src/data/things/MusicVideo.js +++ b/src/data/things/MusicVideo.js @@ -4,7 +4,7 @@ import {colors} from '#cli'; import {input, V} from '#composite'; import {empty} from '#sugar'; import Thing from '#thing'; -import {is, isDate, isStringNonEmpty, isURL, validateArrayItems} +import {is, isCuratedURL, isDate, isStringNonEmpty, validateArrayItems} from '#validators'; import {parseContributors, parseDate} from '#yaml'; @@ -70,7 +70,7 @@ export class MusicVideo extends Thing { flags: {update: true, expose: true}, update: { - validate: isURL, + validate: isCuratedURL, }, expose: { @@ -86,7 +86,7 @@ export class MusicVideo extends Thing { flags: {update: true, expose: true}, update: { - validate: validateArrayItems(isURL), + validate: validateArrayItems(isCuratedURL), }, expose: { diff --git a/src/reformat-urls.js b/src/reformat-urls.js new file mode 100644 index 00000000..69b15de5 --- /dev/null +++ b/src/reformat-urls.js @@ -0,0 +1,218 @@ +// Find-replace calls analogous to isCuratedURL in #validators. +// This can't catch everything, but should automate the greater bulk of it. + +import * as path from 'node:path'; + +import {replaceInFile} from 'replace-in-file'; + +import {colors, logInfo} from '#cli'; +import {escapeRegex, re} from '#sugar'; + +function or(options) { + return options.map(escapeRegex).join('|'); +} + +function https(namespace, domain) { + return [ + `${namespace}: http:// to https://`, + + re('gmi', [ + '^- http://', + `(?=(?:` + domain + ')/)', + ]), + + '- https://', + ]; +} + +function trimQueryParameter(namespace, domain, parameter) { + return [ + `${namespace}: trim ?${parameter} query parameter`, + + re('gmi', [ + '^(', + '- https://', + '(?:' + domain + ')', + '\/.*', + ')', + + '[&?]' + parameter + '=', + '[^\n&?]+', + ]), + + '$1', + ]; +} + +function trimTrailingSlash(namespace, domain) { + return [ + `${namespace}: trim trailing slash`, + + re('gmi', [ + '^(', + '- https://', + '(?:' + domain + ')', + '\/.*', + ')', + + '/', + '(?=[#?]|$)', + ]), + + '$1', + ]; +} + + +// Rules are evaluated top to bottom, in order, +// so each rule can build off previous ones. +const findreplace = []; + +// General + +findreplace.push([ + `general: add slash to stand in for empty path`, + re('gmi', ['^(- [a-z]*://[^\n?#/]+)(?=[?#]|$)']), + '$1/', +]); + +// Apple Music + +findreplace.push([ + `apple music: trim country code`, + /^(- https:\/\/music.apple.com\/)[a-z][a-z]\//gmi, + '$1', +]); + +// SoundCloud + +findreplace.push(trimTrailingSlash('soundcloud', 'soundcloud.com')); + +// Spotify + +findreplace.push(trimQueryParameter('spotify', 'open\.spotify\.com', 'si')); +findreplace.push(trimQueryParameter('spotify', 'open\.spotify\.com', 'nd')); + +// Tumblr + +findreplace.push([ + `tumblr: tumblr.com -> www.tumblr.com`, + /^- https:\/\/tumblr\.com\//gmi, + '- https://www.tumblr.com/', +]); + +// Twitter + +const twitterDomains = + or([ + 'www.twitter.com', + 'x.com', + ]); + +findreplace.push(https('twitter', twitterDomains)); + +findreplace.push([ + `twitter: www.twitter.com -> twitter.com`, + /^- https:\/\/www\.twitter\.com\//gmi, + '- https://twitter.com/', +]); + +findreplace.push([ + `twitter: x.com -> twitter.com`, + /^- https:\/\/x\.com\//gmi, + '- https://twitter.com/', +]); + +// YouTube + +const youtubeDomains = + or([ + 'www.youtube.com', + 'youtube.com', + 'youtu.be', + ]); + +findreplace.push(https('youtube', youtubeDomains)); + +findreplace.push(trimQueryParameter('youtube', youtubeDomains, 'si')); + +findreplace.push([ + `youtube: youtu.be -> www.youtube.com/watch?v=___`, + /^- https:\/\/youtu\.be\/([a-z0-9_-]{11,11})$/gmi, + '- https://www.youtube.com/watch?v=$1' +]); + +findreplace.push([ + `youtube: youtu.be -> www.youtube.com/watch?v=___&t=___`, + /^- https:\/\/youtu\.be\/([a-z0-9_-]{11,11})\?t=(\d+)$/gmi, + '- https://www.youtube.com/watch?v=$1&t=$2', +]); + +findreplace.push([ + `youtube: youtube.com -> www.youtube.com`, + /^- https:\/\/youtube\.com\//gmi, + '- https://www.youtube.com/', +]); + + +export async function reformatCuratedURLs({ + dataPath, + showChangedFiles = true, + showSatisfiedRules = true, +}) { + if (!dataPath) { + throw new Error(`Expected dataPath`); + } + + let changedFiles = new Map(); + let errored = false; + let anyChanged = false; + + try { + for (const [message, find, replace] of findreplace) { + const options = { + files: dataPath + '/**/*.yaml', + from: find, + to: replace, + }; + + let anyChangedForThisRule = false; + for (const result of await replaceInFile(options)) { + if (result.hasChanged) { + anyChanged = true; + anyChangedForThisRule = true; + if (changedFiles.has(result.file)) { + changedFiles.get(result.file).push(message); + } else { + changedFiles.set(result.file, [message]); + } + } + } + + if (showSatisfiedRules && !anyChangedForThisRule) { + logInfo`Already satisfied: ${message}`; + } + } + + return changedFiles; + } catch (caughtError) { + errored = true; + throw caughtError; + } finally { + const entries = Array.from(changedFiles.entries()); + entries.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? +1 : 0); + + if (showChangedFiles) { + for (const [file, messages] of entries) { + logInfo`Updated: ${path.relative(dataPath, file)}`; + for (const message of messages) { + console.log(colors.dim(` - ${message}`)); + } + } + } + + if (!errored) { + return new Map(entries); + } + } +} diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 15bff2e8..daca8347 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -472,6 +472,7 @@ trackList: flashList: underSide: "under {SIDE}:" + underOther: "other flashes:" item: _: "{FLASH}" diff --git a/src/upd8.js b/src/upd8.js index 2091e5ba..e9353007 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -54,6 +54,7 @@ import {bindReverse} from '#reverse'; import {writeSearchData} from '#search'; import {sortByName} from '#sort'; import thingConstructors from '#things'; +import {disableCuratedURLValidation} from '#validators'; import {identifyAllWebRoutes} from '#web-routes'; import { @@ -80,6 +81,7 @@ import { empty, filterMultipleArrays, indentWrap as unboundIndentWrap, + stitchArrays, withEntries, } from '#sugar'; @@ -114,6 +116,7 @@ import { import FileSizePreloader from './file-size-preloader.js'; import {listingSpec, listingTargetSpec} from './listing-spec.js'; import * as buildModes from './write/build-modes/index.js'; +import * as tidyModes from './write/tidy-modes/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -131,6 +134,7 @@ const STATUS_NOT_APPLICABLE = `not applicable`; const STATUS_STARTED_NOT_DONE = `started but not yet done`; const STATUS_DONE_CLEAN = `done without warnings`; const STATUS_FATAL_ERROR = `fatal error`; +const STATUS_INVALID_SIGNAL = `invalid exit signal`; const STATUS_HAS_WARNINGS = `has warnings`; const defaultStepStatus = {status: STATUS_NOT_STARTED, annotation: null}; @@ -199,10 +203,6 @@ async function main() { {...defaultStepStatus, name: `precache nearly all data`, for: ['build']}, - sortWikiDataSourceFiles: - {...defaultStepStatus, name: `apply sorting rules to wiki data files`, - for: ['build']}, - checkWikiDataSourceFileSorting: {...defaultStepStatus, name: `check sorting rules against wiki data files`}, @@ -251,6 +251,14 @@ async function main() { {...defaultStepStatus, name: `identify web routes`, for: ['build']}, + reformatCuratedURLs: + {...defaultStepStatus, name: `reformat curated URLs`, + for: ['build']}, + + sortWikiDataSourceFiles: + {...defaultStepStatus, name: `apply sorting rules to wiki data files`, + for: ['build']}, + performBuild: {...defaultStepStatus, name: `perform selected build mode`, for: ['build']}, @@ -272,32 +280,50 @@ async function main() { const defaultQueueSize = 500; - const buildModeFlagOptions = ( + const buildModeFlagOptions = withEntries(buildModes, entries => entries.map(([key, mode]) => [key, { help: mode.description, type: 'flag', - }]))); + }])); - const selectedBuildModeFlags = Object.keys( - await parseOptions(process.argv.slice(2), { - [parseOptions.handleUnknown]: () => {}, - ...buildModeFlagOptions, - })); + const selectedBuildModeFlags = + Object.keys( + await parseOptions(process.argv.slice(2), { + [parseOptions.handleUnknown]: () => {}, + ...buildModeFlagOptions, + })); - let selectedBuildModeFlag; - let sortInAdditionToBuild = false; + const tidyModeFlagOptions = + withEntries(tidyModes, entries => + entries.map(([key, mode]) => [key, { + help: mode.description, + type: 'flag', + }])); + + const selectedTidyModeFlags = + Object.keys( + await parseOptions(process.argv.slice(2), { + [parseOptions.handleUnknown]: () => {}, + ...tidyModeFlagOptions, + })); - // As an exception, --sort can be combined with another build mode. - if (selectedBuildModeFlags.length >= 2 && selectedBuildModeFlags.includes('sort')) { - sortInAdditionToBuild = true; - selectedBuildModeFlags.splice(selectedBuildModeFlags.indexOf('sort'), 1); + if (selectedTidyModeFlags.includes('format-urls')) { + Object.assign(stepStatusSummary.reformatCuratedURLs, { + status: STATUS_NOT_STARTED, + annotation: `--format-urls provided`, + }); + } else { + Object.assign(stepStatusSummary.reformatCuratedURLs, { + status: STATUS_NOT_APPLICABLE, + annotation: `--format-urls not provided`, + }); } - if (sortInAdditionToBuild) { + if (selectedTidyModeFlags.includes('sort')) { Object.assign(stepStatusSummary.sortWikiDataSourceFiles, { status: STATUS_NOT_STARTED, - annotation: `--sort provided with another build mode`, + annotation: `--sort provided`, }); Object.assign(stepStatusSummary.checkWikiDataSourceFileSorting, { @@ -316,17 +342,15 @@ async function main() { }); } - if (empty(selectedBuildModeFlags)) { - // No build mode selected. This is not a valid state for building the wiki, - // but we want to let access to --help, so we'll show a message about what - // to do later. - selectedBuildModeFlag = null; - } else if (selectedBuildModeFlags.length > 1) { - logError`Building multiple modes (${selectedBuildModeFlags.join(', ')}) at once not supported.`; - logError`Please specify one build mode.`; - return false; - } else { - selectedBuildModeFlag = selectedBuildModeFlags[0]; + let selectedBuildModeFlag; + switch (selectedBuildModeFlags.length) { + case 0: selectedBuildModeFlag = null; break; + case 1: selectedBuildModeFlag = selectedBuildModeFlags[0]; break; + default: { + logError`Building multiple modes (${selectedBuildModeFlags.join(', ')}) at once not supported.`; + logError`Please specify one build mode.`; + return false; + } } const selectedBuildMode = @@ -334,16 +358,25 @@ async function main() { ? buildModes[selectedBuildModeFlag] : null); - // This is about to get a whole lot more stuff put in it. - const wikiData = { - listingSpec, - listingTargetSpec, - }; + const selectedTidyModes = + selectedTidyModeFlags + .map(flag => tidyModes[flag]); - const buildOptions = - (selectedBuildMode - ? selectedBuildMode.getCLIOptions() - : {}); + const tidyingOnly = + !selectedBuildMode && + !empty(selectedTidyModes); + + const selectedBuildModeOptions = + selectedBuildMode?.getCLIOptions?.() ?? + {}; + + const selectedTidyModeOptions = + selectedTidyModes.map(tidyMode => + tidyMode.getCLIOptions?.() ?? + {}); + + const selectedTidyModeOptionsFlat = + Object.fromEntries(selectedTidyModeOptions.flat()); const commonOptions = { 'help': { @@ -452,6 +485,11 @@ async function main() { type: 'flag', }, + 'skip-curated-url-validation': { + help: `Skips checking if URLs match a set of standardizing rules; only intended for use with old data`, + type: 'flag', + }, + 'skip-file-sizes': { help: `Skips preloading file sizes for images and additional files, which will be left blank in the build`, type: 'flag', @@ -580,9 +618,11 @@ async function main() { // here, even though we won't be doing anything with them later. // (This is a bit of a hack.) ...buildModeFlagOptions, + ...tidyModeFlagOptions, ...commonOptions, - ...buildOptions, + ...selectedTidyModeOptionsFlat, + ...selectedBuildModeOptions, }); shouldShowStepStatusSummary = cliOptions['show-step-summary'] ?? false; @@ -599,7 +639,7 @@ async function main() { `and website content/structure ` + `from provided data, media, and language directories.\n` + `\n` + - `CLI options are divided into three groups:\n`)); + `CLI options are divided into five groups:\n`)); console.log(` 1) ` + indentWrap( `Common options: ` + @@ -608,37 +648,63 @@ async function main() { {spaces: 4, bullet: true})); console.log(` 2) ` + indentWrap( + `Tidying mode selection: ` + + `One or more tidying mode may be selected, ` + + `and they adjust the contents of data files ` + + `to satisfy predefined or data-configured standardization rules`, + {spaces: 4, bullet: true})); + + console.log(` 3) ` + indentWrap( + `Tidying mode options: ` + + `Each tidy mode may `)) + + console.log(` 4) ` + indentWrap( `Build mode selection: ` + `One build mode should be selected, ` + `and it decides the main set of behavior to use ` + `for presenting or interacting with site content`, {spaces: 4, bullet: true})); - console.log(` 3) ` + indentWrap( - `Build options: ` + + console.log(` 5) ` + indentWrap( + `Build mode options: ` + `Each build mode has a set of unique options ` + `which customize behavior for that build mode`, {spaces: 4, bullet: true})); + console.log(`All options may be specified in any order.`); + console.log(``); showHelpForOptions({ heading: `Common options`, options: commonOptions, - wrap, }); showHelpForOptions({ + heading: `Tidying mode selection`, + options: tidyModeFlagOptions, + }); + + stitchArrays({ + flag: selectedTidyModeFlags, + options: selectedTidyModeOptions, + }).forEach(({flag, options}) => { + showHelpForOptions({ + heading: `Options for tidying mode --${flag}`, + options, + silentIfNoOptions: false, + }); + }); + + showHelpForOptions({ heading: `Build mode selection`, options: buildModeFlagOptions, - wrap, }); if (selectedBuildMode) { showHelpForOptions({ - heading: `Build options for --${selectedBuildModeFlag}`, - options: buildOptions, - wrap, + heading: `Options for build mode --${selectedBuildModeFlag}`, + options: selectedBuildModeOptions, }); } else { console.log( @@ -704,6 +770,34 @@ async function main() { }); } + if (tidyingOnly) { + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_NOT_APPLICABLE, + annotation: `tidying modes provided`, + }); + + for (const key of [ + 'preloadFileSizes', + 'watchLanguageFiles', + 'verifyImagePaths', + 'buildSearchIndex', + 'generateThumbnails', + 'identifyWebRoutes', + 'checkWikiDataSourceFileSorting', + ]) { + Object.assign(stepStatusSummary[key], { + status: STATUS_NOT_APPLICABLE, + annotation: `tidying modes provided without build mode`, + }); + } + } + + if (cliOptions['skip-curated-url-validation']) { + logWarn`Won't check if any URLs match the curated URL rules this run`; + logWarn `(--skip-curated-url-validation passed).`; + disableCuratedURLValidation(); + } + // Finish setting up defaults by combining information from all options. const _fallbackStep = (stepKey, { @@ -960,6 +1054,10 @@ async function main() { break decideBuildSearchIndex; } + if (tidyingOnly) { + break decideBuildSearchIndex; + } + const indexFile = path.join(wikiCachePath, 'search', 'index.json') let stats; try { @@ -1478,6 +1576,12 @@ async function main() { timeStart: Date.now(), }); + // This is about to get a whole lot more stuff put in it. + const wikiData = { + listingSpec, + listingTargetSpec, + }; + let yamlDataSteps; let yamlDocumentProcessingAggregate; @@ -1993,40 +2097,7 @@ async function main() { }); } - if (stepStatusSummary.sortWikiDataSourceFiles.status === STATUS_NOT_STARTED) { - Object.assign(stepStatusSummary.sortWikiDataSourceFiles, { - status: STATUS_STARTED_NOT_DONE, - timeStart: Date.now(), - }); - - const {SortingRule} = thingConstructors; - const results = - await Array.fromAsync(SortingRule.go({dataPath, wikiData})); - - if (results.some(result => result.changed)) { - logInfo`Updated data files to satisfy sorting.`; - logInfo`Restarting automatically, since that's now needed!`; - - Object.assign(stepStatusSummary.sortWikiDataSourceFiles, { - status: STATUS_DONE_CLEAN, - annotation: `changes cueing restart`, - timeEnd: Date.now(), - memory: process.memoryUsage(), - }); - - return 'restart'; - } else { - logInfo`All sorting rules are satisfied. Nice!`; - paragraph = false; - - Object.assign(stepStatusSummary.sortWikiDataSourceFiles, { - status: STATUS_DONE_CLEAN, - annotation: `no changes needed`, - timeEnd: Date.now(), - memory: process.memoryUsage(), - }); - } - } else if (stepStatusSummary.checkWikiDataSourceFileSorting.status === STATUS_NOT_STARTED) { + if (stepStatusSummary.checkWikiDataSourceFileSorting.status === STATUS_NOT_STARTED) { Object.assign(stepStatusSummary.checkWikiDataSourceFileSorting, { status: STATUS_STARTED_NOT_DONE, timeStart: Date.now(), @@ -3197,10 +3268,79 @@ async function main() { quickstat.reset(); + let restartBeforeBuild = false; + const updatedTidyModes = []; + + for (const [step, tidyMode] of [ + ['reformatCuratedURLs', 'format-urls'], + ['sortWikiDataSourceFiles', 'sort'], + ]) { + if (stepStatusSummary[step].status !== STATUS_NOT_STARTED) { + continue; + } + + Object.assign(stepStatusSummary[step], { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); + + const tidySignal = + await tidyModes[tidyMode].go({ + wikiData, + dataPath, + tidyingOnly, + }); + + switch (tidySignal) { + case 'clean': { + Object.assign(stepStatusSummary[step], { + status: STATUS_DONE_CLEAN, + annotation: `no changes needed`, + timeEnd: Date.now(), + memory: process.memoryUsage(), + }); + + break; + } + + case 'updated': { + Object.assign(stepStatusSummary[step], { + status: STATUS_DONE_CLEAN, + annotation: `changes cueing restart`, + timeEnd: Date.now(), + memory: process.memoryUsage(), + }); + + restartBeforeBuild = true; + updatedTidyModes.push(tidyMode); + + break; + } + + default: { + Object.assign(stepStatusSummary[step], { + status: STATUS_INVALID_SIGNAL, + annotation: `unknown: ${tidySignal}`, + timeEnd: Date.now(), + memory: process.memoryUsage(), + }); + + logError`Invalid exit signal for ${'--' + tidyMode}: ${tidySignal}`; + fileIssue(); + + return false; + } + } + } + if (stepStatusSummary.performBuild.status === STATUS_NOT_APPLICABLE) { return true; } + if (restartBeforeBuild) { + return 'restart'; + } + const developersComment = `<!--\n` + [ wikiData.wikiInfo.canonicalBase @@ -3347,15 +3487,15 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus console.log(''); } - if (numRestarts > 5) { + if (numRestarts > 2) { logError`A restart was cued, but we've restarted a bunch already.`; logError`Exiting because this is probably a bug!`; console.log(''); break; } else { console.log(''); - logInfo`A restart was cued. This is probably normal, and required`; - logInfo`to load updated data files. Restarting automatically now!`; + console.log(`A restart was cued. This is probably normal, and required`); + console.log(`to load updated data files. Restarting automatically now!`); console.log(''); numRestarts++; } @@ -3416,6 +3556,8 @@ function showStepStatusSummary() { .map(({status}) => status === STATUS_HAS_WARNINGS || status === STATUS_FATAL_ERROR || + status === STATUS_INVALID_SIGNAL || + status === STATUS_NOT_STARTED || status === STATUS_STARTED_NOT_DONE); const anyStepsNotClean = @@ -3470,7 +3612,12 @@ function showStepStatusSummary() { message += ` `; message += `${name}: `.padEnd(longestNameLength + 4, '.'); - message += ` `; + if (stepsNotClean[index]) { + message += `! `; + } else { + message += ` `; + } + message += status; if (annotation) { @@ -3482,17 +3629,18 @@ function showStepStatusSummary() { console.error(colors.green(message)); break; - case STATUS_NOT_STARTED: case STATUS_NOT_APPLICABLE: console.error(colors.dim(message)); break; case STATUS_HAS_WARNINGS: + case STATUS_NOT_STARTED: case STATUS_STARTED_NOT_DONE: console.error(colors.yellow(message)); break; case STATUS_FATAL_ERROR: + case STATUS_INVALID_SIGNAL: console.error(colors.red(message)); break; diff --git a/src/validators.js b/src/validators.js index 1c9ce9e3..7dcd7b8c 100644 --- a/src/validators.js +++ b/src/validators.js @@ -22,6 +22,14 @@ export function setValidatorCreatorMeta(validator, creator, meta) { return validator; } +// External configuration + +let disabledCuratedURLValidation = false; + +export function disableCuratedURLValidation() { + disabledCuratedURLValidation = true; +} + // Basic types (primitives) export function a(noun) { @@ -710,7 +718,101 @@ export function isName(name) { export function isURL(string) { isStringNonEmpty(string); - new URL(string); + // This might error. + const url = new URL(string); + + // This might, too. + decodeURIComponent(url.pathname); + + return true; +} + +export function isCuratedURL(string) { + if (disabledCuratedURLValidation) { + return isURL(string); + } + + // Do the same basic checks as in isURL. + // We'll need access to the URL object. + const url = new URL(string); + decodeURIComponent(url.pathname); + + const useHostname = hostname => + new Error( + `Use ${colors.green(hostname)}, ` + + `not ${colors.red(url.hostname)}`); + + switch (url.hostname) { + case 'tumblr.com': + throw useHostname('www.tumblr.com'); + + case 'www.twitter.com': + case 'x.com': + throw useHostname('twitter.com'); + + case 'youtu.be': + case 'youtube.com': + throw useHostname('www.youtube.com'); + } + + const dropSearchParam = (param, message = null) => { + if (url.searchParams.has(param)) { + const index = + Array.from(url.searchParams) + .findIndex(entry => entry[0] === param); + + const prefix = (index === 0 ? '?' : '&'); + + const value = url.searchParams.get(param); + + throw new Error( + `Remove ${colors.red(`${prefix}${param}=${value}`)}` + + (message ? ` (${message})` : '')); + } + }; + + const dropTrailingSlash = () => { + if (url.pathname.length >= 3 && url.pathname.endsWith('/')) { + throw new Error( + `Remove slash at end: ` + + url.pathname.slice(0, -1) + + colors.bright(colors.red('/'))); + } + }; + + switch (url.hostname) { + case 'soundcloud.com': + dropTrailingSlash(); + break; + + case 'open.spotify.com': + dropSearchParam('si', `tracking parameter`); + dropSearchParam('nd', `unnecessary parameter`); + break; + + case 'www.youtube.com': + dropSearchParam('si', `tracking parameter`); + break; + } + + if (url.hostname === 'music.apple.com') { + const countryMatch = + url.pathname.match(/^\/[a-z][a-z]\//); + + if (countryMatch) { + throw new Error(`Remove country code ${colors.red(countryMatch[0])}`); + } + } + + if (url.pathname === '/' && string === url.origin + url.hash + url.search) { + if (url.hash) { + throw new Error(`Add slash before "#" hash`); + } else if (url.search) { + throw new Error(`Add slash before "?" query`); + } else { + throw new Error(`Add slash at end`); + } + } return true; } diff --git a/src/write/build-modes/index.js b/src/write/build-modes/index.js index 4b61619d..3ae2cfc6 100644 --- a/src/write/build-modes/index.js +++ b/src/write/build-modes/index.js @@ -1,4 +1,3 @@ export * as 'live-dev-server' from './live-dev-server.js'; export * as 'repl' from './repl.js'; -export * as 'sort' from './sort.js'; export * as 'static-build' from './static-build.js'; diff --git a/src/write/tidy-modes/format-urls.js b/src/write/tidy-modes/format-urls.js new file mode 100644 index 00000000..5771fe3e --- /dev/null +++ b/src/write/tidy-modes/format-urls.js @@ -0,0 +1,34 @@ +export const description = `Update data files in-place to satisfy formatting rules for curated URLs`; + +import {logInfo} from '#cli'; +import {reformatCuratedURLs} from '#reformat-urls'; + +export async function go({ + dataPath, + tidyingOnly, +}) { + const changedFiles = + await reformatCuratedURLs({ + dataPath, + showChangedFiles: true, + showSatisfiedRules: tidyingOnly, + }); + + if (changedFiles.size === 0) { + if (tidyingOnly) { + logInfo`All URL formatting rules were already satisfied. Good to go!`; + return 'clean'; + } else { + logInfo`All curated URL formatting rules are satisfied - nice!`; + return 'clean'; + } + } else { + const filesPart = + (changedFiles.size === 1 + ? `1 file` + : `${changedFiles.size} files`); + + logInfo`Updated ${filesPart} to satisfy URL formatting rules.`; + return 'updated'; + } +} diff --git a/src/write/tidy-modes/index.js b/src/write/tidy-modes/index.js new file mode 100644 index 00000000..54e2bbf3 --- /dev/null +++ b/src/write/tidy-modes/index.js @@ -0,0 +1,2 @@ +export * as 'format-urls' from './format-urls.js'; +export * as 'sort' from './sort.js'; diff --git a/src/write/build-modes/sort.js b/src/write/tidy-modes/sort.js index 1a738ac8..967a5be1 100644 --- a/src/write/build-modes/sort.js +++ b/src/write/tidy-modes/sort.js @@ -4,48 +4,34 @@ import {logInfo} from '#cli'; import {empty} from '#sugar'; import thingConstructors from '#things'; -export const config = { - fileSizes: { - applicable: false, - }, - - languageReloading: { - applicable: false, - }, - - mediaValidation: { - applicable: false, - }, - - search: { - applicable: false, - }, - - thumbs: { - applicable: false, - }, +export async function go({ + wikiData, + dataPath, + tidyingOnly, +}) { + if (empty(wikiData.sortingRules)) { + if (tidyingOnly) { + logInfo`There aren't any sorting rules in for this wiki.`; + } - webRoutes: { - applicable: false, - }, + return 'clean'; + } - sort: { - applicable: false, - }, -}; + const {SortingRule} = thingConstructors; -export function getCLIOptions() { - return {}; -} + if (!tidyingOnly) { + const results = + await Array.fromAsync(SortingRule.go({dataPath, wikiData})); -export async function go({wikiData, dataPath}) { - if (empty(wikiData.sortingRules)) { - logInfo`There aren't any sorting rules in for this wiki.`; - return true; + if (results.some(result => result.changed)) { + logInfo`Updated data files to satisfy sorting.`; + return 'updated'; + } else { + logInfo`All sorting rules are satisfied - nice!`; + return 'clean'; + } } - const {SortingRule} = thingConstructors; - let numUpdated = 0; let numActive = 0; @@ -56,7 +42,7 @@ export async function go({wikiData, dataPath}) { if (result.changed) { numUpdated++; - logInfo`Updating to satisfy ${niceMessage}.`; + logInfo`Updated to satisfy ${niceMessage}.`; } else { logInfo`Already good: ${niceMessage}`; } @@ -64,13 +50,12 @@ export async function go({wikiData, dataPath}) { if (numUpdated > 1) { logInfo`Updated data files to satisfy ${numUpdated} sorting rules.`; + return 'updated'; } else if (numUpdated === 1) { logInfo`Updated data files to satisfy ${1} sorting rule.` - } else if (numActive >= 1) { - logInfo`All sorting rules were already satisfied. Good to go!`; + return 'updated'; } else { - logInfo`No sorting rules are currently active.`; + logInfo`All sorting rules were already satisfied. Good to go!`; + return 'clean'; } - - return true; } |