diff options
503 files changed, 19890 insertions, 14819 deletions
diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index ac1a4e63..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "env": { - "es2021": true, - "node": true - }, - "extends": "eslint:recommended", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "rules": { - "indent": ["off"], - "no-empty": ["error", { - "allowEmptyCatch": true - }], - "no-unexpected-multiline": ["off"], - "no-unused-labels": ["off"], - "no-unused-vars": ["error", { - "argsIgnorePattern": "^_", - "destructuredArrayIgnorePattern": "^" - }], - "no-cond-assign": ["off"], - "no-constant-condition": ["off"], - "no-unsafe-finally": ["off"] - } -} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..9d969f57 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,139 @@ +import * as path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +import {defineConfig} from 'eslint/config'; +import js from '@eslint/js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const CLIENT_JAVASCRIPT_PATHS = [ + 'src/static/js/**/*.js', +]; + +const WEB_WORKER_PATHS = [ + 'src/static/js/search-worker.js', +]; + +const COMMON_GLOBALS = { + supportedWhenever: { + URL: 'readonly', + + clearInterval: 'readonly', + clearTimeout: 'readonly', + console: 'readonly', + fetch: 'readonly', + setInterval: 'readonly', + setTimeout: 'readonly', + structuredClone: 'readonly', + }, +}; + +export default defineConfig([ + // basic config + + { + files: ['src/**/*.js'], + extends: ['js/recommended'], + plugins: {js}, + + rules: { + indent: ['off'], + + 'no-empty': ['error', { + allowEmptyCatch: true, + }], + + 'no-unexpected-multiline': ['off'], + 'no-unused-labels': ['off'], + + 'no-unused-vars': ['error', { + argsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^', + }], + + 'no-cond-assign': ['off'], + 'no-constant-condition': ['off'], + 'no-unsafe-finally': ['off'], + 'no-self-assign': ['off'], + 'require-yield': ['off'], + }, + }, + + // node.js + + { + files: ['src/**/*.js'], + ignores: [...CLIENT_JAVASCRIPT_PATHS], + + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + + globals: { + ...COMMON_GLOBALS.supportedWhenever, + + process: 'readonly', + setImmediate: 'readonly', + }, + }, + }, + + // client javascript - all + + { + files: [...CLIENT_JAVASCRIPT_PATHS], + + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + + globals: { + ...COMMON_GLOBALS.supportedWhenever, + + CustomEvent: 'readonly', + Response: 'readonly', + Worker: 'readonly', + XMLHttpRequest: 'readonly', + }, + }, + }, + + // client javascript - not web workers + + { + files: [...CLIENT_JAVASCRIPT_PATHS], + ignores: [...WEB_WORKER_PATHS], + + languageOptions: { + globals: { + DOMException: 'readonly', + DOMRect: 'readonly', + MutationObserver: 'readonly', + IntersectionObserver: 'readonly', + + document: 'readonly', + getComputedStyle: 'readonly', + history: 'readonly', + location: 'readonly', + localStorage: 'readonly', + sessionStorage: 'readonly', + window: 'readonly', + }, + }, + }, + + // client javascript - web workers + + { + files: [WEB_WORKER_PATHS], + + languageOptions: { + globals: { + onerror: 'writeable', + onmessage: 'writeable', + onunhandledrejection: 'writeable', + postMessage: 'readonly', + }, + }, + }, +]); diff --git a/package-lock.json b/package-lock.json index 11bc6a41..0288f8f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "chroma-js": "^2.4.2", "command-exists": "^1.2.9", "compress-json": "^3.0.5", - "eslint": "^8.37.0", + "eslint": "^9.37.0", "flexsearch": "^0.7.43", "he": "^1.2.0", "image-size": "^1.0.2", @@ -29,11 +29,11 @@ }, "devDependencies": { "chokidar": "^3.5.3", - "tap": "^19.0.2", + "tap": "^21.1.1", "tcompare": "^6.0.0" }, "engines": { - "node": ">= 20.9.0" + "node": ">= 22.13.0" } }, "node_modules/@alcalzone/ansi-tokenize": { @@ -41,6 +41,7 @@ "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" @@ -50,10 +51,11 @@ } }, "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "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" }, @@ -68,16 +70,21 @@ "dev": true }, "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -86,36 +93,92 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -123,31 +186,79 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/js": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.37.0.tgz", - "integrity": "sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { @@ -162,10 +273,41 @@ "url": "https://github.com/sponsors/nzakas" } }, - "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==" + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -208,11 +350,25 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@isaacs/ts-node-temp-fork-for-pr-2009": { "version": "10.9.7", "resolved": "https://registry.npmjs.org/@isaacs/ts-node-temp-fork-for-pr-2009/-/ts-node-temp-fork-for-pr-2009-10.9.7.tgz", "integrity": "sha512-9f0bhUr9TnwwpgUhEpr3FjxSaH/OHaARkE2F9fM0lS4nIs2GNerrvGwQz493dk0JKlTaGYVrKbq36vA/whZ34g==", "dev": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node14": "*", @@ -253,6 +409,7 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -262,6 +419,7 @@ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -271,21 +429,24 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -375,43 +536,12 @@ "win32" ] }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@npmcli/agent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", - "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", "dev": true, + "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", @@ -420,38 +550,40 @@ "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", - "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "dev": true, + "license": "ISC", "dependencies": { "semver": "^7.3.5" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/git": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.7.tgz", - "integrity": "sha512-WaOVvto604d5IpdCRV2KjQu8PzkfE96d50CQGKgywXh2GxXmDeUO5EWcBC4V57uFyrNqx83+MewuJh3WTR3xPA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.0.tgz", + "integrity": "sha512-vnz7BVGtOctJAIHouCJdvWBhsTVSICMeUgZo2c7XAi5d5Rrl80S1H7oPym7K03cRuinK5Q6s2dw36+PgXQTcMA==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/promise-spawn": "^7.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^9.0.0", - "proc-log": "^4.0.0", - "promise-inflight": "^1.0.1", + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "semver": "^7.3.5", - "which": "^4.0.0" + "which": "^5.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/git/node_modules/isexe": { @@ -459,15 +591,27 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@npmcli/git/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -475,108 +619,66 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/installed-package-contents": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", - "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", "dev": true, + "license": "ISC", "dependencies": { - "npm-bundled": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" }, "bin": { "installed-package-contents": "bin/index.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/node-gyp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/package-json": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.1.1.tgz", - "integrity": "sha512-uTq5j/UqUzbOaOxVy+osfOhpqOiLfUZ0Ut33UbcyyAPJbZcJsf4Mrsyb8r58FoIFlofw0iOFsuCA/oDK14VDJQ==", - "dev": true, - "dependencies": { - "@npmcli/git": "^5.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^7.0.0", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "proc-log": "^4.0.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/package-json/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.1.tgz", + "integrity": "sha512-956YUeI0YITbk2+KnirCkD19HLzES0habV+Els+dyZaVsaM6VGSiNwnRu6t3CZaqDLz4KXy2zx+0N/Zy6YjlAA==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@npmcli/git": "^7.0.0", + "glob": "^11.0.3", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/promise-spawn": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", - "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", + "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==", "dev": true, + "license": "ISC", "dependencies": { - "which": "^4.0.0" + "which": "^5.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/promise-spawn/node_modules/isexe": { @@ -584,15 +686,17 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -600,32 +704,35 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/redact": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", - "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz", + "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==", "dev": true, + "license": "ISC", "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/run-script": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", - "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.0.tgz", + "integrity": "sha512-vaQj4nccJbAslopIvd49pQH2NhUp7G9pY4byUtmwhe37ZZuubGrx0eB9hW2F37uVNRuDDK6byFGXF+7JCuMSZg==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^5.0.0", - "@npmcli/promise-spawn": "^7.0.0", - "node-gyp": "^10.0.0", - "which": "^4.0.0" + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/run-script/node_modules/isexe": { @@ -633,15 +740,17 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, "node_modules/@npmcli/run-script/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -649,7 +758,7 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@pkgjs/parseargs": { @@ -662,203 +771,290 @@ } }, "node_modules/@sigstore/bundle": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", - "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2" + "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", - "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.0.0.tgz", + "integrity": "sha512-NgbJ+aW9gQl/25+GIEGYcCyi8M+ng2/5X04BMuIgoDfgvp18vDcoNHOQjQsG9418HGNYRxG3vfEXaR1ayD37gg==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz", - "integrity": "sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", + "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@sigstore/sign": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", - "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.0.1.tgz", + "integrity": "sha512-KFNGy01gx9Y3IBPG/CergxR9RZpN43N+lt3EozEfeoyqm8vEiLxwRl3ZO5sPx3Obv1ix/p7FWOlPc2Jgwfp9PA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "make-fetch-happen": "^13.0.1", - "proc-log": "^4.2.0", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.2", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/sign/node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/sign/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@sigstore/sign/node_modules/make-fetch-happen": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.2.tgz", + "integrity": "sha512-sI1NY4lWlXBAfjmCtVWIIpBypbBdhHtcjnwnv+gtCnsaOffyFil3aidszGC8hgzJe+fT1qix05sWxmD/Bmf/oQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/tuf": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", - "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.0.tgz", + "integrity": "sha512-0QFuWDHOQmz7t66gfpfNO6aEjoFrdhkJaej/AOqb4kqWZVbPWFZifXZzkxyQBB1OwTbkhdT3LNpMFxwkTvf+2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2", - "tuf-js": "^2.2.1" + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/verify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", - "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.0.0.tgz", + "integrity": "sha512-moXtHH33AobOhTZF8xcX1MpOFqdvfCk7v6+teJL8zymBiDXwEsQH6XG9HGx2VIxnJZNm4cNSzflTLDnQLmIdmw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.1.0", - "@sigstore/protobuf-specs": "^0.3.2" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@tapjs/after": { - "version": "1.1.24", - "resolved": "https://registry.npmjs.org/@tapjs/after/-/after-1.1.24.tgz", - "integrity": "sha512-Qys3CtftkfHGC7thDGm9TBzRCBLAoJKrXufF1zQxI1oNUjclWZP/s8CtHH0mwUTISOTehmBLV3wPPHSslD67Ng==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/after/-/after-3.0.2.tgz", + "integrity": "sha512-Xb0bqJWXfp6VVSx1T96lNJM67v2XxJCG3o7mH77weB+RzwAuf0uzGYy/hxP+nUAWh9yH+lHzuHclL+DR8Zlu3Q==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "is-actual-promise": "^1.0.1" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/after-each": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/after-each/-/after-each-2.0.1.tgz", - "integrity": "sha512-3JXIJ4g9LPjyXmn/1VuIMC0vh7uBgUpQPksjffxv0rL8wq4C8lvmqt8Qu/fVImJucqzA+WrRqVG1b2Ab0ocDOw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/after-each/-/after-each-4.0.2.tgz", + "integrity": "sha512-OQEENy55qtJ7WaMnIQvf0YaV4/YjI2B9+ezn679Vrptg/xMbaG5fSJz/Z5BFroh05HiWRo+MGu66q7Lb00kDJg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "function-loop": "^4.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/asserts": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/asserts/-/asserts-2.0.1.tgz", - "integrity": "sha512-v2xYDLUwMGt8pzoY5LIjDCaw2NM+G01NW4pC3RcpsZLZbzQv1x/phi2RAX0ixI0nCmZZybqRygFKuMcJamS+gg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/asserts/-/asserts-4.0.2.tgz", + "integrity": "sha512-XeAYvYMu61/Gc9Dpn+0QZCjUTbbQE7DckiZNhQNMuMwAXUDEZR/TbxJNysIdMWq2ag75TQQ6ylCUJovy7HPDlg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@tapjs/stack": "2.0.1", + "@tapjs/stack": "4.0.1", "is-actual-promise": "^1.0.1", - "tcompare": "7.0.1", + "tcompare": "9.0.1", "trivial-deferred": "^2.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" + } + }, + "node_modules/@tapjs/asserts/node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" } }, "node_modules/@tapjs/asserts/node_modules/tcompare": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz", - "integrity": "sha512-JN5s7hgmg/Ya5HxZqCnywT+XiOGRFcJRgYhtMyt/1m+h0yWpWwApO7HIM8Bpwyno9hI151ljjp5eAPCHhIGbpQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.0.1.tgz", + "integrity": "sha512-P9i7K3RnKWb3mH0AmDCChVd7FP0LEo4VSBJMhjKB+Rg+Tvq09ytnys8ygsx5ve1di2fI4IW+HhjhIXoeiRVogw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "diff": "^5.2.0", + "diff": "^8.0.2", "react-element-to-jsx-string": "^15.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" } }, "node_modules/@tapjs/before": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/before/-/before-2.0.1.tgz", - "integrity": "sha512-GgnlWPm2PbuyYuG4gkkO2KAvT/BbGnpKs60U4XzPSJ2w73Qc/IYWP0Kz6qfCWongpiLteoco67M89ujUQApYJw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/before/-/before-4.0.2.tgz", + "integrity": "sha512-ODi0rXOqCWZLS1j6fJ2iyqaXy6B/y75x8Y940hRAe0DfPPf48IgZY84+GfWWVIkRqmbYZS/F0rwcegPHxI/xHg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "is-actual-promise": "^1.0.1" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/before-each": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/before-each/-/before-each-2.0.1.tgz", - "integrity": "sha512-gG1nYkvCHtWwhkueulO475KczdQZ3vBRgdkta/Qi42ZjZo6SNhYVjNc/+LRGV5vZoESrvgSd+JrDRGufd+j43w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/before-each/-/before-each-4.0.2.tgz", + "integrity": "sha512-CXNWJ/pvqu3DwNjgnOX9zfmxYz7OvxQ4w7X/1uyFUblWarYYOUqtLsF/FrBHn3rqOowNtP5KJAzY9Wx5UT3r+g==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "function-loop": "^4.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" + } + }, + "node_modules/@tapjs/chdir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/chdir/-/chdir-3.0.2.tgz", + "integrity": "sha512-KYDxkkzGeEhAT33VDWqIx8FRLk+cOzVsj/E0JBRd45n18EHT7C/wE/Nqhu+843nVhABp/c5BZMb+Rib/fCQV1g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/config": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/config/-/config-3.0.1.tgz", - "integrity": "sha512-gAYFzErdSuPQ3afW6iRR99hiJmRLU+x9T+NE89z9UM45iPxglWLrRv1PFfh3tmtX6rpzwD5RY4/FVPcP2+/1LQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/config/-/config-5.0.2.tgz", + "integrity": "sha512-VfvYwcRGC3fbBAuRZs8VheA4YTa1CaJlYonHM2YM1Uu+UGeLCmvxJjIOBauhtp+F/QczUsJxqOjDy9z9ny1UYQ==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@tapjs/core": "2.0.1", - "@tapjs/test": "2.0.1", - "chalk": "^5.2.0", - "jackspeak": "^3.1.2", - "polite-json": "^4.0.1", - "tap-yaml": "2.2.2", - "walk-up-path": "^3.0.1" + "@tapjs/core": "4.0.2", + "@tapjs/test": "4.0.2", + "chalk": "^5.6.2", + "jackspeak": "^4.0.1", + "polite-json": "^5.0.0", + "tap-yaml": "4.0.1", + "walk-up-path": "^4.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" }, "peerDependencies": { - "@tapjs/core": "2.0.1", - "@tapjs/test": "2.0.1" + "@tapjs/core": "4.0.2", + "@tapjs/test": "4.0.2" } }, "node_modules/@tapjs/config/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "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" }, @@ -866,153 +1062,208 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@tapjs/config/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tapjs/core": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/core/-/core-2.0.1.tgz", - "integrity": "sha512-q+8d+ohw5kudktIqgP5ETBcPWAPip+kMIxs2eL2G3dV+7Gc8WrH43cCPrbSGPRITIOSIDPrtpQZEcZwQNqDdQw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/core/-/core-4.0.2.tgz", + "integrity": "sha512-kzM90qsqHAJOTUMVjB3G26c3Ka4/HFg253lSt3pxBFesIHneIZe6Fre1NEWnHZPLmmzBO6HRRffc9zDxXcaeRw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@tapjs/processinfo": "^3.1.7", - "@tapjs/stack": "2.0.1", - "@tapjs/test": "2.0.1", + "@tapjs/processinfo": "^3.1.8", + "@tapjs/stack": "4.0.1", + "@tapjs/test": "4.0.2", "async-hook-domain": "^4.0.1", - "diff": "^5.2.0", + "diff": "^8.0.2", "is-actual-promise": "^1.0.1", "minipass": "^7.0.4", "signal-exit": "4.1", - "tap-parser": "16.0.1", - "tap-yaml": "2.2.2", - "tcompare": "7.0.1", + "tap-parser": "18.0.1", + "tap-yaml": "4.0.1", + "tcompare": "9.0.1", "trivial-deferred": "^2.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" + } + }, + "node_modules/@tapjs/core/node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" } }, "node_modules/@tapjs/core/node_modules/tcompare": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz", - "integrity": "sha512-JN5s7hgmg/Ya5HxZqCnywT+XiOGRFcJRgYhtMyt/1m+h0yWpWwApO7HIM8Bpwyno9hI151ljjp5eAPCHhIGbpQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.0.1.tgz", + "integrity": "sha512-P9i7K3RnKWb3mH0AmDCChVd7FP0LEo4VSBJMhjKB+Rg+Tvq09ytnys8ygsx5ve1di2fI4IW+HhjhIXoeiRVogw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "diff": "^5.2.0", + "diff": "^8.0.2", "react-element-to-jsx-string": "^15.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" } }, "node_modules/@tapjs/error-serdes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/error-serdes/-/error-serdes-2.0.1.tgz", - "integrity": "sha512-P+M4rtcfkDsUveKKmoRNF+07xpbPnRY5KrstIUOnyn483clQ7BJhsnWr162yYNCsyOj4zEfZmAJI1f8Bi7h/ZA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@tapjs/error-serdes/-/error-serdes-4.0.1.tgz", + "integrity": "sha512-8GiOXbgGIRBcSGnPXYuiboy0xJQDMP2OcILnghHX/jzJKi2l9mxX6FTonOWj/0qsf5Ji5Z4/DIKRcYINIxaejg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "minipass": "^7.0.4" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@tapjs/filter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/filter/-/filter-2.0.1.tgz", - "integrity": "sha512-muKEeXK7Tz6VR4hjXfT2qXPvjYES575mtiRerjHf+8qP8D7MvmC8qDZJjzFdo1nZHKhF8snvFosIVuI1BAhvsw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/filter/-/filter-4.0.2.tgz", + "integrity": "sha512-Pfi1u1naoUL4G4AoJOUEUQauaZeF4gGk8MZ8It2ht60gopYsxy9sSmIKlKPvZm13f110pZxJfO+hogappAmDfw==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/fixture": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/fixture/-/fixture-2.0.1.tgz", - "integrity": "sha512-MLgEwsBlCD69iUbZcnKBehP2js5cV4p5GrFoOKSudMuH2DQJInaF/g2bkijue61cVZwPj/MRPCqAlkwA94epjg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/fixture/-/fixture-4.0.2.tgz", + "integrity": "sha512-h161PA9ngT3zG6iLybAUlrSWx31OFx7Av1aR4FHvui9IkseXaF0x6gxS0MMaXwdyRYrD7lFvmU7SRTDlvvdeBA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "mkdirp": "^3.0.0", - "rimraf": "^5.0.5" + "rimraf": "^6.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" + } + }, + "node_modules/@tapjs/fixture/node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@tapjs/intercept": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/intercept/-/intercept-2.0.1.tgz", - "integrity": "sha512-BZgXE3zCAbv4lfbph1r85gihtI3kXltHlFQ8Bf3Yy9fx27DKQlBvXnD7T69ke8kQLRzhz+wTMcR/mcQjo1fa7w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/intercept/-/intercept-4.0.2.tgz", + "integrity": "sha512-rlZcO/Yf9vU8ypfQEO0c/QUCcGCr0IMJltg53805ILuNhTDR7x0vlJSvJ7fLsuoezkUwXlWdlED6ZqIID7RokA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@tapjs/after": "1.1.24", - "@tapjs/stack": "2.0.1" + "@tapjs/after": "3.0.2", + "@tapjs/stack": "4.0.1" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/mock": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/mock/-/mock-2.0.1.tgz", - "integrity": "sha512-i1vkwNgO7uEuQW3+hTuE2L64aC9xk0cC3PtC6DZKqyApk2IstNgoIS38nfsI6v2kvEgZNuWlsNcRAYNDOIEhzA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/mock/-/mock-4.0.2.tgz", + "integrity": "sha512-48o3xz3Xl83Ei25KOR5aWytbCYUl33GQAoQm4khFpTyI/v3fjyR9jDgFLAT7IWix8tRrrkMf+PynmZkWVQ4oKA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@tapjs/after": "1.1.24", - "@tapjs/stack": "2.0.1", - "resolve-import": "^1.4.5", - "walk-up-path": "^3.0.1" + "@tapjs/after": "3.0.2", + "@tapjs/stack": "4.0.1", + "resolve-import": "^2.0.0", + "walk-up-path": "^4.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/node-serialize": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/node-serialize/-/node-serialize-2.0.1.tgz", - "integrity": "sha512-1GtHDa7AXpk8y08llIPfUKRTDNsq+BhXxz7wiIfVEAOEB09kGyfpWteOg+cmvb+aHU1Ays3z+medXTIBm0D5Kg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/node-serialize/-/node-serialize-4.0.2.tgz", + "integrity": "sha512-oparVnZqbwF7O7IQn4nfpnsddtmlkWPB033uzZbvSW6v0U6T8DBENgKAfzJ2sNQH3f2Xaa3dqK+N5brwQa3P2g==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@tapjs/error-serdes": "2.0.1", - "@tapjs/stack": "2.0.1", - "tap-parser": "16.0.1" + "@tapjs/error-serdes": "4.0.1", + "@tapjs/stack": "4.0.1", + "tap-parser": "18.0.1" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/processinfo": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@tapjs/processinfo/-/processinfo-3.1.7.tgz", - "integrity": "sha512-SI5RJQ5HnUKEWnHSAF6hOm6XPdnjZ+CJzIaVHdFebed8iDAPTqb+IwMVu9yq9+VQ7FRsMMlgLL2SW4rss2iJbQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@tapjs/processinfo/-/processinfo-3.1.8.tgz", + "integrity": "sha512-FIriEB+qqArPhmVYc1PZwRHD99myRdl7C9Oe/uts04Q2LOxQ5MEmqP9XOP8vVYzpDOYwmL8OmL6eOYt9eZlQKQ==", "dev": true, + "license": "ISC", "dependencies": { "pirates": "^4.0.5", "process-on-spawn": "^1.0.0", @@ -1024,40 +1275,42 @@ } }, "node_modules/@tapjs/reporter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/reporter/-/reporter-2.0.1.tgz", - "integrity": "sha512-fCdl4vg8vnlqIYtTQ9dc3zOqeXrA5QbATbT4dsPIiPuCM3gvKTbntaNBeaWWZkPx697Dj+b8TIxT/xhNMNv7jQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tapjs/reporter/-/reporter-4.0.3.tgz", + "integrity": "sha512-Mdx3+C3f4q3llawja5V2RVIfFzr1KhEm57i9xUJTgM5kHcPjrMw14aQUCwYxLSYSd5esOubnnV5lD9rxQb+tyQ==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@tapjs/config": "3.0.1", - "@tapjs/stack": "2.0.1", - "chalk": "^5.2.0", - "ink": "^4.4.1", + "@tapjs/config": "5.0.2", + "@tapjs/stack": "4.0.1", + "chalk": "^5.6.2", + "ink": "^5.2.1", "minipass": "^7.0.4", "ms": "^2.1.3", "patch-console": "^2.0.0", "prismjs-terminal": "^1.2.3", "react": "^18.2.0", "string-length": "^6.0.0", - "tap-parser": "16.0.1", - "tap-yaml": "2.2.2", - "tcompare": "7.0.1" + "tap-parser": "18.0.1", + "tap-yaml": "4.0.1", + "tcompare": "9.0.1" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/reporter/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "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" }, @@ -1065,85 +1318,84 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@tapjs/reporter/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "node_modules/@tapjs/reporter/node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } }, "node_modules/@tapjs/reporter/node_modules/tcompare": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz", - "integrity": "sha512-JN5s7hgmg/Ya5HxZqCnywT+XiOGRFcJRgYhtMyt/1m+h0yWpWwApO7HIM8Bpwyno9hI151ljjp5eAPCHhIGbpQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.0.1.tgz", + "integrity": "sha512-P9i7K3RnKWb3mH0AmDCChVd7FP0LEo4VSBJMhjKB+Rg+Tvq09ytnys8ygsx5ve1di2fI4IW+HhjhIXoeiRVogw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "diff": "^5.2.0", + "diff": "^8.0.2", "react-element-to-jsx-string": "^15.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" } }, "node_modules/@tapjs/run": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@tapjs/run/-/run-2.0.2.tgz", - "integrity": "sha512-2hPGlabqbLb3hh4BHHvwE8R9a9OiWumkCkHw5QQUZurDsVOpB94FfteqW9mktTVjZJnN0go+sN3GN2jZUaPWGQ==", - "dev": true, - "dependencies": { - "@tapjs/after": "1.1.24", - "@tapjs/before": "2.0.1", - "@tapjs/config": "3.0.1", - "@tapjs/processinfo": "^3.1.7", - "@tapjs/reporter": "2.0.1", - "@tapjs/spawn": "2.0.1", - "@tapjs/stdin": "2.0.1", - "@tapjs/test": "2.0.1", - "c8": "^9.1.0", - "chalk": "^5.3.0", - "chokidar": "^3.6.0", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tapjs/run/-/run-4.0.3.tgz", + "integrity": "sha512-kkoMk5OJPgxigZZwu47ix2Zo8U64t8YiFiz+Iec5HrSW1BNgQyhurfMbc8uGjBHh3OhvJBaWhWuzctn0ROJrEQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@tapjs/after": "3.0.2", + "@tapjs/before": "4.0.2", + "@tapjs/config": "5.0.2", + "@tapjs/processinfo": "^3.1.8", + "@tapjs/reporter": "4.0.3", + "@tapjs/spawn": "4.0.2", + "@tapjs/stdin": "4.0.2", + "@tapjs/test": "4.0.2", + "c8": "^10.1.3", + "chalk": "^5.6.2", + "chokidar": "^4.0.2", "foreground-child": "^3.1.1", - "glob": "^10.3.16", + "glob": "^11.0.0", "minipass": "^7.0.4", "mkdirp": "^3.0.1", "opener": "^1.5.2", - "pacote": "^17.0.6", - "resolve-import": "^1.4.5", - "rimraf": "^5.0.5", - "semver": "^7.6.0", + "pacote": "^21.0.3", + "path-scurry": "^2.0.0", + "resolve-import": "^2.0.0", + "rimraf": "^6.0.0", + "semver": "^7.7.2", "signal-exit": "^4.1.0", - "tap-parser": "16.0.1", - "tap-yaml": "2.2.2", - "tcompare": "7.0.1", + "tap-parser": "18.0.1", + "tap-yaml": "4.0.1", + "tcompare": "9.0.1", "trivial-deferred": "^2.0.0", - "which": "^4.0.0" + "which": "^5.0.0" }, "bin": { "tap-run": "dist/esm/index.js" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" }, "peerDependencies": { - "@tapjs/core": "2.0.1" - } - }, - "node_modules/@tapjs/run/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/run/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "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" }, @@ -1151,26 +1403,30 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@tapjs/run/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "node_modules/@tapjs/run/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">= 14.16.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tapjs/run/node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" } }, "node_modules/@tapjs/run/node_modules/isexe": { @@ -1178,43 +1434,92 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, - "node_modules/@tapjs/run/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "node_modules/@tapjs/run/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@tapjs/run/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/run/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tapjs/run/node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@tapjs/run/node_modules/tcompare": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz", - "integrity": "sha512-JN5s7hgmg/Ya5HxZqCnywT+XiOGRFcJRgYhtMyt/1m+h0yWpWwApO7HIM8Bpwyno9hI151ljjp5eAPCHhIGbpQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.0.1.tgz", + "integrity": "sha512-P9i7K3RnKWb3mH0AmDCChVd7FP0LEo4VSBJMhjKB+Rg+Tvq09ytnys8ygsx5ve1di2fI4IW+HhjhIXoeiRVogw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "diff": "^5.2.0", + "diff": "^8.0.2", "react-element-to-jsx-string": "^15.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" } }, "node_modules/@tapjs/run/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -1222,254 +1527,272 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@tapjs/snapshot": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/snapshot/-/snapshot-2.0.1.tgz", - "integrity": "sha512-ZnbCxL+9fiJ38tec6wvRtRBZz9ChRUq0Bov7dltdZMNkXqudKyB+Zzbg25bqDEIgcczyp6A9hOwTX6VybDGqpg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/snapshot/-/snapshot-4.0.2.tgz", + "integrity": "sha512-PWlWNEH+4x0oN8nemk+2rk3jub2L/7c6A383SD15GadGOT4hYvckqY2mCZarMAoV5xErFZOglGTD9do83TWMPQ==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "is-actual-promise": "^1.0.1", - "tcompare": "7.0.1", + "tcompare": "9.0.1", "trivial-deferred": "^2.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" + } + }, + "node_modules/@tapjs/snapshot/node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" } }, "node_modules/@tapjs/snapshot/node_modules/tcompare": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz", - "integrity": "sha512-JN5s7hgmg/Ya5HxZqCnywT+XiOGRFcJRgYhtMyt/1m+h0yWpWwApO7HIM8Bpwyno9hI151ljjp5eAPCHhIGbpQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.0.1.tgz", + "integrity": "sha512-P9i7K3RnKWb3mH0AmDCChVd7FP0LEo4VSBJMhjKB+Rg+Tvq09ytnys8ygsx5ve1di2fI4IW+HhjhIXoeiRVogw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "diff": "^5.2.0", + "diff": "^8.0.2", "react-element-to-jsx-string": "^15.0.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" } }, "node_modules/@tapjs/spawn": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/spawn/-/spawn-2.0.1.tgz", - "integrity": "sha512-3VaQKJjHV5frMZj3Ef+QlJyB6b7VsGMil223zAEz8Ttgy2hDYtcb29nvsLPUcowFyOUrsydnXEnHgpR79wEPOA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/spawn/-/spawn-4.0.2.tgz", + "integrity": "sha512-CNOrZFXh+mm2iul/cGRgVA4HMv9GxmtbgQSICMjRrj8VLDzMboW862P+wEvfYrHeXdCGAGWKqTIcsX5pXQVQRA==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/stack": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/stack/-/stack-2.0.1.tgz", - "integrity": "sha512-3rKbZkRkLeJl9ilV/6b80YfI4C4+OYf7iEz5/d0MIVhmVvxv0ttIy5JnZutAc4Gy9eRp5Ne5UTAIFOVY5k36cg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@tapjs/stack/-/stack-4.0.1.tgz", + "integrity": "sha512-Rbyz4XMuZWNxCs+/j0c5idFz4MKBo7uSaNvk6R7Al9jQJzk7Lv0WC2lWW0CV+7t/TUynTFxEwAaY5pIM752WQg==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@tapjs/stdin": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/stdin/-/stdin-2.0.1.tgz", - "integrity": "sha512-5Oe13Fzpnt9seAi8h3bsMxtJp8S+DQI6ncBD9JBcS91XKLbqyKrb1bNzeXQN2PrHBs6Atw8cOzFZh0TjL+bIaA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/stdin/-/stdin-4.0.2.tgz", + "integrity": "sha512-6e/jQ0I9G6DC9m6Yj3jC6sNAmiwvrPVni3iMpJn3GICs0dROyx1m9wnMBK5wgKNgN2AXfvEOtf5Cby124eHeJQ==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/test": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/test/-/test-2.0.1.tgz", - "integrity": "sha512-PKazf7r4+bLFATML2f/h8glGcSirXmzXUYlhFuxb4xHoOhHojyKgo1p8kSj+Ksxb3hVSCQlvyXgM8QYYaoMwog==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/test/-/test-4.0.2.tgz", + "integrity": "sha512-J8WOSesfqp6/P5UbChDI5xzREQ96787ZFHLliPva4oi5XevG1TWtfSL47HtbQUtKvWSC7YIWW3CQhcBHRGk6Vg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/ts-node-temp-fork-for-pr-2009": "^10.9.7", - "@tapjs/after": "1.1.24", - "@tapjs/after-each": "2.0.1", - "@tapjs/asserts": "2.0.1", - "@tapjs/before": "2.0.1", - "@tapjs/before-each": "2.0.1", - "@tapjs/filter": "2.0.1", - "@tapjs/fixture": "2.0.1", - "@tapjs/intercept": "2.0.1", - "@tapjs/mock": "2.0.1", - "@tapjs/node-serialize": "2.0.1", - "@tapjs/snapshot": "2.0.1", - "@tapjs/spawn": "2.0.1", - "@tapjs/stdin": "2.0.1", - "@tapjs/typescript": "1.4.6", - "@tapjs/worker": "2.0.1", - "glob": "^10.3.16", - "jackspeak": "^3.1.2", + "@tapjs/after": "3.0.2", + "@tapjs/after-each": "4.0.2", + "@tapjs/asserts": "4.0.2", + "@tapjs/before": "4.0.2", + "@tapjs/before-each": "4.0.2", + "@tapjs/chdir": "3.0.2", + "@tapjs/filter": "4.0.2", + "@tapjs/fixture": "4.0.2", + "@tapjs/intercept": "4.0.2", + "@tapjs/mock": "4.0.2", + "@tapjs/node-serialize": "4.0.2", + "@tapjs/snapshot": "4.0.2", + "@tapjs/spawn": "4.0.2", + "@tapjs/stdin": "4.0.2", + "@tapjs/typescript": "3.1.1", + "@tapjs/worker": "4.0.2", + "glob": "^11.0.3", + "jackspeak": "^4.0.1", "mkdirp": "^3.0.0", - "resolve-import": "^1.4.5", - "rimraf": "^5.0.5", - "sync-content": "^1.0.1", - "tap-parser": "16.0.1", - "tshy": "^1.14.0", - "typescript": "5.4", - "walk-up-path": "^3.0.1" + "package-json-from-dist": "^1.0.0", + "resolve-import": "^2.0.0", + "rimraf": "^6.0.0", + "sync-content": "^2.0.1", + "tap-parser": "18.0.1", + "tshy": "^3.0.3", + "typescript": "5.9", + "walk-up-path": "^4.0.0" }, "bin": { - "generate-tap-test-class": "scripts/build.mjs" + "generate-tap-test-class": "dist/esm/build.mjs" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, - "node_modules/@tapjs/test/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@tapjs/test/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "node_modules/@tapjs/test/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@tapjs/test/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "node_modules/@tapjs/test/node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@tapjs/typescript": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@tapjs/typescript/-/typescript-1.4.6.tgz", - "integrity": "sha512-6jxUQ7Mdb+Y2q8RJcwgZZ6dCR+X2u3hCL+xb1GDAtO7k1+B6z2b+z+I+FdhuO4YgrP0SLRjocL5rJM/xi9K7qw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@tapjs/typescript/-/typescript-3.1.1.tgz", + "integrity": "sha512-2fxDTeL8X3sm9g0KnHZSD/p9o8tFWYhswRKUq//jv9FA/4XetsKs+ApddPUJEi3AX8+Ma1P1EzjSy/f0z5KIJA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/ts-node-temp-fork-for-pr-2009": "^10.9.7" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tapjs/worker": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/worker/-/worker-2.0.1.tgz", - "integrity": "sha512-wegz8IxNEPIIAA+R76/avZgNmZ4iC7QGFbtXKGBU962/1lXTITxshRV6e21r0IBa7YLkSVgDuVSVB3+Qzve0Yg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/worker/-/worker-4.0.2.tgz", + "integrity": "sha512-o2jzyjEnpRf3xHmduIfO/HXb5m9qRz+tCoiiFmPav+LC7eilSrDP3eGiQwMGbew64PK52KQwQW/LYNRrQsuMxA==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "peerDependencies": { - "@tapjs/core": "2.0.1" + "@tapjs/core": "4.0.2" } }, "node_modules/@tsconfig/node14": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-14.1.2.tgz", - "integrity": "sha512-1vncsbfCZ3TBLPxesRYz02Rn7SNJfbLoDVkcZ7F/ixOV6nwxwgdhD1mdPcc5YQ413qBJ8CvMxXMFfJ7oawjo7Q==", - "dev": true + "version": "14.1.5", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-14.1.5.tgz", + "integrity": "sha512-hsldDMdbjF18BgvqFX6rHwqb0wlDh4lxyXmo3VATa7LwL4AFrHijv8Or9ySXBSg9TyysRkldJyAC/kplyF/Mmg==", + "dev": true, + "license": "MIT" }, "node_modules/@tsconfig/node16": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-16.1.3.tgz", - "integrity": "sha512-9nTOUBn+EMKO6rtSZJk+DcqsfgtlERGT9XPJ5PRj/HNENPCBY1yu/JEj5wT6GLtbCLBO2k46SeXDaY0pjMqypw==", - "dev": true + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-16.1.5.tgz", + "integrity": "sha512-P4GBZdpi6i5NGB12pwLH4oAIPrxl1ZvD8gxA4E7VdUUCik34J7FerrxzoDXtT4ugbrRjLbK5WuHPOUNZJOxuaQ==", + "dev": true, + "license": "MIT" }, "node_modules/@tsconfig/node18": { "version": "18.2.4", "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.4.tgz", "integrity": "sha512-5xxU8vVs9/FNcvm3gE07fPbn9tl6tqGGWA9tSlwsUEkBxtRnTsNmwrV8gasZ9F/EobaSv9+nu8AxUKccw77JpQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@tsconfig/node20": { - "version": "20.1.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz", - "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", - "dev": true + "version": "20.1.6", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.6.tgz", + "integrity": "sha512-sz+Hqx9zwZDpZIV871WSbUzSqNIsXzghZydypnfgzPKLltVJfkINfUeTct31n/tTSa9ZE1ZOfKdRre1uHHquYQ==", + "dev": true, + "license": "MIT" }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", "dev": true, + "license": "MIT", "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@tufjs/models": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", - "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.0.0.tgz", + "integrity": "sha512-h5x5ga/hh82COe+GoD4+gKUeV4T3iaYOxqLt41GRKApinPI7DMidhCmNVTjKfhCWFJIGXaFJee07XczdT4jdZQ==", "dev": true, + "license": "MIT", "dependencies": { "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.4" + "minimatch": "^9.0.5" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1480,35 +1803,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" }, "node_modules/@types/node": { - "version": "20.13.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.13.0.tgz", - "integrity": "sha512-FM6AOb3khNkNIXPnHFDYaHerSv8uN22C91z098AnGccVu+Pcdhi+pNUFDi0iLmPIsVE0JBD0KVS7mzUYt4nRzQ==", + "version": "24.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", + "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.14.0" } }, "node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1520,57 +1859,39 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { "node": ">=0.4.0" } }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, + "license": "MIT", "engines": { "node": ">= 14" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aggregate-error/node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1583,12 +1904,16 @@ } }, "node_modules/ansi-escapes": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", - "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1633,7 +1958,8 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", @@ -1645,6 +1971,7 @@ "resolved": "https://registry.npmjs.org/async-hook-domain/-/async-hook-domain-4.0.1.tgz", "integrity": "sha512-bSktexGodAjfHWIrSrrqxqWzf1hWBZBpmPNZv+TYUMyWa2eoefFc6q6H1+KtdHYSz35lrhWdmXt/XK9wNEZvww==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } @@ -1654,6 +1981,7 @@ "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -1676,9 +2004,10 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1698,19 +2027,20 @@ } }, "node_modules/c8": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", - "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", "dev": true, + "license": "ISC", "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", + "@bcoe/v8-coverage": "^1.0.1", "@istanbuljs/schema": "^0.1.3", "find-up": "^5.0.0", "foreground-child": "^3.1.1", "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.1.6", - "test-exclude": "^6.0.0", + "test-exclude": "^7.0.1", "v8-to-istanbul": "^9.0.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1" @@ -1719,82 +2049,55 @@ "c8": "bin/c8.js" }, "engines": { - "node": ">=14.14.0" + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } } }, "node_modules/cacache": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", - "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.1.tgz", + "integrity": "sha512-+7LYcYGBYoNqTp1Rv7Ny1YjUo5E0/ftkQtraH3vkfAGgVHc+ouWdC8okAwQgQR7EVIdW6JTzTmhKFwzb+4okAQ==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/fs": "^3.1.0", + "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", + "glob": "^11.0.3", + "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "unique-filename": "^4.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "license": "ISC", "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "20 || >=22" } }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } @@ -1851,12 +2154,13 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/chroma-js": { @@ -1864,35 +2168,12 @@ "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1905,6 +2186,7 @@ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^4.0.0" }, @@ -1916,26 +2198,41 @@ } }, "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, + "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" + "string-width": "^7.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/cli-truncate/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "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" }, @@ -1943,11 +2240,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/cli-truncate/node_modules/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, + "license": "MIT" + }, "node_modules/cli-truncate/node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" @@ -1959,11 +2264,46 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/cli-truncate/node_modules/string-width": { + "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", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1977,13 +2317,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cliui/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1993,6 +2335,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2007,6 +2350,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2024,6 +2368,7 @@ "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", "dev": true, + "license": "MIT", "dependencies": { "convert-to-spaces": "^2.0.1" }, @@ -2066,21 +2411,24 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/convert-to-spaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, "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==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2091,11 +2439,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2109,7 +2458,8 @@ "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==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" }, "node_modules/diff": { "version": "5.2.0", @@ -2120,17 +2470,6 @@ "node": ">=0.3.1" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2146,6 +2485,7 @@ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -2156,21 +2496,48 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz", + "integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "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" } @@ -2187,95 +2554,105 @@ } }, "node_modules/eslint": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.37.0.tgz", - "integrity": "sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.37.0", - "@humanwhocodes/config-array": "^0.11.8", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "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", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "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==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2296,6 +2673,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -2315,6 +2693,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -2324,48 +2703,46 @@ "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-2.0.3.tgz", "integrity": "sha512-f/qE2gImHRa4Cp2y1stEOSgw8wTFyUdVJX7G//bMwbaV9JqISFxg99NbmVQeP7YLnDUZ2un851jlaDrlpmGehQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", - "dev": true + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.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==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "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==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" }, "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==" - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dependencies": { - "reusify": "^1.0.4" - } + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" }, "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==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -2397,37 +2774,23 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "license": "MIT", "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "license": "ISC" }, "node_modules/flexsearch": { "version": "0.7.43", @@ -2435,11 +2798,12 @@ "integrity": "sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==" }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -2467,13 +2831,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -2481,11 +2847,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -2500,44 +2861,55 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/function-loop": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/function-loop/-/function-loop-4.0.0.tgz", "integrity": "sha512-f34iQBedYF3XcI93uewZZOnyscDragxgTK/eTvVB74k3fCD0ZorOi5BV9GS4M8rz/JoNi0Kl3qX5Y9MH3S/CLQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/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, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2554,15 +2926,72 @@ "node": ">=10.13.0" } }, - "node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "node_modules/glob/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "type-fest": "^0.20.2" + "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=8" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2572,12 +3001,8 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + "dev": true, + "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", @@ -2587,18 +3012,6 @@ "node": ">=8" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2608,34 +3021,48 @@ } }, "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", "dev": true, + "license": "ISC", "dependencies": { - "lru-cache": "^10.0.1" + "lru-cache": "^11.1.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" } }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -2645,12 +3072,13 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -2662,6 +3090,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -2671,44 +3100,38 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/ignore-walk": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", - "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", "dev": true, + "license": "ISC", "dependencies": { - "minimatch": "^9.0.0" + "minimatch": "^10.0.3" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/ignore-walk/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2729,9 +3152,10 @@ } }, "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==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2756,6 +3180,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2763,54 +3188,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/ink": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/ink/-/ink-4.4.1.tgz", - "integrity": "sha512-rXckvqPBB0Krifk5rn/5LvQGmyXwCUpBfmTwbkQNBY9JY8RSl3b8OftBNEYxg4+SWUhEKcPifgope28uL9inlA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", "dev": true, + "license": "MIT", "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", - "ansi-escapes": "^6.0.0", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", - "chalk": "^5.2.0", + "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", - "cli-truncate": "^3.1.0", + "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", - "is-ci": "^3.0.1", - "is-lower-case": "^2.0.2", - "is-upper-case": "^2.0.2", - "lodash": "^4.17.21", + "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.29.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", - "slice-ansi": "^6.0.0", + "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", - "string-width": "^5.1.2", - "type-fest": "^0.12.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.1.0", - "ws": "^8.12.0", - "yoga-wasm-web": "~0.3.3" + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "peerDependencies": { "@types/react": ">=18.0.0", @@ -2826,11 +3252,38 @@ } } }, + "node_modules/ink/node_modules/ansi-regex": { + "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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ink/node_modules/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, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/ink/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "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" }, @@ -2838,34 +3291,79 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/ink/node_modules/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, + "license": "MIT" + }, "node_modules/ink/node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/ink/node_modules/type-fest": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", - "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==", + "node_modules/ink/node_modules/string-width": { + "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", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "node_modules/ink/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, + "license": "MIT", "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/ink/node_modules/wrap-ansi": { + "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", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { "node": ">= 12" } }, @@ -2873,7 +3371,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-actual-promise/-/is-actual-promise-1.0.2.tgz", "integrity": "sha512-xsFiO1of0CLsQnPZ1iXHNTyR9YszOeWKYv+q6n8oSFW3ipooFJ1j1lbRMgiMCr+pp2gLruESI4zb5Ak6eK5OnQ==", - "dev": true + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -2887,30 +3386,6 @@ "node": ">=8" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "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", @@ -2924,6 +3399,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2942,19 +3418,20 @@ "node": ">=0.10.0" } }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true - }, - "node_modules/is-lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz", - "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==", + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", "dev": true, - "dependencies": { - "tslib": "^2.0.3" + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-number": { @@ -2967,14 +3444,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -2984,15 +3453,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", - "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3003,6 +3463,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } @@ -3012,6 +3473,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -3022,10 +3484,11 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -3051,15 +3514,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3082,25 +3536,27 @@ "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", - "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", "dev": true, + "license": "MIT", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "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==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -3114,12 +3570,23 @@ "dev": true, "engines": [ "node >= 0.2.0" - ] + ], + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } }, "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==", + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -3142,12 +3609,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3178,6 +3639,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -3192,29 +3654,101 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/make-fetch-happen": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", - "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", + "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1", - "ssri": "^10.0.0" + "ssri": "^12.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/make-fetch-happen/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/make-fetch-happen/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/make-fetch-happen/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/marked": { @@ -3233,6 +3767,7 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3261,6 +3796,7 @@ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -3269,17 +3805,18 @@ } }, "node_modules/minipass-fetch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", - "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", "dev": true, + "license": "MIT", "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "minizlib": "^3.0.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" }, "optionalDependencies": { "encoding": "^0.1.13" @@ -3290,6 +3827,7 @@ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -3302,6 +3840,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -3309,33 +3848,19 @@ "node": ">=8" } }, - "node_modules/minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", - "dev": true, - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "node_modules/minipass-json-stream/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "ISC" }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -3348,6 +3873,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -3355,11 +3881,19 @@ "node": ">=8" } }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/minipass-sized": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -3372,6 +3906,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -3379,29 +3914,24 @@ "node": ">=8" } }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } + "license": "ISC" }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">= 18" } }, "node_modules/mkdirp": { @@ -3409,6 +3939,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true, + "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" }, @@ -3420,9 +3951,10 @@ } }, "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==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/msgpackr": { "version": "1.10.2", @@ -3459,36 +3991,38 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/node-gyp": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", - "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", "dev": true, + "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^13.0.0", - "nopt": "^7.0.0", - "proc-log": "^3.0.0", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^4.0.0" + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/node-gyp-build-optional-packages": { @@ -3502,75 +4036,22 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/node-gyp/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/node-gyp/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, - "node_modules/node-gyp/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/node-gyp/node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/node-gyp/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -3578,37 +4059,23 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", "dev": true, + "license": "ISC", "dependencies": { - "abbrev": "^2.0.0" + "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", - "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", - "dev": true, - "dependencies": { - "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/normalize-path": { @@ -3621,105 +4088,155 @@ } }, "node_modules/npm-bundled": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", - "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", "dev": true, + "license": "ISC", "dependencies": { - "npm-normalize-package-bin": "^3.0.0" + "npm-normalize-package-bin": "^4.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm-install-checks": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", - "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.2.tgz", + "integrity": "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm-package-arg": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.2.tgz", - "integrity": "sha512-IGN0IAwmhDJwy13Wc8k+4PEbTPhpJnMtfR53ZbOyjkvmEcLS4nCwp6mvMWjS5sUjeiW3mpx6cHmuhKEu9XmcQw==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.1.tgz", + "integrity": "sha512-6zqls5xFvJbgFjB1B2U6yITtyGBjDBORB7suI4zA4T/sZ1OmkMFlaQSNB/4K0LtXNA1t4OprAFxPisadK5O2ag==", "dev": true, + "license": "ISC", "dependencies": { - "hosted-git-info": "^7.0.0", - "proc-log": "^4.0.0", + "hosted-git-info": "^9.0.0", + "proc-log": "^5.0.0", "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" + "validate-npm-package-name": "^6.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-packlist": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", - "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.2.tgz", + "integrity": "sha512-DrIWNiWT0FTdDRjGOYfEEZUNe1IzaSZ+up7qBTKnrQDySpdmuOQvytrqQlpK5QrCA4IThMvL4wTumqaa1ZvVIQ==", "dev": true, + "license": "ISC", "dependencies": { - "ignore-walk": "^6.0.4" + "ignore-walk": "^8.0.0", + "proc-log": "^5.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-pick-manifest": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.1.tgz", - "integrity": "sha512-Udm1f0l2nXb3wxDpKjfohwgdFUSV50UVwzEIpDXVsbDMXVIEF81a/i0UhuQbhrPMMmdiq3+YMFLFIRVLs3hxQw==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.1.tgz", + "integrity": "sha512-HnU7FYSWbo7dTVHtK0G+BXbZ0aIfxz/aUCVLN0979Ec6rGUX5cJ6RbgVx5fqb5G31ufz+BVFA7y1SkRTPVNoVQ==", "dev": true, + "license": "ISC", "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^11.0.0", + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^13.0.0", "semver": "^7.3.5" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-registry-fetch": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.2.1.tgz", - "integrity": "sha512-8l+7jxhim55S85fjiDGJ1rZXBWGtRLi1OSb4Z3BPLObPuIaeKRlPRiYMSHU4/81ck3t71Z+UwDDl47gcpmfQQA==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.0.0.tgz", + "integrity": "sha512-DFxSAemHUwT/POaXAOY4NJmEWBPB0oKbwD6FFDE9hnt1nORkt/FXvgjD4hQjoKoHw9u0Ezws9SPXwV7xE/Gyww==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/redact": "^1.1.0", - "make-fetch-happen": "^13.0.0", + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^11.0.0", - "proc-log": "^4.0.0" + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^5.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "node_modules/npm-registry-fetch/node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.2.tgz", + "integrity": "sha512-sI1NY4lWlXBAfjmCtVWIIpBypbBdhHtcjnwnv+gtCnsaOffyFil3aidszGC8hgzJe+fT1qix05sWxmD/Bmf/oQ==", + "dev": true, + "license": "ISC", "dependencies": { - "wrappy": "1" + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/onetime": { @@ -3727,6 +4244,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -3742,21 +4260,23 @@ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true, + "license": "(WTFPL OR MIT)", "bin": { "opener": "bin/opener-bin.js" } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", "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" + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -3791,56 +4311,62 @@ } }, "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pacote": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.7.tgz", - "integrity": "sha512-sgvnoUMlkv9xHwDUKjKQFXVyUi8dtJGKp3vg6sYy+TxbDic5RjZCHF3ygv0EJgNRZ2GfRONjlKPUfokJ9lDpwQ==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.3.tgz", + "integrity": "sha512-itdFlanxO0nmQv4ORsvA9K1wv40IPfB9OmWqfaJWvoJ30VKyHsqNgDVeG+TVhI7Gk7XW8slUy7cA9r6dF5qohw==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/git": "^5.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/promise-spawn": "^7.0.0", - "@npmcli/run-script": "^7.0.0", - "cacache": "^18.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", - "npm-package-arg": "^11.0.0", - "npm-packlist": "^8.0.0", - "npm-pick-manifest": "^9.0.0", - "npm-registry-fetch": "^16.0.0", - "proc-log": "^4.0.0", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1", - "read-package-json": "^7.0.0", - "read-package-json-fast": "^3.0.0", - "sigstore": "^2.2.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" + "sigstore": "^4.0.0", + "ssri": "^12.0.0", + "tar": "^7.4.3" }, "bin": { - "pacote": "lib/bin.js" + "pacote": "bin/index.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.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==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -3853,6 +4379,7 @@ "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } @@ -3865,14 +4392,6 @@ "node": ">=8" } }, - "node_modules/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=", - "engines": { - "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", @@ -3909,19 +4428,21 @@ } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/polite-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/polite-json/-/polite-json-4.0.1.tgz", - "integrity": "sha512-8LI5ZeCPBEb4uBbcYKNVwk4jgqNx1yHReWoW4H4uUihWlSqZsUDfSITrRhjliuPgxsNPFhNSudGO2Zu4cbWinQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/polite-json/-/polite-json-5.0.0.tgz", + "integrity": "sha512-OLS/0XeUAcE8a2fdwemNja+udKgXNnY6yKVIXqAD2zVRx1KvY6Ato/rZ2vdzbxqYwPW0u6SCNC/bAMPNzpzxbw==", "dev": true, + "license": "MIT", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" }, @@ -3933,15 +4454,17 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3951,6 +4474,7 @@ "resolved": "https://registry.npmjs.org/prismjs-terminal/-/prismjs-terminal-1.2.3.tgz", "integrity": "sha512-xc0zuJ5FMqvW+DpiRkvxURlz98DdfDsZcFHdO699+oL+ykbFfgI7O4VDEgUyc07BSL2NHl3zdb8m/tZ/aaqUrw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "chalk": "^5.2.0", "prismjs": "^1.29.0", @@ -3964,10 +4488,11 @@ } }, "node_modules/prismjs-terminal/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "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" }, @@ -3976,19 +4501,21 @@ } }, "node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/process-on-spawn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", - "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", "dev": true, + "license": "MIT", "dependencies": { "fromentries": "^1.2.0" }, @@ -3996,17 +4523,12 @@ "node": ">=8" } }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, + "license": "MIT", "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -4016,9 +4538,10 @@ } }, "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==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -4031,25 +4554,6 @@ "inherits": "~2.0.3" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -4102,6 +4606,7 @@ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4113,81 +4618,6 @@ "react": "^18.3.1" } }, - "node_modules/read-package-json": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", - "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", - "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", - "dev": true, - "dependencies": { - "glob": "^10.2.2", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/read-package-json-fast": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", - "dev": true, - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-package-json/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/read-package-json/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/read-package-json/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4205,6 +4635,7 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4213,67 +4644,23 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/resolve-import": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/resolve-import/-/resolve-import-1.4.5.tgz", - "integrity": "sha512-HXb4YqODuuXT7Icq1Z++0g2JmhgbUHSs3VT2xR83gqvAPUikYT2Xk+562KHQgiaNkbBOlPddYrDLsC44qQggzw==", - "dev": true, - "dependencies": { - "glob": "^10.3.3", - "walk-up-path": "^3.0.1" - }, - "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/resolve-import/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/resolve-import/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/resolve-import/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-import/-/resolve-import-2.0.0.tgz", + "integrity": "sha512-jpKjLibLuc8D1XEV2+7zb0aqN7I8d12u89g/v6IsgCzdVlccMQJq4TKkPw5fbhHdxhm7nbVtN+KvOTnjFf+nEA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "glob": "^11.0.0", + "walk-up-path": "^4.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4284,6 +4671,7 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -4299,26 +4687,19 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rimraf": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", @@ -4338,9 +4719,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4383,33 +4764,12 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, + "license": "MIT", "optional": true }, "node_modules/scheduler": { @@ -4422,10 +4782,11 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4464,43 +4825,46 @@ } }, "node_modules/sigstore": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", - "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.0.0.tgz", + "integrity": "sha512-Gw/FgHtrLM9WP8P5lLcSGh9OQcrTruWCELAiS48ik1QbL0cH+dfjomiRTUE9zzz+D1N6rOLkwXUvVmXZAsNE0Q==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "@sigstore/sign": "^2.3.2", - "@sigstore/tuf": "^2.3.4", - "@sigstore/verify": "^1.2.1" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.0.0", + "@sigstore/tuf": "^4.0.0", + "@sigstore/verify": "^3.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/slice-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-6.0.0.tgz", - "integrity": "sha512-6bn4hRfkTvDfUoEQYkERg0BVF1D0vrX9HEkMl08uDiNWvVvjylLHvZFZWkDo6wjT8tUctbYl1nCOuE66ZTaUtA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^4.0.0" + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "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" }, @@ -4508,23 +4872,41 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dev": true, + "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -4533,14 +4915,15 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "^7.1.1", + "agent-base": "^7.1.2", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" }, "engines": { "node": ">= 14" @@ -4551,6 +4934,7 @@ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -4560,40 +4944,38 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true + "dev": true, + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, + "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", - "dev": true - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" }, "node_modules/ssri": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", - "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/stack-utils": { @@ -4601,6 +4983,7 @@ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -4613,6 +4996,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4622,6 +5006,7 @@ "resolved": "https://registry.npmjs.org/string-length/-/string-length-6.0.0.tgz", "integrity": "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==", "dev": true, + "license": "MIT", "dependencies": { "strip-ansi": "^7.1.0" }, @@ -4633,10 +5018,11 @@ } }, "node_modules/string-length/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "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" }, @@ -4645,10 +5031,11 @@ } }, "node_modules/string-length/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -4754,6 +5141,7 @@ "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==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -4778,230 +5166,283 @@ } }, "node_modules/sync-content": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-1.0.2.tgz", - "integrity": "sha512-znd3rYiiSxU3WteWyS9a6FXkTA/Wjk8WQsOyzHbineeL837dLn3DA4MRhsIX3qGcxDMH6+uuFV4axztssk7wEQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-2.0.1.tgz", + "integrity": "sha512-NI1mo514yFhr8pV/5Etvgh+pSBUIpoAKoiBIUwALVlQQNAwb40bTw8hhPFaip/dvv0GhpHVOq0vq8iY02ppLTg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^10.2.6", + "glob": "^11.0.0", "mkdirp": "^3.0.1", - "path-scurry": "^1.9.2", - "rimraf": "^5.0.1" + "path-scurry": "^2.0.0", + "rimraf": "^6.0.0", + "tshy": "^3.0.0" }, "bin": { - "sync-content": "dist/mjs/bin.mjs" + "sync-content": "dist/esm/bin.mjs" }, "engines": { - "node": ">=14" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sync-content/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/sync-content/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "license": "ISC", + "engines": { + "node": "20 || >=22" } }, - "node_modules/sync-content/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "node_modules/sync-content/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sync-content/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "node_modules/sync-content/node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/tap": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/tap/-/tap-19.0.2.tgz", - "integrity": "sha512-SRGulk1RKlVuYtnPeephj+xyE0sG9CvGlKYP4lymBZykLtkwBPnEBjQ2iQmLX5z0BFEMfKh8G4bvZkhoSJb3kg==", - "dev": true, - "dependencies": { - "@tapjs/after": "1.1.24", - "@tapjs/after-each": "2.0.1", - "@tapjs/asserts": "2.0.1", - "@tapjs/before": "2.0.1", - "@tapjs/before-each": "2.0.1", - "@tapjs/core": "2.0.1", - "@tapjs/filter": "2.0.1", - "@tapjs/fixture": "2.0.1", - "@tapjs/intercept": "2.0.1", - "@tapjs/mock": "2.0.1", - "@tapjs/node-serialize": "2.0.1", - "@tapjs/run": "2.0.2", - "@tapjs/snapshot": "2.0.1", - "@tapjs/spawn": "2.0.1", - "@tapjs/stdin": "2.0.1", - "@tapjs/test": "2.0.1", - "@tapjs/typescript": "1.4.6", - "@tapjs/worker": "2.0.1", - "resolve-import": "^1.4.5" + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/tap/-/tap-21.1.1.tgz", + "integrity": "sha512-WQQkoJw2LbusXPq9d6A3N4SHCpiog1AbjSVyNlqRh6uiCrtra24bZRCURX8cgBjKV4W22dcRevvhMp24+N/oVg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@tapjs/after": "3.0.2", + "@tapjs/after-each": "4.0.2", + "@tapjs/asserts": "4.0.2", + "@tapjs/before": "4.0.2", + "@tapjs/before-each": "4.0.2", + "@tapjs/chdir": "3.0.2", + "@tapjs/core": "4.0.2", + "@tapjs/filter": "4.0.2", + "@tapjs/fixture": "4.0.2", + "@tapjs/intercept": "4.0.2", + "@tapjs/mock": "4.0.2", + "@tapjs/node-serialize": "4.0.2", + "@tapjs/run": "4.0.3", + "@tapjs/snapshot": "4.0.2", + "@tapjs/spawn": "4.0.2", + "@tapjs/stdin": "4.0.2", + "@tapjs/test": "4.0.2", + "@tapjs/typescript": "3.1.1", + "@tapjs/worker": "4.0.2", + "resolve-import": "2" }, "bin": { "tap": "dist/esm/run.mjs" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/tap-parser": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-16.0.1.tgz", - "integrity": "sha512-vKianJzSSzLkJ3bHBwzvZDDRi9yGMwkRANJxwPAjAue50owB8rlluYySmTN4tZVH0nsh6stvrQbg9kuCL5svdg==", + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-18.0.1.tgz", + "integrity": "sha512-Jcu51jzPCDXzuFvyCDGRfJWm2mJ0yRLf+aqWINk4P03HM9uyrq2SLavn26N8qwVz//GZysNt3DQNvKrYjidB3g==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "events-to-array": "^2.0.3", - "tap-yaml": "2.2.2" + "tap-yaml": "4.0.1" }, "bin": { "tap-parser": "bin/cmd.cjs" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" } }, "node_modules/tap-yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-2.2.2.tgz", - "integrity": "sha512-MWG4OpAKtNoNVjCz/BqlDJiwTM99tiHRhHPS4iGOe1ZS0CgM4jSFH92lthSFvvy4EdDjQZDV7uYqUFlU9JuNhw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-4.0.1.tgz", + "integrity": "sha512-2D5FSjxSP8v3LCh3N1KZa+FoIaVh2f7bFaUNd4mm74Kx/a5JqKu6QspEQZpAwH1Ez2SlbguOMWKHPTels2xbzA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "yaml": "^2.4.1", - "yaml-types": "^0.3.0" + "yaml": "^2.8.1", + "yaml-types": "^0.4.0" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": "20 || >=22" } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", "dev": true, + "license": "ISC", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "node_modules/tcompare": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-6.4.5.tgz", + "integrity": "sha512-Whuz9xlKKI2XXICKDSDRKjXdBuC6gBNOgmEUtH7UFyQeYzfUMQ19DyjZULarGKDGFhgOg3CJ+IQUEfpkOPg0Uw==", "dev": true, "dependencies": { - "minipass": "^3.0.0" + "diff": "^5.1.0", + "react-element-to-jsx-string": "^15.0.0" }, "engines": { - "node": ">= 8" + "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" } }, - "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, + "license": "ISC", "dependencies": { - "yallist": "^4.0.0" + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, "bin": { - "mkdirp": "bin/cmd.js" + "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/tcompare": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-6.4.5.tgz", - "integrity": "sha512-Whuz9xlKKI2XXICKDSDRKjXdBuC6gBNOgmEUtH7UFyQeYzfUMQ19DyjZULarGKDGFhgOg3CJ+IQUEfpkOPg0Uw==", + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { - "diff": "^5.1.0", - "react-element-to-jsx-string": "^15.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "16 >=16.17.0 || 18 >= 18.6.0 || >=20" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, + "license": "MIT", "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=8" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "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==" + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -5021,49 +5462,43 @@ "resolved": "https://registry.npmjs.org/trivial-deferred/-/trivial-deferred-2.0.0.tgz", "integrity": "sha512-iGbM7X2slv9ORDVj2y2FFUq3cP/ypbtu2nQ8S38ufjL0glBABvmR9pTdsib1XtS2LUhhLMbelaBUaf/s5J3dSw==", "dev": true, + "license": "ISC", "engines": { "node": ">= 8" } }, "node_modules/tshy": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/tshy/-/tshy-1.14.0.tgz", - "integrity": "sha512-YiUujgi4Jb+t2I48LwSRzHkBpniH9WjjktNozn+nlsGmVemKSjDNY7EwBRPvPCr5zAC/3ITAYWH9Z7kUinGSrw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tshy/-/tshy-3.0.3.tgz", + "integrity": "sha512-bUX6HQCvVdPyPLy2VZuKw95CtYD5aRSEgYEK7IPV9l9xN/z284kl5/hIwOfLY/mZOOdhrO34dFOOcL1VUMVyaw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "chalk": "^5.3.0", - "chokidar": "^3.6.0", - "foreground-child": "^3.1.1", - "minimatch": "^9.0.4", + "chalk": "^5.6.2", + "chokidar": "^4.0.3", + "foreground-child": "^3.3.1", + "minimatch": "^10.0.3", "mkdirp": "^3.0.1", - "polite-json": "^4.0.1", - "resolve-import": "^1.4.5", - "rimraf": "^5.0.1", - "sync-content": "^1.0.2", - "typescript": "^5.4.5", - "walk-up-path": "^3.0.1" + "polite-json": "^5.0.0", + "resolve-import": "^2.0.0", + "rimraf": "^6.0.1", + "sync-content": "^2.0.1", + "typescript": "^5.9.3", + "walk-up-path": "^4.0.0" }, "bin": { "tshy": "dist/esm/index.js" }, "engines": { - "node": "16 >=16.17 || 18 >=18.15.0 || >=20.6.1" - } - }, - "node_modules/tshy/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "node": "20 || >=22" } }, "node_modules/tshy/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "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" }, @@ -5071,16 +5506,67 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/tshy/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/tshy/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tshy/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/tshy/node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5092,23 +5578,75 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tuf-js": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", - "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.0.0.tgz", + "integrity": "sha512-Lq7ieeGvXDXwpoSmOSgLWVdsGGV9J4a77oDTAPe/Ltrqnnm/ETaRlBAQTH5JatEh8KXuE6sddf9qAv1Q2282Hg==", "dev": true, + "license": "MIT", "dependencies": { - "@tufjs/models": "2.0.1", - "debug": "^4.3.4", - "make-fetch-happen": "^13.0.1" + "@tufjs/models": "4.0.0", + "debug": "^4.4.1", + "make-fetch-happen": "^15.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/tuf-js/node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/tuf-js/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/tuf-js/node_modules/make-fetch-happen": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.2.tgz", + "integrity": "sha512-sI1NY4lWlXBAfjmCtVWIIpBypbBdhHtcjnwnv+gtCnsaOffyFil3aidszGC8hgzJe+fT1qix05sWxmD/Bmf/oQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "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==", + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -5117,21 +5655,24 @@ } }, "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==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5141,40 +5682,44 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "dev": true, + "license": "ISC", "dependencies": { - "unique-slug": "^4.0.0" + "unique-slug": "^5.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", "dev": true, + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -5184,6 +5729,7 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } @@ -5192,13 +5738,15 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/v8-to-istanbul": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", - "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, + "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -5209,10 +5757,11 @@ } }, "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -5223,25 +5772,31 @@ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "node_modules/validate-npm-package-name": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", - "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", + "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/walk-up-path": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", - "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } }, "node_modules/which": { "version": "2.0.2", @@ -5258,20 +5813,75 @@ } }, "node_modules/widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", "dev": true, + "license": "MIT", "dependencies": { - "string-width": "^5.0.1" + "string-width": "^7.0.0" }, "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "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" }, "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/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, + "license": "MIT" + }, + "node_modules/widest-line/node_modules/string-width": { + "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", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5375,15 +5985,10 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", "engines": { @@ -5407,33 +6012,40 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } }, "node_modules/yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yaml-types": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yaml-types/-/yaml-types-0.3.0.tgz", - "integrity": "sha512-i9RxAO/LZBiE0NJUy9pbN5jFz5EasYDImzRkj8Y81kkInTi1laia3P3K/wlMKzOxFQutZip8TejvQP/DwgbU7A==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/yaml-types/-/yaml-types-0.4.0.tgz", + "integrity": "sha512-XfbA30NUg4/LWUiplMbiufUiwYhgB9jvBhTWel7XQqjV+GaB79c2tROu/8/Tu7jO0HvDvnKWtBk5ksWRrhQ/0g==", "dev": true, + "license": "ISC", "engines": { "node": ">= 16", "npm": ">= 7" @@ -5447,6 +6059,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -5465,6 +6078,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -5473,13 +6087,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5489,6 +6105,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5509,11 +6126,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoga-wasm-web": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", - "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", - "dev": true + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "dev": true, + "license": "MIT" } }, "dependencies": { @@ -5528,9 +6146,9 @@ }, "dependencies": { "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true } } @@ -5542,9 +6160,9 @@ "dev": true }, "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true }, "@cspotcode/source-map-support": { @@ -5557,27 +6175,60 @@ } }, "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "requires": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + } } }, "@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==" + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==" + }, + "@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "requires": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + } + }, + "@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "requires": { + "@eslint/core": "^0.16.0" + } + }, + "@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "requires": { + "@types/json-schema": "^7.0.15" + } }, "@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -5586,18 +6237,43 @@ } }, "@eslint/js": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.37.0.tgz", - "integrity": "sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==" + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==" + }, + "@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==" }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + } + }, + "@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" + }, + "@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "requires": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "dependencies": { + "@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==" + } } }, "@humanwhocodes/module-importer": { @@ -5605,10 +6281,25 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" }, - "@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==" + "@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" + }, + "@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true + }, + "@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "requires": { + "@isaacs/balanced-match": "^4.0.1" + } }, "@isaacs/cliui": { "version": "8.0.2", @@ -5638,6 +6329,15 @@ } } }, + "@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "requires": { + "minipass": "^7.0.4" + } + }, "@isaacs/ts-node-temp-fork-for-pr-2009": { "version": "10.9.7", "resolved": "https://registry.npmjs.org/@isaacs/ts-node-temp-fork-for-pr-2009/-/ts-node-temp-fork-for-pr-2009-10.9.7.tgz", @@ -5678,9 +6378,9 @@ "dev": true }, "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, "@jridgewell/trace-mapping": { @@ -5738,33 +6438,10 @@ "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==", "optional": true }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, "@npmcli/agent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", - "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", "dev": true, "requires": { "agent-base": "^7.1.0", @@ -5775,28 +6452,28 @@ } }, "@npmcli/fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", - "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "dev": true, "requires": { "semver": "^7.3.5" } }, "@npmcli/git": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.7.tgz", - "integrity": "sha512-WaOVvto604d5IpdCRV2KjQu8PzkfE96d50CQGKgywXh2GxXmDeUO5EWcBC4V57uFyrNqx83+MewuJh3WTR3xPA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.0.tgz", + "integrity": "sha512-vnz7BVGtOctJAIHouCJdvWBhsTVSICMeUgZo2c7XAi5d5Rrl80S1H7oPym7K03cRuinK5Q6s2dw36+PgXQTcMA==", "dev": true, "requires": { - "@npmcli/promise-spawn": "^7.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^9.0.0", - "proc-log": "^4.0.0", - "promise-inflight": "^1.0.1", + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "semver": "^7.3.5", - "which": "^4.0.0" + "which": "^5.0.0" }, "dependencies": { "isexe": { @@ -5805,10 +6482,16 @@ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true }, + "lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true + }, "which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "requires": { "isexe": "^3.1.1" @@ -5817,76 +6500,43 @@ } }, "@npmcli/installed-package-contents": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", - "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", "dev": true, "requires": { - "npm-bundled": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" } }, "@npmcli/node-gyp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", "dev": true }, "@npmcli/package-json": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.1.1.tgz", - "integrity": "sha512-uTq5j/UqUzbOaOxVy+osfOhpqOiLfUZ0Ut33UbcyyAPJbZcJsf4Mrsyb8r58FoIFlofw0iOFsuCA/oDK14VDJQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.1.tgz", + "integrity": "sha512-956YUeI0YITbk2+KnirCkD19HLzES0habV+Els+dyZaVsaM6VGSiNwnRu6t3CZaqDLz4KXy2zx+0N/Zy6YjlAA==", "dev": true, "requires": { - "@npmcli/git": "^5.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^7.0.0", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "proc-log": "^4.0.0", - "semver": "^7.5.3" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - } - }, - "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } + "@npmcli/git": "^7.0.0", + "glob": "^11.0.3", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" } }, "@npmcli/promise-spawn": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", - "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", + "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==", "dev": true, "requires": { - "which": "^4.0.0" + "which": "^5.0.0" }, "dependencies": { "isexe": { @@ -5896,9 +6546,9 @@ "dev": true }, "which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "requires": { "isexe": "^3.1.1" @@ -5907,22 +6557,23 @@ } }, "@npmcli/redact": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", - "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz", + "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==", "dev": true }, "@npmcli/run-script": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", - "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.0.tgz", + "integrity": "sha512-vaQj4nccJbAslopIvd49pQH2NhUp7G9pY4byUtmwhe37ZZuubGrx0eB9hW2F37uVNRuDDK6byFGXF+7JCuMSZg==", "dev": true, "requires": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^5.0.0", - "@npmcli/promise-spawn": "^7.0.0", - "node-gyp": "^10.0.0", - "which": "^4.0.0" + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" }, "dependencies": { "isexe": { @@ -5932,9 +6583,9 @@ "dev": true }, "which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "requires": { "isexe": "^3.1.1" @@ -5949,239 +6600,319 @@ "optional": true }, "@sigstore/bundle": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", - "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", "dev": true, "requires": { - "@sigstore/protobuf-specs": "^0.3.2" + "@sigstore/protobuf-specs": "^0.5.0" } }, "@sigstore/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", - "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.0.0.tgz", + "integrity": "sha512-NgbJ+aW9gQl/25+GIEGYcCyi8M+ng2/5X04BMuIgoDfgvp18vDcoNHOQjQsG9418HGNYRxG3vfEXaR1ayD37gg==", "dev": true }, "@sigstore/protobuf-specs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz", - "integrity": "sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", + "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", "dev": true }, "@sigstore/sign": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", - "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.0.1.tgz", + "integrity": "sha512-KFNGy01gx9Y3IBPG/CergxR9RZpN43N+lt3EozEfeoyqm8vEiLxwRl3ZO5sPx3Obv1ix/p7FWOlPc2Jgwfp9PA==", "dev": true, "requires": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "make-fetch-happen": "^13.0.1", - "proc-log": "^4.2.0", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.2", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1" + }, + "dependencies": { + "@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + } + }, + "lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true + }, + "make-fetch-happen": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.2.tgz", + "integrity": "sha512-sI1NY4lWlXBAfjmCtVWIIpBypbBdhHtcjnwnv+gtCnsaOffyFil3aidszGC8hgzJe+fT1qix05sWxmD/Bmf/oQ==", + "dev": true, + "requires": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + } + } } }, "@sigstore/tuf": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", - "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.0.tgz", + "integrity": "sha512-0QFuWDHOQmz7t66gfpfNO6aEjoFrdhkJaej/AOqb4kqWZVbPWFZifXZzkxyQBB1OwTbkhdT3LNpMFxwkTvf+2w==", "dev": true, "requires": { - "@sigstore/protobuf-specs": "^0.3.2", - "tuf-js": "^2.2.1" + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.0.0" } }, "@sigstore/verify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", - "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.0.0.tgz", + "integrity": "sha512-moXtHH33AobOhTZF8xcX1MpOFqdvfCk7v6+teJL8zymBiDXwEsQH6XG9HGx2VIxnJZNm4cNSzflTLDnQLmIdmw==", "dev": true, "requires": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.1.0", - "@sigstore/protobuf-specs": "^0.3.2" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0" } }, "@tapjs/after": { - "version": "1.1.24", - "resolved": "https://registry.npmjs.org/@tapjs/after/-/after-1.1.24.tgz", - "integrity": "sha512-Qys3CtftkfHGC7thDGm9TBzRCBLAoJKrXufF1zQxI1oNUjclWZP/s8CtHH0mwUTISOTehmBLV3wPPHSslD67Ng==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/after/-/after-3.0.2.tgz", + "integrity": "sha512-Xb0bqJWXfp6VVSx1T96lNJM67v2XxJCG3o7mH77weB+RzwAuf0uzGYy/hxP+nUAWh9yH+lHzuHclL+DR8Zlu3Q==", "dev": true, "requires": { "is-actual-promise": "^1.0.1" } }, "@tapjs/after-each": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/after-each/-/after-each-2.0.1.tgz", - "integrity": "sha512-3JXIJ4g9LPjyXmn/1VuIMC0vh7uBgUpQPksjffxv0rL8wq4C8lvmqt8Qu/fVImJucqzA+WrRqVG1b2Ab0ocDOw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/after-each/-/after-each-4.0.2.tgz", + "integrity": "sha512-OQEENy55qtJ7WaMnIQvf0YaV4/YjI2B9+ezn679Vrptg/xMbaG5fSJz/Z5BFroh05HiWRo+MGu66q7Lb00kDJg==", "dev": true, "requires": { "function-loop": "^4.0.0" } }, "@tapjs/asserts": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/asserts/-/asserts-2.0.1.tgz", - "integrity": "sha512-v2xYDLUwMGt8pzoY5LIjDCaw2NM+G01NW4pC3RcpsZLZbzQv1x/phi2RAX0ixI0nCmZZybqRygFKuMcJamS+gg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/asserts/-/asserts-4.0.2.tgz", + "integrity": "sha512-XeAYvYMu61/Gc9Dpn+0QZCjUTbbQE7DckiZNhQNMuMwAXUDEZR/TbxJNysIdMWq2ag75TQQ6ylCUJovy7HPDlg==", "dev": true, "requires": { - "@tapjs/stack": "2.0.1", + "@tapjs/stack": "4.0.1", "is-actual-promise": "^1.0.1", - "tcompare": "7.0.1", + "tcompare": "9.0.1", "trivial-deferred": "^2.0.0" }, "dependencies": { + "diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true + }, "tcompare": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz", - "integrity": "sha512-JN5s7hgmg/Ya5HxZqCnywT+XiOGRFcJRgYhtMyt/1m+h0yWpWwApO7HIM8Bpwyno9hI151ljjp5eAPCHhIGbpQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.0.1.tgz", + "integrity": "sha512-P9i7K3RnKWb3mH0AmDCChVd7FP0LEo4VSBJMhjKB+Rg+Tvq09ytnys8ygsx5ve1di2fI4IW+HhjhIXoeiRVogw==", "dev": true, "requires": { - "diff": "^5.2.0", + "diff": "^8.0.2", "react-element-to-jsx-string": "^15.0.0" } } } }, "@tapjs/before": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/before/-/before-2.0.1.tgz", - "integrity": "sha512-GgnlWPm2PbuyYuG4gkkO2KAvT/BbGnpKs60U4XzPSJ2w73Qc/IYWP0Kz6qfCWongpiLteoco67M89ujUQApYJw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/before/-/before-4.0.2.tgz", + "integrity": "sha512-ODi0rXOqCWZLS1j6fJ2iyqaXy6B/y75x8Y940hRAe0DfPPf48IgZY84+GfWWVIkRqmbYZS/F0rwcegPHxI/xHg==", "dev": true, "requires": { "is-actual-promise": "^1.0.1" } }, "@tapjs/before-each": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/before-each/-/before-each-2.0.1.tgz", - "integrity": "sha512-gG1nYkvCHtWwhkueulO475KczdQZ3vBRgdkta/Qi42ZjZo6SNhYVjNc/+LRGV5vZoESrvgSd+JrDRGufd+j43w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/before-each/-/before-each-4.0.2.tgz", + "integrity": "sha512-CXNWJ/pvqu3DwNjgnOX9zfmxYz7OvxQ4w7X/1uyFUblWarYYOUqtLsF/FrBHn3rqOowNtP5KJAzY9Wx5UT3r+g==", "dev": true, "requires": { "function-loop": "^4.0.0" } }, + "@tapjs/chdir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/chdir/-/chdir-3.0.2.tgz", + "integrity": "sha512-KYDxkkzGeEhAT33VDWqIx8FRLk+cOzVsj/E0JBRd45n18EHT7C/wE/Nqhu+843nVhABp/c5BZMb+Rib/fCQV1g==", + "dev": true, + "requires": {} + }, "@tapjs/config": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/config/-/config-3.0.1.tgz", - "integrity": "sha512-gAYFzErdSuPQ3afW6iRR99hiJmRLU+x9T+NE89z9UM45iPxglWLrRv1PFfh3tmtX6rpzwD5RY4/FVPcP2+/1LQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/config/-/config-5.0.2.tgz", + "integrity": "sha512-VfvYwcRGC3fbBAuRZs8VheA4YTa1CaJlYonHM2YM1Uu+UGeLCmvxJjIOBauhtp+F/QczUsJxqOjDy9z9ny1UYQ==", "dev": true, "requires": { - "@tapjs/core": "2.0.1", - "@tapjs/test": "2.0.1", - "chalk": "^5.2.0", - "jackspeak": "^3.1.2", - "polite-json": "^4.0.1", - "tap-yaml": "2.2.2", - "walk-up-path": "^3.0.1" + "@tapjs/core": "4.0.2", + "@tapjs/test": "4.0.2", + "chalk": "^5.6.2", + "jackspeak": "^4.0.1", + "polite-json": "^5.0.0", + "tap-yaml": "4.0.1", + "walk-up-path": "^4.0.0" }, "dependencies": { "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true + }, + "jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2" + } } } }, "@tapjs/core": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/core/-/core-2.0.1.tgz", - "integrity": "sha512-q+8d+ohw5kudktIqgP5ETBcPWAPip+kMIxs2eL2G3dV+7Gc8WrH43cCPrbSGPRITIOSIDPrtpQZEcZwQNqDdQw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/core/-/core-4.0.2.tgz", + "integrity": "sha512-kzM90qsqHAJOTUMVjB3G26c3Ka4/HFg253lSt3pxBFesIHneIZe6Fre1NEWnHZPLmmzBO6HRRffc9zDxXcaeRw==", "dev": true, "requires": { - "@tapjs/processinfo": "^3.1.7", - "@tapjs/stack": "2.0.1", - "@tapjs/test": "2.0.1", + "@tapjs/processinfo": "^3.1.8", + "@tapjs/stack": "4.0.1", + "@tapjs/test": "4.0.2", "async-hook-domain": "^4.0.1", - "diff": "^5.2.0", + "diff": "^8.0.2", "is-actual-promise": "^1.0.1", "minipass": "^7.0.4", "signal-exit": "4.1", - "tap-parser": "16.0.1", - "tap-yaml": "2.2.2", - "tcompare": "7.0.1", + "tap-parser": "18.0.1", + "tap-yaml": "4.0.1", + "tcompare": "9.0.1", "trivial-deferred": "^2.0.0" }, "dependencies": { + "diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true + }, "tcompare": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz", - "integrity": "sha512-JN5s7hgmg/Ya5HxZqCnywT+XiOGRFcJRgYhtMyt/1m+h0yWpWwApO7HIM8Bpwyno9hI151ljjp5eAPCHhIGbpQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.0.1.tgz", + "integrity": "sha512-P9i7K3RnKWb3mH0AmDCChVd7FP0LEo4VSBJMhjKB+Rg+Tvq09ytnys8ygsx5ve1di2fI4IW+HhjhIXoeiRVogw==", "dev": true, "requires": { - "diff": "^5.2.0", + "diff": "^8.0.2", "react-element-to-jsx-string": "^15.0.0" } } } }, "@tapjs/error-serdes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/error-serdes/-/error-serdes-2.0.1.tgz", - "integrity": "sha512-P+M4rtcfkDsUveKKmoRNF+07xpbPnRY5KrstIUOnyn483clQ7BJhsnWr162yYNCsyOj4zEfZmAJI1f8Bi7h/ZA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@tapjs/error-serdes/-/error-serdes-4.0.1.tgz", + "integrity": "sha512-8GiOXbgGIRBcSGnPXYuiboy0xJQDMP2OcILnghHX/jzJKi2l9mxX6FTonOWj/0qsf5Ji5Z4/DIKRcYINIxaejg==", "dev": true, "requires": { "minipass": "^7.0.4" } }, "@tapjs/filter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/filter/-/filter-2.0.1.tgz", - "integrity": "sha512-muKEeXK7Tz6VR4hjXfT2qXPvjYES575mtiRerjHf+8qP8D7MvmC8qDZJjzFdo1nZHKhF8snvFosIVuI1BAhvsw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/filter/-/filter-4.0.2.tgz", + "integrity": "sha512-Pfi1u1naoUL4G4AoJOUEUQauaZeF4gGk8MZ8It2ht60gopYsxy9sSmIKlKPvZm13f110pZxJfO+hogappAmDfw==", "dev": true, "requires": {} }, "@tapjs/fixture": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/fixture/-/fixture-2.0.1.tgz", - "integrity": "sha512-MLgEwsBlCD69iUbZcnKBehP2js5cV4p5GrFoOKSudMuH2DQJInaF/g2bkijue61cVZwPj/MRPCqAlkwA94epjg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/fixture/-/fixture-4.0.2.tgz", + "integrity": "sha512-h161PA9ngT3zG6iLybAUlrSWx31OFx7Av1aR4FHvui9IkseXaF0x6gxS0MMaXwdyRYrD7lFvmU7SRTDlvvdeBA==", "dev": true, "requires": { "mkdirp": "^3.0.0", - "rimraf": "^5.0.5" + "rimraf": "^6.0.0" + }, + "dependencies": { + "rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "requires": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + } + } } }, "@tapjs/intercept": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/intercept/-/intercept-2.0.1.tgz", - "integrity": "sha512-BZgXE3zCAbv4lfbph1r85gihtI3kXltHlFQ8Bf3Yy9fx27DKQlBvXnD7T69ke8kQLRzhz+wTMcR/mcQjo1fa7w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/intercept/-/intercept-4.0.2.tgz", + "integrity": "sha512-rlZcO/Yf9vU8ypfQEO0c/QUCcGCr0IMJltg53805ILuNhTDR7x0vlJSvJ7fLsuoezkUwXlWdlED6ZqIID7RokA==", "dev": true, "requires": { - "@tapjs/after": "1.1.24", - "@tapjs/stack": "2.0.1" + "@tapjs/after": "3.0.2", + "@tapjs/stack": "4.0.1" } }, "@tapjs/mock": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/mock/-/mock-2.0.1.tgz", - "integrity": "sha512-i1vkwNgO7uEuQW3+hTuE2L64aC9xk0cC3PtC6DZKqyApk2IstNgoIS38nfsI6v2kvEgZNuWlsNcRAYNDOIEhzA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/mock/-/mock-4.0.2.tgz", + "integrity": "sha512-48o3xz3Xl83Ei25KOR5aWytbCYUl33GQAoQm4khFpTyI/v3fjyR9jDgFLAT7IWix8tRrrkMf+PynmZkWVQ4oKA==", "dev": true, "requires": { - "@tapjs/after": "1.1.24", - "@tapjs/stack": "2.0.1", - "resolve-import": "^1.4.5", - "walk-up-path": "^3.0.1" + "@tapjs/after": "3.0.2", + "@tapjs/stack": "4.0.1", + "resolve-import": "^2.0.0", + "walk-up-path": "^4.0.0" } }, "@tapjs/node-serialize": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/node-serialize/-/node-serialize-2.0.1.tgz", - "integrity": "sha512-1GtHDa7AXpk8y08llIPfUKRTDNsq+BhXxz7wiIfVEAOEB09kGyfpWteOg+cmvb+aHU1Ays3z+medXTIBm0D5Kg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/node-serialize/-/node-serialize-4.0.2.tgz", + "integrity": "sha512-oparVnZqbwF7O7IQn4nfpnsddtmlkWPB033uzZbvSW6v0U6T8DBENgKAfzJ2sNQH3f2Xaa3dqK+N5brwQa3P2g==", "dev": true, "requires": { - "@tapjs/error-serdes": "2.0.1", - "@tapjs/stack": "2.0.1", - "tap-parser": "16.0.1" + "@tapjs/error-serdes": "4.0.1", + "@tapjs/stack": "4.0.1", + "tap-parser": "18.0.1" } }, "@tapjs/processinfo": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@tapjs/processinfo/-/processinfo-3.1.7.tgz", - "integrity": "sha512-SI5RJQ5HnUKEWnHSAF6hOm6XPdnjZ+CJzIaVHdFebed8iDAPTqb+IwMVu9yq9+VQ7FRsMMlgLL2SW4rss2iJbQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@tapjs/processinfo/-/processinfo-3.1.8.tgz", + "integrity": "sha512-FIriEB+qqArPhmVYc1PZwRHD99myRdl7C9Oe/uts04Q2LOxQ5MEmqP9XOP8vVYzpDOYwmL8OmL6eOYt9eZlQKQ==", "dev": true, "requires": { "pirates": "^4.0.5", @@ -6191,141 +6922,158 @@ } }, "@tapjs/reporter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/reporter/-/reporter-2.0.1.tgz", - "integrity": "sha512-fCdl4vg8vnlqIYtTQ9dc3zOqeXrA5QbATbT4dsPIiPuCM3gvKTbntaNBeaWWZkPx697Dj+b8TIxT/xhNMNv7jQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tapjs/reporter/-/reporter-4.0.3.tgz", + "integrity": "sha512-Mdx3+C3f4q3llawja5V2RVIfFzr1KhEm57i9xUJTgM5kHcPjrMw14aQUCwYxLSYSd5esOubnnV5lD9rxQb+tyQ==", "dev": true, "requires": { - "@tapjs/config": "3.0.1", - "@tapjs/stack": "2.0.1", - "chalk": "^5.2.0", - "ink": "^4.4.1", + "@tapjs/config": "5.0.2", + "@tapjs/stack": "4.0.1", + "chalk": "^5.6.2", + "ink": "^5.2.1", "minipass": "^7.0.4", "ms": "^2.1.3", "patch-console": "^2.0.0", "prismjs-terminal": "^1.2.3", "react": "^18.2.0", "string-length": "^6.0.0", - "tap-parser": "16.0.1", - "tap-yaml": "2.2.2", - "tcompare": "7.0.1" + "tap-parser": "18.0.1", + "tap-yaml": "4.0.1", + "tcompare": "9.0.1" }, "dependencies": { "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", "dev": true }, "tcompare": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz", - "integrity": "sha512-JN5s7hgmg/Ya5HxZqCnywT+XiOGRFcJRgYhtMyt/1m+h0yWpWwApO7HIM8Bpwyno9hI151ljjp5eAPCHhIGbpQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.0.1.tgz", + "integrity": "sha512-P9i7K3RnKWb3mH0AmDCChVd7FP0LEo4VSBJMhjKB+Rg+Tvq09ytnys8ygsx5ve1di2fI4IW+HhjhIXoeiRVogw==", "dev": true, "requires": { - "diff": "^5.2.0", + "diff": "^8.0.2", "react-element-to-jsx-string": "^15.0.0" } } } }, "@tapjs/run": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@tapjs/run/-/run-2.0.2.tgz", - "integrity": "sha512-2hPGlabqbLb3hh4BHHvwE8R9a9OiWumkCkHw5QQUZurDsVOpB94FfteqW9mktTVjZJnN0go+sN3GN2jZUaPWGQ==", - "dev": true, - "requires": { - "@tapjs/after": "1.1.24", - "@tapjs/before": "2.0.1", - "@tapjs/config": "3.0.1", - "@tapjs/processinfo": "^3.1.7", - "@tapjs/reporter": "2.0.1", - "@tapjs/spawn": "2.0.1", - "@tapjs/stdin": "2.0.1", - "@tapjs/test": "2.0.1", - "c8": "^9.1.0", - "chalk": "^5.3.0", - "chokidar": "^3.6.0", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tapjs/run/-/run-4.0.3.tgz", + "integrity": "sha512-kkoMk5OJPgxigZZwu47ix2Zo8U64t8YiFiz+Iec5HrSW1BNgQyhurfMbc8uGjBHh3OhvJBaWhWuzctn0ROJrEQ==", + "dev": true, + "requires": { + "@tapjs/after": "3.0.2", + "@tapjs/before": "4.0.2", + "@tapjs/config": "5.0.2", + "@tapjs/processinfo": "^3.1.8", + "@tapjs/reporter": "4.0.3", + "@tapjs/spawn": "4.0.2", + "@tapjs/stdin": "4.0.2", + "@tapjs/test": "4.0.2", + "c8": "^10.1.3", + "chalk": "^5.6.2", + "chokidar": "^4.0.2", "foreground-child": "^3.1.1", - "glob": "^10.3.16", + "glob": "^11.0.0", "minipass": "^7.0.4", "mkdirp": "^3.0.1", "opener": "^1.5.2", - "pacote": "^17.0.6", - "resolve-import": "^1.4.5", - "rimraf": "^5.0.5", - "semver": "^7.6.0", + "pacote": "^21.0.3", + "path-scurry": "^2.0.0", + "resolve-import": "^2.0.0", + "rimraf": "^6.0.0", + "semver": "^7.7.2", "signal-exit": "^4.1.0", - "tap-parser": "16.0.1", - "tap-yaml": "2.2.2", - "tcompare": "7.0.1", + "tap-parser": "18.0.1", + "tap-yaml": "4.0.1", + "tcompare": "9.0.1", "trivial-deferred": "^2.0.0", - "which": "^4.0.0" + "which": "^5.0.0" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true }, - "glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" + "readdirp": "^4.0.1" } }, + "diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true + }, "isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true }, - "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true + }, + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + } + }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true + }, + "rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "requires": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" } }, "tcompare": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz", - "integrity": "sha512-JN5s7hgmg/Ya5HxZqCnywT+XiOGRFcJRgYhtMyt/1m+h0yWpWwApO7HIM8Bpwyno9hI151ljjp5eAPCHhIGbpQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.0.1.tgz", + "integrity": "sha512-P9i7K3RnKWb3mH0AmDCChVd7FP0LEo4VSBJMhjKB+Rg+Tvq09ytnys8ygsx5ve1di2fI4IW+HhjhIXoeiRVogw==", "dev": true, "requires": { - "diff": "^5.2.0", + "diff": "^8.0.2", "react-element-to-jsx-string": "^15.0.0" } }, "which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "requires": { "isexe": "^3.1.1" @@ -6334,141 +7082,137 @@ } }, "@tapjs/snapshot": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/snapshot/-/snapshot-2.0.1.tgz", - "integrity": "sha512-ZnbCxL+9fiJ38tec6wvRtRBZz9ChRUq0Bov7dltdZMNkXqudKyB+Zzbg25bqDEIgcczyp6A9hOwTX6VybDGqpg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/snapshot/-/snapshot-4.0.2.tgz", + "integrity": "sha512-PWlWNEH+4x0oN8nemk+2rk3jub2L/7c6A383SD15GadGOT4hYvckqY2mCZarMAoV5xErFZOglGTD9do83TWMPQ==", "dev": true, "requires": { "is-actual-promise": "^1.0.1", - "tcompare": "7.0.1", + "tcompare": "9.0.1", "trivial-deferred": "^2.0.0" }, "dependencies": { + "diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true + }, "tcompare": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz", - "integrity": "sha512-JN5s7hgmg/Ya5HxZqCnywT+XiOGRFcJRgYhtMyt/1m+h0yWpWwApO7HIM8Bpwyno9hI151ljjp5eAPCHhIGbpQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.0.1.tgz", + "integrity": "sha512-P9i7K3RnKWb3mH0AmDCChVd7FP0LEo4VSBJMhjKB+Rg+Tvq09ytnys8ygsx5ve1di2fI4IW+HhjhIXoeiRVogw==", "dev": true, "requires": { - "diff": "^5.2.0", + "diff": "^8.0.2", "react-element-to-jsx-string": "^15.0.0" } } } }, "@tapjs/spawn": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/spawn/-/spawn-2.0.1.tgz", - "integrity": "sha512-3VaQKJjHV5frMZj3Ef+QlJyB6b7VsGMil223zAEz8Ttgy2hDYtcb29nvsLPUcowFyOUrsydnXEnHgpR79wEPOA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/spawn/-/spawn-4.0.2.tgz", + "integrity": "sha512-CNOrZFXh+mm2iul/cGRgVA4HMv9GxmtbgQSICMjRrj8VLDzMboW862P+wEvfYrHeXdCGAGWKqTIcsX5pXQVQRA==", "dev": true, "requires": {} }, "@tapjs/stack": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/stack/-/stack-2.0.1.tgz", - "integrity": "sha512-3rKbZkRkLeJl9ilV/6b80YfI4C4+OYf7iEz5/d0MIVhmVvxv0ttIy5JnZutAc4Gy9eRp5Ne5UTAIFOVY5k36cg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@tapjs/stack/-/stack-4.0.1.tgz", + "integrity": "sha512-Rbyz4XMuZWNxCs+/j0c5idFz4MKBo7uSaNvk6R7Al9jQJzk7Lv0WC2lWW0CV+7t/TUynTFxEwAaY5pIM752WQg==", "dev": true }, "@tapjs/stdin": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/stdin/-/stdin-2.0.1.tgz", - "integrity": "sha512-5Oe13Fzpnt9seAi8h3bsMxtJp8S+DQI6ncBD9JBcS91XKLbqyKrb1bNzeXQN2PrHBs6Atw8cOzFZh0TjL+bIaA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/stdin/-/stdin-4.0.2.tgz", + "integrity": "sha512-6e/jQ0I9G6DC9m6Yj3jC6sNAmiwvrPVni3iMpJn3GICs0dROyx1m9wnMBK5wgKNgN2AXfvEOtf5Cby124eHeJQ==", "dev": true, "requires": {} }, "@tapjs/test": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/test/-/test-2.0.1.tgz", - "integrity": "sha512-PKazf7r4+bLFATML2f/h8glGcSirXmzXUYlhFuxb4xHoOhHojyKgo1p8kSj+Ksxb3hVSCQlvyXgM8QYYaoMwog==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/test/-/test-4.0.2.tgz", + "integrity": "sha512-J8WOSesfqp6/P5UbChDI5xzREQ96787ZFHLliPva4oi5XevG1TWtfSL47HtbQUtKvWSC7YIWW3CQhcBHRGk6Vg==", "dev": true, "requires": { "@isaacs/ts-node-temp-fork-for-pr-2009": "^10.9.7", - "@tapjs/after": "1.1.24", - "@tapjs/after-each": "2.0.1", - "@tapjs/asserts": "2.0.1", - "@tapjs/before": "2.0.1", - "@tapjs/before-each": "2.0.1", - "@tapjs/filter": "2.0.1", - "@tapjs/fixture": "2.0.1", - "@tapjs/intercept": "2.0.1", - "@tapjs/mock": "2.0.1", - "@tapjs/node-serialize": "2.0.1", - "@tapjs/snapshot": "2.0.1", - "@tapjs/spawn": "2.0.1", - "@tapjs/stdin": "2.0.1", - "@tapjs/typescript": "1.4.6", - "@tapjs/worker": "2.0.1", - "glob": "^10.3.16", - "jackspeak": "^3.1.2", + "@tapjs/after": "3.0.2", + "@tapjs/after-each": "4.0.2", + "@tapjs/asserts": "4.0.2", + "@tapjs/before": "4.0.2", + "@tapjs/before-each": "4.0.2", + "@tapjs/chdir": "3.0.2", + "@tapjs/filter": "4.0.2", + "@tapjs/fixture": "4.0.2", + "@tapjs/intercept": "4.0.2", + "@tapjs/mock": "4.0.2", + "@tapjs/node-serialize": "4.0.2", + "@tapjs/snapshot": "4.0.2", + "@tapjs/spawn": "4.0.2", + "@tapjs/stdin": "4.0.2", + "@tapjs/typescript": "3.1.1", + "@tapjs/worker": "4.0.2", + "glob": "^11.0.3", + "jackspeak": "^4.0.1", "mkdirp": "^3.0.0", - "resolve-import": "^1.4.5", - "rimraf": "^5.0.5", - "sync-content": "^1.0.1", - "tap-parser": "16.0.1", - "tshy": "^1.14.0", - "typescript": "5.4", - "walk-up-path": "^3.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "package-json-from-dist": "^1.0.0", + "resolve-import": "^2.0.0", + "rimraf": "^6.0.0", + "sync-content": "^2.0.1", + "tap-parser": "18.0.1", + "tshy": "^3.0.3", + "typescript": "5.9", + "walk-up-path": "^4.0.0" + }, + "dependencies": { + "jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, "requires": { - "balanced-match": "^1.0.0" + "@isaacs/cliui": "^8.0.2" } }, - "glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - } - }, - "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" } } } }, "@tapjs/typescript": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@tapjs/typescript/-/typescript-1.4.6.tgz", - "integrity": "sha512-6jxUQ7Mdb+Y2q8RJcwgZZ6dCR+X2u3hCL+xb1GDAtO7k1+B6z2b+z+I+FdhuO4YgrP0SLRjocL5rJM/xi9K7qw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@tapjs/typescript/-/typescript-3.1.1.tgz", + "integrity": "sha512-2fxDTeL8X3sm9g0KnHZSD/p9o8tFWYhswRKUq//jv9FA/4XetsKs+ApddPUJEi3AX8+Ma1P1EzjSy/f0z5KIJA==", "dev": true, "requires": { "@isaacs/ts-node-temp-fork-for-pr-2009": "^10.9.7" } }, "@tapjs/worker": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tapjs/worker/-/worker-2.0.1.tgz", - "integrity": "sha512-wegz8IxNEPIIAA+R76/avZgNmZ4iC7QGFbtXKGBU962/1lXTITxshRV6e21r0IBa7YLkSVgDuVSVB3+Qzve0Yg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@tapjs/worker/-/worker-4.0.2.tgz", + "integrity": "sha512-o2jzyjEnpRf3xHmduIfO/HXb5m9qRz+tCoiiFmPav+LC7eilSrDP3eGiQwMGbew64PK52KQwQW/LYNRrQsuMxA==", "dev": true, "requires": {} }, "@tsconfig/node14": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-14.1.2.tgz", - "integrity": "sha512-1vncsbfCZ3TBLPxesRYz02Rn7SNJfbLoDVkcZ7F/ixOV6nwxwgdhD1mdPcc5YQ413qBJ8CvMxXMFfJ7oawjo7Q==", + "version": "14.1.5", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-14.1.5.tgz", + "integrity": "sha512-hsldDMdbjF18BgvqFX6rHwqb0wlDh4lxyXmo3VATa7LwL4AFrHijv8Or9ySXBSg9TyysRkldJyAC/kplyF/Mmg==", "dev": true }, "@tsconfig/node16": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-16.1.3.tgz", - "integrity": "sha512-9nTOUBn+EMKO6rtSZJk+DcqsfgtlERGT9XPJ5PRj/HNENPCBY1yu/JEj5wT6GLtbCLBO2k46SeXDaY0pjMqypw==", + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-16.1.5.tgz", + "integrity": "sha512-P4GBZdpi6i5NGB12pwLH4oAIPrxl1ZvD8gxA4E7VdUUCik34J7FerrxzoDXtT4ugbrRjLbK5WuHPOUNZJOxuaQ==", "dev": true }, "@tsconfig/node18": { @@ -6478,9 +7222,9 @@ "dev": true }, "@tsconfig/node20": { - "version": "20.1.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz", - "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", + "version": "20.1.6", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.6.tgz", + "integrity": "sha512-sz+Hqx9zwZDpZIV871WSbUzSqNIsXzghZydypnfgzPKLltVJfkINfUeTct31n/tTSa9ZE1ZOfKdRre1uHHquYQ==", "dev": true }, "@tufjs/canonical-json": { @@ -6490,28 +7234,28 @@ "dev": true }, "@tufjs/models": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", - "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.0.0.tgz", + "integrity": "sha512-h5x5ga/hh82COe+GoD4+gKUeV4T3iaYOxqLt41GRKApinPI7DMidhCmNVTjKfhCWFJIGXaFJee07XczdT4jdZQ==", "dev": true, "requires": { "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.4" + "minimatch": "^9.0.5" }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" } }, "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -6519,32 +7263,42 @@ } } }, + "@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" + }, "@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, "@types/node": { - "version": "20.13.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.13.0.tgz", - "integrity": "sha512-FM6AOb3khNkNIXPnHFDYaHerSv8uN22C91z098AnGccVu+Pcdhi+pNUFDi0iLmPIsVE0JBD0KVS7mzUYt4nRzQ==", + "version": "24.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", + "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "dev": true, "peer": true, "requires": { - "undici-types": "~5.26.4" + "undici-types": "~7.14.0" } }, "abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", "dev": true }, "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==" + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==" }, "acorn-jsx": { "version": "5.3.2", @@ -6553,37 +7307,19 @@ "requires": {} }, "acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true - }, - "agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, "requires": { - "debug": "^4.3.4" + "acorn": "^8.11.0" } }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "dependencies": { - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - } - } + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true }, "ajv": { "version": "6.12.6", @@ -6597,10 +7333,13 @@ } }, "ansi-escapes": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", - "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", - "dev": true + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "dev": true, + "requires": { + "environment": "^1.0.0" + } }, "ansi-regex": { "version": "5.0.1", @@ -6660,9 +7399,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6678,74 +7417,48 @@ } }, "c8": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", - "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", "dev": true, "requires": { - "@bcoe/v8-coverage": "^0.2.3", + "@bcoe/v8-coverage": "^1.0.1", "@istanbuljs/schema": "^0.1.3", "find-up": "^5.0.0", "foreground-child": "^3.1.1", "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.1.6", - "test-exclude": "^6.0.0", + "test-exclude": "^7.0.1", "v8-to-istanbul": "^9.0.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1" } }, "cacache": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", - "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.1.tgz", + "integrity": "sha512-+7LYcYGBYoNqTp1Rv7Ny1YjUo5E0/ftkQtraH3vkfAGgVHc+ouWdC8okAwQgQR7EVIdW6JTzTmhKFwzb+4okAQ==", "dev": true, "requires": { - "@npmcli/fs": "^3.1.0", + "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", + "glob": "^11.0.3", + "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "unique-filename": "^4.0.0" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - } - }, - "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } + "lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true } } }, @@ -6791,9 +7504,9 @@ } }, "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true }, "chroma-js": { @@ -6801,18 +7514,6 @@ "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" }, - "ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, "cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -6829,19 +7530,31 @@ } }, "cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, "requires": { "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" + "string-width": "^7.0.0" }, "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + }, "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true + }, + "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 }, "slice-ansi": { @@ -6853,6 +7566,26 @@ "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } + }, + "string-width": { + "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", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } } } }, @@ -6953,9 +7686,9 @@ "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==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -6963,11 +7696,11 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "deep-is": { @@ -6981,14 +7714,6 @@ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "requires": { - "esutils": "^2.0.2" - } - }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7015,16 +7740,28 @@ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true }, + "environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true + }, "err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true }, + "es-toolkit": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz", + "integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==", + "dev": true + }, "escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, "escape-string-regexp": { @@ -7033,74 +7770,69 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" }, "eslint": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.37.0.tgz", - "integrity": "sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==", - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.37.0", - "@humanwhocodes/config-array": "^0.11.8", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "requires": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "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", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" + "optionator": "^0.9.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==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==" + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==" }, "espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "requires": { - "acorn": "^8.8.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^4.2.1" } }, "esquery": { @@ -7136,9 +7868,9 @@ "dev": true }, "exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "dev": true }, "fast-deep-equal": { @@ -7156,20 +7888,12 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "requires": { - "reusify": "^1.0.4" - } - }, "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==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "requires": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" } }, "fill-range": { @@ -7191,28 +7915,18 @@ } }, "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - } + "flatted": "^3.2.9", + "keyv": "^4.5.4" } }, "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" }, "flexsearch": { "version": "0.7.43", @@ -7220,11 +7934,11 @@ "integrity": "sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==" }, "foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, @@ -7243,11 +7957,6 @@ "minipass": "^7.0.3" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -7255,12 +7964,6 @@ "dev": true, "optional": true }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true - }, "function-loop": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/function-loop/-/function-loop-4.0.0.tgz", @@ -7273,17 +7976,60 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true + }, "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "dependencies": { + "jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2" + } + }, + "lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true + }, + "minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "requires": { + "@isaacs/brace-expansion": "^5.0.0" + } + }, + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + } + } } }, "glob-parent": { @@ -7295,12 +8041,9 @@ } }, "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", - "requires": { - "type-fest": "^0.20.2" - } + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" }, "graceful-fs": { "version": "4.2.11", @@ -7308,37 +8051,31 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, - "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "requires": { - "function-bind": "^1.1.2" - } - }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", "dev": true, "requires": { - "lru-cache": "^10.0.1" + "lru-cache": "^11.1.0" + }, + "dependencies": { + "lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true + } } }, "html-escaper": { @@ -7348,9 +8085,9 @@ "dev": true }, "http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true }, "http-proxy-agent": { @@ -7364,12 +8101,12 @@ } }, "https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "requires": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" } }, @@ -7384,35 +8121,26 @@ } }, "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" }, "ignore-walk": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", - "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", "dev": true, "requires": { - "minimatch": "^9.0.0" + "minimatch": "^10.0.3" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" } } } @@ -7426,9 +8154,9 @@ } }, "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -7445,57 +8173,71 @@ "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "dev": true }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "dev": true + }, "ink": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/ink/-/ink-4.4.1.tgz", - "integrity": "sha512-rXckvqPBB0Krifk5rn/5LvQGmyXwCUpBfmTwbkQNBY9JY8RSl3b8OftBNEYxg4+SWUhEKcPifgope28uL9inlA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", "dev": true, "requires": { "@alcalzone/ansi-tokenize": "^0.1.3", - "ansi-escapes": "^6.0.0", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", - "chalk": "^5.2.0", + "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", - "cli-truncate": "^3.1.0", + "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", - "is-ci": "^3.0.1", - "is-lower-case": "^2.0.2", - "is-upper-case": "^2.0.2", - "lodash": "^4.17.21", + "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.29.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", - "slice-ansi": "^6.0.0", + "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", - "string-width": "^5.1.2", - "type-fest": "^0.12.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.1.0", - "ws": "^8.12.0", - "yoga-wasm-web": "~0.3.3" + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" }, "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + }, + "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 + }, "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true + }, + "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 }, "signal-exit": { @@ -7504,23 +8246,44 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "type-fest": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", - "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==", - "dev": true + "string-width": { + "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", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "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", + "strip-ansi": "^7.1.0" + } } } }, "ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dev": true, - "requires": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - } + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true }, "is-actual-promise": { "version": "1.0.2", @@ -7537,24 +8300,6 @@ "binary-extensions": "^2.0.0" } }, - "is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "requires": { - "ci-info": "^3.2.0" - } - }, - "is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "requires": { - "hasown": "^2.0.0" - } - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7574,47 +8319,24 @@ "is-extglob": "^2.1.1" } }, - "is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", "dev": true }, - "is-lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz", - "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" - }, "is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true }, - "is-upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", - "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7638,9 +8360,9 @@ } }, "istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -7656,11 +8378,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7680,16 +8397,15 @@ "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" }, - "jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "json-parse-even-better-errors": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", - "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", "dev": true }, "json-schema-traverse": { @@ -7708,6 +8424,14 @@ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "dev": true }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "requires": { + "json-buffer": "3.0.1" + } + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7725,12 +8449,6 @@ "p-locate": "^5.0.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7766,23 +8484,76 @@ "dev": true }, "make-fetch-happen": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", - "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", "dev": true, "requires": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", + "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1", - "ssri": "^10.0.0" + "ssri": "^12.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "requires": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "marked": { @@ -7819,15 +8590,15 @@ } }, "minipass-fetch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", - "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", "dev": true, "requires": { "encoding": "^0.1.13", "minipass": "^7.0.3", "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "minizlib": "^3.0.1" } }, "minipass-flush": { @@ -7847,27 +8618,12 @@ "requires": { "yallist": "^4.0.0" } - } - } - }, - "minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", - "dev": true, - "requires": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, @@ -7888,6 +8644,12 @@ "requires": { "yallist": "^4.0.0" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, @@ -7908,28 +8670,22 @@ "requires": { "yallist": "^4.0.0" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - } + "minipass": "^7.1.2" } }, "mkdirp": { @@ -7939,9 +8695,9 @@ "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==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "msgpackr": { "version": "1.10.2", @@ -7972,76 +8728,39 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true }, "node-gyp": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", - "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", "dev": true, "requires": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^13.0.0", - "nopt": "^7.0.0", - "proc-log": "^3.0.0", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^4.0.0" + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - } - }, "isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true }, - "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", - "dev": true - }, "which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "requires": { "isexe": "^3.1.1" @@ -8056,24 +8775,12 @@ "optional": true }, "nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", - "dev": true, - "requires": { - "abbrev": "^2.0.0" - } - }, - "normalize-package-data": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", - "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", "dev": true, "requires": { - "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" + "abbrev": "^3.0.0" } }, "normalize-path": { @@ -8083,84 +8790,117 @@ "dev": true }, "npm-bundled": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", - "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", "dev": true, "requires": { - "npm-normalize-package-bin": "^3.0.0" + "npm-normalize-package-bin": "^4.0.0" } }, "npm-install-checks": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", - "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.2.tgz", + "integrity": "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==", "dev": true, "requires": { "semver": "^7.1.1" } }, "npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", "dev": true }, "npm-package-arg": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.2.tgz", - "integrity": "sha512-IGN0IAwmhDJwy13Wc8k+4PEbTPhpJnMtfR53ZbOyjkvmEcLS4nCwp6mvMWjS5sUjeiW3mpx6cHmuhKEu9XmcQw==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.1.tgz", + "integrity": "sha512-6zqls5xFvJbgFjB1B2U6yITtyGBjDBORB7suI4zA4T/sZ1OmkMFlaQSNB/4K0LtXNA1t4OprAFxPisadK5O2ag==", "dev": true, "requires": { - "hosted-git-info": "^7.0.0", - "proc-log": "^4.0.0", + "hosted-git-info": "^9.0.0", + "proc-log": "^5.0.0", "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" + "validate-npm-package-name": "^6.0.0" } }, "npm-packlist": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", - "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.2.tgz", + "integrity": "sha512-DrIWNiWT0FTdDRjGOYfEEZUNe1IzaSZ+up7qBTKnrQDySpdmuOQvytrqQlpK5QrCA4IThMvL4wTumqaa1ZvVIQ==", "dev": true, "requires": { - "ignore-walk": "^6.0.4" + "ignore-walk": "^8.0.0", + "proc-log": "^5.0.0" } }, "npm-pick-manifest": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.1.tgz", - "integrity": "sha512-Udm1f0l2nXb3wxDpKjfohwgdFUSV50UVwzEIpDXVsbDMXVIEF81a/i0UhuQbhrPMMmdiq3+YMFLFIRVLs3hxQw==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.1.tgz", + "integrity": "sha512-HnU7FYSWbo7dTVHtK0G+BXbZ0aIfxz/aUCVLN0979Ec6rGUX5cJ6RbgVx5fqb5G31ufz+BVFA7y1SkRTPVNoVQ==", "dev": true, "requires": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^11.0.0", + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^13.0.0", "semver": "^7.3.5" } }, "npm-registry-fetch": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.2.1.tgz", - "integrity": "sha512-8l+7jxhim55S85fjiDGJ1rZXBWGtRLi1OSb4Z3BPLObPuIaeKRlPRiYMSHU4/81ck3t71Z+UwDDl47gcpmfQQA==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.0.0.tgz", + "integrity": "sha512-DFxSAemHUwT/POaXAOY4NJmEWBPB0oKbwD6FFDE9hnt1nORkt/FXvgjD4hQjoKoHw9u0Ezws9SPXwV7xE/Gyww==", "dev": true, "requires": { - "@npmcli/redact": "^1.1.0", - "make-fetch-happen": "^13.0.0", + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^11.0.0", - "proc-log": "^4.0.0" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^5.0.0" + }, + "dependencies": { + "@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + } + }, + "lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true + }, + "make-fetch-happen": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.2.tgz", + "integrity": "sha512-sI1NY4lWlXBAfjmCtVWIIpBypbBdhHtcjnwnv+gtCnsaOffyFil3aidszGC8hgzJe+fT1qix05sWxmD/Bmf/oQ==", + "dev": true, + "requires": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + } + } } }, "onetime": { @@ -8179,16 +8919,16 @@ "dev": true }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "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" + "word-wrap": "^1.2.5" } }, "p-limit": { @@ -8208,38 +8948,40 @@ } }, "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true + }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true }, "pacote": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.7.tgz", - "integrity": "sha512-sgvnoUMlkv9xHwDUKjKQFXVyUi8dtJGKp3vg6sYy+TxbDic5RjZCHF3ygv0EJgNRZ2GfRONjlKPUfokJ9lDpwQ==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.3.tgz", + "integrity": "sha512-itdFlanxO0nmQv4ORsvA9K1wv40IPfB9OmWqfaJWvoJ30VKyHsqNgDVeG+TVhI7Gk7XW8slUy7cA9r6dF5qohw==", "dev": true, "requires": { - "@npmcli/git": "^5.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/promise-spawn": "^7.0.0", - "@npmcli/run-script": "^7.0.0", - "cacache": "^18.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", - "npm-package-arg": "^11.0.0", - "npm-packlist": "^8.0.0", - "npm-pick-manifest": "^9.0.0", - "npm-registry-fetch": "^16.0.0", - "proc-log": "^4.0.0", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1", - "read-package-json": "^7.0.0", - "read-package-json-fast": "^3.0.0", - "sigstore": "^2.2.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" + "sigstore": "^4.0.0", + "ssri": "^12.0.0", + "tar": "^7.4.3" } }, "parent-module": { @@ -8261,11 +9003,6 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" }, - "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=" - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -8287,15 +9024,15 @@ "dev": true }, "pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true }, "polite-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/polite-json/-/polite-json-4.0.1.tgz", - "integrity": "sha512-8LI5ZeCPBEb4uBbcYKNVwk4jgqNx1yHReWoW4H4uUihWlSqZsUDfSITrRhjliuPgxsNPFhNSudGO2Zu4cbWinQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/polite-json/-/polite-json-5.0.0.tgz", + "integrity": "sha512-OLS/0XeUAcE8a2fdwemNja+udKgXNnY6yKVIXqAD2zVRx1KvY6Ato/rZ2vdzbxqYwPW0u6SCNC/bAMPNzpzxbw==", "dev": true }, "prelude-ls": { @@ -8304,9 +9041,9 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, "prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "dev": true }, "prismjs-terminal": { @@ -8321,34 +9058,28 @@ }, "dependencies": { "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true } } }, "proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true }, "process-on-spawn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", - "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", "dev": true, "requires": { "fromentries": "^1.2.0" } }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, "promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -8360,9 +9091,9 @@ } }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, "queue": { "version": "6.0.2", @@ -8372,11 +9103,6 @@ "inherits": "~2.0.3" } }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" - }, "react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -8424,61 +9150,6 @@ "scheduler": "^0.23.2" } }, - "read-package-json": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", - "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", - "dev": true, - "requires": { - "glob": "^10.2.2", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - } - }, - "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "read-package-json-fast": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", - "dev": true, - "requires": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - } - }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -8500,46 +9171,13 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, "resolve-import": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/resolve-import/-/resolve-import-1.4.5.tgz", - "integrity": "sha512-HXb4YqODuuXT7Icq1Z++0g2JmhgbUHSs3VT2xR83gqvAPUikYT2Xk+562KHQgiaNkbBOlPddYrDLsC44qQggzw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-import/-/resolve-import-2.0.0.tgz", + "integrity": "sha512-jpKjLibLuc8D1XEV2+7zb0aqN7I8d12u89g/v6IsgCzdVlccMQJq4TKkPw5fbhHdxhm7nbVtN+KvOTnjFf+nEA==", "dev": true, "requires": { - "glob": "^10.3.3", - "walk-up-path": "^3.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - } - }, - "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } + "glob": "^11.0.0", + "walk-up-path": "^4.0.0" } }, "restore-cursor": { @@ -8566,11 +9204,6 @@ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" - }, "rimraf": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", @@ -8580,9 +9213,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "requires": { "balanced-match": "^1.0.0" } @@ -8609,14 +9242,6 @@ } } }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "requires": { - "queue-microtask": "^1.2.2" - } - }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -8634,9 +9259,9 @@ } }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true }, "shebang-command": { @@ -8658,34 +9283,43 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" }, "sigstore": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", - "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.0.0.tgz", + "integrity": "sha512-Gw/FgHtrLM9WP8P5lLcSGh9OQcrTruWCELAiS48ik1QbL0cH+dfjomiRTUE9zzz+D1N6rOLkwXUvVmXZAsNE0Q==", "dev": true, "requires": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "@sigstore/sign": "^2.3.2", - "@sigstore/tuf": "^2.3.4", - "@sigstore/verify": "^1.2.1" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.0.0", + "@sigstore/tuf": "^4.0.0", + "@sigstore/verify": "^3.0.0" } }, "slice-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-6.0.0.tgz", - "integrity": "sha512-6bn4hRfkTvDfUoEQYkERg0BVF1D0vrX9HEkMl08uDiNWvVvjylLHvZFZWkDo6wjT8tUctbYl1nCOuE66ZTaUtA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "requires": { "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^4.0.0" + "is-fullwidth-code-point": "^5.0.0" }, "dependencies": { "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true + }, + "is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "requires": { + "get-east-asian-width": "^1.3.1" + } } } }, @@ -8696,24 +9330,24 @@ "dev": true }, "socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dev": true, "requires": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, "requires": { - "agent-base": "^7.1.1", + "agent-base": "^7.1.2", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" } }, "spdx-correct": { @@ -8743,21 +9377,15 @@ } }, "spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", - "dev": true - }, - "sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "dev": true }, "ssri": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", - "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "dev": true, "requires": { "minipass": "^7.0.3" @@ -8790,15 +9418,15 @@ }, "dependencies": { "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true }, "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "requires": { "ansi-regex": "^6.0.1" @@ -8888,143 +9516,105 @@ } }, "sync-content": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-1.0.2.tgz", - "integrity": "sha512-znd3rYiiSxU3WteWyS9a6FXkTA/Wjk8WQsOyzHbineeL837dLn3DA4MRhsIX3qGcxDMH6+uuFV4axztssk7wEQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-2.0.1.tgz", + "integrity": "sha512-NI1mo514yFhr8pV/5Etvgh+pSBUIpoAKoiBIUwALVlQQNAwb40bTw8hhPFaip/dvv0GhpHVOq0vq8iY02ppLTg==", "dev": true, "requires": { - "glob": "^10.2.6", + "glob": "^11.0.0", "mkdirp": "^3.0.1", - "path-scurry": "^1.9.2", - "rimraf": "^5.0.1" + "path-scurry": "^2.0.0", + "rimraf": "^6.0.0", + "tshy": "^3.0.0" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } + "lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true }, - "glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" } }, - "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" } } } }, "tap": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/tap/-/tap-19.0.2.tgz", - "integrity": "sha512-SRGulk1RKlVuYtnPeephj+xyE0sG9CvGlKYP4lymBZykLtkwBPnEBjQ2iQmLX5z0BFEMfKh8G4bvZkhoSJb3kg==", - "dev": true, - "requires": { - "@tapjs/after": "1.1.24", - "@tapjs/after-each": "2.0.1", - "@tapjs/asserts": "2.0.1", - "@tapjs/before": "2.0.1", - "@tapjs/before-each": "2.0.1", - "@tapjs/core": "2.0.1", - "@tapjs/filter": "2.0.1", - "@tapjs/fixture": "2.0.1", - "@tapjs/intercept": "2.0.1", - "@tapjs/mock": "2.0.1", - "@tapjs/node-serialize": "2.0.1", - "@tapjs/run": "2.0.2", - "@tapjs/snapshot": "2.0.1", - "@tapjs/spawn": "2.0.1", - "@tapjs/stdin": "2.0.1", - "@tapjs/test": "2.0.1", - "@tapjs/typescript": "1.4.6", - "@tapjs/worker": "2.0.1", - "resolve-import": "^1.4.5" + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/tap/-/tap-21.1.1.tgz", + "integrity": "sha512-WQQkoJw2LbusXPq9d6A3N4SHCpiog1AbjSVyNlqRh6uiCrtra24bZRCURX8cgBjKV4W22dcRevvhMp24+N/oVg==", + "dev": true, + "requires": { + "@tapjs/after": "3.0.2", + "@tapjs/after-each": "4.0.2", + "@tapjs/asserts": "4.0.2", + "@tapjs/before": "4.0.2", + "@tapjs/before-each": "4.0.2", + "@tapjs/chdir": "3.0.2", + "@tapjs/core": "4.0.2", + "@tapjs/filter": "4.0.2", + "@tapjs/fixture": "4.0.2", + "@tapjs/intercept": "4.0.2", + "@tapjs/mock": "4.0.2", + "@tapjs/node-serialize": "4.0.2", + "@tapjs/run": "4.0.3", + "@tapjs/snapshot": "4.0.2", + "@tapjs/spawn": "4.0.2", + "@tapjs/stdin": "4.0.2", + "@tapjs/test": "4.0.2", + "@tapjs/typescript": "3.1.1", + "@tapjs/worker": "4.0.2", + "resolve-import": "2" } }, "tap-parser": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-16.0.1.tgz", - "integrity": "sha512-vKianJzSSzLkJ3bHBwzvZDDRi9yGMwkRANJxwPAjAue50owB8rlluYySmTN4tZVH0nsh6stvrQbg9kuCL5svdg==", + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-18.0.1.tgz", + "integrity": "sha512-Jcu51jzPCDXzuFvyCDGRfJWm2mJ0yRLf+aqWINk4P03HM9uyrq2SLavn26N8qwVz//GZysNt3DQNvKrYjidB3g==", "dev": true, "requires": { "events-to-array": "^2.0.3", - "tap-yaml": "2.2.2" + "tap-yaml": "4.0.1" } }, "tap-yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-2.2.2.tgz", - "integrity": "sha512-MWG4OpAKtNoNVjCz/BqlDJiwTM99tiHRhHPS4iGOe1ZS0CgM4jSFH92lthSFvvy4EdDjQZDV7uYqUFlU9JuNhw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-4.0.1.tgz", + "integrity": "sha512-2D5FSjxSP8v3LCh3N1KZa+FoIaVh2f7bFaUNd4mm74Kx/a5JqKu6QspEQZpAwH1Ez2SlbguOMWKHPTels2xbzA==", "dev": true, "requires": { - "yaml": "^2.4.1", - "yaml-types": "^0.3.0" + "yaml": "^2.8.1", + "yaml-types": "^0.4.0" } }, "tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", "dev": true, "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "dependencies": { - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - } - } - }, - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - } + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" } }, "tcompare": { @@ -9038,20 +9628,74 @@ } }, "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "requires": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, - "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==" + "tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "requires": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + } + } }, "to-regex-range": { "version": "5.0.1", @@ -9069,46 +9713,62 @@ "dev": true }, "tshy": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/tshy/-/tshy-1.14.0.tgz", - "integrity": "sha512-YiUujgi4Jb+t2I48LwSRzHkBpniH9WjjktNozn+nlsGmVemKSjDNY7EwBRPvPCr5zAC/3ITAYWH9Z7kUinGSrw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tshy/-/tshy-3.0.3.tgz", + "integrity": "sha512-bUX6HQCvVdPyPLy2VZuKw95CtYD5aRSEgYEK7IPV9l9xN/z284kl5/hIwOfLY/mZOOdhrO34dFOOcL1VUMVyaw==", "dev": true, "requires": { - "chalk": "^5.3.0", - "chokidar": "^3.6.0", - "foreground-child": "^3.1.1", - "minimatch": "^9.0.4", + "chalk": "^5.6.2", + "chokidar": "^4.0.3", + "foreground-child": "^3.3.1", + "minimatch": "^10.0.3", "mkdirp": "^3.0.1", - "polite-json": "^4.0.1", - "resolve-import": "^1.4.5", - "rimraf": "^5.0.1", - "sync-content": "^1.0.2", - "typescript": "^5.4.5", - "walk-up-path": "^3.0.1" + "polite-json": "^5.0.0", + "resolve-import": "^2.0.0", + "rimraf": "^6.0.1", + "sync-content": "^2.0.1", + "typescript": "^5.9.3", + "walk-up-path": "^4.0.0" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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 + }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "requires": { - "balanced-match": "^1.0.0" + "readdirp": "^4.0.1" } }, - "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "requires": { + "@isaacs/brace-expansion": "^5.0.0" + } + }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true }, - "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" } } } @@ -9119,14 +9779,54 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tuf-js": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", - "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.0.0.tgz", + "integrity": "sha512-Lq7ieeGvXDXwpoSmOSgLWVdsGGV9J4a77oDTAPe/Ltrqnnm/ETaRlBAQTH5JatEh8KXuE6sddf9qAv1Q2282Hg==", "dev": true, "requires": { - "@tufjs/models": "2.0.1", - "debug": "^4.3.4", - "make-fetch-happen": "^13.0.1" + "@tufjs/models": "4.0.0", + "debug": "^4.4.1", + "make-fetch-happen": "^15.0.0" + }, + "dependencies": { + "@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + } + }, + "lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true + }, + "make-fetch-happen": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.2.tgz", + "integrity": "sha512-sI1NY4lWlXBAfjmCtVWIIpBypbBdhHtcjnwnv+gtCnsaOffyFil3aidszGC8hgzJe+fT1qix05sWxmD/Bmf/oQ==", + "dev": true, + "requires": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + } + } } }, "type-check": { @@ -9138,36 +9838,37 @@ } }, "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==" + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true }, "typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true }, "undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "dev": true, "peer": true }, "unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "dev": true, "requires": { - "unique-slug": "^4.0.0" + "unique-slug": "^5.0.0" } }, "unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", "dev": true, "requires": { "imurmurhash": "^0.1.4" @@ -9194,9 +9895,9 @@ "dev": true }, "v8-to-istanbul": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", - "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.12", @@ -9205,9 +9906,9 @@ }, "dependencies": { "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", @@ -9227,15 +9928,15 @@ } }, "validate-npm-package-name": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", - "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", + "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", "dev": true }, "walk-up-path": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", - "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", "dev": true }, "which": { @@ -9247,12 +9948,46 @@ } }, "widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", "dev": true, "requires": { - "string-width": "^5.0.1" + "string-width": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + }, + "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 + }, + "string-width": { + "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", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } } }, "word-wrap": { @@ -9322,15 +10057,10 @@ } } }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, "ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "requires": {} }, @@ -9341,21 +10071,21 @@ "dev": true }, "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true }, "yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true }, "yaml-types": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yaml-types/-/yaml-types-0.3.0.tgz", - "integrity": "sha512-i9RxAO/LZBiE0NJUy9pbN5jFz5EasYDImzRkj8Y81kkInTi1laia3P3K/wlMKzOxFQutZip8TejvQP/DwgbU7A==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/yaml-types/-/yaml-types-0.4.0.tgz", + "integrity": "sha512-XfbA30NUg4/LWUiplMbiufUiwYhgB9jvBhTWel7XQqjV+GaB79c2tROu/8/Tu7jO0HvDvnKWtBk5ksWRrhQ/0g==", "dev": true, "requires": {} }, @@ -9410,10 +10140,10 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" }, - "yoga-wasm-web": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", - "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", + "yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", "dev": true } } diff --git a/package.json b/package.json index f92058d2..e8ccb552 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "test": "tap", - "dev": "eslint src && node src/upd8.js" + "lint": "eslint src" }, "imports": { "#aggregate": "./src/aggregate.js", @@ -23,6 +23,8 @@ "#composite/things/album": "./src/data/composite/things/album/index.js", "#composite/things/art-tag": "./src/data/composite/things/art-tag/index.js", "#composite/things/artist": "./src/data/composite/things/artist/index.js", + "#composite/things/artwork": "./src/data/composite/things/artwork/index.js", + "#composite/things/content": "./src/data/composite/things/content/index.js", "#composite/things/contribution": "./src/data/composite/things/contribution/index.js", "#composite/things/flash": "./src/data/composite/things/flash/index.js", "#composite/things/flash-act": "./src/data/composite/things/flash-act/index.js", @@ -43,7 +45,8 @@ "#replacer": "./src/replacer.js", "#reverse": "./src/reverse.js", "#search": "./src/search.js", - "#search-spec": "./src/common-util/search-spec.js", + "#search-shape": "./src/common-util/search-shape.js", + "#search-select": "./src/search-select.js", "#serialize": "./src/data/serialize.js", "#sort": "./src/common-util/sort.js", "#sugar": "./src/common-util/sugar.js", @@ -65,7 +68,7 @@ "chroma-js": "^2.4.2", "command-exists": "^1.2.9", "compress-json": "^3.0.5", - "eslint": "^8.37.0", + "eslint": "^9.37.0", "flexsearch": "^0.7.43", "he": "^1.2.0", "image-size": "^1.0.2", @@ -79,11 +82,7 @@ "license": "MIT", "devDependencies": { "chokidar": "^3.5.3", - "tap": "^19.0.2", + "tap": "^21.1.1", "tcompare": "^6.0.0" - }, - "tap": { - "coverage": false, - "coverage-report": false } } diff --git a/src/aggregate.js b/src/aggregate.js index 92c66b73..3ff1846b 100644 --- a/src/aggregate.js +++ b/src/aggregate.js @@ -371,11 +371,12 @@ export function _withAggregate(mode, aggregateOpts, fn) { } export const unhelpfulTraceLines = [ - /sugar/, - /sort/, - /aggregate/, - /composite/, - /cacheable-object/, + /sugar\.js/, + /sort\.js/, + /aggregate\.js/, + /composite\.js/, + /cacheable-object\.js/, + /html\.js/, /node:/, /<anonymous>/, ]; @@ -440,7 +441,15 @@ export function showAggregate(topError, { } } - return determineCauseHelper(cause.cause); + if (cause.cause) { + return determineCauseHelper(cause.cause); + } + + if (cause.errors) { + return determineErrorsHelper(cause); + } + + return cause; }; const determineCause = error => @@ -478,7 +487,7 @@ export function showAggregate(topError, { : error.errors?.flatMap(determineErrorsHelper) ?? null); const flattenErrorStructure = (error, level = 0) => { - const cause = determineCause(error); + const cause = determineCause(error); // may be an array! const errors = determineErrors(error); return { @@ -493,7 +502,9 @@ export function showAggregate(topError, { : error.stack), cause: - (cause + (Array.isArray(cause) + ? cause.map(cause => flattenErrorStructure(cause, level + 1)) + : cause ? flattenErrorStructure(cause, level + 1) : null), @@ -528,15 +539,29 @@ export function showAggregate(topError, { unhelpfulTraceLines: ownUnhelpfulTraceLines, }, }, index, apparentSiblings) => { + const causeSingle = Array.isArray(cause) ? null : cause; + const causeArray = Array.isArray(cause) ? cause : null; + const subApparentSiblings = - (cause && errors - ? [cause, ...errors] - : cause - ? [cause] + (causeSingle && errors + ? [causeSingle, ...errors] + : causeSingle + ? [causeSingle] + : causeArray && errors + ? [...causeArray, ...errors] + : causeArray + ? causeArray : errors ? errors : []); + const presentedAsErrors = + (causeArray && errors + ? [...causeArray, ...errors] + : causeArray + ? causeArray + : errors); + const anythingHasErrorsThisLayer = apparentSiblings.some(({errors}) => !empty(errors)); @@ -580,12 +605,12 @@ export function showAggregate(topError, { headerPart += ` ${colors.dim(tracePart)}`; } - const head1 = level % 2 === 0 ? '\u21aa' : colors.dim('\u21aa'); + const head1 = '\u21aa'; const bar1 = ' '; const causePart = - (cause - ? recursive(cause, 0, subApparentSiblings) + (causeSingle + ? recursive(causeSingle, 0, subApparentSiblings) .split('\n') .map((line, i) => i === 0 ? ` ${head1} ${line}` : ` ${bar1} ${line}`) .join('\n') @@ -595,8 +620,8 @@ export function showAggregate(topError, { const bar2 = level % 2 === 0 ? '\u2502' : colors.dim('\u254e'); const errorsPart = - (errors - ? errors + (presentedAsErrors + ? presentedAsErrors .map((error, index) => recursive(error, index + 1, subApparentSiblings)) .flatMap(str => str.split('\n')) .map((line, i) => i === 0 ? ` ${head2} ${line}` : ` ${bar2} ${line}`) diff --git a/src/cli.js b/src/cli.js index bd4ec685..ec72a625 100644 --- a/src/cli.js +++ b/src/cli.js @@ -376,77 +376,79 @@ decorateTime.displayTime = function () { } }; -export function progressPromiseAll(msgOrMsgFn, array) { +const progressUpdateInterval = 1000 / 60; + +function progressShow(message, total) { + let start = Date.now(), last = 0, done = 0; + + const progress = () => { + const messagePart = + (typeof message === 'function' + ? message() + : message); + + const percent = + Math.round((done / total) * 1000) / 10 + '%'; + + const percentPart = + percent.padEnd('99.9%'.length, ' '); + + return `${messagePart} [${percentPart}]`; + }; + + process.stdout.write(`\r` + progress()); + + return () => { + done++; + + if (done === total) { + process.stdout.write( + `\r\x1b[2m` + progress() + + `\x1b[0;32m Done! ` + + `\x1b[0;2m(${formatDuration(Date.now() - start)}) ` + + `\x1b[0m\n` + ); + } else if (Date.now() - last >= progressUpdateInterval) { + process.stdout.write('\r' + progress()); + last = Date.now(); + } + }; +} + +export function progressPromiseAll(message, array) { if (!array.length) { return Promise.resolve([]); } - const msgFn = - typeof msgOrMsgFn === 'function' ? msgOrMsgFn : () => msgOrMsgFn; - - let done = 0, - total = array.length; - process.stdout.write(`\r${msgFn()} [0/${total}]`); - const start = Date.now(); - return Promise.all( - array.map((promise) => - Promise.resolve(promise).then((val) => { - done++; - // const pc = `${done}/${total}`; - const pc = (Math.round((done / total) * 1000) / 10 + '%').padEnd( - '99.9%'.length, - ' ' - ); - if (done === total) { - const time = Date.now() - start; - process.stdout.write( - `\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n` - ); - } else { - process.stdout.write(`\r${msgFn()} [${pc}] `); - } - return val; - }) - ) - ); + const show = progressShow(message, array.length); + + const next = value => { + show(); + + return value; + }; + + const promises = + array.map(promise => Promise.resolve(promise).then(next)); + + return Promise.all(promises); } -export function progressCallAll(msgOrMsgFn, array) { +export function progressCallAll(message, array) { if (!array.length) { return []; } - const msgFn = - typeof msgOrMsgFn === 'function' ? msgOrMsgFn : () => msgOrMsgFn; + const show = progressShow(message, array.length); - const updateInterval = 1000 / 60; - - let done = 0, - total = array.length; - process.stdout.write(`\r${msgFn()} [0/${total}]`); - const start = Date.now(); - const vals = []; - let lastTime = 0; + const values = []; for (const fn of array) { - const val = fn(); - done++; - - if (done === total) { - const pc = '100%'.padEnd('99.9%'.length, ' '); - const time = Date.now() - start; - process.stdout.write( - `\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n` - ); - } else if (Date.now() - lastTime >= updateInterval) { - const pc = (Math.round((done / total) * 1000) / 10 + '%').padEnd('99.9%'.length, ' '); - process.stdout.write(`\r${msgFn()} [${pc}] `); - lastTime = Date.now(); - } - vals.push(val); + values.push(fn()); + show(); } - return vals; + return values; } export function fileIssue({ @@ -459,6 +461,24 @@ export function fileIssue({ console.error(colors.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`)); } +// Quick'n dirty function to present a duration nicely for command-line use. +export function formatDuration(timeDelta) { + const seconds = timeDelta / 1000; + + if (seconds > 90) { + const modSeconds = Math.floor(seconds % 60); + const minutes = Math.floor(seconds - seconds % 60) / 60; + return `${minutes}m${modSeconds}s`; + } + + if (seconds < 0.1) { + return 'instant'; + } + + const precision = (seconds > 1 ? 3 : 2); + return `${seconds.toPrecision(precision)}s`; +} + export async function logicalCWD() { if (process.env.PWD) { return process.env.PWD; @@ -469,7 +489,7 @@ export async function logicalCWD() { try { await stat('/bin/sh'); - } catch (error) { + } catch { // Not logical, so sad. return process.cwd(); } diff --git a/src/common-util/search-shape.js b/src/common-util/search-shape.js new file mode 100644 index 00000000..e0819ed6 --- /dev/null +++ b/src/common-util/search-shape.js @@ -0,0 +1,58 @@ +// Index structures shared by client and server, and relevant interfaces. +// First and foremost, this is complemented by src/search-select.js, which +// actually fills the search indexes up with stuff. During build this all +// gets consumed by src/search.js to make an index, fill it with stuff +// (as described by search-select.js), and export it to disk; then on +// the client that export is consumed by src/static/js/search-worker.js, +// which builds an index in the same shape and imports the data for query. + +const baselineStore = [ + 'primaryName', + 'disambiguator', + 'artwork', + 'color', +]; + +const genericStore = baselineStore; + +const searchShape = { + generic: { + index: [ + 'primaryName', + 'parentName', + 'artTags', + 'additionalNames', + 'contributors', + 'groups', + ].map(field => ({field, tokenize: 'forward'})), + + store: genericStore, + }, + + verbatim: { + index: [ + 'primaryName', + 'parentName', + 'artTags', + 'additionalNames', + 'contributors', + 'groups', + ], + + store: genericStore, + }, +}; + +export default searchShape; + +export function makeSearchIndex(descriptor, {FlexSearch}) { + return new FlexSearch.Document({ + id: 'reference', + index: descriptor.index, + store: descriptor.store, + + // Disable scoring, always return results according to provided order + // (specified above in `genericQuery`, etc). + resolution: 1, + }); +} diff --git a/src/common-util/search-spec.js b/src/common-util/search-spec.js deleted file mode 100644 index 75de0d16..00000000 --- a/src/common-util/search-spec.js +++ /dev/null @@ -1,259 +0,0 @@ -// Index structures shared by client and server, and relevant interfaces. - -function getArtworkPath(thing) { - switch (thing.constructor[Symbol.for('Thing.referenceType')]) { - case 'album': { - return [ - 'media.albumCover', - thing.directory, - thing.coverArtFileExtension, - ]; - } - - case 'flash': { - return [ - 'media.flashArt', - thing.directory, - thing.coverArtFileExtension, - ]; - } - - case 'track': { - if (thing.hasUniqueCoverArt) { - return [ - 'media.trackCover', - thing.album.directory, - thing.directory, - thing.coverArtFileExtension, - ]; - } else if (thing.album.hasCoverArt) { - return [ - 'media.albumCover', - thing.album.directory, - thing.album.coverArtFileExtension, - ]; - } else { - return null; - } - } - - default: - return null; - } -} - -function prepareArtwork(thing, { - checkIfImagePathHasCachedThumbnails, - getThumbnailEqualOrSmaller, - urls, -}) { - const hasWarnings = - thing.artTags?.some(artTag => artTag.isContentWarning); - - const artworkPath = - getArtworkPath(thing); - - if (!artworkPath) { - return undefined; - } - - const mediaSrc = - urls - .from('media.root') - .to(...artworkPath); - - if (!checkIfImagePathHasCachedThumbnails(mediaSrc)) { - return undefined; - } - - const selectedSize = - getThumbnailEqualOrSmaller( - (hasWarnings ? 'mini' : 'adorb'), - mediaSrc); - - const mediaSrcJpeg = - mediaSrc.replace(/\.(png|jpg)$/, `.${selectedSize}.jpg`); - - const displaySrc = - urls - .from('thumb.root') - .to('thumb.path', mediaSrcJpeg); - - const serializeSrc = - displaySrc.replace(thing.directory, '<>'); - - return serializeSrc; -} - -export const searchSpec = { - generic: { - query: ({ - albumData, - artTagData, - artistData, - flashData, - groupData, - trackData, - }) => [ - albumData, - - artTagData, - - artistData - .filter(artist => !artist.isAlias), - - flashData, - - groupData, - - trackData - // Exclude rereleases - there's no reasonable way to differentiate - // them from the main release as part of this query. - .filter(track => !track.mainReleaseTrack), - ].flat(), - - process(thing, opts) { - const fields = {}; - - fields.primaryName = - thing.name; - - const kind = - thing.constructor[Symbol.for('Thing.referenceType')]; - - fields.parentName = - (kind === 'track' - ? thing.album.name - : kind === 'group' - ? thing.category.name - : kind === 'flash' - ? thing.act.name - : null); - - fields.color = - thing.color; - - fields.artTags = - (thing.constructor.hasPropertyDescriptor('artTags') - ? thing.artTags.map(artTag => artTag.nameShort) - : []); - - fields.additionalNames = - (thing.constructor.hasPropertyDescriptor('additionalNames') - ? thing.additionalNames.map(entry => entry.name) - : thing.constructor.hasPropertyDescriptor('aliasNames') - ? thing.aliasNames - : []); - - const contribKeys = [ - 'artistContribs', - 'bannerArtistContribs', - 'contributorContribs', - 'coverArtistContribs', - 'wallpaperArtistContribs', - ]; - - const contributions = - contribKeys - .filter(key => Object.hasOwn(thing, key)) - .flatMap(key => thing[key]); - - fields.contributors = - contributions - .flatMap(({artist}) => [ - artist.name, - ...artist.aliasNames, - ]); - - const groups = - (Object.hasOwn(thing, 'groups') - ? thing.groups - : Object.hasOwn(thing, 'album') - ? thing.album.groups - : []); - - const mainContributorNames = - contributions - .map(({artist}) => artist.name); - - fields.groups = - groups - .filter(group => !mainContributorNames.includes(group.name)) - .map(group => group.name); - - fields.artwork = - prepareArtwork(thing, opts); - - return fields; - }, - - index: [ - 'primaryName', - 'parentName', - 'artTags', - 'additionalNames', - 'contributors', - 'groups', - ], - - store: [ - 'primaryName', - 'artwork', - 'color', - ], - }, -}; - -export function makeSearchIndex(descriptor, {FlexSearch}) { - return new FlexSearch.Document({ - id: 'reference', - index: descriptor.index, - store: descriptor.store, - }); -} - -// TODO: This function basically mirrors bind-utilities.js, which isn't -// exactly robust, but... binding might need some more thought across the -// codebase in *general.* -function bindSearchUtilities({ - checkIfImagePathHasCachedThumbnails, - getThumbnailEqualOrSmaller, - thumbsCache, - urls, -}) { - const bound = { - urls, - }; - - bound.checkIfImagePathHasCachedThumbnails = - (imagePath) => - checkIfImagePathHasCachedThumbnails(imagePath, thumbsCache); - - bound.getThumbnailEqualOrSmaller = - (preferred, imagePath) => - getThumbnailEqualOrSmaller(preferred, imagePath, thumbsCache); - - return bound; -} - -export function populateSearchIndex(index, descriptor, opts) { - const {wikiData} = opts; - const bound = bindSearchUtilities(opts); - - const collection = descriptor.query(wikiData); - - for (const thing of collection) { - const reference = thing.constructor.getReference(thing); - - let processed; - try { - processed = descriptor.process(thing, bound); - } catch (caughtError) { - throw new Error( - `Failed to process searchable thing ${reference}`, - {cause: caughtError}); - } - - index.add({reference, ...processed}); - } -} diff --git a/src/common-util/sort.js b/src/common-util/sort.js index fd382033..bbe4e551 100644 --- a/src/common-util/sort.js +++ b/src/common-util/sort.js @@ -370,11 +370,12 @@ export function sortAlbumsTracksChronologically(data, { getDate, } = {}) { // Sort albums before tracks... - sortByConditions(data, [(t) => t.album === undefined]); + sortByConditions(data, [t => t.isAlbum]); - // Group tracks by album... - sortByDirectory(data, { - getDirectory: (t) => (t.album ? t.album.directory : t.directory), + // Put albums alphabetically, and group with them... + sortAlphabetically(data, { + getDirectory: t => t.isTrack ? t.album.directory : t.directory, + getName: t => t.isTrack ? t.album.name : t.name, }); // Sort tracks by position in album... @@ -389,6 +390,22 @@ export function sortAlbumsTracksChronologically(data, { return data; } +export function sortArtworksChronologically(data, { + latestFirst = false, +} = {}) { + // Artworks conveniently describe their things as artwork.thing, so they + // work in sortEntryThingPairs. (Yes, this is just assuming the artworks + // are only for albums and tracks... sorry... TODO...) + sortEntryThingPairs(data, things => + sortAlbumsTracksChronologically(things, {latestFirst})); + + // Artworks' own dates always matter before however the thing places itself, + // and accommodate per-thing properties like coverArtDate anyway. + sortByDate(data, {latestFirst}); + + return data; +} + export function sortFlashesChronologically(data, { latestFirst = false, getDate, @@ -413,6 +430,7 @@ export function sortFlashesChronologically(data, { export function sortContributionsChronologically(data, sortThings, { latestFirst = false, + getThing = contrib => contrib.thing, } = {}) { // Contributions only have one date property (which is provided when // the contribution is created). They're sorted by this most primarily, @@ -421,7 +439,7 @@ export function sortContributionsChronologically(data, sortThings, { const entries = data.map(contrib => ({ entry: contrib, - thing: contrib.thing, + thing: getThing(contrib), })); sortEntryThingPairs( diff --git a/src/common-util/sugar.js b/src/common-util/sugar.js index 66e160aa..354cf5cc 100644 --- a/src/common-util/sugar.js +++ b/src/common-util/sugar.js @@ -70,6 +70,16 @@ export function pick(array) { return array[Math.floor(Math.random() * array.length)]; } +// Gets the only item in a single-item array (strictly, length === 1). +// If the array has more than one item, or is empty, this is null. +export function onlyItem(array) { + if (array.length === 1) { + return array[0]; + } else { + return null; + } +} + // Gets the item at an index relative to another index. export function atOffset(array, index, offset, { wrap = false, @@ -116,10 +126,14 @@ export function findIndexOrEnd(array, fn) { // returns null (or values in the array are nullish), they'll just be skipped in // the sum. export function accumulateSum(array, fn = x => x) { + if (!Array.isArray(array)) { + return accumulateSum(Array.from(array, fn)); + } + return array.reduce( (accumulator, value, index, array) => accumulator + - fn(value, index, array) ?? 0, + (fn(value, index, array) ?? 0), 0); } @@ -221,6 +235,9 @@ export const compareArrays = (arr1, arr2, {checkOrder = true} = {}) => ? arr1.every((x, i) => arr2[i] === x) : arr1.every((x) => arr2.includes(x))); +export const exhaust = (generatorFunction) => + Array.from(generatorFunction()); + export function compareObjects(obj1, obj2, { checkOrder = false, checkSymbols = true, @@ -251,11 +268,20 @@ export function compareObjects(obj1, obj2, { // Stolen from jq! Which pro8a8ly stole the concept from other places. Nice. export const withEntries = (obj, fn) => { - const result = fn(Object.entries(obj)); - if (result instanceof Promise) { - return result.then(entries => Object.fromEntries(entries)); + if (obj instanceof Map) { + const result = fn(Array.from(obj.entries())); + if (result instanceof Promise) { + return result.then(entries => new Map(entries)); + } else { + return new Map(result); + } } else { - return Object.fromEntries(result); + const result = fn(Object.entries(obj)); + if (result instanceof Promise) { + return result.then(entries => Object.fromEntries(entries)); + } else { + return Object.fromEntries(result); + } } } @@ -299,34 +325,74 @@ export function filterProperties(object, properties, { return filteredObject; } -export function queue(array, max = 50) { - if (max === 0) { - return array.map((fn) => fn()); +export function queue(functionList, queueSize = 50) { + if (queueSize === 0) { + return functionList.map(fn => fn()); } - const begin = []; - let current = 0; - const ret = array.map( - (fn) => - new Promise((resolve, reject) => { - begin.push(() => { - current++; - Promise.resolve(fn()).then((value) => { - current--; - if (current < max && begin.length) { - begin.shift()(); - } - resolve(value); - }, reject); - }); - }) - ); + const promiseList = []; + const resolveList = []; + const rejectList = []; - for (let i = 0; i < max && begin.length; i++) { - begin.shift()(); + for (let i = 0; i < functionList.length; i++) { + const promiseWithResolvers = Promise.withResolvers(); + promiseList.push(promiseWithResolvers.promise); + resolveList.push(promiseWithResolvers.resolve); + rejectList.push(promiseWithResolvers.reject); } - return ret; + let cursor = 0; + let running = 0; + + const next = async () => { + if (running >= queueSize) { + return; + } + + if (cursor === functionList.length) { + return; + } + + const thisFunction = functionList[cursor]; + const thisResolve = resolveList[cursor]; + const thisReject = rejectList[cursor]; + + delete functionList[cursor]; + delete resolveList[cursor]; + delete rejectList[cursor]; + + cursor++; + running++; + + try { + thisResolve(await thisFunction()); + } catch (error) { + thisReject(error); + } finally { + running--; + + // If the cursor is at 1, this is the first promise that resolved, + // so we're now done the "kick start", and can start the remaining + // promises (up to queueSize). + if (cursor === 1) { + // Since only one promise is used for the "kick start", and that one + // has just resolved, we know there's none running at all right now, + // and can start as many as specified in the queueSize right away. + for (let i = 0; i < queueSize; i++) { + next(); + } + } else { + next(); + } + } + }; + + // Only start a single promise, as a "kick start", so that it resolves as + // early as possible (it will resolve before we use CPU to start the rest + // of the promises, up to queueSize). + next(); + + return promiseList; } export function delay(ms) { @@ -357,15 +423,23 @@ export function splitKeys(key) { // Follows a key path like 'foo.bar.baz' to get an item nested deeply inside // an object. If a value partway through the chain is an array, the values -// down the rest of the chain are gotten for each item in the array. +// down the rest of the chain are gotten for each item in the array. If a value +// partway through the chain is missing the next key, the chain stops and is +// undefined (or null) at that point. // // obj: {x: [{y: ['a']}, {y: ['b', 'c']}]} // key: 'x.y' // -> [['a'], ['b', 'c']] // +// obj: {x: [{y: ['a']}, {y: ['b', 'c']}, {z: ['d', 'e']}]} +// key: 'x.z' +// -> [undefined, undefined, ['d', 'e']] +// export function getNestedProp(obj, key) { const recursive = (o, k) => - (k.length === 1 + (o === undefined || o === null + ? o + : k.length === 1 ? o[k[0]] : Array.isArray(o[k[0]]) ? o[k[0]].map(v => recursive(v, k.slice(1))) diff --git a/src/common-util/wiki-data.js b/src/common-util/wiki-data.js index 4bbef8ab..3fde2495 100644 --- a/src/common-util/wiki-data.js +++ b/src/common-util/wiki-data.js @@ -11,7 +11,7 @@ export {filterMultipleArrays} from './sugar.js'; // Generic value operations -export function getKebabCase(name) { +export function getCaseSensitiveKebabCase(name) { return name // Spaces to dashes @@ -34,6 +34,9 @@ export function getKebabCase(name) { // General punctuation which always separates surrounding words .replace(/[/@#$%*()_=,[\]{}|\\;:<>?`~]/g, '-') + // More punctuation which always separates surrounding words + .replace(/[\u{2013}-\u{2014}]/u, '-') // En Dash, Em Dash + // Accented characters .replace(/[áâäàå]/gi, 'a') .replace(/[çč]/gi, 'c') @@ -50,17 +53,17 @@ export function getKebabCase(name) { // Trim dashes on boundaries .replace(/^-+|-+$/g, '') +} - // Always lowercase - .toLowerCase(); +export function getKebabCase(name) { + return getCaseSensitiveKebabCase(name).toLowerCase(); } // Specific data utilities -// Matches heading details from commentary data in roughly the formats: +// Matches heading details from commentary data in roughly the format: // -// <i>artistReference:</i> (annotation, date) -// <i>artistReference|artistDisplayText:</i> (annotation, date) +// <i>artistText:</i> (annotation, date) // // where capturing group "annotation" can be any text at all, except that the // last entry (past a comma or the only content within parentheses), if parsed @@ -83,8 +86,9 @@ export function getKebabCase(name) { // parentheses can be part of the actual annotation content. // // Capturing group "artistReference" is all the characters between <i> and </i> -// (apart from the pipe and "artistDisplayText" text, if present), and is either -// the name of an artist or an "artist:directory"-style reference. +// (apart from the pipe and the "artistText" group, if present), and is either +// the name of one or more artist or "artist:directory"-style references, +// joined by commas, if multiple. // // This regular expression *doesn't* match bodies, which will need to be parsed // out of the original string based on the indices matched using this. @@ -94,7 +98,7 @@ const dateRegex = groupName => String.raw`(?<${groupName}>[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,4}[-/][0-9]{1,4}[-/][0-9]{1,4})`; const commentaryRegexRaw = - String.raw`^<i>(?<artistReferences>.+?)(?:\|(?<artistDisplayText>.+))?:<\/i>(?: \((?<annotation>(?:.*?(?=,|\)[^)]*$))*?)(?:,? ?(?:(?<dateKind>sometime|throughout|around) )?${dateRegex('date')}(?: ?- ?${dateRegex('secondDate')})?(?: (?<accessKind>captured|accessed) ${dateRegex('accessDate')})?)?\))?`; + String.raw`^<i>(?<artistText>.+?):<\/i>(?: \((?<annotation>(?:.*?(?=,|\)[^)]*$))*?)(?:,? ?(?:(?<dateKind>sometime|throughout|around) )?${dateRegex('date')}(?: ?- ?${dateRegex('secondDate')})?(?: (?<accessKind>captured|accessed) ${dateRegex('accessDate')})?)?\))?`; export const commentaryRegexCaseInsensitive = new RegExp(commentaryRegexRaw, 'gmi'); export const commentaryRegexCaseSensitive = @@ -102,6 +106,43 @@ export const commentaryRegexCaseSensitive = export const commentaryRegexCaseSensitiveOneShot = new RegExp(commentaryRegexRaw); +// The #validators function isOldStyleLyrics() describes +// what this regular expression detects against. +export const multipleLyricsDetectionRegex = + /^<i>.*:<\/i>/m; + +export function matchContentEntries(sourceText) { + const matchEntries = []; + + let previousMatchEntry = null; + let previousEndIndex = null; + + const trimBody = body => + body + .replace(/^\n*/, '') + .replace(/\n*$/, ''); + + for (const {0: matchText, index: startIndex, groups: matchEntry} + of sourceText.matchAll(commentaryRegexCaseSensitive)) { + if (previousMatchEntry) { + previousMatchEntry.body = + trimBody(sourceText.slice(previousEndIndex, startIndex)); + } + + matchEntries.push(matchEntry); + + previousMatchEntry = matchEntry; + previousEndIndex = startIndex + matchText.length; + } + + if (previousMatchEntry) { + previousMatchEntry.body = + trimBody(sourceText.slice(previousEndIndex)); + } + + return matchEntries; +} + export function filterAlbumsByCommentary(albums) { return albums .filter((album) => [album, ...album.tracks].some((x) => x.commentary)); @@ -492,3 +533,52 @@ export function combineWikiDataArrays(arrays) { return combined; } } + +// Markdown stuff + +export function* matchMarkdownLinks(markdownSource, {marked}) { + const plausibleLinkRegexp = /\[(?=.*?\))/g; + + // Pedantic rules use more particular parentheses detection in link + // destinations - they allow one level of balanced parentheses, and + // otherwise, parentheses must be escaped. This allows for entire links + // to be wrapped in parentheses, e.g below: + // + // This is so cool. ([You know??](https://example.com)) + // + const definiteLinkRegexp = marked.Lexer.rules.inline.pedantic.link; + + let plausibleMatch = null; + while (plausibleMatch = plausibleLinkRegexp.exec(markdownSource)) { + const definiteMatch = + definiteLinkRegexp.exec(markdownSource.slice(plausibleMatch.index)); + + if (!definiteMatch) { + continue; + } + + const [{length}, label, href] = definiteMatch; + const index = plausibleMatch.index + definiteMatch.index; + + yield {label, href, index, length}; + } +} + +export function* matchInlineLinks(source) { + const plausibleLinkRegexp = /\b[a-z]*:\/\/[^ ]*?(?=(?:[,.!?]*)(?:\s|$))/gm; + + let plausibleMatch = null; + while (plausibleMatch = plausibleLinkRegexp.exec(source)) { + const [href] = plausibleMatch; + const {index} = plausibleMatch; + const [{length}] = plausibleMatch; + + try { + new URL(href); + } catch { + continue; + } + + yield {href, length, index}; + } +} diff --git a/src/content-function.js b/src/content-function.js index 44f8b842..e141a686 100644 --- a/src/content-function.js +++ b/src/content-function.js @@ -3,7 +3,7 @@ import {inspect as nodeInspect} from 'node:util'; import {decorateError} from '#aggregate'; import {colors, decorateTime, ENABLE_COLOR} from '#cli'; import {Template} from '#html'; -import {annotateFunction, empty, setIntersection} from '#sugar'; +import {empty} from '#sugar'; function inspect(value, opts = {}) { return nodeInspect(value, {colors: ENABLE_COLOR, ...opts}); @@ -13,167 +13,103 @@ const DECORATE_TIME = process.env.HSMUSIC_DEBUG_CONTENT_PERF === '1'; export class ContentFunctionSpecError extends Error {} -export default function contentFunction({ - contentDependencies = [], - extraDependencies = [], - - slots, - sprawl, - query, - relations, - data, - generate, -}) { - const expectedContentDependencyKeys = new Set(contentDependencies); - const expectedExtraDependencyKeys = new Set(extraDependencies); - - // Initial checks. These only need to be run once per description of a - // content function, and don't depend on any mutable context (e.g. which - // dependencies have been fulfilled so far). - - const overlappingContentExtraDependencyKeys = - setIntersection(expectedContentDependencyKeys, expectedExtraDependencyKeys); - - if (!empty(overlappingContentExtraDependencyKeys)) { - throw new ContentFunctionSpecError(`Overlap in content and extra dependency keys: ${[...overlappingContentExtraDependencyKeys].join(', ')}`); +function optionalDecorateTime(prefix, dependency, fn) { + if (DECORATE_TIME) { + return decorateTime(`${prefix}/${dependency}`, fn); + } else { + return fn; } +} - if (!generate) { +export default function contentFunction(spec) { + if (!spec.generate) { throw new ContentFunctionSpecError(`Expected generate function`); } - if (sprawl && !expectedExtraDependencyKeys.has('wikiData')) { - throw new ContentFunctionSpecError(`Content functions which sprawl must specify wikiData in extraDependencies`); + if (spec.slots) { + Template.validateSlotsDescription(spec.slots); } - if (slots && !expectedExtraDependencyKeys.has('html')) { - throw new ContentFunctionSpecError(`Content functions with slots must specify html in extraDependencies`); - } + return expectExtraDependencies(spec, null); +} - if (slots) { - Template.validateSlotsDescription(slots); +contentFunction.identifyingSymbol = Symbol(`Is a content function?`); + +export function expectExtraDependencies(spec, boundExtraDependencies) { + const generate = + (boundExtraDependencies + ? prepareWorkingGenerateFunction(spec, boundExtraDependencies) + : () => { + throw new Error(`Not bound with extraDependencies yet`); + }); + + generate[contentFunction.identifyingSymbol] = true; + + const dependency = spec.generate.name; + for (const key of ['sprawl', 'query', 'relations', 'data']) { + if (spec[key]) { + generate[key] = optionalDecorateTime(key, dependency, spec[key]); + } } - // Pass all the details to expectDependencies, which will recursively build - // up a set of fulfilled dependencies and make functions like `relations` - // and `generate` callable only with sufficient fulfilled dependencies. - - return expectDependencies({ - slots, - sprawl, - query, - relations, - data, - generate, - - expectedContentDependencyKeys, - expectedExtraDependencyKeys, - missingContentDependencyKeys: new Set(expectedContentDependencyKeys), - missingExtraDependencyKeys: new Set(expectedExtraDependencyKeys), - invalidatingDependencyKeys: new Set(), - fulfilledDependencyKeys: new Set(), - fulfilledDependencies: {}, - }); + generate.bindExtraDependencies = (extraDependencies) => + expectExtraDependencies(spec, extraDependencies); + + return generate; } -contentFunction.identifyingSymbol = Symbol(`Is a content function?`); +function prepareWorkingGenerateFunction(spec, boundExtraDependencies) { + const dependency = spec.generate.name; -export function expectDependencies({ - slots, - sprawl, - query, - relations, - data, - generate, - - expectedContentDependencyKeys, - expectedExtraDependencyKeys, - missingContentDependencyKeys, - missingExtraDependencyKeys, - invalidatingDependencyKeys, - fulfilledDependencyKeys, - fulfilledDependencies, -}) { - const hasSprawlFunction = !!sprawl; - const hasQueryFunction = !!query; - const hasRelationsFunction = !!relations; - const hasDataFunction = !!data; - const hasSlotsDescription = !!slots; - - const isInvalidated = !empty(invalidatingDependencyKeys); - const isMissingContentDependencies = !empty(missingContentDependencyKeys); - const isMissingExtraDependencies = !empty(missingExtraDependencyKeys); - - let wrappedGenerate; - - const optionalDecorateTime = (prefix, fn) => - (DECORATE_TIME - ? decorateTime(`${prefix}/${generate.name}`, fn) - : fn); - - if (isInvalidated) { - wrappedGenerate = function() { - throw new Error(`Generate invalidated because unfulfilled dependencies provided: ${[...invalidatingDependencyKeys].join(', ')}`); - }; + let generate = ([arg1, arg2], ...extraArgs) => { + if (spec.data && !arg1) { + throw new Error(`Expected data`); + } - annotateFunction(wrappedGenerate, {name: generate, trait: 'invalidated'}); - wrappedGenerate.fulfilled = false; - } else if (isMissingContentDependencies || isMissingExtraDependencies) { - wrappedGenerate = function() { - throw new Error(`Dependencies still needed: ${[...missingContentDependencyKeys, ...missingExtraDependencyKeys].join(', ')}`); - }; + if (spec.data && spec.relations && !arg2) { + throw new Error(`Expected relations`); + } - annotateFunction(wrappedGenerate, {name: generate, trait: 'unfulfilled'}); - wrappedGenerate.fulfilled = false; - } else { - let callUnderlyingGenerate = ([arg1, arg2], ...extraArgs) => { - if (hasDataFunction && !arg1) { - throw new Error(`Expected data`); - } + if (spec.relations && !arg1) { + throw new Error(`Expected relations`); + } - if (hasDataFunction && hasRelationsFunction && !arg2) { - throw new Error(`Expected relations`); + try { + if (spec.data && spec.relations) { + return spec.generate(arg1, arg2, ...extraArgs, boundExtraDependencies); + } else if (spec.data || spec.relations) { + return spec.generate(arg1, ...extraArgs, boundExtraDependencies); + } else { + return spec.generate(...extraArgs, boundExtraDependencies); } + } catch (caughtError) { + const error = new Error( + `Error generating content for ${dependency}`, + {cause: caughtError}); - if (hasRelationsFunction && !arg1) { - throw new Error(`Expected relations`); - } + error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)] = true; + error[Symbol.for(`hsmusic.aggregate.traceFrom`)] = caughtError; - try { - if (hasDataFunction && hasRelationsFunction) { - return generate(arg1, arg2, ...extraArgs, fulfilledDependencies); - } else if (hasDataFunction || hasRelationsFunction) { - return generate(arg1, ...extraArgs, fulfilledDependencies); - } else { - return generate(...extraArgs, fulfilledDependencies); - } - } catch (caughtError) { - const error = new Error( - `Error generating content for ${generate.name}`, - {cause: caughtError}); - - error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)] = true; - error[Symbol.for(`hsmusic.aggregate.traceFrom`)] = caughtError; - - error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)] = [ - /content-function\.js/, - /util\/html\.js/, - ]; - - error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)] = [ - /content\/dependencies\/(.*\.js:.*(?=\)))/, - ]; - - throw error; - } - }; + error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)] = [ + /content-function\.js/, + /util\/html\.js/, + ]; + + error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)] = [ + /content\/dependencies\/(.*\.js:.*(?=\)))/, + ]; - callUnderlyingGenerate = - optionalDecorateTime(`generate`, callUnderlyingGenerate); + throw error; + } + }; + + generate = optionalDecorateTime(`generate`, dependency, generate); - if (hasSlotsDescription) { - const stationery = fulfilledDependencies.html.stationery({ - annotation: generate.name, + if (spec.slots) { + let stationery = null; + return (...args) => { + stationery ??= boundExtraDependencies.html.stationery({ + annotation: dependency, // These extra slots are for the data and relations (positional) args. // No hacks to store them temporarily or otherwise "invisibly" alter @@ -182,170 +118,23 @@ export function expectDependencies({ slots: { _cfArg1: {validate: v => v.isObject}, _cfArg2: {validate: v => v.isObject}, - ...slots, + ...spec.slots, }, content(slots) { const args = [slots._cfArg1, slots._cfArg2]; - return callUnderlyingGenerate(args, slots); + return generate(args, slots); }, }); - wrappedGenerate = function(...args) { - return stationery.template().slots({ - _cfArg1: args[0] ?? null, - _cfArg2: args[1] ?? null, - }); - }; - } else { - wrappedGenerate = function(...args) { - return callUnderlyingGenerate(args); - }; - } - - wrappedGenerate.fulfill = function() { - throw new Error(`All dependencies already fulfilled (${generate.name})`); - }; - - annotateFunction(wrappedGenerate, {name: generate, trait: 'fulfilled'}); - wrappedGenerate.fulfilled = true; - } - - wrappedGenerate[contentFunction.identifyingSymbol] = true; - - if (hasSprawlFunction) { - wrappedGenerate.sprawl = optionalDecorateTime(`sprawl`, sprawl); - } - - if (hasQueryFunction) { - wrappedGenerate.query = optionalDecorateTime(`query`, query); - } - - if (hasRelationsFunction) { - wrappedGenerate.relations = optionalDecorateTime(`relations`, relations); - } - - if (hasDataFunction) { - wrappedGenerate.data = optionalDecorateTime(`data`, data); - } - - wrappedGenerate.fulfill ??= function fulfill(dependencies) { - // To avoid unneeded destructuring, `fullfillDependencies` is a mutating - // function. But `fulfill` itself isn't meant to mutate! We create a copy - // of these variables, so their original values are kept for additional - // calls to this same `fulfill`. - const newlyMissingContentDependencyKeys = new Set(missingContentDependencyKeys); - const newlyMissingExtraDependencyKeys = new Set(missingExtraDependencyKeys); - const newlyInvalidatingDependencyKeys = new Set(invalidatingDependencyKeys); - const newlyFulfilledDependencyKeys = new Set(fulfilledDependencyKeys); - const newlyFulfilledDependencies = {...fulfilledDependencies}; - - try { - fulfillDependencies(dependencies, { - missingContentDependencyKeys: newlyMissingContentDependencyKeys, - missingExtraDependencyKeys: newlyMissingExtraDependencyKeys, - invalidatingDependencyKeys: newlyInvalidatingDependencyKeys, - fulfilledDependencyKeys: newlyFulfilledDependencyKeys, - fulfilledDependencies: newlyFulfilledDependencies, + return stationery.template().slots({ + _cfArg1: args[0] ?? null, + _cfArg2: args[1] ?? null, }); - } catch (error) { - error.message += ` (${generate.name})`; - throw error; - } - - return expectDependencies({ - slots, - sprawl, - query, - relations, - data, - generate, - - expectedContentDependencyKeys, - expectedExtraDependencyKeys, - missingContentDependencyKeys: newlyMissingContentDependencyKeys, - missingExtraDependencyKeys: newlyMissingExtraDependencyKeys, - invalidatingDependencyKeys: newlyInvalidatingDependencyKeys, - fulfilledDependencyKeys: newlyFulfilledDependencyKeys, - fulfilledDependencies: newlyFulfilledDependencies, - }); - - }; - - Object.assign(wrappedGenerate, { - contentDependencies: expectedContentDependencyKeys, - extraDependencies: expectedExtraDependencyKeys, - }); - - return wrappedGenerate; -} - -export function fulfillDependencies(dependencies, { - missingContentDependencyKeys, - missingExtraDependencyKeys, - invalidatingDependencyKeys, - fulfilledDependencyKeys, - fulfilledDependencies, -}) { - // This is a mutating function. Be aware: it WILL mutate the provided sets - // and objects EVEN IF there are errors. This function doesn't exit early, - // so all provided dependencies which don't have an associated error should - // be treated as fulfilled (this is reflected via fulfilledDependencyKeys). - - const errors = []; - - for (let [key, value] of Object.entries(dependencies)) { - if (fulfilledDependencyKeys.has(key)) { - errors.push(new Error(`Dependency ${key} is already fulfilled`)); - continue; - } - - const isContentKey = missingContentDependencyKeys.has(key); - const isExtraKey = missingExtraDependencyKeys.has(key); - - if (!isContentKey && !isExtraKey) { - errors.push(new Error(`Dependency ${key} is not expected`)); - continue; - } - - if (value === undefined) { - errors.push(new Error(`Dependency ${key} was provided undefined`)); - continue; - } - - const isContentFunction = - !!value?.[contentFunction.identifyingSymbol]; - - const isFulfilledContentFunction = - isContentFunction && value.fulfilled; - - if (isContentKey) { - if (!isContentFunction) { - errors.push(new Error(`Content dependency ${key} is not a content function (got ${value})`)); - continue; - } - - if (!isFulfilledContentFunction) { - invalidatingDependencyKeys.add(key); - } - - missingContentDependencyKeys.delete(key); - } else if (isExtraKey) { - if (isContentFunction) { - errors.push(new Error(`Extra dependency ${key} is a content function`)); - continue; - } - - missingExtraDependencyKeys.delete(key); - } - - fulfilledDependencyKeys.add(key); - fulfilledDependencies[key] = value; + }; } - if (!empty(errors)) { - throw new AggregateError(errors, `Errors fulfilling dependencies`); - } + return (...args) => generate(args); } export function getArgsForRelationsAndData(contentFunction, wikiData, ...args) { @@ -393,8 +182,6 @@ export function getRelationsTree(dependencies, contentFunctionName, wikiData, .. }; if (contentFunction.relations) { - const listedDependencies = new Set(contentFunction.contentDependencies); - // Note: "slots" here is a completely separate concept from HTML template // slots, which are handled completely within the content function. Here, // relation slots are just references to a position within the relations @@ -408,10 +195,6 @@ export function getRelationsTree(dependencies, contentFunctionName, wikiData, .. })(); const relationFunction = (name, ...args) => { - if (!listedDependencies.has(name)) { - throw new Error(`Called relation('${name}') but ${contentFunctionName} doesn't list that dependency`); - } - const relationSymbol = Symbol(relationSymbolMessage(name)); const traceError = new Error(); @@ -502,27 +285,11 @@ export function fillRelationsLayoutFromSlotResults(relationIdentifier, results, return recursive(layout); } -export function getNeededContentDependencyNames(contentDependencies, name) { - const set = new Set(); - - function recursive(name) { - const contentFunction = contentDependencies[name]; - for (const dependencyName of contentFunction?.contentDependencies ?? []) { - recursive(dependencyName); - } - set.add(name); - } - - recursive(name); - - return set; -} - export const decorateErrorWithRelationStack = (fn, traceStack) => decorateError(fn, caughtError => { let cause = caughtError; - for (const {name, args, traceError} of traceStack.slice().reverse()) { + for (const {name, args, traceError} of traceStack.toReversed()) { const nameText = colors.green(`"${name}"`); const namePart = `Error in relation(${nameText})`; @@ -579,65 +346,10 @@ export function quickEvaluate({ const flatTreeInfo = flattenRelationsTree(treeInfo); const {root, relationIdentifier, flatRelationSlots} = flatTreeInfo; - const neededContentDependencyNames = - getNeededContentDependencyNames(allContentDependencies, name); - - // Content functions aren't recursive, so by following the set above - // sequentually, we will always provide fulfilled content functions as the - // dependencies for later content functions. - const fulfilledContentDependencies = {}; - for (const name of neededContentDependencyNames) { - const unfulfilledContentFunction = allContentDependencies[name]; - if (!unfulfilledContentFunction) continue; - - const {contentDependencies, extraDependencies} = unfulfilledContentFunction; - - if (empty(contentDependencies) && empty(extraDependencies)) { - fulfilledContentDependencies[name] = unfulfilledContentFunction; - continue; - } - - const fulfillments = {}; - - for (const dependencyName of contentDependencies ?? []) { - if (dependencyName in fulfilledContentDependencies) { - fulfillments[dependencyName] = - fulfilledContentDependencies[dependencyName]; - } - } - - for (const dependencyName of extraDependencies ?? []) { - if (dependencyName in allExtraDependencies) { - fulfillments[dependencyName] = - allExtraDependencies[dependencyName]; - } - } - - fulfilledContentDependencies[name] = - unfulfilledContentFunction.fulfill(fulfillments); - } - - // There might still be unfulfilled content functions if dependencies weren't - // provided as part of allContentDependencies or allExtraDependencies. - // Catch and report these early, together in an aggregate error. - const unfulfilledErrors = []; - const unfulfilledNames = []; - for (const name of neededContentDependencyNames) { - const contentFunction = fulfilledContentDependencies[name]; - if (!contentFunction) continue; - if (!contentFunction.fulfilled) { - try { - contentFunction(); - } catch (error) { - error.message = `(${name}) ${error.message}`; - unfulfilledErrors.push(error); - unfulfilledNames.push(name); - } - } - } - - if (!empty(unfulfilledErrors)) { - throw new AggregateError(unfulfilledErrors, `Content functions unfulfilled (${unfulfilledNames.join(', ')})`); + allContentDependencies = {...allContentDependencies}; + for (const [name, contentFunction] of Object.entries(allContentDependencies)) { + allContentDependencies[name] = + contentFunction.bindExtraDependencies(allExtraDependencies); } const slotResults = {}; @@ -646,10 +358,9 @@ export function quickEvaluate({ const callDecorated = (fn, ...args) => decorateErrorWithRelationStack(fn, traceStack)(...args); - const contentFunction = fulfilledContentDependencies[name]; - + const contentFunction = allContentDependencies[name]; if (!contentFunction) { - throw new Error(`Content function ${name} unfulfilled or not listed`); + throw new Error(`Content function ${name} not listed`); } const generateArgs = []; diff --git a/src/content/dependencies/generateAbsoluteDatetimestamp.js b/src/content/dependencies/generateAbsoluteDatetimestamp.js index 930b6f13..2250ded3 100644 --- a/src/content/dependencies/generateAbsoluteDatetimestamp.js +++ b/src/content/dependencies/generateAbsoluteDatetimestamp.js @@ -1,11 +1,4 @@ export default { - contentDependencies: [ - 'generateDatetimestampTemplate', - 'generateTooltip', - ], - - extraDependencies: ['html', 'language'], - data: (date) => ({date}), diff --git a/src/content/dependencies/generateAdditionalFilesList.js b/src/content/dependencies/generateAdditionalFilesList.js index 68120b23..699c5f86 100644 --- a/src/content/dependencies/generateAdditionalFilesList.js +++ b/src/content/dependencies/generateAdditionalFilesList.js @@ -1,26 +1,19 @@ -import {stitchArrays} from '#sugar'; - export default { - extraDependencies: ['html'], + relations: (relation, additionalFiles) => ({ + chunks: + additionalFiles + .map(file => relation('generateAdditionalFilesListChunk', file)), + }), slots: { - chunks: { - validate: v => v.strictArrayOf(v.isHTML), - }, - - chunkItems: { - validate: v => v.strictArrayOf(v.isHTML), - }, + showFileSizes: {type: 'boolean', default: true}, }, - generate: (slots, {html}) => + generate: (relations, slots, {html}) => html.tag('ul', {class: 'additional-files-list'}, {[html.onlyIfContent]: true}, - stitchArrays({ - chunk: slots.chunks, - items: slots.chunkItems, - }).map(({chunk, items}) => - chunk.clone() - .slot('items', items))), + relations.chunks.map(chunk => chunk.slots({ + showFileSizes: slots.showFileSizes, + }))), }; diff --git a/src/content/dependencies/generateAdditionalFilesListChunk.js b/src/content/dependencies/generateAdditionalFilesListChunk.js index 507b2329..466a5d8d 100644 --- a/src/content/dependencies/generateAdditionalFilesListChunk.js +++ b/src/content/dependencies/generateAdditionalFilesListChunk.js @@ -1,46 +1,78 @@ +import {stitchArrays} from '#sugar'; + export default { - extraDependencies: ['html', 'language'], + relations: (relation, file) => ({ + description: + relation('transformContent', file.description), - slots: { - title: { - type: 'html', - mutable: false, - }, + links: + file.filenames + .map(filename => relation('linkAdditionalFile', file, filename)), + }), - description: { - type: 'html', - mutable: false, - }, + data: (file) => ({ + title: + file.title, + + paths: + file.paths, + }), - items: { - validate: v => v.looseArrayOf(v.isHTML), + slots: { + showFileSizes: { + type: 'boolean', }, }, - generate: (slots, {html, language}) => - language.encapsulate('releaseInfo.additionalFiles.entry', capsule => + generate: (data, relations, slots, {getSizeOfMediaFile, html, language, urls}) => + language.encapsulate('releaseInfo.additionalFiles', capsule => html.tag('li', html.tag('details', - html.isBlank(slots.items) && + html.isBlank(relations.links) && {open: true}, [ html.tag('summary', html.tag('span', - language.$(capsule, { + language.$(capsule, 'entry', { title: - html.tag('b', slots.title), + html.tag('b', data.title), }))), html.tag('ul', [ html.tag('li', {class: 'entry-description'}, {[html.onlyIfContent]: true}, - slots.description), - (html.isBlank(slots.items) + relations.description.slot('mode', 'inline')), + + (html.isBlank(relations.links) ? html.tag('li', - language.$(capsule, 'noFilesAvailable')) - : slots.items), + language.$(capsule, 'entry.noFilesAvailable')) + + : stitchArrays({ + link: relations.links, + path: data.paths, + }).map(({link, path}) => + html.tag('li', + language.encapsulate(capsule, 'file', workingCapsule => { + const workingOptions = {file: link}; + + if (slots.showFileSizes) { + const fileSize = + getSizeOfMediaFile( + urls + .from('media.root') + .to(...path)); + + if (fileSize) { + workingCapsule += '.withSize'; + workingOptions.size = + language.formatFileSize(fileSize); + } + } + + return language.$(workingCapsule, workingOptions); + })))), ]), ]))), }; diff --git a/src/content/dependencies/generateAdditionalFilesListChunkItem.js b/src/content/dependencies/generateAdditionalFilesListChunkItem.js deleted file mode 100644 index c37d6bb2..00000000 --- a/src/content/dependencies/generateAdditionalFilesListChunkItem.js +++ /dev/null @@ -1,30 +0,0 @@ -export default { - extraDependencies: ['html', 'language'], - - slots: { - fileLink: { - type: 'html', - mutable: false, - }, - - fileSize: { - validate: v => v.isWholeNumber, - }, - }, - - generate(slots, {html, language}) { - const itemParts = ['releaseInfo.additionalFiles.file']; - const itemOptions = {file: slots.fileLink}; - - if (slots.fileSize) { - itemParts.push('withSize'); - itemOptions.size = language.formatFileSize(slots.fileSize); - } - - const li = - html.tag('li', - language.$(...itemParts, itemOptions)); - - return li; - }, -}; diff --git a/src/content/dependencies/generateAdditionalNamesBox.js b/src/content/dependencies/generateAdditionalNamesBox.js index b7392dfd..6bd1ab42 100644 --- a/src/content/dependencies/generateAdditionalNamesBox.js +++ b/src/content/dependencies/generateAdditionalNamesBox.js @@ -1,18 +1,25 @@ export default { - contentDependencies: ['generateAdditionalNamesBoxItem'], - extraDependencies: ['html', 'language'], - relations: (relation, additionalNames) => ({ items: additionalNames .map(entry => relation('generateAdditionalNamesBoxItem', entry)), }), - generate: (relations, {html, language}) => + slots: { + alwaysVisible: { + type: 'boolean', + default: false, + }, + }, + + generate: (relations, slots, {html, language}) => html.tag('div', {id: 'additional-names-box'}, {class: 'drop'}, {[html.onlyIfContent]: true}, + slots.alwaysVisible && + {class: 'always-visible'}, + [ html.tag('p', {[html.onlyIfSiblings]: true}, diff --git a/src/content/dependencies/generateAdditionalNamesBoxItem.js b/src/content/dependencies/generateAdditionalNamesBoxItem.js index e3e59a34..a39711c1 100644 --- a/src/content/dependencies/generateAdditionalNamesBoxItem.js +++ b/src/content/dependencies/generateAdditionalNamesBoxItem.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['transformContent'], - extraDependencies: ['html', 'language'], - relations: (relation, entry) => ({ nameContent: relation('transformContent', entry.name), diff --git a/src/content/dependencies/generateAlbumAdditionalFilesList.js b/src/content/dependencies/generateAlbumAdditionalFilesList.js deleted file mode 100644 index ad17206f..00000000 --- a/src/content/dependencies/generateAlbumAdditionalFilesList.js +++ /dev/null @@ -1,96 +0,0 @@ -import {stitchArrays} from '#sugar'; - -export default { - contentDependencies: [ - 'generateAdditionalFilesList', - 'generateAdditionalFilesListChunk', - 'generateAdditionalFilesListChunkItem', - 'linkAlbumAdditionalFile', - 'transformContent', - ], - - extraDependencies: ['getSizeOfMediaFile', 'html', 'urls'], - - relations: (relation, album, additionalFiles) => ({ - list: - relation('generateAdditionalFilesList', additionalFiles), - - chunks: - additionalFiles - .map(() => relation('generateAdditionalFilesListChunk')), - - chunkDescriptions: - additionalFiles - .map(({description}) => - (description - ? relation('transformContent', description) - : null)), - - chunkItems: - additionalFiles - .map(({files}) => - (files ?? []) - .map(() => relation('generateAdditionalFilesListChunkItem'))), - - chunkItemFileLinks: - additionalFiles - .map(({files}) => - (files ?? []) - .map(file => relation('linkAlbumAdditionalFile', album, file))), - }), - - data: (album, additionalFiles) => ({ - albumDirectory: album.directory, - - chunkTitles: - additionalFiles - .map(({title}) => title), - - chunkItemLocations: - additionalFiles - .map(({files}) => files ?? []), - }), - - slots: { - showFileSizes: {type: 'boolean', default: true}, - }, - - generate: (data, relations, slots, {getSizeOfMediaFile, urls}) => - relations.list.slots({ - chunks: - stitchArrays({ - chunk: relations.chunks, - description: relations.chunkDescriptions, - title: data.chunkTitles, - }).map(({chunk, title, description}) => - chunk.slots({ - title, - description: - (description - ? description.slot('mode', 'inline') - : null), - })), - - chunkItems: - stitchArrays({ - items: relations.chunkItems, - fileLinks: relations.chunkItemFileLinks, - locations: data.chunkItemLocations, - }).map(({items, fileLinks, locations}) => - stitchArrays({ - item: items, - fileLink: fileLinks, - location: locations, - }).map(({item, fileLink, location}) => - item.slots({ - fileLink: fileLink, - fileSize: - (slots.showFileSizes - ? getSizeOfMediaFile( - urls - .from('media.root') - .to('media.albumAdditionalFile', data.albumDirectory, location)) - : 0), - }))), - }), -}; diff --git a/src/content/dependencies/generateAlbumArtInfoBox.js b/src/content/dependencies/generateAlbumArtInfoBox.js new file mode 100644 index 00000000..5491192a --- /dev/null +++ b/src/content/dependencies/generateAlbumArtInfoBox.js @@ -0,0 +1,36 @@ +export default { + relations: (relation, album) => ({ + wallpaperArtistContributionsLine: + (album.wallpaperArtwork + ? relation('generateReleaseInfoContributionsLine', + album.wallpaperArtwork.artistContribs) + : null), + + bannerArtistContributionsLine: + (album.bannerArtwork + ? relation('generateReleaseInfoContributionsLine', + album.bannerArtwork.artistContribs) + : null), + }), + + generate: (relations, {html, language}) => + language.encapsulate('releaseInfo', capsule => + html.tag('div', {class: 'album-art-info'}, + {[html.onlyIfContent]: true}, + + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + [ + relations.wallpaperArtistContributionsLine?.slots({ + stringKey: capsule + '.wallpaperArtBy', + chronologyKind: 'wallpaperArt', + }), + + relations.bannerArtistContributionsLine?.slots({ + stringKey: capsule + '.bannerArtBy', + chronologyKind: 'bannerArt', + }), + ]))), +}; diff --git a/src/content/dependencies/generateAlbumArtworkColumn.js b/src/content/dependencies/generateAlbumArtworkColumn.js new file mode 100644 index 00000000..5346e56b --- /dev/null +++ b/src/content/dependencies/generateAlbumArtworkColumn.js @@ -0,0 +1,51 @@ +export default { + query: (album) => ({ + nonAttachingArtworkIndex: + (album.hasCoverArt + ? album.coverArtworks.findIndex((artwork, index) => + index > 1 && + !artwork.attachAbove) + : null), + }), + + relations: (relation, query, album) => ({ + firstCovers: + (album.hasCoverArt && query.nonAttachingArtworkIndex >= 1 + ? album.coverArtworks + .slice(0, query.nonAttachingArtworkIndex) + .map(artwork => relation('generateCoverArtwork', artwork)) + + : album.hasCoverArt + ? album.coverArtworks + .map(artwork => relation('generateCoverArtwork', artwork)) + + : []), + + albumArtInfoBox: + relation('generateAlbumArtInfoBox', album), + + restCovers: + (album.hasCoverArt && query.nonAttachingArtworkIndex >= 1 + ? album.coverArtworks + .slice(query.nonAttachingArtworkIndex) + .map(artwork => relation('generateCoverArtwork', artwork)) + + : []), + }), + + generate(relations, {html}) { + for (const cover of [...relations.firstCovers, ...relations.restCovers]) { + cover.setSlots({ + showOriginDetails: true, + showArtTagDetails: true, + showReferenceDetails: true, + }); + } + + return html.tags([ + relations.firstCovers, + relations.albumArtInfoBox, + relations.restCovers, + ]); + }, +}; diff --git a/src/content/dependencies/generateAlbumBanner.js b/src/content/dependencies/generateAlbumBanner.js index 3cc141bc..dce258de 100644 --- a/src/content/dependencies/generateAlbumBanner.js +++ b/src/content/dependencies/generateAlbumBanner.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateBanner'], - extraDependencies: ['html', 'language'], - relations(relation, album) { if (!album.hasBannerArt) { return {}; diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index f5df7c3d..4c203877 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -1,23 +1,6 @@ import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateAlbumCommentarySidebar', - 'generateAlbumCoverArtwork', - 'generateAlbumNavAccent', - 'generateAlbumSecondaryNav', - 'generateAlbumStyleRules', - 'generateCommentaryEntry', - 'generateContentHeading', - 'generateTrackCoverArtwork', - 'generatePageLayout', - 'linkAlbum', - 'linkExternal', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - query(album) { const query = {}; @@ -45,8 +28,8 @@ export default { relations.sidebar = relation('generateAlbumCommentarySidebar', album); - relations.albumStyleRules = - relation('generateAlbumStyleRules', album, null); + relations.albumStyleTags = + relation('generateAlbumStyleTags', album, null); relations.albumLink = relation('linkAlbum', album); @@ -66,7 +49,7 @@ export default { if (album.hasCoverArt) { relations.albumCommentaryCover = - relation('generateAlbumCoverArtwork', album); + relation('generateCoverArtwork', album.coverArtworks[0]); } relations.albumCommentaryEntries = @@ -91,7 +74,7 @@ export default { query.tracksWithCommentary .map(track => (track.hasUniqueCoverArt - ? relation('generateTrackCoverArtwork', track) + ? relation('generateCoverArtwork', track.trackArtworks[0]) : null)); relations.trackCommentaryEntries = @@ -152,7 +135,7 @@ export default { headingMode: 'sticky', color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, mainClasses: ['long-content'], mainContent: [ @@ -267,7 +250,10 @@ export default { }), })), - cover?.slots({mode: 'commentary'}), + cover?.slots({ + mode: 'commentary', + color: true, + }), trackDate && trackDate !== data.date && diff --git a/src/content/dependencies/generateAlbumCommentarySidebar.js b/src/content/dependencies/generateAlbumCommentarySidebar.js index 9ecec66d..4863f059 100644 --- a/src/content/dependencies/generateAlbumCommentarySidebar.js +++ b/src/content/dependencies/generateAlbumCommentarySidebar.js @@ -1,15 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateAlbumSidebarTrackSection', - 'generatePageSidebar', - 'generatePageSidebarBox', - 'linkAlbum', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, album) => ({ sidebar: relation('generatePageSidebar'), diff --git a/src/content/dependencies/generateAlbumCoverArtwork.js b/src/content/dependencies/generateAlbumCoverArtwork.js deleted file mode 100644 index ff7d2b85..00000000 --- a/src/content/dependencies/generateAlbumCoverArtwork.js +++ /dev/null @@ -1,100 +0,0 @@ -export default { - contentDependencies: [ - 'generateCoverArtwork', - 'generateCoverArtworkArtTagDetails', - 'generateCoverArtworkArtistDetails', - 'generateCoverArtworkReferenceDetails', - 'image', - 'linkAlbumReferencedArtworks', - 'linkAlbumReferencingArtworks', - ], - - extraDependencies: ['html', 'language'], - - relations: (relation, album) => ({ - coverArtwork: - relation('generateCoverArtwork'), - - image: - relation('image'), - - artTagDetails: - relation('generateCoverArtworkArtTagDetails', album.artTags), - - artistDetails: - relation('generateCoverArtworkArtistDetails', album.coverArtistContribs), - - referenceDetails: - relation('generateCoverArtworkReferenceDetails', - album.referencedArtworks, - album.referencedByArtworks), - - referencedArtworksLink: - relation('linkAlbumReferencedArtworks', album), - - referencingArtworksLink: - relation('linkAlbumReferencingArtworks', album), - }), - - data: (album) => ({ - path: - ['media.albumCover', album.directory, album.coverArtFileExtension], - - color: - album.color, - - dimensions: - album.coverArtDimensions, - - warnings: - album.artTags - .filter(tag => tag.isContentWarning) - .map(tag => tag.name), - }), - - slots: { - mode: {type: 'string'}, - - details: { - validate: v => v.is('tags', 'artists'), - default: 'tags', - }, - - showReferenceLinks: { - type: 'boolean', - default: false, - }, - }, - - generate: (data, relations, slots, {language}) => - relations.coverArtwork.slots({ - mode: slots.mode, - - image: - relations.image.slots({ - path: data.path, - color: data.color, - alt: language.$('misc.alt.albumCover'), - }), - - dimensions: data.dimensions, - warnings: data.warnings, - - details: [ - slots.details === 'tags' && - relations.artTagDetails, - - slots.details === 'artists' && - relations.artistDetails, - - slots.showReferenceLinks && - relations.referenceDetails.slots({ - referencedLink: - relations.referencedArtworksLink, - - referencingLink: - relations.referencingArtworksLink, - }), - ], - }), -}; diff --git a/src/content/dependencies/generateAlbumGalleryAlbumGrid.js b/src/content/dependencies/generateAlbumGalleryAlbumGrid.js new file mode 100644 index 00000000..f9cd027e --- /dev/null +++ b/src/content/dependencies/generateAlbumGalleryAlbumGrid.js @@ -0,0 +1,82 @@ +import {stitchArrays} from '#sugar'; + +export default { + query: (album) => ({ + artworks: + (album.hasCoverArt + ? album.coverArtworks + : []), + }), + + relations: (relation, query, album) => ({ + coverGrid: + relation('generateCoverGrid'), + + albumLinks: + query.artworks.map(_artwork => + relation('linkAlbum', album)), + + images: + query.artworks + .map(artwork => relation('image', artwork)), + }), + + data: (query, album) => ({ + albumName: + album.name, + + artworkLabels: + query.artworks + .map(artwork => artwork.label), + + artworkArtists: + query.artworks + .map(artwork => artwork.artistContribs + .map(contrib => contrib.artist.name)), + }), + + slots: { + attributes: {type: 'attributes', mutable: false}, + }, + + generate: (data, relations, slots, {html, language}) => + html.tag('div', + {[html.onlyIfContent]: true}, + + slots.attributes, + + [ + relations.coverArtistsLine, + + relations.coverGrid.slots({ + links: + relations.albumLinks, + + names: + data.artworkLabels + .map(label => label ?? data.albumName), + + images: + stitchArrays({ + image: relations.images, + label: data.artworkLabels, + }).map(({image, label}) => + image.slots({ + missingSourceContent: + language.$('misc.albumGalleryGrid.noCoverArt', { + name: + label ?? data.albumName, + }), + })), + + info: + data.artworkArtists.map(artists => + language.$('misc.coverGrid.details.coverArtists', { + [language.onlyIfOptions]: ['artists'], + + artists: + language.formatUnitList(artists), + })), + }), + ]), +}; diff --git a/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js b/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js index 7dcdf6de..0322e227 100644 --- a/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js +++ b/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkArtistGallery'], - extraDependencies: ['html', 'language'], - relations(relation, coverArtists) { return { coverArtistLinks: diff --git a/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js index ad99cb87..5932514e 100644 --- a/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js +++ b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'language'], - generate: ({html, language}) => html.tag('p', {class: 'quick-info'}, language.$('albumGalleryPage.noTrackArtworksLine')), diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js index b48d92af..85b0fb74 100644 --- a/src/content/dependencies/generateAlbumGalleryPage.js +++ b/src/content/dependencies/generateAlbumGalleryPage.js @@ -1,166 +1,86 @@ -import {compareArrays, stitchArrays} from '#sugar'; +import {stitchArrays, unique} from '#sugar'; +import {getKebabCase} from '#wiki-data'; export default { - contentDependencies: [ - 'generateAlbumGalleryCoverArtistsLine', - 'generateAlbumGalleryNoTrackArtworksLine', - 'generateAlbumGalleryStatsLine', - 'generateAlbumNavAccent', - 'generateAlbumSecondaryNav', - 'generateAlbumStyleRules', - 'generateCoverGrid', - 'generatePageLayout', - 'image', - 'linkAlbum', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - query(album) { const query = {}; - const tracksWithUniqueCoverArt = + const trackArtworkLabels = album.tracks - .filter(track => track.hasUniqueCoverArt); - - // Don't display "all artwork by..." for albums where there's - // only one unique artwork in the first place. - if (tracksWithUniqueCoverArt.length > 1) { - const allCoverArtistArrays = - tracksWithUniqueCoverArt - .map(track => track.coverArtistContribs) - .map(contribs => contribs.map(contrib => contrib.artist)); - - const allSameCoverArtists = - allCoverArtistArrays - .slice(1) - .every(artists => compareArrays(artists, allCoverArtistArrays[0])); - - if (allSameCoverArtists) { - query.coverArtistsForAllTracks = - allCoverArtistArrays[0]; - } - } + .map(track => track.trackArtworks + .map(artwork => artwork.label)); + + const recurranceThreshold = 2; + + // This list may include null, if some artworks are not labelled! + // That's expected. + query.recurringTrackArtworkLabels = + unique(trackArtworkLabels.flat()) + .filter(label => + trackArtworkLabels + .filter(labels => labels.includes(label)) + .length >= + (label === null + ? 1 + : recurranceThreshold)); return query; }, - relations(relation, query, album) { - const relations = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.albumStyleRules = - relation('generateAlbumStyleRules', album, null); - - relations.albumLink = - relation('linkAlbum', album); - - relations.albumNavAccent = - relation('generateAlbumNavAccent', album, null); - - relations.secondaryNav = - relation('generateAlbumSecondaryNav', album); + relations: (relation, query, album) => ({ + layout: + relation('generatePageLayout'), - relations.statsLine = - relation('generateAlbumGalleryStatsLine', album); + albumStyleTags: + relation('generateAlbumStyleTags', album, null), - if (album.tracks.every(track => !track.hasUniqueCoverArt)) { - relations.noTrackArtworksLine = - relation('generateAlbumGalleryNoTrackArtworksLine'); - } - - if (query.coverArtistsForAllTracks) { - relations.coverArtistsLine = - relation('generateAlbumGalleryCoverArtistsLine', query.coverArtistsForAllTracks); - } - - relations.coverGrid = - relation('generateCoverGrid'); - - relations.links = [ + albumLink: relation('linkAlbum', album), - ... - album.tracks - .map(track => relation('linkTrack', track)), - ]; - - relations.images = [ - (album.hasCoverArt - ? relation('image', album.artTags) - : relation('image')), - - ... - album.tracks.map(track => - (track.hasUniqueCoverArt - ? relation('image', track.artTags) - : relation('image'))), - ]; + albumNavAccent: + relation('generateAlbumNavAccent', album, null), - return relations; - }, - - data(query, album) { - const data = {}; - - data.name = album.name; - data.color = album.color; + secondaryNav: + relation('generateAlbumSecondaryNav', album), - data.names = [ - album.name, - ...album.tracks.map(track => track.name), - ]; + statsLine: + relation('generateAlbumGalleryStatsLine', album), - data.coverArtists = [ - (album.hasCoverArt - ? album.coverArtistContribs.map(({artist}) => artist.name) + noTrackArtworksLine: + (album.tracks.every(track => !track.hasUniqueCoverArt) + ? relation('generateAlbumGalleryNoTrackArtworksLine') : null), - ... - album.tracks.map(track => { - if (query.coverArtistsForAllTracks) { - return null; - } + setSwitcher: + relation('generateIntrapageDotSwitcher'), - if (track.hasUniqueCoverArt) { - return track.coverArtistContribs.map(({artist}) => artist.name); - } + albumGrid: + relation('generateAlbumGalleryAlbumGrid', album), - return null; - }), - ]; + trackGrids: + query.recurringTrackArtworkLabels.map(label => + relation('generateAlbumGalleryTrackGrid', album, label)), + }), - data.paths = [ - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null), + data: (query, album) => ({ + trackGridLabels: + query.recurringTrackArtworkLabels, - ... - album.tracks.map(track => - (track.hasUniqueCoverArt - ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] - : null)), - ]; + trackGridIDs: + query.recurringTrackArtworkLabels.map(label => + 'track-grid-' + + (label + ? getKebabCase(label) + : 'no-label')), - data.dimensions = [ - (album.hasCoverArt - ? album.coverArtDimensions - : null), - - ... - album.tracks.map(track => - (track.hasUniqueCoverArt - ? track.coverArtDimensions - : null)), - ]; + name: + album.name, - return data; - }, + color: + album.color, + }), - generate: (data, relations, {language}) => + generate: (data, relations, {html, language}) => language.encapsulate('albumGalleryPage', pageCapsule => relations.layout.slots({ title: @@ -171,39 +91,44 @@ export default { headingMode: 'static', color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, mainClasses: ['top-index'], mainContent: [ relations.statsLine, - relations.coverArtistsLine, + + relations.albumGrid, + relations.noTrackArtworksLine, - relations.coverGrid - .slots({ - links: relations.links, - names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - name: data.names, - }).map(({image, path, dimensions, name}) => - image.slots({ - path, - dimensions, - missingSourceContent: - language.$('misc.albumGalleryGrid.noCoverArt', {name}), - })), - info: - data.coverArtists.map(names => - (names === null - ? null - : language.$('misc.coverGrid.details.coverArtists', { - artists: language.formatUnitList(names), - }))), - }), + data.trackGridLabels.some(value => value !== null) && + html.tag('p', {class: 'gallery-set-switcher'}, + language.encapsulate(pageCapsule, 'setSwitcher', switcherCapsule => + language.$(switcherCapsule, { + sets: + relations.setSwitcher.slots({ + initialOptionIndex: 0, + + titles: + data.trackGridLabels.map(label => + label ?? + language.$(switcherCapsule, 'unlabeledSet')), + + targetIDs: + data.trackGridIDs, + }), + }))), + + stitchArrays({ + grid: relations.trackGrids, + id: data.trackGridIDs, + }).map(({grid, id}, index) => + grid.slots({ + attributes: [ + {id}, + index >= 1 && {style: 'display: none'}, + ], + })), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateAlbumGalleryStatsLine.js b/src/content/dependencies/generateAlbumGalleryStatsLine.js index 75bffb36..75341937 100644 --- a/src/content/dependencies/generateAlbumGalleryStatsLine.js +++ b/src/content/dependencies/generateAlbumGalleryStatsLine.js @@ -1,38 +1,56 @@ import {getTotalDuration} from '#wiki-data'; export default { - extraDependencies: ['html', 'language'], - - data(album) { - return { - name: album.name, - date: album.date, - duration: getTotalDuration(album.tracks), - numTracks: album.tracks.length, - }; - }, - - generate(data, {html, language}) { - const parts = ['albumGalleryPage.statsLine']; - const options = {}; - - options.tracks = - html.tag('b', - language.countTracks(data.numTracks, {unit: true})); - - options.duration = - html.tag('b', - language.formatDuration(data.duration, {unit: true})); - - if (data.date) { - parts.push('withDate'); - options.date = - html.tag('b', - language.formatDate(data.date)); - } - - return ( - html.tag('p', {class: 'quick-info'}, - language.formatString(...parts, options))); - }, + data: (album) => ({ + date: + album.date, + + hideDuration: + album.hideDuration, + + duration: + (album.hideDuration + ? null + : getTotalDuration(album.tracks)), + + tracks: + (album.hideDuration + ? null + : album.tracks.length), + }), + + generate: (data, {html, language}) => + html.tag('p', {class: 'quick-info'}, + {[html.onlyIfContent]: true}, + + language.encapsulate('albumGalleryPage.statsLine', workingCapsule => { + const workingOptions = {}; + + if (data.hideDuration && !data.date) { + return html.blank(); + } + + if (!data.hideDuration) { + workingOptions.tracks = + html.tag('b', + language.countTracks(data.tracks, {unit: true})); + + workingOptions.duration = + html.tag('b', + language.formatDuration(data.duration, {unit: true})); + } + + if (data.date) { + workingCapsule += '.withDate'; + workingOptions.date = + html.tag('b', + language.formatDate(data.date)); + } + + if (data.hideDuration) { + workingCapsule += '.noDuration'; + } + + return language.$(workingCapsule, workingOptions); + })), }; diff --git a/src/content/dependencies/generateAlbumGalleryTrackGrid.js b/src/content/dependencies/generateAlbumGalleryTrackGrid.js new file mode 100644 index 00000000..a50448c6 --- /dev/null +++ b/src/content/dependencies/generateAlbumGalleryTrackGrid.js @@ -0,0 +1,118 @@ +import {compareArrays, stitchArrays} from '#sugar'; + +export default { + query(album, label) { + const query = {}; + + query.artworks = + album.tracks.map(track => + track.trackArtworks.find(artwork => artwork.label === label) ?? + null); + + const presentArtworks = + query.artworks.filter(Boolean); + + if (presentArtworks.length > 1) { + const allArtistArrays = + presentArtworks + .map(artwork => artwork.artistContribs + .map(contrib => contrib.artist)); + + const allSameArtists = + allArtistArrays + .slice(1) + .every(artists => compareArrays(artists, allArtistArrays[0])); + + if (allSameArtists) { + query.artistsForAllTrackArtworks = + allArtistArrays[0]; + } + } + + return query; + }, + + relations: (relation, query, album, _label) => ({ + coverArtistsLine: + (query.artistsForAllTrackArtworks + ? relation('generateAlbumGalleryCoverArtistsLine', + query.artistsForAllTrackArtworks) + : null), + + coverGrid: + relation('generateCoverGrid'), + + albumLink: + relation('linkAlbum', album), + + trackLinks: + album.tracks + .map(track => relation('linkTrack', track)), + + images: + query.artworks + .map(artwork => relation('image', artwork)), + }), + + data: (query, album, _label) => ({ + trackNames: + album.tracks + .map(track => track.name), + + artworkArtists: + query.artworks.map(artwork => + (query.artistsForAllTrackArtworks + ? null + : artwork + ? artwork.artistContribs + .map(contrib => contrib.artist.name) + : null)), + + allWarnings: + query.artworks.flatMap(artwork => artwork?.contentWarnings), + }), + + slots: { + attributes: {type: 'attributes', mutable: false}, + }, + + generate: (data, relations, slots, {html, language}) => + html.tag('div', + {[html.onlyIfContent]: true}, + + slots.attributes, + + [ + relations.coverArtistsLine, + + relations.coverGrid.slots({ + links: + relations.trackLinks, + + names: + data.trackNames, + + images: + stitchArrays({ + image: relations.images, + name: data.trackNames, + }).map(({image, name}) => + image.slots({ + missingSourceContent: + language.$('misc.albumGalleryGrid.noCoverArt', {name}), + })), + + info: + data.artworkArtists.map(artists => + language.$('misc.coverGrid.details.coverArtists', { + [language.onlyIfOptions]: ['artists'], + + artists: + language.formatUnitList(artists), + })), + + revealAllWarnings: + data.allWarnings, + }), + ]), +}; diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index aae56637..a27074ff 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -1,33 +1,12 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateAdditionalNamesBox', - 'generateAlbumAdditionalFilesList', - 'generateAlbumBanner', - 'generateAlbumCoverArtwork', - 'generateAlbumNavAccent', - 'generateAlbumReleaseInfo', - 'generateAlbumSecondaryNav', - 'generateAlbumSidebar', - 'generateAlbumSocialEmbed', - 'generateAlbumStyleRules', - 'generateAlbumTrackList', - 'generateCommentaryEntry', - 'generateContentHeading', - 'generatePageLayout', - 'linkAlbumCommentary', - 'linkAlbumGallery', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, album) => ({ layout: relation('generatePageLayout'), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), socialEmbed: relation('generateAlbumSocialEmbed', album), @@ -44,10 +23,8 @@ export default { additionalNamesBox: relation('generateAdditionalNamesBox', album.additionalNames), - cover: - (album.hasCoverArt - ? relation('generateAlbumCoverArtwork', album) - : null), + artworkColumn: + relation('generateAlbumArtworkColumn', album), banner: (album.hasBannerArt @@ -66,25 +43,30 @@ export default { : null), commentaryLink: - ([album, ...album.tracks].some(({commentary}) => !empty(commentary)) + (album.tracks.some(track => !empty(track.commentary)) ? relation('linkAlbumCommentary', album) : null), + readCommentaryLine: + relation('generateReadCommentaryLine', album), + trackList: relation('generateAlbumTrackList', album), additionalFilesList: - relation('generateAlbumAdditionalFilesList', - album, - album.additionalFiles), + relation('generateAdditionalFilesList', album.additionalFiles), + + commentaryContentHeading: + relation('generateCommentaryContentHeading', album), artistCommentaryEntries: album.commentary .map(entry => relation('generateCommentaryEntry', entry)), - creditSourceEntries: - album.creditSources - .map(entry => relation('generateCommentaryEntry', entry)), + creditingSourcesSection: + relation('generateCollapsedContentEntrySection', + album.creditingSources, + album), }), data: (album) => ({ @@ -108,16 +90,12 @@ export default { color: data.color, headingMode: 'sticky', - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, additionalNames: relations.additionalNamesBox, - cover: - (relations.cover - ? relations.cover.slots({ - showReferenceLinks: true, - }) - : null), + artworkColumnContent: + relations.artworkColumn, mainContent: [ relations.releaseInfo, @@ -164,12 +142,16 @@ export default { : html.blank()), - !html.isBlank(relations.creditSourceEntries) && - language.encapsulate(capsule, 'readCreditSources', capsule => + !relations.commentaryLink && + !html.isBlank(relations.artistCommentaryEntries) && + relations.readCommentaryLine, + + !html.isBlank(relations.creditingSourcesSection) && + language.encapsulate(capsule, 'readCreditingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#credit-sources'}, + {href: '#crediting-sources'}, language.$(capsule, 'link')), })), ])), @@ -178,14 +160,14 @@ export default { html.tag('p', {[html.onlyIfContent]: true}, - {[html.joinChildren]: html.tag('br')}, - language.encapsulate('releaseInfo', capsule => [ - language.$(capsule, 'addedToWiki', { - [language.onlyIfOptions]: ['date'], - date: language.formatDate(data.dateAddedToWiki), - }), - ])), + language.$('releaseInfo.addedToWiki', { + [language.onlyIfOptions]: ['date'], + date: language.formatDate(data.dateAddedToWiki), + })), + + !html.isBlank(relations.artistCommentaryEntries) && + html.tag('hr', {class: 'main-separator'}), language.encapsulate('releaseInfo.additionalFiles', capsule => html.tags([ @@ -199,24 +181,14 @@ export default { ])), html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'artist-commentary'}, - title: language.$('misc.artistCommentary'), - }), - + relations.commentaryContentHeading, relations.artistCommentaryEntries, ]), - html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'credit-sources'}, - title: language.$('misc.creditSources'), - }), - - relations.creditSourceEntries, - ]), + relations.creditingSourcesSection.slots({ + id: 'crediting-sources', + string: 'misc.creditingSources', + }), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js index 432c5f3d..237120f3 100644 --- a/src/content/dependencies/generateAlbumNavAccent.js +++ b/src/content/dependencies/generateAlbumNavAccent.js @@ -1,17 +1,6 @@ import {atOffset, empty} from '#sugar'; export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'generateNextLink', - 'generatePreviousLink', - 'linkTrack', - 'linkAlbumCommentary', - 'linkAlbumGallery', - ], - - extraDependencies: ['html', 'language'], - query(album, track) { const query = {}; @@ -64,9 +53,8 @@ export default { hasMultipleTracks: album.tracks.length > 1, - commentaryPageIsStub: - [album, ...album.tracks] - .every(({commentary}) => empty(commentary)), + hasSubstantialCommentaryPage: + album.tracks.some(track => !empty(track.commentary)), galleryIsStub: album.tracks.every(t => !t.hasUniqueCoverArt), @@ -97,14 +85,16 @@ export default { relations.nextLink.slot('link', relations.nextTrackLink); const galleryLink = - (!data.galleryIsStub || slots.currentExtra === 'gallery') && + (!data.galleryIsStub || + slots.currentExtra === 'gallery') && relations.albumGalleryLink.slots({ attributes: {class: slots.currentExtra === 'gallery' && 'current'}, content: language.$(albumNavCapsule, 'gallery'), }); const commentaryLink = - (!data.commentaryPageIsStub || slots.currentExtra === 'commentary') && + (data.hasSubstantialCommentaryPage || + slots.currentExtra === 'commentary') && relations.albumCommentaryLink.slots({ attributes: {class: slots.currentExtra === 'commentary' && 'current'}, content: language.$(albumNavCapsule, 'commentary'), diff --git a/src/content/dependencies/generateAlbumReferencedArtworksPage.js b/src/content/dependencies/generateAlbumReferencedArtworksPage.js index 3f3d77b3..e4022f0d 100644 --- a/src/content/dependencies/generateAlbumReferencedArtworksPage.js +++ b/src/content/dependencies/generateAlbumReferencedArtworksPage.js @@ -1,37 +1,21 @@ export default { - contentDependencies: [ - 'generateAlbumCoverArtwork', - 'generateAlbumStyleRules', - 'generateBackToAlbumLink', - 'generateReferencedArtworksPage', - 'linkAlbum', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, album) => ({ page: - relation('generateReferencedArtworksPage', album.referencedArtworks), + relation('generateReferencedArtworksPage', album.coverArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), albumLink: relation('linkAlbum', album), backToAlbumLink: relation('generateBackToAlbumLink', album), - - cover: - relation('generateAlbumCoverArtwork', album), }), data: (album) => ({ name: album.name, - - color: - album.color, }), generate: (data, relations, {html, language}) => @@ -42,10 +26,7 @@ export default { data.name, }), - color: data.color, - styleRules: [relations.albumStyleRules], - - cover: relations.cover, + styleTags: relations.albumStyleTags, navLinks: [ {auto: 'home'}, diff --git a/src/content/dependencies/generateAlbumReferencingArtworksPage.js b/src/content/dependencies/generateAlbumReferencingArtworksPage.js index 8f2349f9..0dc1bf15 100644 --- a/src/content/dependencies/generateAlbumReferencingArtworksPage.js +++ b/src/content/dependencies/generateAlbumReferencingArtworksPage.js @@ -1,37 +1,21 @@ export default { - contentDependencies: [ - 'generateAlbumCoverArtwork', - 'generateAlbumStyleRules', - 'generateBackToAlbumLink', - 'generateReferencingArtworksPage', - 'linkAlbum', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, album) => ({ page: - relation('generateReferencingArtworksPage', album.referencedByArtworks), + relation('generateReferencingArtworksPage', album.coverArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', album, null), + albumStyleTags: + relation('generateAlbumStyleTags', album, null), albumLink: relation('linkAlbum', album), backToAlbumLink: relation('generateBackToAlbumLink', album), - - cover: - relation('generateAlbumCoverArtwork', album), }), data: (album) => ({ name: album.name, - - color: - album.color, }), generate: (data, relations, {html, language}) => @@ -42,10 +26,7 @@ export default { data.name, }), - color: data.color, - styleRules: [relations.albumStyleRules], - - cover: relations.cover, + styleTags: relations.albumStyleTags, navLinks: [ {auto: 'home'}, diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js index 217282c0..4cec4120 100644 --- a/src/content/dependencies/generateAlbumReleaseInfo.js +++ b/src/content/dependencies/generateAlbumReleaseInfo.js @@ -1,31 +1,14 @@ import {accumulateSum, empty} from '#sugar'; export default { - contentDependencies: [ - 'generateReleaseInfoContributionsLine', - 'linkExternal', - ], - - extraDependencies: ['html', 'language'], - relations(relation, album) { const relations = {}; relations.artistContributionsLine = relation('generateReleaseInfoContributionsLine', album.artistContribs); - relations.coverArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.coverArtistContribs); - - relations.wallpaperArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.wallpaperArtistContribs); - - relations.bannerArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', album.bannerArtistContribs); - - relations.externalLinks = - album.urls.map(url => - relation('linkExternal', url)); + relations.listenLine = + relation('generateReleaseInfoListenLine', album); return relations; }, @@ -46,7 +29,7 @@ export default { .map(track => track.duration) .filter(value => value > 0); - if (empty(durationTerms)) { + if (empty(durationTerms) || album.hideDuration) { data.duration = null; data.durationApproximate = null; } else { @@ -73,31 +56,11 @@ export default { chronologyKind: 'album', }), - relations.coverArtistContributionsLine.slots({ - stringKey: capsule + '.coverArtBy', - chronologyKind: 'coverArt', - }), - - relations.wallpaperArtistContributionsLine.slots({ - stringKey: capsule + '.wallpaperArtBy', - chronologyKind: 'wallpaperArt', - }), - - relations.bannerArtistContributionsLine.slots({ - stringKey: capsule + '.bannerArtBy', - chronologyKind: 'bannerArt', - }), - language.$(capsule, 'released', { [language.onlyIfOptions]: ['date'], date: language.formatDate(data.date), }), - language.$(capsule, 'artReleased', { - [language.onlyIfOptions]: ['date'], - date: language.formatDate(data.coverArtDate), - }), - language.$(capsule, 'duration', { [language.onlyIfOptions]: ['duration'], duration: @@ -110,21 +73,16 @@ export default { html.tag('p', {[html.onlyIfContent]: true}, - language.$(capsule, 'listenOn', { - [language.onlyIfOptions]: ['links'], - - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => - link.slot('context', [ - 'album', - (data.numTracks === 0 - ? 'albumNoTracks' - : data.numTracks === 1 - ? 'albumOneTrack' - : 'albumMultipleTracks'), - ]))), + relations.listenLine.slots({ + context: [ + 'album', + + (data.numTracks === 0 + ? 'albumNoTracks' + : data.numTracks === 1 + ? 'albumOneTrack' + : 'albumMultipleTracks'), + ], })), ])), }; diff --git a/src/content/dependencies/generateAlbumSecondaryNav.js b/src/content/dependencies/generateAlbumSecondaryNav.js index bfa48f03..2140bfdb 100644 --- a/src/content/dependencies/generateAlbumSecondaryNav.js +++ b/src/content/dependencies/generateAlbumSecondaryNav.js @@ -1,15 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateAlbumSecondaryNavGroupPart', - 'generateAlbumSecondaryNavSeriesPart', - 'generateDotSwitcherTemplate', - 'generateSecondaryNav', - ], - - extraDependencies: ['html', 'wikiData'], - sprawl: ({groupData}) => ({ // TODO: Series aren't their own things, so we access them weirdly. seriesData: diff --git a/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js b/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js index 9f9aaf23..2f08804b 100644 --- a/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js +++ b/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js @@ -2,15 +2,6 @@ import {sortChronologically} from '#sort'; import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateSecondaryNavParentSiblingsPart', - 'linkAlbumDynamically', - 'linkGroup', - ], - - extraDependencies: ['html'], - query(group, album) { const query = {}; @@ -67,6 +58,8 @@ export default { generate: (relations, slots) => relations.parentSiblingsPart.slots({ + attributes: {class: 'group-nav-links'}, + showPreviousNext: slots.mode === 'album', colorStyle: relations.colorStyle, diff --git a/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js b/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js index f579cdc9..ee180f16 100644 --- a/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js +++ b/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js @@ -1,15 +1,6 @@ import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateSecondaryNavParentSiblingsPart', - 'linkAlbumDynamically', - 'linkGroup', - ], - - extraDependencies: ['html', 'language'], - query(series, album) { const query = {}; @@ -62,7 +53,7 @@ export default { generate: (data, relations, slots, {language}) => relations.parentSiblingsPart.slots({ - attributes: {class: 'series-nav-link'}, + attributes: {class: 'series-nav-links'}, showPreviousNext: slots.mode === 'album', diff --git a/src/content/dependencies/generateAlbumSidebar.js b/src/content/dependencies/generateAlbumSidebar.js index 7cf689cc..83a637b0 100644 --- a/src/content/dependencies/generateAlbumSidebar.js +++ b/src/content/dependencies/generateAlbumSidebar.js @@ -2,17 +2,6 @@ import {sortAlbumsTracksChronologically} from '#sort'; import {stitchArrays, transposeArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateAlbumSidebarGroupBox', - 'generateAlbumSidebarSeriesBox', - 'generateAlbumSidebarTrackListBox', - 'generatePageSidebar', - 'generatePageSidebarConjoinedBox', - 'generateTrackReleaseBox', - ], - - extraDependencies: ['html', 'wikiData'], - sprawl: ({groupData}) => ({ // TODO: Series aren't their own things, so we access them weirdly. seriesData: @@ -46,7 +35,8 @@ export default { const allReleaseAlbums = sortAlbumsTracksChronologically( - Array.from(albumTrackMap.keys())); + Array.from(albumTrackMap.keys()), + {getDate: album => albumTrackMap.get(album).date}); const currentReleaseIndex = allReleaseAlbums.indexOf(track.album); @@ -108,39 +98,65 @@ export default { : null), }), - data: (_query, _sprawl, _album, track) => ({ + data: (_query, _sprawl, album, track) => ({ isAlbumPage: !track, isTrackPage: !!track, + + albumStyle: album.style, }), generate(data, relations, {html}) { + const presentGroupsLikeAlbum = + data.isAlbumPage || + data.albumStyle === 'single'; + for (const box of [ ...relations.groupBoxes, ...relations.seriesBoxes.flat(), ...relations.disconnectedSeriesBoxes, ]) { - box.setSlot('mode', - data.isAlbumPage ? 'album' : 'track'); + box.setSlot('mode', presentGroupsLikeAlbum ? 'album' : 'track'); } + const groupBoxes = + (presentGroupsLikeAlbum + ? [ + relations.disconnectedSeriesBoxes, + + stitchArrays({ + groupBox: relations.groupBoxes, + seriesBoxes: relations.seriesBoxes, + }).map(({groupBox, seriesBoxes}) => [ + groupBox, + seriesBoxes.map(seriesBox => [ + html.tag('div', + {class: 'sidebar-box-joiner'}, + {class: 'collapsible'}), + seriesBox, + ]), + ]), + ] + : [ + relations.conjoinedBox.slots({ + attributes: {class: 'conjoined-group-sidebar-box'}, + boxes: + ([relations.disconnectedSeriesBoxes, + stitchArrays({ + groupBox: relations.groupBoxes, + seriesBoxes: relations.seriesBoxes, + }).flatMap(({groupBox, seriesBoxes}) => [ + groupBox, + ...seriesBoxes, + ]), + ]).flat() + .map(box => box.content), /* TODO: Kludge. */ + }) + ]); + return relations.sidebar.slots({ boxes: [ - data.isAlbumPage && [ - relations.disconnectedSeriesBoxes, - - stitchArrays({ - groupBox: relations.groupBoxes, - seriesBoxes: relations.seriesBoxes, - }).map(({groupBox, seriesBoxes}) => [ - groupBox, - seriesBoxes.map(seriesBox => [ - html.tag('div', - {class: 'sidebar-box-joiner'}, - {class: 'collapsible'}), - seriesBox, - ]), - ]), - ], + data.isAlbumPage && + groupBoxes, data.isTrackPage && relations.earlierTrackReleaseBoxes, @@ -151,20 +167,7 @@ export default { relations.laterTrackReleaseBoxes, data.isTrackPage && - relations.conjoinedBox.slots({ - attributes: {class: 'conjoined-group-sidebar-box'}, - boxes: - ([relations.disconnectedSeriesBoxes, - stitchArrays({ - groupBox: relations.groupBoxes, - seriesBoxes: relations.seriesBoxes, - }).flatMap(({groupBox, seriesBoxes}) => [ - groupBox, - ...seriesBoxes, - ]), - ]).flat() - .map(box => box.content), /* TODO: Kludge. */ - }), + groupBoxes, ], }); }, diff --git a/src/content/dependencies/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js index f3be74f7..0a9c0db9 100644 --- a/src/content/dependencies/generateAlbumSidebarGroupBox.js +++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js @@ -2,16 +2,6 @@ import {sortChronologically} from '#sort'; import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generatePageSidebarBox', - 'linkAlbum', - 'linkExternal', - 'linkGroup', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - query(album, group) { const query = {}; diff --git a/src/content/dependencies/generateAlbumSidebarSeriesBox.js b/src/content/dependencies/generateAlbumSidebarSeriesBox.js index 37616cb2..22f1fe72 100644 --- a/src/content/dependencies/generateAlbumSidebarSeriesBox.js +++ b/src/content/dependencies/generateAlbumSidebarSeriesBox.js @@ -1,15 +1,6 @@ import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generatePageSidebarBox', - 'linkAlbum', - 'linkGroup', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - query(album, series) { const query = {}; diff --git a/src/content/dependencies/generateAlbumSidebarTrackListBox.js b/src/content/dependencies/generateAlbumSidebarTrackListBox.js index 3a244e3a..4e9437c9 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackListBox.js +++ b/src/content/dependencies/generateAlbumSidebarTrackListBox.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateAlbumSidebarTrackSection', - 'generatePageSidebarBox', - 'linkAlbum', - ], - - extraDependencies: ['html'], - relations: (relation, album, track) => ({ box: relation('generatePageSidebarBox'), @@ -24,7 +16,9 @@ export default { attributes: {class: 'track-list-sidebar-box'}, content: [ - html.tag('h1', relations.albumLink), + html.tag('h1', {[html.onlyIfSiblings]: true}, + relations.albumLink), + relations.trackSections, ], }) diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js index 88aea409..68281bfe 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackSection.js +++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js @@ -1,9 +1,6 @@ -import {empty} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: ['linkTrack'], - extraDependencies: ['getColors', 'html', 'language'], - relations(relation, album, track, trackSection) { const relations = {}; @@ -17,23 +14,27 @@ export default { data(album, track, trackSection) { const data = {}; - data.hasTrackNumbers = album.hasTrackNumbers; + data.hasTrackNumbers = + album.hasTrackNumbers && + !empty(trackSection.tracks); + data.isTrackPage = !!track; + data.albumStyle = album.style; data.name = trackSection.name; data.color = trackSection.color; data.isDefaultTrackSection = trackSection.isDefaultTrackSection; + data.hasSiblingSections = album.trackSections.length > 1; - data.firstTrackNumber = trackSection.startIndex + 1; - data.lastTrackNumber = trackSection.startIndex + trackSection.tracks.length; + data.firstTrackNumber = + (data.hasTrackNumbers + ? trackSection.tracks.at(0).trackNumber + : null); - if (track) { - const index = trackSection.tracks.indexOf(track); - if (index !== -1) { - data.includesCurrentTrack = true; - data.currentTrackIndex = index; - } - } + data.lastTrackNumber = + (data.hasTrackNumbers + ? trackSection.tracks.at(-1).trackNumber + : null); data.trackDirectories = trackSection.tracks @@ -43,6 +44,13 @@ export default { trackSection.tracks .map(track => empty(track.commentary)); + data.tracksAreCurrentTrack = + trackSection.tracks + .map(traaaaaaaack => traaaaaaaack === track); + + data.includesCurrentTrack = + data.tracksAreCurrentTrack.includes(true); + return data; }, @@ -72,29 +80,54 @@ export default { } const trackListItems = - relations.trackLinks.map((trackLink, index) => - html.tag('li', - data.includesCurrentTrack && - index === data.currentTrackIndex && - {class: 'current'}, - - slots.mode === 'commentary' && - data.tracksAreMissingCommentary[index] && - {class: 'no-commentary'}, - - language.$(capsule, 'item', { - track: - (slots.mode === 'commentary' && data.tracksAreMissingCommentary[index] - ? trackLink.slots({ - linkless: true, - }) - : slots.anchor - ? trackLink.slots({ - anchor: true, - hash: data.trackDirectories[index], - }) - : trackLink), - }))); + stitchArrays({ + trackLink: relations.trackLinks, + directory: data.trackDirectories, + isCurrentTrack: data.tracksAreCurrentTrack, + missingCommentary: data.tracksAreMissingCommentary, + }).map(({ + trackLink, + directory, + isCurrentTrack, + missingCommentary, + }) => + html.tag('li', + data.includesCurrentTrack && + isCurrentTrack && + {class: 'current'}, + + slots.mode === 'commentary' && + missingCommentary && + {class: 'no-commentary'}, + + language.$(capsule, 'item', { + track: + (slots.mode === 'commentary' && missingCommentary + ? trackLink.slots({ + linkless: true, + }) + : slots.anchor + ? trackLink.slots({ + anchor: true, + hash: directory, + }) + : trackLink), + }))); + + const list = + (data.hasTrackNumbers + ? html.tag('ol', + {start: data.firstTrackNumber}, + trackListItems) + : html.tag('ul', trackListItems)); + + if (data.albumStyle === 'single' && !data.hasSiblingSections) { + if (trackListItems.length <= 1) { + return html.blank(); + } else { + return list; + } + } return html.tag('details', data.includesCurrentTrack && @@ -121,23 +154,24 @@ export default { colorStyle, html.tag('span', - language.encapsulate(capsule, 'group', workingCapsule => { - const workingOptions = {group: sectionName}; - - if (data.hasTrackNumbers) { - workingCapsule += '.withRange'; - workingOptions.range = - `${data.firstTrackNumber}–${data.lastTrackNumber}`; - } - - return language.$(workingCapsule, workingOptions); - }))), - - (data.hasTrackNumbers - ? html.tag('ol', - {start: data.firstTrackNumber}, - trackListItems) - : html.tag('ul', trackListItems)), + language.encapsulate(capsule, 'group', groupCapsule => + language.encapsulate(groupCapsule, workingCapsule => { + const workingOptions = {group: sectionName}; + + if (data.hasTrackNumbers) { + workingCapsule += '.withRange'; + workingOptions.rangePart = + html.tag('span', {class: 'track-section-range'}, + language.$(groupCapsule, 'withRange.rangePart', { + range: + `${data.firstTrackNumber}–${data.lastTrackNumber}`, + })); + } + + return language.$(workingCapsule, workingOptions); + })))), + + list, ]); }, }; diff --git a/src/content/dependencies/generateAlbumSocialEmbed.js b/src/content/dependencies/generateAlbumSocialEmbed.js index ad02e180..1200ec8b 100644 --- a/src/content/dependencies/generateAlbumSocialEmbed.js +++ b/src/content/dependencies/generateAlbumSocialEmbed.js @@ -1,13 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateSocialEmbed', - 'generateAlbumSocialEmbedDescription', - ], - - extraDependencies: ['absoluteTo', 'language'], - relations(relation, album) { return { socialEmbed: @@ -32,8 +25,7 @@ export default { data.hasImage = album.hasCoverArt; if (data.hasImage) { - data.coverArtDirectory = album.directory; - data.coverArtFileExtension = album.coverArtFileExtension; + data.imagePath = album.coverArtworks[0].path; } data.albumName = album.name; @@ -65,7 +57,7 @@ export default { imagePath: (data.hasImage - ? ['media.albumCover', data.coverArtDirectory, data.coverArtFileExtension] + ? data.imagePath : null), })), }; diff --git a/src/content/dependencies/generateAlbumSocialEmbedDescription.js b/src/content/dependencies/generateAlbumSocialEmbedDescription.js index 69c39c3a..db6da5b7 100644 --- a/src/content/dependencies/generateAlbumSocialEmbedDescription.js +++ b/src/content/dependencies/generateAlbumSocialEmbedDescription.js @@ -1,8 +1,6 @@ import {accumulateSum} from '#sugar'; export default { - extraDependencies: ['language'], - data: (album) => ({ duration: accumulateSum(album.tracks, track => track.duration), diff --git a/src/content/dependencies/generateAlbumStyleRules.js b/src/content/dependencies/generateAlbumStyleRules.js deleted file mode 100644 index 6bfcc62e..00000000 --- a/src/content/dependencies/generateAlbumStyleRules.js +++ /dev/null @@ -1,107 +0,0 @@ -import {empty, stitchArrays} from '#sugar'; - -export default { - extraDependencies: ['to'], - - data(album, track) { - const data = {}; - - data.hasWallpaper = !empty(album.wallpaperArtistContribs); - data.hasBanner = !empty(album.bannerArtistContribs); - - if (data.hasWallpaper) { - if (!empty(album.wallpaperParts)) { - data.wallpaperMode = 'parts'; - - data.wallpaperPaths = - album.wallpaperParts.map(part => - (part.asset - ? ['media.albumWallpaperPart', album.directory, part.asset] - : null)); - - data.wallpaperStyles = - album.wallpaperParts.map(part => part.style); - } else { - data.wallpaperMode = 'one'; - data.wallpaperPath = ['media.albumWallpaper', album.directory, album.wallpaperFileExtension]; - data.wallpaperStyle = album.wallpaperStyle; - } - } - - if (data.hasBanner) { - data.hasBannerStyle = !!album.bannerStyle; - data.bannerStyle = album.bannerStyle; - } - - data.albumDirectory = album.directory; - - if (track) { - data.trackDirectory = track.directory; - } - - return data; - }, - - generate(data, {to}) { - const indent = parts => - (parts ?? []) - .filter(Boolean) - .join('\n') - .split('\n') - .map(line => ' '.repeat(4) + line) - .join('\n'); - - const rule = (selector, parts) => - (!empty(parts.filter(Boolean)) - ? [`${selector} {`, indent(parts), `}`] - : []); - - const oneWallpaperRule = - data.wallpaperMode === 'one' && - rule(`body::before`, [ - `background-image: url("${to(...data.wallpaperPath)}");`, - data.wallpaperStyle, - ]); - - const wallpaperPartRules = - data.wallpaperMode === 'parts' && - stitchArrays({ - path: data.wallpaperPaths, - style: data.wallpaperStyles, - }).map(({path, style}, index) => - rule(`.wallpaper-part:nth-child(${index + 1})`, [ - path && `background-image: url("${to(...path)}");`, - style, - ])); - - const nukeBasicWallpaperRule = - data.wallpaperMode === 'parts' && - rule(`body::before`, ['display: none']); - - const wallpaperRules = [ - oneWallpaperRule, - ...wallpaperPartRules || [], - nukeBasicWallpaperRule, - ]; - - const bannerRule = - data.hasBanner && - rule(`#banner img`, [ - data.bannerStyle, - ]); - - const dataRule = - rule(`:root`, [ - data.albumDirectory && - `--album-directory: ${data.albumDirectory};`, - data.trackDirectory && - `--track-directory: ${data.trackDirectory};`, - ]); - - return ( - [...wallpaperRules, bannerRule, dataRule] - .filter(Boolean) - .flat() - .join('\n')); - }, -}; diff --git a/src/content/dependencies/generateAlbumStyleTags.js b/src/content/dependencies/generateAlbumStyleTags.js new file mode 100644 index 00000000..caf21dc4 --- /dev/null +++ b/src/content/dependencies/generateAlbumStyleTags.js @@ -0,0 +1,62 @@ +import {empty} from '#sugar'; + +export default { + relations: (relation, album, _track) => ({ + styleTag: + relation('generateStyleTag'), + + wallpaperStyleTag: + relation('generateAlbumWallpaperStyleTag', album), + }), + + data(album, track) { + const data = {}; + + data.hasBanner = !empty(album.bannerArtistContribs); + + if (data.hasBanner) { + data.hasBannerStyle = !!album.bannerStyle; + data.bannerStyle = album.bannerStyle; + } + + data.albumDirectory = album.directory; + + if (track) { + data.trackDirectory = track.directory; + } + + return data; + }, + + generate: (data, relations, {html}) => + html.tags([ + relations.wallpaperStyleTag, + + relations.styleTag.clone().slots({ + attributes: {class: 'album-banner-style'}, + + rules: [ + data.hasBanner && { + select: '#banner img', + declare: [data.bannerStyle], + }, + ], + }), + + relations.styleTag.clone().slots({ + attributes: {class: 'album-directory-style'}, + + rules: [ + { + select: ':root', + declare: [ + data.albumDirectory && + `--album-directory: ${data.albumDirectory};`, + data.trackDirectory && + `--track-directory: ${data.trackDirectory};`, + ], + }, + ] + }), + ], {[html.joinChildren]: ''}), +}; diff --git a/src/content/dependencies/generateAlbumTrackList.js b/src/content/dependencies/generateAlbumTrackList.js index 9743c750..93cb420b 100644 --- a/src/content/dependencies/generateAlbumTrackList.js +++ b/src/content/dependencies/generateAlbumTrackList.js @@ -35,14 +35,6 @@ function getDisplayMode(album) { } export default { - contentDependencies: [ - 'generateAlbumTrackListItem', - 'generateContentHeading', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - query(album) { return { displayMode: getDisplayMode(album), @@ -102,11 +94,11 @@ export default { .map(section => section.tracks.length > 1); if (album.hasTrackNumbers) { - data.trackSectionStartIndices = + data.trackSectionsStartCountingFrom = album.trackSections - .map(section => section.startIndex); + .map(section => section.startCountingFrom); } else { - data.trackSectionStartIndices = + data.trackSectionsStartCountingFrom = album.trackSections .map(() => null); } @@ -147,7 +139,7 @@ export default { name: data.trackSectionNames, duration: data.trackSectionDurations, durationApproximate: data.trackSectionDurationsApproximate, - startIndex: data.trackSectionStartIndices, + startCountingFrom: data.trackSectionsStartCountingFrom, }).map(({ heading, description, @@ -156,7 +148,7 @@ export default { name, duration, durationApproximate, - startIndex, + startCountingFrom, }) => [ language.encapsulate('trackList.section', capsule => heading.slots({ @@ -190,7 +182,7 @@ export default { html.tag(listTag, data.hasTrackNumbers && - {start: startIndex + 1}, + {start: startCountingFrom}, slotItems(items)), ]), diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js index 44297c15..ab8d477d 100644 --- a/src/content/dependencies/generateAlbumTrackListItem.js +++ b/src/content/dependencies/generateAlbumTrackListItem.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateTrackListItem'], - extraDependencies: ['html'], - query: (track, album) => ({ trackHasDuration: !!track.duration, @@ -20,7 +17,7 @@ export default { item: relation('generateTrackListItem', track, - track.album.artistContribs), + track.album.trackArtistContribs), }), data: (query, track, album) => ({ diff --git a/src/content/dependencies/generateAlbumWallpaperStyleTag.js b/src/content/dependencies/generateAlbumWallpaperStyleTag.js new file mode 100644 index 00000000..b3f74716 --- /dev/null +++ b/src/content/dependencies/generateAlbumWallpaperStyleTag.js @@ -0,0 +1,35 @@ +export default { + relations: (relation, album) => ({ + wallpaperStyleTag: + (album.hasWallpaperArt + ? relation('generateWallpaperStyleTag') + : null), + }), + + data: (album) => ({ + singleWallpaperPath: + ['media.albumWallpaper', album.directory, album.wallpaperFileExtension], + + singleWallpaperStyle: + album.wallpaperStyle, + + wallpaperPartPaths: + album.wallpaperParts.map(part => + (part.asset + ? ['media.albumWallpaperPart', album.directory, part.asset] + : null)), + + wallpaperPartStyles: + album.wallpaperParts.map(part => part.style), + }), + + generate: (data, relations, {html}) => + (relations.wallpaperStyleTag + ? relations.wallpaperStyleTag.slots({ + singleWallpaperPath: data.singleWallpaperPath, + singleWallpaperStyle: data.singleWallpaperStyle, + wallpaperPartPaths: data.wallpaperPartPaths, + wallpaperPartStyles: data.wallpaperPartStyles, + }) + : html.blank()), +}; diff --git a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js index 89150615..37a32a94 100644 --- a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js +++ b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js @@ -6,9 +6,6 @@ import { } from '#sugar'; export default { - contentDependencies: ['linkArtTagDynamically'], - extraDependencies: ['html', 'language'], - // Recursion ain't too pretty! query(ancestorArtTag, targetArtTag) { @@ -33,8 +30,8 @@ export default { const artTagsTimesFeaturedTotal = artTags.map(artTag => unique([ - ...artTag.directlyTaggedInThings, - ...artTag.indirectlyTaggedInThings, + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, ]).length); const sublists = diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js index b633e58f..f20babba 100644 --- a/src/content/dependencies/generateArtTagGalleryPage.js +++ b/src/content/dependencies/generateArtTagGalleryPage.js @@ -1,24 +1,7 @@ -import {sortAlbumsTracksChronologically} from '#sort'; +import {sortArtworksChronologically} from '#sort'; import {empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: [ - 'generateAdditionalNamesBox', - 'generateArtTagGalleryPageFeaturedLine', - 'generateArtTagGalleryPageShowingLine', - 'generateArtTagNavLinks', - 'generateCoverGrid', - 'generatePageLayout', - 'generateQuickDescription', - 'image', - 'linkAlbum', - 'linkArtTagGallery', - 'linkExternal', - 'linkTrack', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({wikiInfo}) { return { enableListings: wikiInfo.enableListings, @@ -26,16 +9,13 @@ export default { }, query(sprawl, artTag) { - const directThings = artTag.directlyTaggedInThings; - const indirectThings = artTag.indirectlyTaggedInThings; - const allThings = unique([...directThings, ...indirectThings]); + const directArtworks = artTag.directlyFeaturedInArtworks; + const indirectArtworks = artTag.indirectlyFeaturedInArtworks; + const allArtworks = unique([...directArtworks, ...indirectArtworks]); - sortAlbumsTracksChronologically(allThings, { - getDate: thing => thing.coverArtDate ?? thing.date, - latestFirst: true, - }); + sortArtworksChronologically(allArtworks, {latestFirst: true}); - return {directThings, indirectThings, allThings}; + return {directArtworks, indirectArtworks, allArtworks}; }, relations(relation, query, sprawl, artTag) { @@ -81,15 +61,12 @@ export default { relation('generateCoverGrid'); relations.links = - query.allThings - .map(thing => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))); + query.allArtworks + .map(artwork => relation('linkAnythingMan', artwork.thing)); relations.images = - query.allThings - .map(thing => relation('image', thing.artTags)); + query.allArtworks + .map(artwork => relation('image', artwork)); return relations; }, @@ -102,30 +79,26 @@ export default { data.name = artTag.name; data.color = artTag.color; - data.numArtworksIndirectly = query.indirectThings.length; - data.numArtworksDirectly = query.directThings.length; - data.numArtworksTotal = query.allThings.length; + data.numArtworksIndirectly = query.indirectArtworks.length; + data.numArtworksDirectly = query.directArtworks.length; + data.numArtworksTotal = query.allArtworks.length; data.names = - query.allThings.map(thing => thing.name); - - data.paths = - query.allThings.map(thing => - (thing.album - ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] - : ['media.albumCover', thing.directory, thing.coverArtFileExtension])); + query.allArtworks + .map(artwork => artwork.thing.name); - data.dimensions = - query.allThings.map(thing => thing.coverArtDimensions); + data.artworkArtists = + query.allArtworks + .map(artwork => artwork.artistContribs + .map(contrib => contrib.artist.name)); - data.coverArtists = - query.allThings.map(thing => - thing.coverArtistContribs - .map(({artist}) => artist.name)); + data.artworkLabels = + query.allArtworks + .map(artwork => artwork.label) data.onlyFeaturedIndirectly = - query.allThings.map(thing => - !query.directThings.includes(thing)); + query.allArtworks.map(artwork => + !query.directArtworks.includes(artwork)); data.hasMixedDirectIndirect = data.onlyFeaturedIndirectly.includes(true) && @@ -210,6 +183,7 @@ export default { relations.coverGrid .slots({ links: relations.links, + images: relations.images, names: data.names, lazy: 12, @@ -217,24 +191,25 @@ export default { data.onlyFeaturedIndirectly.map(onlyFeaturedIndirectly => (onlyFeaturedIndirectly ? 'featured-indirectly' : '')), - images: + info: stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - }).map(({image, path, dimensions}) => - image.slots({ - path, - dimensions, + artists: data.artworkArtists, + label: data.artworkLabels, + }).map(({artists, label}) => + language.encapsulate('misc.coverGrid.details.coverArtists', workingCapsule => { + const workingOptions = {}; + + workingOptions[language.onlyIfOptions] = ['artists']; + workingOptions.artists = + language.formatUnitList(artists); + + if (label) { + workingCapsule += '.customLabel'; + workingOptions.label = label; + } + + return language.$(workingCapsule, workingOptions); })), - - info: - data.coverArtists.map(names => - (names === null - ? null - : language.$('misc.coverGrid.details.coverArtists', { - artists: language.formatUnitList(names), - }))), }), ], diff --git a/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js b/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js index b4620fa4..8593cc21 100644 --- a/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js +++ b/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'language'], - slots: { showing: { validate: v => v.is('all', 'direct', 'indirect'), diff --git a/src/content/dependencies/generateArtTagGalleryPageShowingLine.js b/src/content/dependencies/generateArtTagGalleryPageShowingLine.js index 6df4d0e5..2a34ae57 100644 --- a/src/content/dependencies/generateArtTagGalleryPageShowingLine.js +++ b/src/content/dependencies/generateArtTagGalleryPageShowingLine.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'language'], - slots: { showing: { validate: v => v.is('all', 'direct', 'indirect'), diff --git a/src/content/dependencies/generateArtTagInfoPage.js b/src/content/dependencies/generateArtTagInfoPage.js index 7765f159..683eeab6 100644 --- a/src/content/dependencies/generateArtTagInfoPage.js +++ b/src/content/dependencies/generateArtTagInfoPage.js @@ -1,20 +1,6 @@ import {empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: [ - 'generateAdditionalNamesBox', - 'generateArtTagNavLinks', - 'generateArtTagSidebar', - 'generateContentHeading', - 'generatePageLayout', - 'linkArtTagGallery', - 'linkArtTagInfo', - 'linkExternal', - 'transformContent', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({ enableListings: wikiInfo.enableListings, }), @@ -23,10 +9,10 @@ export default { const query = {}; query.directThings = - artTag.directlyTaggedInThings; + artTag.directlyFeaturedInArtworks; query.indirectThings = - artTag.indirectlyTaggedInThings; + artTag.indirectlyFeaturedInArtworks; query.allThings = unique([...query.directThings, ...query.indirectThings]); @@ -111,8 +97,8 @@ export default { directDescendantTimesFeaturedTotal: artTag.directDescendantArtTags.map(artTag => unique([ - ...artTag.directlyTaggedInThings, - ...artTag.indirectlyTaggedInThings, + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, ]).length), }), @@ -182,12 +168,12 @@ export default { artTagLink: relations.relatedArtTagLinks, annotation: data.relatedArtTagAnnotations, }).map(({artTagLink, annotation}) => - (html.isBlank(annotation) - ? artTagLink - : language.$(capsule, 'tagWithAnnotation', { + (annotation + ? language.$(capsule, 'tagWithAnnotation', { tag: artTagLink, annotation, - })))), + }) + : artTagLink))), }))), html.tag('blockquote', diff --git a/src/content/dependencies/generateArtTagNavLinks.js b/src/content/dependencies/generateArtTagNavLinks.js index 9061a09f..1298ce99 100644 --- a/src/content/dependencies/generateArtTagNavLinks.js +++ b/src/content/dependencies/generateArtTagNavLinks.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'linkArtTagInfo', - 'linkArtTagGallery', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({enableListings: wikiInfo.enableListings}), diff --git a/src/content/dependencies/generateArtTagSidebar.js b/src/content/dependencies/generateArtTagSidebar.js index c281b93d..60ea504f 100644 --- a/src/content/dependencies/generateArtTagSidebar.js +++ b/src/content/dependencies/generateArtTagSidebar.js @@ -1,15 +1,6 @@ import {collectTreeLeaves, empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: [ - 'generatePageSidebar', - 'generatePageSidebarBox', - 'generateArtTagAncestorDescendantMapList', - 'linkArtTagDynamically', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({artTagData}) => ({artTagData}), @@ -54,8 +45,8 @@ export default { directDescendantTimesFeaturedTotal: artTag.directDescendantArtTags.map(artTag => unique([ - ...artTag.directlyTaggedInThings, - ...artTag.indirectlyTaggedInThings, + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, ]).length), furthestAncestorArtTagNames: diff --git a/src/content/dependencies/generateArtistArtworkColumn.js b/src/content/dependencies/generateArtistArtworkColumn.js new file mode 100644 index 00000000..19c66b8a --- /dev/null +++ b/src/content/dependencies/generateArtistArtworkColumn.js @@ -0,0 +1,11 @@ +export default { + relations: (relation, artist) => ({ + coverArtwork: + (artist.hasAvatar + ? relation('generateCoverArtwork', artist.avatarArtwork) + : null), + }), + + generate: (relations) => + relations.coverArtwork, +}; diff --git a/src/content/dependencies/generateArtistCredit.js b/src/content/dependencies/generateArtistCredit.js index 72d55854..389de740 100644 --- a/src/content/dependencies/generateArtistCredit.js +++ b/src/content/dependencies/generateArtistCredit.js @@ -1,14 +1,7 @@ -import {compareArrays, empty} from '#sugar'; +import {compareArrays, empty, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateArtistCreditWikiEditsPart', - 'linkContribution', - ], - - extraDependencies: ['html', 'language'], - - query: (creditContributions, contextContributions) => { + query: (creditContributions, contextContributions, _formatText) => { const query = {}; const featuringFilter = contribution => @@ -36,16 +29,26 @@ export default { // Note that the normal contributions will implicitly *always* // "differ from context" if no context contributions are given, // as in release info lines. - query.normalContributionsDifferFromContext = + + query.normalContributionArtistsDifferFromContext = !compareArrays( query.normalContributions.map(({artist}) => artist), contextNormalContributions.map(({artist}) => artist), - {checkOrder: false}); + {checkOrder: true}); + + query.normalContributionAnnotationsDifferFromContext = + !compareArrays( + query.normalContributions.map(({annotation}) => annotation), + contextNormalContributions.map(({annotation}) => annotation), + {checkOrder: true}); return query; }, - relations: (relation, query, _creditContributions, _contextContributions) => ({ + relations: (relation, query, + _creditContributions, + _contextContributions, + formatText) => ({ normalContributionLinks: query.normalContributions .map(contrib => relation('linkContribution', contrib)), @@ -57,11 +60,25 @@ export default { wikiEditsPart: relation('generateArtistCreditWikiEditsPart', query.wikiEditContributions), + + formatText: + relation('transformContent', formatText), }), - data: (query, _creditContributions, _contextContributions) => ({ - normalContributionsDifferFromContext: - query.normalContributionsDifferFromContext, + data: (query, _creditContributions, _contextContributions, _formatText) => ({ + normalContributionArtistsDifferFromContext: + query.normalContributionArtistsDifferFromContext, + + normalContributionAnnotationsDifferFromContext: + query.normalContributionAnnotationsDifferFromContext, + + normalContributionArtistDirectories: + query.normalContributions + .map(contrib => contrib.artist.directory), + + featuringContributionArtistDirectories: + query.featuringContributions + .map(contrib => contrib.artist.directory), hasWikiEdits: !empty(query.wikiEditContributions), @@ -80,6 +97,8 @@ export default { // It won't be used if contextContributions isn't provided. featuringStringKey: {type: 'string'}, + additionalStringOptions: {validate: v => v.isObject}, + showAnnotation: {type: 'boolean', default: false}, showExternalLinks: {type: 'boolean', default: false}, showChronology: {type: 'boolean', default: false}, @@ -93,6 +112,10 @@ export default { generate(data, relations, slots, {html, language}) { if (!slots.normalStringKey) return html.blank(); + const effectivelyDiffers = + (slots.showAnnotation && data.normalContributionAnnotationsDifferFromContext) || + (data.normalContributionArtistsDifferFromContext); + for (const link of [ ...relations.normalContributionLinks, ...relations.featuringContributionLinks, @@ -120,49 +143,112 @@ export default { }); } - if (empty(relations.normalContributionLinks)) { - return html.blank(); + let formattedArtistList = null; + + if (!html.isBlank(relations.formatText)) { + formattedArtistList = relations.formatText; + + const substituteContrib = ({link, directory}) => ({ + match: {replacerKey: 'artist', replacerValue: directory}, + substitute: link, + + apply(link, node) { + if (node.data.label) { + link.setSlot('content', language.sanitize(node.data.label)); + } + }, + }); + + relations.formatText.setSlots({ + mode: 'inline', + + substitute: [ + stitchArrays({ + link: relations.normalContributionLinks, + directory: data.normalContributionArtistDirectories, + }).map(substituteContrib), + + stitchArrays({ + link: relations.featuringContributionLinks, + directory: data.featuringContributionArtistDirectories, + }).map(substituteContrib), + ].flat(), + }); } - const artistsList = - (data.hasWikiEdits && slots.showWikiEdits - ? language.$('misc.artistLink.withEditsForWiki', { - artists: - language.formatConjunctionList(relations.normalContributionLinks), - - edits: - relations.wikiEditsPart.slots({ - showAnnotation: slots.showAnnotation, - }), - }) - : language.formatConjunctionList(relations.normalContributionLinks)); - - const featuringList = - language.formatConjunctionList(relations.featuringContributionLinks); - - const everyoneList = - language.formatConjunctionList([ - ...relations.normalContributionLinks, - ...relations.featuringContributionLinks, - ]); - - if (empty(relations.featuringContributionLinks)) { - if (data.normalContributionsDifferFromContext) { - return language.$(slots.normalStringKey, {artists: artistsList}); - } else { + let content; + + if (formattedArtistList) { + if (effectivelyDiffers) { + content = + language.$(slots.normalStringKey, { + ...slots.additionalStringOptions, + artists: formattedArtistList, + }); + } + } else { + if (empty(relations.normalContributionLinks)) { return html.blank(); } - } - if (data.normalContributionsDifferFromContext && slots.normalFeaturingStringKey) { - return language.$(slots.normalFeaturingStringKey, { - artists: artistsList, - featuring: featuringList, - }); - } else if (slots.featuringStringKey) { - return language.$(slots.featuringStringKey, {artists: featuringList}); - } else { - return language.$(slots.normalStringKey, {artists: everyoneList}); + const artistsList = + (data.hasWikiEdits && slots.showWikiEdits + ? language.$('misc.artistLink.withEditsForWiki', { + artists: + language.formatConjunctionList(relations.normalContributionLinks), + + edits: + relations.wikiEditsPart.slots({ + showAnnotation: slots.showAnnotation, + }), + }) + + : language.formatConjunctionList(relations.normalContributionLinks)); + + const featuringList = + language.formatConjunctionList(relations.featuringContributionLinks); + + const everyoneList = + language.formatConjunctionList([ + ...relations.normalContributionLinks, + ...relations.featuringContributionLinks, + ]); + + if (empty(relations.featuringContributionLinks)) { + if (effectivelyDiffers) { + content = + language.$(slots.normalStringKey, { + ...slots.additionalStringOptions, + artists: artistsList, + }); + } else { + return html.blank(); + } + } else if (effectivelyDiffers && slots.normalFeaturingStringKey) { + content = + language.$(slots.normalFeaturingStringKey, { + ...slots.additionalStringOptions, + artists: artistsList, + featuring: featuringList, + }); + } else if (slots.featuringStringKey) { + content = + language.$(slots.featuringStringKey, { + ...slots.additionalStringOptions, + artists: featuringList, + }); + } else { + content = + language.$(slots.normalStringKey, { + ...slots.additionalStringOptions, + artists: everyoneList, + }); + } } + + // TODO: This is obviously evil. + return ( + html.metatag('chunkwrap', {split: /,| (?=and)/}, + html.resolve(content))); }, }; diff --git a/src/content/dependencies/generateArtistCreditWikiEditsPart.js b/src/content/dependencies/generateArtistCreditWikiEditsPart.js index 70296e39..4178928d 100644 --- a/src/content/dependencies/generateArtistCreditWikiEditsPart.js +++ b/src/content/dependencies/generateArtistCreditWikiEditsPart.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateTextWithTooltip', - 'generateTooltip', - 'linkContribution', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, contributions) => ({ textWithTooltip: relation('generateTextWithTooltip'), @@ -48,6 +40,7 @@ export default { showAnnotation: slots.showAnnotation, trimAnnotation: true, preventTooltip: true, + preventWrapping: true, }))), }), }), diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js index 7a76188a..d8f1c4b1 100644 --- a/src/content/dependencies/generateArtistGalleryPage.js +++ b/src/content/dependencies/generateArtistGalleryPage.js @@ -1,89 +1,58 @@ -import {sortAlbumsTracksChronologically} from '#sort'; -import {stitchArrays} from '#sugar'; +import {sortArtworksChronologically} from '#sort'; export default { - contentDependencies: [ - 'generateArtistNavLinks', - 'generateCoverGrid', - 'generatePageLayout', - 'image', - 'linkAlbum', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - - query(artist) { - const things = - ([ - artist.albumCoverArtistContributions, - artist.trackCoverArtistContributions, - ]).flat() - .filter(({annotation}) => !annotation?.startsWith(`edits for wiki`)) - .map(({thing}) => thing); - - sortAlbumsTracksChronologically(things, { - latestFirst: true, - getDate: thing => thing.coverArtDate ?? thing.date, - }); - - return {things}; - }, - - relations(relation, query, artist) { - const relations = {}; - - relations.layout = - relation('generatePageLayout'); - - relations.artistNavLinks = - relation('generateArtistNavLinks', artist); - - relations.coverGrid = - relation('generateCoverGrid'); - - relations.links = - query.things.map(thing => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))); - - relations.images = - query.things.map(thing => - relation('image', thing.artTags)); - - return relations; - }, - - data(query, artist) { - const data = {}; - - data.name = artist.name; - - data.numArtworks = query.things.length; - - data.names = - query.things.map(thing => thing.name); - - data.paths = - query.things.map(thing => - (thing.album - ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] - : ['media.albumCover', thing.directory, thing.coverArtFileExtension])); - - data.dimensions = - query.things.map(thing => thing.coverArtDimensions); - - data.otherCoverArtists = - query.things.map(thing => - (thing.coverArtistContribs.length > 1 - ? thing.coverArtistContribs - .filter(({artist: otherArtist}) => otherArtist !== artist) - .map(({artist: otherArtist}) => otherArtist.name) - : null)); - - return data; - }, + query: (artist) => ({ + artworks: + sortArtworksChronologically( + ([ + artist.albumCoverArtistContributions, + artist.trackCoverArtistContributions, + ]).flat() + .filter(contrib => !contrib.annotation?.startsWith(`edits for wiki`)) + .map(contrib => contrib.thing), + {latestFirst: true}), + }), + + relations: (relation, query, artist) => ({ + layout: + relation('generatePageLayout'), + + artistNavLinks: + relation('generateArtistNavLinks', artist), + + coverGrid: + relation('generateCoverGrid'), + + links: + query.artworks + .map(artwork => relation('linkAnythingMan', artwork.thing)), + + images: + query.artworks + .map(artwork => relation('image', artwork)), + }), + + data: (query, artist) => ({ + name: + artist.name, + + numArtworks: + query.artworks.length, + + names: + query.artworks + .map(artwork => artwork.thing.name), + + otherCoverArtists: + query.artworks + .map(artwork => artwork.artistContribs + .filter(contrib => contrib.artist !== artist) + .map(contrib => contrib.artist.name)), + + allWarnings: + query.artworks + .flatMap(artwork => artwork.contentWarnings), + }), generate: (data, relations, {html, language}) => language.encapsulate('artistGalleryPage', pageCapsule => @@ -100,7 +69,7 @@ export default { html.tag('p', {class: 'quick-info'}, language.$(pageCapsule, 'infoLine', { coverArts: - language.countCoverArts(data.numArtworks, { + language.countArtworks(data.numArtworks, { unit: true, }), })), @@ -108,27 +77,18 @@ export default { relations.coverGrid .slots({ links: relations.links, + images: relations.images, names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - }).map(({image, path, dimensions}) => - image.slots({ - path, - dimensions, - })), - - // TODO: Can this be [language.onlyIfOptions]? info: data.otherCoverArtists.map(names => - (names === null - ? null - : language.$('misc.coverGrid.details.otherCoverArtists', { - artists: language.formatUnitList(names), - }))), + language.$('misc.coverGrid.details.otherCoverArtists', { + [language.onlyIfOptions]: ['artists'], + + artists: language.formatUnitList(names), + })), + + revealAllWarnings: data.allWarnings, }), ], diff --git a/src/content/dependencies/generateArtistGroupContributionsInfo.js b/src/content/dependencies/generateArtistGroupContributionsInfo.js index 3e0cd1d2..6940053f 100644 --- a/src/content/dependencies/generateArtistGroupContributionsInfo.js +++ b/src/content/dependencies/generateArtistGroupContributionsInfo.js @@ -1,83 +1,87 @@ -import {empty, filterProperties, stitchArrays, unique} from '#sugar'; +import {accumulateSum, empty, stitchArrays, withEntries} from '#sugar'; export default { - contentDependencies: ['linkGroup'], - extraDependencies: ['html', 'language', 'wikiData'], + sprawl: ({groupCategoryData}) => ({ + groupOrder: + groupCategoryData.flatMap(category => category.groups), + }), - sprawl({groupCategoryData}) { - return { - groupOrder: groupCategoryData.flatMap(category => category.groups), - } - }, + query(sprawl, contributions) { + const allGroupsUnordered = + new Set(contributions.flatMap(contrib => contrib.groups)); - query(sprawl, tracksAndAlbums) { - const filteredAlbums = tracksAndAlbums.filter(thing => !thing.album); - const filteredTracks = tracksAndAlbums.filter(thing => thing.album); + const allGroupsOrdered = + sprawl.groupOrder.filter(group => allGroupsUnordered.has(group)); - const allAlbums = unique([ - ...filteredAlbums, - ...filteredTracks.map(track => track.album), - ]); + const groupToThingsCountedForContributions = + new Map(allGroupsOrdered.map(group => [group, new Set])); - const allGroupsUnordered = new Set(Array.from(allAlbums).flatMap(album => album.groups)); - const allGroupsOrdered = sprawl.groupOrder.filter(group => allGroupsUnordered.has(group)); + const groupToThingsCountedForDuration = + new Map(allGroupsOrdered.map(group => [group, new Set])); - const mapTemplate = allGroupsOrdered.map(group => [group, 0]); - const groupToCountMap = new Map(mapTemplate); - const groupToDurationMap = new Map(mapTemplate); - const groupToDurationCountMap = new Map(mapTemplate); - - for (const album of filteredAlbums) { - for (const group of album.groups) { - groupToCountMap.set(group, groupToCountMap.get(group) + 1); - } - } + for (const contrib of contributions) { + for (const group of contrib.groups) { + if (contrib.countInContributionTotals) { + groupToThingsCountedForContributions.get(group).add(contrib.thing); + } - for (const track of filteredTracks) { - for (const group of track.album.groups) { - groupToCountMap.set(group, groupToCountMap.get(group) + 1); - if (track.duration && track.mainReleaseTrack === null) { - groupToDurationMap.set(group, groupToDurationMap.get(group) + track.duration); - groupToDurationCountMap.set(group, groupToDurationCountMap.get(group) + 1); + if (contrib.countInDurationTotals) { + groupToThingsCountedForDuration.get(group).add(contrib.thing); } } } + const groupToTotalContributions = + withEntries( + groupToThingsCountedForContributions, + entries => entries.map( + ([group, things]) => + ([group, things.size]))); + + const groupToTotalDuration = + withEntries( + groupToThingsCountedForDuration, + entries => entries.map( + ([group, things]) => + ([group, accumulateSum(things, thing => thing.duration)]))) + const groupsSortedByCount = allGroupsOrdered - .slice() - .sort((a, b) => groupToCountMap.get(b) - groupToCountMap.get(a)); + .filter(group => groupToTotalContributions.get(group) > 0) + .sort((a, b) => + (groupToTotalContributions.get(b) + - groupToTotalContributions.get(a))); - // The filter here ensures all displayed groups have at least some duration - // when sorting by duration. const groupsSortedByDuration = allGroupsOrdered - .filter(group => groupToDurationMap.get(group) > 0) - .sort((a, b) => groupToDurationMap.get(b) - groupToDurationMap.get(a)); + .filter(group => groupToTotalDuration.get(group) > 0) + .sort((a, b) => + (groupToTotalDuration.get(b) + - groupToTotalDuration.get(a))); const groupCountsSortedByCount = groupsSortedByCount - .map(group => groupToCountMap.get(group)); + .map(group => groupToTotalContributions.get(group)); const groupDurationsSortedByCount = groupsSortedByCount - .map(group => groupToDurationMap.get(group)); + .map(group => groupToTotalDuration.get(group)); const groupDurationsApproximateSortedByCount = groupsSortedByCount - .map(group => groupToDurationCountMap.get(group) > 1); + .map(group => groupToThingsCountedForDuration.get(group).size > 1); const groupCountsSortedByDuration = groupsSortedByDuration - .map(group => groupToCountMap.get(group)); + .map(group => groupToTotalContributions.get(group)); const groupDurationsSortedByDuration = groupsSortedByDuration - .map(group => groupToDurationMap.get(group)); + .map(group => groupToTotalDuration.get(group)); const groupDurationsApproximateSortedByDuration = groupsSortedByDuration - .map(group => groupToDurationCountMap.get(group) > 1); + .map(group => groupToThingsCountedForDuration.get(group).size > 1); return { groupsSortedByCount, @@ -93,29 +97,35 @@ export default { }; }, - relations(relation, query) { - return { - groupLinksSortedByCount: - query.groupsSortedByCount - .map(group => relation('linkGroup', group)), + relations: (relation, query) => ({ + groupLinksSortedByCount: + query.groupsSortedByCount + .map(group => relation('linkGroup', group)), - groupLinksSortedByDuration: - query.groupsSortedByDuration - .map(group => relation('linkGroup', group)), - }; - }, + groupLinksSortedByDuration: + query.groupsSortedByDuration + .map(group => relation('linkGroup', group)), + }), - data(query) { - return filterProperties(query, [ - 'groupCountsSortedByCount', - 'groupDurationsSortedByCount', - 'groupDurationsApproximateSortedByCount', + data: (query) => ({ + groupCountsSortedByCount: + query.groupCountsSortedByCount, - 'groupCountsSortedByDuration', - 'groupDurationsSortedByDuration', - 'groupDurationsApproximateSortedByDuration', - ]); - }, + groupDurationsSortedByCount: + query.groupDurationsSortedByCount, + + groupDurationsApproximateSortedByCount: + query.groupDurationsApproximateSortedByCount, + + groupCountsSortedByDuration: + query.groupCountsSortedByDuration, + + groupDurationsSortedByDuration: + query.groupDurationsSortedByDuration, + + groupDurationsApproximateSortedByDuration: + query.groupDurationsApproximateSortedByDuration, + }), slots: { title: { diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index 0c4e4189..cf8ce994 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -1,49 +1,18 @@ import {empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: [ - 'generateArtistGroupContributionsInfo', - 'generateArtistInfoPageArtworksChunkedList', - 'generateArtistInfoPageCommentaryChunkedList', - 'generateArtistInfoPageFlashesChunkedList', - 'generateArtistInfoPageTracksChunkedList', - 'generateArtistNavLinks', - 'generateContentHeading', - 'generateCoverArtwork', - 'generatePageLayout', - 'image', - 'linkArtistGallery', - 'linkExternal', - 'linkGroup', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - query: (artist) => ({ - // Even if an artist has served as both "artist" (compositional) and - // "contributor" (instruments, production, etc) on the same track, that - // track only counts as one unique contribution in the list. - allTracks: - unique( - ([ - artist.trackArtistContributions, - artist.trackContributorContributions, - ]).flat() - .map(({thing}) => thing)), - - // Artworks are different, though. We intentionally duplicate album data - // objects when the artist has contributed some combination of cover art, - // wallpaper, and banner - these each count as a unique contribution. - allArtworks: - ([ - artist.albumCoverArtistContributions, - artist.albumWallpaperArtistContributions, - artist.albumBannerArtistContributions, - artist.trackCoverArtistContributions, - ]).flat() - .filter(({annotation}) => !annotation?.startsWith('edits for wiki')) - .map(({thing}) => thing), + trackContributions: [ + ...artist.trackArtistContributions, + ...artist.trackContributorContributions, + ], + + artworkContributions: [ + ...artist.albumCoverArtistContributions, + ...artist.albumWallpaperArtistContributions, + ...artist.albumBannerArtistContributions, + ...artist.trackCoverArtistContributions, + ], // Banners and wallpapers don't show up in the artist gallery page, only // cover art. @@ -69,15 +38,8 @@ export default { artistNavLinks: relation('generateArtistNavLinks', artist), - cover: - (artist.hasAvatar - ? relation('generateCoverArtwork', [], []) - : null), - - image: - (artist.hasAvatar - ? relation('image') - : null), + artworkColumn: + relation('generateArtistArtworkColumn', artist), contentHeading: relation('generateContentHeading'), @@ -101,7 +63,7 @@ export default { relation('generateArtistInfoPageTracksChunkedList', artist), tracksGroupInfo: - relation('generateArtistGroupContributionsInfo', query.allTracks), + relation('generateArtistGroupContributionsInfo', query.trackContributions), artworksChunkedList: relation('generateArtistInfoPageArtworksChunkedList', artist, false), @@ -110,7 +72,7 @@ export default { relation('generateArtistInfoPageArtworksChunkedList', artist, true), artworksGroupInfo: - relation('generateArtistGroupContributionsInfo', query.allArtworks), + relation('generateArtistGroupContributionsInfo', query.artworkContributions), artistGalleryLink: (query.hasGallery @@ -131,20 +93,16 @@ export default { name: artist.name, - directory: - artist.directory, - - avatarFileExtension: - (artist.hasAvatar - ? artist.avatarFileExtension - : null), - closeGroupAnnotations: query.generalLinkedGroups .map(({annotation}) => annotation), totalTrackCount: - query.allTracks.length, + unique( + query.trackContributions + .filter(contrib => contrib.countInContributionTotals) + .map(contrib => contrib.thing)) + .length, totalDuration: artist.totalDuration, @@ -156,19 +114,8 @@ export default { title: data.name, headingMode: 'sticky', - cover: - (relations.cover - ? relations.cover.slots({ - image: - relations.image.slots({ - path: [ - 'media.artistAvatar', - data.directory, - data.avatarFileExtension, - ], - }), - }) - : null), + artworkColumnContent: + relations.artworkColumn, mainContent: [ html.tags([ diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js index 66e4204a..f4c9439a 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateArtistInfoPageChunk', - 'generateArtistInfoPageArtworksChunkItem', - 'linkAlbum', - ], - - extraDependencies: ['html'], - relations: (relation, album, contribs) => ({ template: relation('generateArtistInfoPageChunk'), diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js index 089cfb8d..8259e91e 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js @@ -1,19 +1,17 @@ -export default { - contentDependencies: [ - 'generateArtistInfoPageChunkItem', - 'generateArtistInfoPageOtherArtistLinks', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], +import {empty} from '#sugar'; +export default { query: (contrib) => ({ kind: - (contrib.isBannerArtistContribution + (contrib.thingProperty === 'bannerArtistContribs' || + (contrib.thing.isArtwork && + contrib.thing.thingProperty === 'bannerArtwork') ? 'banner' - : contrib.isWallpaperArtistContribution + : contrib.thingProperty === 'wallpaperArtistContribs' || + (contrib.thing.isArtwork && + contrib.thing.thingProperty === 'wallpaperArtwork') ? 'wallpaper' - : contrib.isForAlbum + : contrib.thing.isAlbum ? 'album-cover' : 'track-cover'), }), @@ -24,11 +22,14 @@ export default { trackLink: (query.kind === 'track-cover' - ? relation('linkTrack', contrib.thing) + ? relation('linkTrack', contrib.thing.thing) : null), otherArtistLinks: relation('generateArtistInfoPageOtherArtistLinks', [contrib]), + + originDetails: + relation('transformContent', contrib.thing.originDetails), }), data: (query, contrib) => ({ @@ -37,6 +38,9 @@ export default { annotation: contrib.annotation, + + label: + contrib.thing.label, }), slots: { @@ -51,9 +55,33 @@ export default { otherArtistLinks: relations.otherArtistLinks, annotation: - (slots.filterEditsForWiki - ? data.annotation?.replace(/^edits for wiki(: )?/, '') - : data.annotation), + language.encapsulate('artistPage.creditList.entry.artwork.accent', workingCapsule => { + const workingOptions = {}; + + const artworkLabel = data.label; + + if (artworkLabel) { + workingCapsule += '.withLabel'; + workingOptions.label = + language.typicallyLowerCase(artworkLabel); + } + + const contribAnnotation = + (slots.filterEditsForWiki + ? data.annotation?.replace(/^edits for wiki(: )?/, '') + : data.annotation); + + if (contribAnnotation) { + workingCapsule += '.withAnnotation'; + workingOptions.annotation = contribAnnotation; + } + + if (empty(Object.keys(workingOptions))) { + return html.blank(); + } + + return language.$(workingCapsule, workingOptions); + }), content: language.encapsulate('artistPage.creditList.entry', capsule => @@ -68,5 +96,11 @@ export default { : data.kind === 'banner' ? language.$(capsule, 'bannerArt') : language.$(capsule, 'coverArt')))))), + + originDetails: + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }), }), }; diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js index 8b024147..40ffc5dd 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js @@ -3,11 +3,6 @@ import {sortAlbumsTracksChronologically, sortContributionsChronologically} import {chunkByConditions, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunkedList', - 'generateArtistInfoPageArtworksChunk', - ], - query(artist, filterEditsForWiki) { const query = {}; @@ -27,20 +22,21 @@ export default { sortContributionsChronologically( filteredContributions, - sortAlbumsTracksChronologically); + sortAlbumsTracksChronologically, + {getThing: contrib => contrib.thing.thing}); query.contribs = chunkByConditions(filteredContributions, [ ({date: date1}, {date: date2}) => +date1 !== +date2, - ({thing: thing1}, {thing: thing2}) => + ({thing: {thing: thing1}}, {thing: {thing: thing2}}) => (thing1.album ?? thing1) !== (thing2.album ?? thing2), ]); query.albums = query.contribs - .map(contribs => contribs[0].thing) + .map(contribs => contribs[0].thing.thing) .map(thing => thing.album ?? thing); return query; diff --git a/src/content/dependencies/generateArtistInfoPageChunk.js b/src/content/dependencies/generateArtistInfoPageChunk.js index fce68a7d..80429912 100644 --- a/src/content/dependencies/generateArtistInfoPageChunk.js +++ b/src/content/dependencies/generateArtistInfoPageChunk.js @@ -1,8 +1,6 @@ import {empty} from '#sugar'; export default { - extraDependencies: ['html', 'language'], - slots: { mode: { validate: v => v.is('flash', 'album'), diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js index 7987b642..8117ca9a 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js @@ -1,9 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: ['generateTextWithTooltip'], - extraDependencies: ['html', 'language'], - relations: (relation) => ({ textWithTooltip: relation('generateTextWithTooltip'), @@ -33,6 +30,11 @@ export default { type: 'html', mutable: false, }, + + originDetails: { + type: 'html', + mutable: false, + }, }, generate: (relations, slots, {html, language}) => @@ -40,52 +42,59 @@ export default { html.tag('li', slots.rerelease && {class: 'rerelease'}, - language.encapsulate(entryCapsule, workingCapsule => { - const workingOptions = {entry: slots.content}; - - if (!html.isBlank(slots.rereleaseTooltip)) { - workingCapsule += '.rerelease'; - workingOptions.rerelease = - relations.textWithTooltip.slots({ - attributes: {class: 'rerelease'}, - text: language.$(entryCapsule, 'rerelease.term'), - tooltip: slots.rereleaseTooltip, - }); - - return language.$(workingCapsule, workingOptions); - } - - if (!html.isBlank(slots.firstReleaseTooltip)) { - workingCapsule += '.firstRelease'; - workingOptions.firstRelease = - relations.textWithTooltip.slots({ - attributes: {class: 'first-release'}, - text: language.$(entryCapsule, 'firstRelease.term'), - tooltip: slots.firstReleaseTooltip, - }); - - return language.$(workingCapsule, workingOptions); - } - - let anyAccent = false; - - if (!empty(slots.otherArtistLinks)) { - anyAccent = true; - workingCapsule += '.withArtists'; - workingOptions.artists = - language.formatConjunctionList(slots.otherArtistLinks); - } - - if (!html.isBlank(slots.annotation)) { - anyAccent = true; - workingCapsule += '.withAnnotation'; - workingOptions.annotation = slots.annotation; - } - - if (anyAccent) { - return language.$(workingCapsule, workingOptions); - } else { - return slots.content; - } - }))), + html.tags([ + language.encapsulate(entryCapsule, workingCapsule => { + const workingOptions = {entry: slots.content}; + + if (!html.isBlank(slots.rereleaseTooltip)) { + workingCapsule += '.rerelease'; + workingOptions.rerelease = + relations.textWithTooltip.slots({ + attributes: {class: 'rerelease'}, + text: language.$(entryCapsule, 'rerelease.term'), + tooltip: slots.rereleaseTooltip, + }); + + return language.$(workingCapsule, workingOptions); + } + + if (!html.isBlank(slots.firstReleaseTooltip)) { + workingCapsule += '.firstRelease'; + workingOptions.firstRelease = + relations.textWithTooltip.slots({ + attributes: {class: 'first-release'}, + text: language.$(entryCapsule, 'firstRelease.term'), + tooltip: slots.firstReleaseTooltip, + }); + + return language.$(workingCapsule, workingOptions); + } + + let anyAccent = false; + + if (!empty(slots.otherArtistLinks)) { + anyAccent = true; + workingCapsule += '.withArtists'; + workingOptions.artists = + language.formatConjunctionList(slots.otherArtistLinks); + } + + if (!html.isBlank(slots.annotation)) { + anyAccent = true; + workingCapsule += '.withAnnotation'; + workingOptions.annotation = slots.annotation; + } + + if (anyAccent) { + return language.$(workingCapsule, workingOptions); + } else { + return slots.content; + } + }), + + html.tag('span', {class: 'origin-details'}, + {[html.onlyIfContent]: true}, + + slots.originDetails), + ]))), }; diff --git a/src/content/dependencies/generateArtistInfoPageChunkedList.js b/src/content/dependencies/generateArtistInfoPageChunkedList.js index e7915ab7..54577885 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageChunkedList.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { groupInfo: { type: 'html', diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js index d0c5e14e..caec58d6 100644 --- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js @@ -7,18 +7,6 @@ import { } from '#sort'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunk', - 'generateArtistInfoPageChunkItem', - 'linkAlbum', - 'linkFlash', - 'linkFlashAct', - 'linkTrack', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - query(artist, filterWikiEditorCommentary) { const processEntry = ({ thing, @@ -43,6 +31,7 @@ export default { flash, annotation: entry.annotation, + annotationParts: entry.annotationParts, }, }); @@ -88,10 +77,10 @@ export default { thing.commentary .filter(entry => entry.artists.includes(artist)) - .filter(({annotation}) => + .filter(entry => (filterWikiEditorCommentary - ? annotation?.match(/^wiki editor/i) - : !annotation?.match(/^wiki editor/i))) + ? entry.isWikiEditorCommentary + : !entry.isWikiEditorCommentary)) .map(entry => processEntry({thing, entry}))); @@ -184,11 +173,13 @@ export default { itemAnnotations: query.chunks .map(({chunk}) => chunk - .map(({annotation}) => + .map(entry => relation('transformContent', (filterWikiEditorCommentary - ? annotation?.replace(/^wiki editor(, )?/i, '') - : annotation)))), + ? entry.annotationParts + .filter(part => part !== 'wiki editor') + .join(', ') + : entry.annotation)))), }), data: (query, _artist, _filterWikiEditorCommentary) => ({ diff --git a/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js b/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js index f86dead7..eb32cebf 100644 --- a/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js +++ b/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js @@ -1,18 +1,10 @@ -import {sortChronologically} from '#sort'; +import {sortAlbumsTracksChronologically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateTooltip', - 'linkOtherReleaseOnArtistInfoPage', - ], - - extraDependencies: ['html', 'language'], - query: (track) => ({ rereleases: - sortChronologically(track.allReleases).slice(1), + sortAlbumsTracksChronologically(track.allReleases).slice(1), }), relations: (relation, query, track, artist) => ({ diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunk.js b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js index 8aa7223a..acdb9897 100644 --- a/src/content/dependencies/generateArtistInfoPageFlashesChunk.js +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js @@ -1,10 +1,4 @@ export default { - contentDependencies: [ - 'generateArtistInfoPageChunk', - 'generateArtistInfoPageFlashesChunkItem', - 'linkFlashAct', - ], - relations: (relation, flashAct, contribs) => ({ template: relation('generateArtistInfoPageChunk'), diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js index e4908bf9..36d7945d 100644 --- a/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js @@ -1,8 +1,4 @@ export default { - contentDependencies: ['generateArtistInfoPageChunkItem', 'linkFlash'], - - extraDependencies: ['language'], - relations: (relation, contrib) => ({ // Flashes and games can list multiple contributors as collaborative // credits, but we don't display these on the artist page, since they diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js index b347faf5..762386a2 100644 --- a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js @@ -3,13 +3,6 @@ import {sortContributionsChronologically, sortFlashesChronologically} import {chunkByConditions, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunkedList', - 'generateArtistInfoPageFlashesChunk', - ], - - extraDependencies: ['wikiData'], - sprawl: ({wikiInfo}) => ({ enableFlashesAndGames: wikiInfo.enableFlashesAndGames, diff --git a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js index dcee9c00..afb61c33 100644 --- a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js +++ b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js @@ -1,8 +1,6 @@ import {unique} from '#sugar'; export default { - contentDependencies: ['linkArtist'], - query(contribs) { const associatedContributionsByOtherArtists = contribs diff --git a/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js b/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js index 1d849919..70bada19 100644 --- a/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js +++ b/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js @@ -1,17 +1,9 @@ -import {sortChronologically} from '#sort'; +import {sortAlbumsTracksChronologically} from '#sort'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateTooltip', - 'linkOtherReleaseOnArtistInfoPage' - ], - - extraDependencies: ['html', 'language'], - query: (track) => ({ firstRelease: - sortChronologically(track.allReleases)[0], + sortAlbumsTracksChronologically(track.allReleases)[0], }), relations: (relation, query, track, artist) => ({ diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunk.js b/src/content/dependencies/generateArtistInfoPageTracksChunk.js index f6d70901..3e4cc4e9 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunk.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunk.js @@ -2,12 +2,6 @@ import {unique} from '#sugar'; import {getTotalDuration} from '#wiki-data'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunk', - 'generateArtistInfoPageTracksChunkItem', - 'linkAlbum', - ], - relations: (relation, artist, album, trackContribLists) => ({ template: relation('generateArtistInfoPageChunk'), diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js index a42d6fee..895cc0d8 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js @@ -1,18 +1,8 @@ -import {sortChronologically} from '#sort'; +import {sortAlbumsTracksChronologically} from '#sort'; import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunkItem', - 'generateArtistInfoPageFirstReleaseTooltip', - 'generateArtistInfoPageOtherArtistLinks', - 'generateArtistInfoPageRereleaseTooltip', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - - query (_artist, contribs) { + query(_artist, contribs) { const query = {}; // TODO: Very mysterious what to do if the set of contributions is, @@ -22,11 +12,11 @@ export default { const creditedAsArtist = contribs - .some(contrib => contrib.isArtistContribution); + .some(contrib => contrib.thingProperty === 'artistContribs'); const creditedAsContributor = contribs - .some(contrib => contrib.isContributorContribution); + .some(contrib => contrib.thingProperty === 'contributorContribs'); const annotatedContribs = contribs @@ -34,11 +24,11 @@ export default { const annotatedArtistContribs = annotatedContribs - .filter(contrib => contrib.isArtistContribution); + .filter(contrib => contrib.thingProperty === 'artistContribs'); const annotatedContributorContribs = annotatedContribs - .filter(contrib => contrib.isContributorContribution); + .filter(contrib => contrib.thingProperty === 'contributorContribs'); // Don't display annotations associated with crediting in the // Contributors field if the artist is also credited as an Artist @@ -73,7 +63,7 @@ export default { // different - and it's the latter that determines whether the // track is a rerelease! const allReleasesChronologically = - sortChronologically(query.track.allReleases); + sortAlbumsTracksChronologically(query.track.allReleases); query.isFirstRelease = allReleasesChronologically[0] === query.track; diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js index 84eb29ac..15588ed3 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js @@ -4,11 +4,6 @@ import {stitchArrays} from '#sugar'; import {chunkArtistTrackContributions} from '#wiki-data'; export default { - contentDependencies: [ - 'generateArtistInfoPageChunkedList', - 'generateArtistInfoPageTracksChunk', - ], - query(artist) { const query = {}; diff --git a/src/content/dependencies/generateArtistNavLinks.js b/src/content/dependencies/generateArtistNavLinks.js index 1b4b6eca..69ae3e19 100644 --- a/src/content/dependencies/generateArtistNavLinks.js +++ b/src/content/dependencies/generateArtistNavLinks.js @@ -1,14 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'linkArtist', - 'linkArtistGallery', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({ enableListings: wikiInfo.enableListings, @@ -34,6 +26,9 @@ export default { (query.hasGallery ? relation('linkArtistGallery', artist) : null), + + artistRollingWindowLink: + relation('linkArtistRollingWindow', artist), }), data: (_query, sprawl) => ({ @@ -45,7 +40,7 @@ export default { showExtraLinks: {type: 'boolean', default: false}, currentExtra: { - validate: v => v.is('gallery'), + validate: v => v.is('gallery', 'rolling-window'), }, }, @@ -79,6 +74,7 @@ export default { }), slots.showExtraLinks && + slots.currentExtra !== 'rolling-window' && relations.artistGalleryLink?.slots({ attributes: [ slots.currentExtra === 'gallery' && @@ -87,6 +83,12 @@ export default { content: language.$('misc.nav.gallery'), }), + + slots.currentExtra === 'rolling-window' && + relations.artistRollingWindowLink.slots({ + attributes: {class: 'current'}, + content: language.$('misc.nav.rollingWindow'), + }), ], }), }, diff --git a/src/content/dependencies/generateArtistRollingWindowPage.js b/src/content/dependencies/generateArtistRollingWindowPage.js new file mode 100644 index 00000000..aafd1b55 --- /dev/null +++ b/src/content/dependencies/generateArtistRollingWindowPage.js @@ -0,0 +1,418 @@ +import {sortAlbumsTracksChronologically} from '#sort'; +import Thing from '#thing'; + +import { + chunkByConditions, + filterMultipleArrays, + empty, + sortMultipleArrays, + stitchArrays, + unique, +} from '#sugar'; + +export default { + sprawl: ({groupCategoryData}) => ({ + groupCategoryData, + }), + + query(sprawl, artist) { + const query = {}; + + const musicContributions = + artist.musicContributions + .filter(contrib => contrib.date); + + const artworkContributions = + artist.artworkContributions + .filter(contrib => + contrib.date && + contrib.thingProperty !== 'wallpaperArtistContribs' && + contrib.thingProperty !== 'bannerArtistContribs'); + + const musicThings = + musicContributions + .map(contrib => contrib.thing); + + const artworkThings = + artworkContributions + .map(contrib => contrib.thing.thing); + + const musicContributionDates = + musicContributions + .map(contrib => contrib.date); + + const artworkContributionDates = + artworkContributions + .map(contrib => contrib.date); + + const musicContributionKinds = + musicContributions + .map(() => 'music'); + + const artworkContributionKinds = + artworkContributions + .map(() => 'artwork'); + + const allThings = [ + ...artworkThings, + ...musicThings, + ]; + + const allContributionDates = [ + ...artworkContributionDates, + ...musicContributionDates, + ]; + + const allContributionKinds = [ + ...artworkContributionKinds, + ...musicContributionKinds, + ]; + + const sortedThings = + sortAlbumsTracksChronologically(allThings.slice(), {latestFirst: true}); + + sortMultipleArrays( + allThings, + allContributionDates, + allContributionKinds, + (thing1, thing2) => + sortedThings.indexOf(thing1) - + sortedThings.indexOf(thing2)); + + const sourceIndices = + Array.from({length: allThings.length}, (_, i) => i); + + const sourceChunks = + chunkByConditions(sourceIndices, [ + (index1, index2) => + allThings[index1] !== + allThings[index2], + ]); + + const indicesTo = array => index => array[index]; + + query.things = + sourceChunks + .map(chunks => allThings[chunks[0]]); + + query.thingGroups = + query.things.map(thing => + (thing.constructor[Thing.referenceType] === 'album' + ? thing.groups + : thing.constructor[Thing.referenceType] === 'track' + ? thing.album.groups + : null)); + + query.thingContributionDates = + sourceChunks + .map(indices => indices + .map(indicesTo(allContributionDates))); + + query.thingContributionKinds = + sourceChunks + .map(indices => indices + .map(indicesTo(allContributionKinds))); + + // Matches the "kind" dropdown. + const kinds = ['artwork', 'music', 'flash']; + + const allKinds = + unique(query.thingContributionKinds.flat(2)); + + query.kinds = + kinds + .filter(kind => allKinds.includes(kind)); + + query.firstKind = + query.kinds.at(0); + + query.thingArtworks = + stitchArrays({ + thing: query.things, + kinds: query.thingContributionKinds, + }).map(({thing, kinds}) => + (kinds.includes('artwork') + ? (thing.coverArtworks ?? thing.trackArtworks ?? []) + .find(artwork => artwork.artistContribs + .some(contrib => contrib.artist === artist)) + : (thing.coverArtworks ?? thing.trackArtworks)?.[0] ?? + thing.album?.coverArtworks[0] ?? + null)); + + const allGroups = + unique(query.thingGroups.flat()); + + query.groupCategories = + sprawl.groupCategoryData.slice(); + + query.groupCategoryGroups = + sprawl.groupCategoryData + .map(category => category.groups + .filter(group => allGroups.includes(group))); + + filterMultipleArrays( + query.groupCategories, + query.groupCategoryGroups, + (_category, groups) => !empty(groups)); + + const groupsMatchingFirstKind = + unique( + stitchArrays({ + thing: query.things, + groups: query.thingGroups, + kinds: query.thingContributionKinds, + }).filter(({kinds}) => kinds.includes(query.firstKind)) + .flatMap(({groups}) => groups)); + + query.firstGroup = + sprawl.groupCategoryData + .flatMap(category => category.groups) + .find(group => groupsMatchingFirstKind.includes(group)); + + query.firstGroupCategory = + query.firstGroup.category; + + return query; + }, + + relations: (relation, query, sprawl, artist) => ({ + layout: + relation('generatePageLayout'), + + artistNavLinks: + relation('generateArtistNavLinks', artist), + + sourceGrid: + relation('generateCoverGrid'), + + sourceGridImages: + query.thingArtworks + .map(artwork => relation('image', artwork)), + + sourceGridLinks: + query.things + .map(thing => relation('linkAnythingMan', thing)), + }), + + data: (query, sprawl, artist) => ({ + name: + artist.name, + + categoryGroupDirectories: + query.groupCategoryGroups + .map(groups => groups + .map(group => group.directory)), + + categoryGroupNames: + query.groupCategoryGroups + .map(groups => groups + .map(group => group.name)), + + firstGroupCategoryIndex: + query.groupCategories + .indexOf(query.firstGroupCategory), + + firstGroupIndex: + stitchArrays({ + category: query.groupCategories, + groups: query.groupCategoryGroups, + }).find(({category}) => category === query.firstGroupCategory) + .groups + .indexOf(query.firstGroup), + + kinds: + query.kinds, + + sourceGridNames: + query.things + .map(thing => thing.name), + + sourceGridGroupDirectories: + query.thingGroups + .map(groups => groups + .map(group => group.directory)), + + sourceGridGroupNames: + query.thingGroups + .map(groups => groups + .map(group => group.name)), + + sourceGridContributionKinds: + query.thingContributionKinds, + + sourceGridContributionDates: + query.thingContributionDates, + }), + + generate: (data, relations, {html, language}) => + relations.layout.slots({ + title: + language.$('artistRollingWindowPage.title', { + artist: data.name, + }), + + mainClasses: ['top-index'], + mainContent: [ + html.tag('p', {id: 'timeframe-configuration'}, + language.$('artistRollingWindowPage.windowConfigurationLine', { + timeBefore: + language.$('artistRollingWindowPage.timeframe.months', { + input: + html.tag('input', {id: 'timeframe-months-before'}, + {type: 'number'}, + {value: 3, min: 0}), + }), + + timeAfter: + language.$('artistRollingWindowPage.timeframe.months', { + input: + html.tag('input', {id: 'timeframe-months-after'}, + {type: 'number'}, + {value: 3, min: 1}), + }), + + peek: + language.$('artistRollingWindowPage.timeframe.months', { + input: + html.tag('input', {id: 'timeframe-months-peek'}, + {type: 'number'}, + {value: 1, min: 0}), + }), + })), + + html.tag('p', {id: 'contribution-configuration'}, + language.$('artistRollingWindowPage.contributionConfigurationLine', { + kind: + html.tag('select', {id: 'contribution-kind'}, + data.kinds.map(kind => + html.tag('option', {value: kind}, + language.$('artistRollingWindowPage.contributionKind', kind)))), + + group: + html.tag('select', {id: 'contribution-group'}, [ + html.tag('option', {value: '-'}, + language.$('artistRollingWindowPage.contributionGroup.all')), + + stitchArrays({ + names: data.categoryGroupNames, + directories: data.categoryGroupDirectories, + }).map(({names, directories}, categoryIndex) => [ + html.tag('hr'), + + stitchArrays({name: names, directory: directories}) + .map(({name, directory}, groupIndex) => + html.tag('option', {value: directory}, + categoryIndex === data.firstGroupCategoryIndex && + groupIndex === data.firstGroupIndex && + {selected: true}, + + language.$('artistRollingWindowPage.contributionGroup.group', { + group: name, + }))), + ]), + ]), + })), + + html.tag('p', {id: 'timeframe-selection-info'}, [ + html.tag('span', {id: 'timeframe-selection-some'}, + {style: 'display: none'}, + + language.$('artistRollingWindowPage.timeframeSelectionLine', { + contributions: + html.tag('b', {id: 'timeframe-selection-contribution-count'}), + + timeframes: + html.tag('b', {id: 'timeframe-selection-timeframe-count'}), + + firstDate: + html.tag('b', {id: 'timeframe-selection-first-date'}), + + lastDate: + html.tag('b', {id: 'timeframe-selection-last-date'}), + })), + + html.tag('span', {id: 'timeframe-selection-none'}, + {style: 'display: none'}, + language.$('artistRollingWindowPage.timeframeSelectionLine.none')), + ]), + + html.tag('p', {id: 'timeframe-selection-control'}, + {style: 'display: none'}, + + language.$('artistRollingWindowPage.timeframeSelectionControl', { + timeframes: + html.tag('select', {id: 'timeframe-selection-menu'}), + + previous: + html.tag('a', {id: 'timeframe-selection-previous'}, + {href: '#'}, + language.$('artistRollingWindowPage.timeframeSelectionControl.previous')), + + next: + html.tag('a', {id: 'timeframe-selection-next'}, + {href: '#'}, + language.$('artistRollingWindowPage.timeframeSelectionControl.next')), + })), + + html.tag('div', {id: 'timeframe-source-area'}, [ + html.tag('p', {id: 'timeframe-empty'}, + {style: 'display: none'}, + language.$('artistRollingWindowPage.emptyTimeframeLine')), + + relations.sourceGrid.slots({ + attributes: {style: 'display: none'}, + + lazy: true, + + links: + relations.sourceGridLinks.map(link => + link.slot('attributes', {target: '_blank'})), + + names: + data.sourceGridNames, + + images: + relations.sourceGridImages, + + info: + stitchArrays({ + contributionKinds: data.sourceGridContributionKinds, + contributionDates: data.sourceGridContributionDates, + groupDirectories: data.sourceGridGroupDirectories, + groupNames: data.sourceGridGroupNames, + }).map(({ + contributionKinds, + contributionDates, + groupDirectories, + groupNames, + }) => [ + stitchArrays({ + directory: groupDirectories, + name: groupNames, + }).map(({directory, name}) => + html.tag('data', {class: 'contribution-group'}, + {value: directory}, + name)), + + stitchArrays({ + kind: contributionKinds, + date: contributionDates, + }).map(({kind, date}) => + html.tag('time', {class: `${kind}-contribution-date`}, + {datetime: date.toUTCString()}, + language.formatDate(date))), + ]), + }), + ]), + ], + + navLinkStyle: 'hierarchical', + navLinks: + relations.artistNavLinks + .slots({ + showExtraLinks: true, + currentExtra: 'rolling-window', + }) + .content, + }), +} diff --git a/src/content/dependencies/generateBackToAlbumLink.js b/src/content/dependencies/generateBackToAlbumLink.js index 6648b463..08d33348 100644 --- a/src/content/dependencies/generateBackToAlbumLink.js +++ b/src/content/dependencies/generateBackToAlbumLink.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkAlbum'], - extraDependencies: ['language'], - relations: (relation, track) => ({ trackLink: relation('linkAlbum', track), diff --git a/src/content/dependencies/generateBackToTrackLink.js b/src/content/dependencies/generateBackToTrackLink.js index 8677d811..90dfb6d5 100644 --- a/src/content/dependencies/generateBackToTrackLink.js +++ b/src/content/dependencies/generateBackToTrackLink.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkTrack'], - extraDependencies: ['language'], - relations: (relation, track) => ({ trackLink: relation('linkTrack', track), diff --git a/src/content/dependencies/generateBanner.js b/src/content/dependencies/generateBanner.js index 15eb08eb..509b15c2 100644 --- a/src/content/dependencies/generateBanner.js +++ b/src/content/dependencies/generateBanner.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'to'], - slots: { path: { validate: v => v.validateArrayItems(v.isString), diff --git a/src/content/dependencies/generateCollapsedContentEntrySection.js b/src/content/dependencies/generateCollapsedContentEntrySection.js new file mode 100644 index 00000000..aec5fe28 --- /dev/null +++ b/src/content/dependencies/generateCollapsedContentEntrySection.js @@ -0,0 +1,37 @@ +export default { + relations: (relation, entries, thing) => ({ + contentContentHeading: + relation('generateContentContentHeading', thing), + + entries: + entries + .map(entry => relation('generateCommentaryEntry', entry)), + }), + + slots: { + id: {type: 'string'}, + string: {type: 'string'}, + }, + + generate: (relations, slots, {html}) => + html.tag('details', + {[html.onlyIfContent]: true}, + + slots.id && [ + {class: 'memorable'}, + {'data-memorable-id': slots.id}, + ], + + [ + relations.contentContentHeading.slots({ + attributes: [ + slots.id && {id: slots.id}, + ], + + string: slots.string, + summary: true, + }), + + relations.entries, + ]), +}; diff --git a/src/content/dependencies/generateColorStyleAttribute.js b/src/content/dependencies/generateColorStyleAttribute.js index 03d95ac5..277ec434 100644 --- a/src/content/dependencies/generateColorStyleAttribute.js +++ b/src/content/dependencies/generateColorStyleAttribute.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateColorStyleVariables'], - extraDependencies: ['html'], - relations: (relation) => ({ colorVariables: relation('generateColorStyleVariables'), diff --git a/src/content/dependencies/generateColorStyleRules.js b/src/content/dependencies/generateColorStyleRules.js deleted file mode 100644 index c412b8f2..00000000 --- a/src/content/dependencies/generateColorStyleRules.js +++ /dev/null @@ -1,42 +0,0 @@ -export default { - contentDependencies: ['generateColorStyleVariables'], - extraDependencies: ['html'], - - relations: (relation) => ({ - variables: - relation('generateColorStyleVariables'), - }), - - data: (color) => ({ - color: - color ?? null, - }), - - slots: { - color: { - validate: v => v.isColor, - }, - }, - - generate(data, relations, slots) { - const color = data.color ?? slots.color; - - if (!color) { - return ''; - } - - return [ - `:root {`, - ...( - relations.variables - .slots({ - color, - context: 'page-root', - mode: 'property-list', - }) - .content - .map(line => line + ';')), - `}`, - ].join('\n'); - }, -}; diff --git a/src/content/dependencies/generateColorStyleTag.js b/src/content/dependencies/generateColorStyleTag.js new file mode 100644 index 00000000..b378fd1d --- /dev/null +++ b/src/content/dependencies/generateColorStyleTag.js @@ -0,0 +1,48 @@ +export default { + relations: (relation) => ({ + styleTag: + relation('generateStyleTag'), + + variables: + relation('generateColorStyleVariables'), + }), + + data: (color) => ({ + color: + color ?? null, + }), + + slots: { + color: { + validate: v => v.isColor, + }, + }, + + generate(data, relations, slots, {html}) { + const color = + data.color ?? slots.color; + + if (!color) { + return html.blank(); + } + + return relations.styleTag.slots({ + attributes: [ + {class: 'color-style'}, + {'data-color': color}, + ], + + rules: [ + { + select: ':root', + declare: + relations.variables.slots({ + color, + context: 'page-root', + mode: 'declarations', + }).content, + }, + ], + }); + }, +}; diff --git a/src/content/dependencies/generateColorStyleVariables.js b/src/content/dependencies/generateColorStyleVariables.js index 5270dbe4..0865ed3e 100644 --- a/src/content/dependencies/generateColorStyleVariables.js +++ b/src/content/dependencies/generateColorStyleVariables.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'getColors'], - slots: { color: { validate: v => v.isColor, @@ -18,7 +16,7 @@ export default { }, mode: { - validate: v => v.is('style', 'property-list'), + validate: v => v.is('style', 'declarations'), default: 'style', }, }, @@ -50,15 +48,15 @@ export default { `--shadow-color: ${shadow}`, ]; - let selectedProperties; + let selectedDeclarations; switch (slots.context) { case 'any-content': - selectedProperties = anyContent; + selectedDeclarations = anyContent; break; case 'image-box': - selectedProperties = [ + selectedDeclarations = [ `--primary-color: ${primary}`, `--dim-color: ${dim}`, `--deep-color: ${deep}`, @@ -67,14 +65,14 @@ export default { break; case 'page-root': - selectedProperties = [ + selectedDeclarations = [ ...anyContent, `--page-primary-color: ${primary}`, ]; break; case 'primary-only': - selectedProperties = [ + selectedDeclarations = [ `--primary-color: ${primary}`, ]; break; @@ -82,10 +80,10 @@ export default { switch (slots.mode) { case 'style': - return selectedProperties.join('; '); + return selectedDeclarations.join('; '); - case 'property-list': - return selectedProperties; + case 'declarations': + return selectedDeclarations.map(declaration => declaration + ';'); } }, }; diff --git a/src/content/dependencies/generateCommentaryContentHeading.js b/src/content/dependencies/generateCommentaryContentHeading.js new file mode 100644 index 00000000..691762aa --- /dev/null +++ b/src/content/dependencies/generateCommentaryContentHeading.js @@ -0,0 +1,43 @@ +import {empty} from '#sugar'; + +export default { + query: (thing) => ({ + entries: + (thing.isTrack + ? [...thing.commentary, ...thing.commentaryFromMainRelease] + : thing.commentary), + }), + + relations: (relation, _query, thing) => ({ + contentContentHeading: + relation('generateContentContentHeading', thing), + }), + + data: (query, _thing) => ({ + hasWikiEditorCommentary: + query.entries.some(entry => entry.isWikiEditorCommentary), + + onlyWikiEditorCommentary: + !empty(query.entries) && + query.entries.every(entry => entry.isWikiEditorCommentary), + + hasAnyCommentary: + !empty(query.entries), + }), + + generate: (data, relations, {language}) => + relations.contentContentHeading.slots({ + // It's #artist-commentary for legacy reasons... Sorry... + attributes: {id: 'artist-commentary'}, + + string: + language.encapsulate('misc.artistCommentary', capsule => + (data.onlyWikiEditorCommentary + ? language.encapsulate(capsule, 'onlyWikiCommentary') + : data.hasWikiEditorCommentary + ? language.encapsulate(capsule, 'withWikiCommentary') + : data.hasAnyCommentary + ? capsule + : null)), + }), +}; diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js index c93020f3..38eb6b43 100644 --- a/src/content/dependencies/generateCommentaryEntry.js +++ b/src/content/dependencies/generateCommentaryEntry.js @@ -1,25 +1,16 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateCommentaryEntryDate', - 'generateColorStyleAttribute', - 'linkArtist', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, entry) => ({ artistLinks: - (!empty(entry.artists) && !entry.artistDisplayText + (!empty(entry.artists) && !entry.artistText ? entry.artists .map(artist => relation('linkArtist', artist)) : null), artistsContent: - (entry.artistDisplayText - ? relation('transformContent', entry.artistDisplayText) + (entry.artistText + ? relation('transformContent', entry.artistText) : null), annotationContent: @@ -39,11 +30,16 @@ export default { relation('generateCommentaryEntryDate', entry), }), + data: (entry) => ({ + isWikiEditorCommentary: + entry.isWikiEditorCommentary, + }), + slots: { color: {validate: v => v.isColor}, }, - generate: (relations, slots, {html, language}) => + generate: (data, relations, slots, {html, language}) => language.encapsulate('misc.artistCommentary.entry', entryCapsule => html.tags([ html.tag('p', {class: 'commentary-entry-heading'}, @@ -107,6 +103,9 @@ export default { relations.colorStyle.clone() .slot('color', slots.color), + data.isWikiEditorCommentary && + {class: 'wiki-commentary'}, + relations.bodyContent.slot('mode', 'multiline')), ])), }; diff --git a/src/content/dependencies/generateCommentaryEntryDate.js b/src/content/dependencies/generateCommentaryEntryDate.js index f1cf5cb3..e924f244 100644 --- a/src/content/dependencies/generateCommentaryEntryDate.js +++ b/src/content/dependencies/generateCommentaryEntryDate.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateTextWithTooltip', 'generateTooltip'], - extraDependencies: ['html', 'language'], - relations: (relation, _entry) => ({ textWithTooltip: relation('generateTextWithTooltip'), diff --git a/src/content/dependencies/generateCommentaryIndexPage.js b/src/content/dependencies/generateCommentaryIndexPage.js index d68ba42e..4da3ecb9 100644 --- a/src/content/dependencies/generateCommentaryIndexPage.js +++ b/src/content/dependencies/generateCommentaryIndexPage.js @@ -2,9 +2,6 @@ import {sortChronologically} from '#sort'; import {accumulateSum, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generatePageLayout', 'linkAlbumCommentary'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/generateContentContentHeading.js b/src/content/dependencies/generateContentContentHeading.js new file mode 100644 index 00000000..9ed2d9f0 --- /dev/null +++ b/src/content/dependencies/generateContentContentHeading.js @@ -0,0 +1,73 @@ +export default { + relations: (relation, _thing) => ({ + contentHeading: + relation('generateContentHeading'), + }), + + data: (thing) => ({ + name: + (thing + ? thing.name + : null), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + string: { + type: 'string', + }, + + summary: { + type: 'boolean', + default: false, + }, + }, + + generate: (data, relations, slots, {html, language}) => + relations.contentHeading.slots({ + attributes: slots.attributes, + + title: + (() => { + if (!slots.string) return html.blank(); + + const options = {}; + + if (slots.summary) { + options.cue = + html.tag('span', {class: 'cue'}, + language.$(slots.string, 'cue')); + } + + if (data.name) { + options.thing = html.tag('i', data.name); + } + + if (slots.summary) { + return html.tags([ + html.tag('span', {class: 'when-open'}, + language.$(slots.string, options)), + + html.tag('span', {class: 'when-collapsed'}, + language.$(slots.string, 'collapsed', options)), + ]); + } else { + return language.$(slots.string, options); + } + })(), + + stickyTitle: + (slots.string + ? language.$(slots.string, 'sticky') + : html.blank()), + + tag: + (slots.summary + ? 'summary' + : 'p'), + }), +}; diff --git a/src/content/dependencies/generateContentHeading.js b/src/content/dependencies/generateContentHeading.js index f52bc043..a7cf201f 100644 --- a/src/content/dependencies/generateContentHeading.js +++ b/src/content/dependencies/generateContentHeading.js @@ -1,7 +1,5 @@ export default { extraDependencies: ['html'], - contentDependencies: ['generateColorStyleAttribute'], - relations: (relation) => ({ colorStyle: relation('generateColorStyleAttribute'), }), diff --git a/src/content/dependencies/generateContributionList.js b/src/content/dependencies/generateContributionList.js index d1c3de0f..3716bcd6 100644 --- a/src/content/dependencies/generateContributionList.js +++ b/src/content/dependencies/generateContributionList.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkContribution'], - extraDependencies: ['html'], - relations: (relation, contributions) => ({ contributionLinks: contributions diff --git a/src/content/dependencies/generateContributionTooltip.js b/src/content/dependencies/generateContributionTooltip.js index 3a31014d..fd16371b 100644 --- a/src/content/dependencies/generateContributionTooltip.js +++ b/src/content/dependencies/generateContributionTooltip.js @@ -1,21 +1,79 @@ -export default { - contentDependencies: [ - 'generateContributionTooltipChronologySection', - 'generateContributionTooltipExternalLinkSection', - 'generateTooltip', - ], +function compareReleaseContributions(a, b) { + if (a === b) { + return true; + } + + const {previous: aPrev, next: aNext} = getSiblings(a); + const {previous: bPrev, next: bNext} = getSiblings(b); + + const effective = contrib => + (contrib?.thing.isAlbum && contrib.thing.style === 'single' + ? contrib.thing.tracks[0] + : contrib?.thing); + + return ( + effective(aPrev) === effective(bPrev) && + effective(aNext) === effective(bNext) + ); +} - extraDependencies: ['html'], +function getSiblings(contribution) { + let previous = contribution; + while (previous && previous.thing === contribution.thing) { + previous = previous.previousBySameArtist; + } - relations: (relation, contribution) => ({ + let next = contribution; + while (next && next.thing === contribution.thing) { + next = next.nextBySameArtist; + } + + return {previous, next}; +} + +export default { + query: (contribution) => ({ + albumArtistContribution: + (contribution.thing.isTrack + ? contribution.thing.album.artistContribs + .find(artistContrib => artistContrib.artist === contribution.artist) + : null), + }), + + relations: (relation, query, contribution) => ({ tooltip: relation('generateTooltip'), externalLinkSection: relation('generateContributionTooltipExternalLinkSection', contribution), - chronologySection: + ownChronologySection: relation('generateContributionTooltipChronologySection', contribution), + + artistReleaseChronologySection: + (query.albumArtistContribution + ? relation('generateContributionTooltipChronologySection', + query.albumArtistContribution) + : null), + }), + + data: (query, contribution) => ({ + artistName: + contribution.artist.name, + + isAlbumArtistContribution: + contribution.thing.isAlbum && + contribution.thingProperty === 'artistContribs', + + isSingleTrackArtistContribution: + contribution.thing.isTrack && + contribution.thingProperty === 'artistContribs' && + contribution.thing.album.style === 'single', + + artistReleaseChronologySectionDiffers: + (query.albumArtistContribution + ? !compareReleaseContributions(contribution, query.albumArtistContribution) + : null), }), slots: { @@ -25,24 +83,64 @@ export default { chronologyKind: {type: 'string'}, }, - generate: (relations, slots, {html}) => - relations.tooltip.slots({ - attributes: - {class: 'contribution-tooltip'}, - - contentAttributes: { - [html.joinChildren]: - html.tag('span', {class: 'tooltip-divider'}), - }, - - content: [ - slots.showExternalLinks && - relations.externalLinkSection, - - slots.showChronology && - relations.chronologySection.slots({ - kind: slots.chronologyKind, - }), - ], - }), + generate: (data, relations, slots, {html, language}) => + language.encapsulate('misc.artistLink', capsule => + relations.tooltip.slots({ + attributes: + {class: 'contribution-tooltip'}, + + contentAttributes: { + [html.joinChildren]: + html.tag('span', {class: 'tooltip-divider'}), + }, + + content: [ + slots.showExternalLinks && + relations.externalLinkSection, + + slots.showChronology && + language.encapsulate(capsule, 'chronology', capsule => { + const chronologySections = []; + + if (data.isAlbumArtistContribution) { + relations.ownChronologySection.setSlots({ + kind: 'release', + heading: + language.$(capsule, 'heading.artistReleases', { + artist: data.artistName, + }), + }); + } else { + relations.ownChronologySection.setSlot('kind', slots.chronologyKind); + } + + if ( + data.isSingleTrackArtistContribution && + relations.artistReleaseChronologySection + ) { + relations.artistReleaseChronologySection.setSlot('kind', 'release'); + + relations.artistReleaseChronologySection.setSlot('heading', + language.$(capsule, 'heading.artistReleases', { + artist: data.artistName, + })); + + chronologySections.push(relations.artistReleaseChronologySection); + + if (data.artistReleaseChronologySectionDiffers) { + relations.ownChronologySection.setSlot('heading', + language.$(capsule, 'heading.artistTracks', { + artist: data.artistName, + })); + + chronologySections.push(relations.ownChronologySection); + } + } else { + chronologySections.push(relations.ownChronologySection); + } + + return chronologySections; + }), + ], + })), }; diff --git a/src/content/dependencies/generateContributionTooltipChronologySection.js b/src/content/dependencies/generateContributionTooltipChronologySection.js index 78c9051c..e4b9bfda 100644 --- a/src/content/dependencies/generateContributionTooltipChronologySection.js +++ b/src/content/dependencies/generateContributionTooltipChronologySection.js @@ -1,20 +1,33 @@ -export default { - contentDependencies: ['linkAnythingMan'], - extraDependencies: ['html', 'language'], +function getName(thing) { + if (!thing) { + return null; + } - query(contribution) { - let previous = contribution; - while (previous && previous.thing === contribution.thing) { - previous = previous.previousBySameArtist; - } + if (thing.isArtwork) { + return thing.thing.name; + } - let next = contribution; - while (next && next.thing === contribution.thing) { - next = next.nextBySameArtist; - } + return thing.name; +} - return {previous, next}; - }, +function getSiblings(contribution) { + let previous = contribution; + while (previous && previous.thing === contribution.thing) { + previous = previous.previousBySameArtist; + } + + let next = contribution; + while (next && next.thing === contribution.thing) { + next = next.nextBySameArtist; + } + + return {previous, next}; +} + +export default { + query: (contribution) => ({ + ...getSiblings(contribution), + }), relations: (relation, query, _contribution) => ({ previousLink: @@ -30,34 +43,26 @@ export default { data: (query, _contribution) => ({ previousName: - (query.previous - ? query.previous.thing.name - : null), + getName(query.previous?.thing), nextName: - (query.next - ? query.next.thing.name - : null), + getName(query.next?.thing), }), slots: { - kind: { - validate: v => - v.is( - 'album', - 'bannerArt', - 'coverArt', - 'flash', - 'track', - 'trackArt', - 'trackContribution', - 'wallpaperArt'), - }, + heading: {type: 'html', mutable: false}, + kind: {type: 'string'}, }, generate: (data, relations, slots, {html, language}) => language.encapsulate('misc.artistLink.chronology', capsule => html.tags([ + html.tag('span', {class: 'chronology-heading'}, + {[html.onlyIfContent]: true}, + {[html.onlyIfSiblings]: true}, + + slots.heading), + html.tags([ relations.previousLink?.slots({ attributes: {class: 'chronology-link'}, diff --git a/src/content/dependencies/generateContributionTooltipExternalLinkSection.js b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js index 4f9a23ed..210db1e9 100644 --- a/src/content/dependencies/generateContributionTooltipExternalLinkSection.js +++ b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js @@ -1,14 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateExternalHandle', - 'generateExternalIcon', - 'generateExternalPlatform', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, contribution) => ({ icons: contribution.artist.urls diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index 06972d6b..89b66ce0 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -1,11 +1,49 @@ export default { - contentDependencies: ['image'], - extraDependencies: ['html'], + relations: (relation, artwork) => ({ + colorStyleAttribute: + relation('generateColorStyleAttribute'), + + image: + relation('image', artwork), + + originDetails: + relation('generateCoverArtworkOriginDetails', artwork), + + artTagDetails: + relation('generateCoverArtworkArtTagDetails', artwork), + + artistDetails: + relation('generateCoverArtworkArtistDetails', artwork), + + referenceDetails: + relation('generateCoverArtworkReferenceDetails', artwork), + }), + + data: (artwork) => ({ + attachAbove: + artwork.attachAbove, + + attachedArtworkIsMainArtwork: + (artwork.attachAbove + ? artwork.attachedArtwork.isMainArtwork + : null), + + color: + artwork.thing.color ?? null, + + dimensions: + artwork.dimensions, + + style: + artwork.style, + }), slots: { - image: { - type: 'html', - mutable: true, + alt: {type: 'string'}, + + color: { + validate: v => v.anyOf(v.isBoolean, v.isColor), + default: false, }, mode: { @@ -13,13 +51,10 @@ export default { default: 'primary', }, - dimensions: { - validate: v => v.isDimensions, - }, - - warnings: { - validate: v => v.looseArrayOf(v.isString), - }, + showOriginDetails: {type: 'boolean', default: false}, + showArtTagDetails: {type: 'boolean', default: false}, + showArtistDetails: {type: 'boolean', default: false}, + showReferenceDetails: {type: 'boolean', default: false}, details: { type: 'html', @@ -27,60 +62,96 @@ export default { }, }, - generate(slots, {html}) { + generate(data, relations, slots, {html}) { + const {image} = relations; + + const imgAttributes = html.attributes(); + + if (data.style) { + imgAttributes.add('style', data.style.split('\n').join(' ')); + } + + image.setSlot('imgAttributes', imgAttributes); + + image.setSlot('alt', slots.alt); + const square = - (slots.dimensions - ? slots.dimensions[0] === slots.dimensions[1] + (data.dimensions + ? data.dimensions[0] === data.dimensions[1] : true); - const sizeSlots = - (square - ? {square: true} - : {dimensions: slots.dimensions}); - - switch (slots.mode) { - case 'primary': - return html.tags([ - slots.image.slots({ - thumb: 'medium', - reveal: true, - link: true, - - warnings: slots.warnings, - ...sizeSlots, - }), - - slots.details, - ]); - - case 'thumbnail': - return ( - slots.image.slots({ - thumb: 'small', - reveal: false, - link: false, - - warnings: slots.warnings, - ...sizeSlots, - })); - - case 'commentary': - return ( - slots.image.slots({ - thumb: 'medium', - reveal: true, - link: true, - lazy: true, - - warnings: slots.warnings, - ...sizeSlots, - - attributes: - {class: 'commentary-art'}, - })); - - default: - return html.blank(); + if (square) { + image.setSlot('square', true); + } else { + image.setSlot('dimensions', data.dimensions); } + + const attributes = html.attributes(); + + let color = null; + if (typeof slots.color === 'boolean') { + if (slots.color) { + color = data.color; + } + } else if (slots.color) { + color = slots.color; + } + + if (color) { + relations.colorStyleAttribute.setSlot('color', color); + attributes.add(relations.colorStyleAttribute); + } + + return html.tags([ + data.attachAbove && + html.tag('div', {class: 'cover-artwork-joiner'}), + + html.tag('div', {class: 'cover-artwork'}, + slots.mode === 'commentary' && + {class: 'commentary-art'}, + + data.attachAbove && + data.attachedArtworkIsMainArtwork && + {class: 'attached-artwork-is-main-artwork'}, + + attributes, + + (slots.mode === 'primary' + ? [ + relations.image.slots({ + thumb: 'medium', + reveal: true, + link: true, + }), + + slots.showOriginDetails && + relations.originDetails, + + slots.showArtTagDetails && + relations.artTagDetails, + + slots.showArtistDetails && + relations.artistDetails, + + slots.showReferenceDetails && + relations.referenceDetails, + + slots.details, + ] + : slots.mode === 'thumbnail' + ? relations.image.slots({ + thumb: 'small', + reveal: false, + link: false, + }) + : slots.mode === 'commentary' + ? relations.image.slots({ + thumb: 'medium', + reveal: true, + link: true, + lazy: true, + }) + : html.blank())), + ]); }, }; diff --git a/src/content/dependencies/generateCoverArtworkArtTagDetails.js b/src/content/dependencies/generateCoverArtworkArtTagDetails.js index b4edbbdd..50571a4f 100644 --- a/src/content/dependencies/generateCoverArtworkArtTagDetails.js +++ b/src/content/dependencies/generateCoverArtworkArtTagDetails.js @@ -1,22 +1,39 @@ -import {stitchArrays} from '#sugar'; +import {compareArrays, empty, stitchArrays} from '#sugar'; -export default { - contentDependencies: ['linkArtTagGallery'], - extraDependencies: ['html'], +function linkable(tag) { + return !tag.isContentWarning; +} - query: (artTags) => ({ +export default { + query: (artwork) => ({ linkableArtTags: - artTags - .filter(tag => !tag.isContentWarning), + artwork.artTags.filter(linkable), + + mainArtworkLinkableArtTags: + (artwork.mainArtwork + ? artwork.mainArtwork.artTags.filter(linkable) + : null), }), - relations: (relation, query, _artTags) => ({ + relations: (relation, query, _artwork) => ({ artTagLinks: query.linkableArtTags .map(tag => relation('linkArtTagGallery', tag)), }), - data: (query, _artTags) => { + data: (query, artwork) => { + const data = {}; + + data.attachAbove = artwork.attachAbove; + + data.sameAsMainArtwork = + !artwork.isMainArtwork && + query.mainArtworkLinkableArtTags && + !empty(query.mainArtworkLinkableArtTags) && + compareArrays( + query.mainArtworkLinkableArtTags, + query.linkableArtTags); + const seenShortNames = new Set(); const duplicateShortNames = new Set(); @@ -28,23 +45,28 @@ export default { } } - const preferShortName = + data.preferShortName = query.linkableArtTags .map(artTag => !duplicateShortNames.has(artTag.nameShort)); - return {preferShortName}; + return data; }, - generate: (data, relations, {html}) => - html.tag('ul', {class: 'image-details'}, - {[html.onlyIfContent]: true}, + generate: (data, relations, {html, language}) => + language.encapsulate('misc.coverArtwork', capsule => + html.tag('ul', {class: 'image-details'}, + {[html.onlyIfContent]: true}, - {class: 'art-tag-details'}, + {class: 'art-tag-details'}, - stitchArrays({ - artTagLink: relations.artTagLinks, - preferShortName: data.preferShortName, - }).map(({artTagLink, preferShortName}) => - html.tag('li', - artTagLink.slot('preferShortName', preferShortName)))), + (data.sameAsMainArtwork && data.attachAbove + ? html.blank() + : data.sameAsMainArtwork && relations.artTagLinks.length >= 3 + ? language.$(capsule, 'sameTagsAsMainArtwork') + : stitchArrays({ + artTagLink: relations.artTagLinks, + preferShortName: data.preferShortName, + }).map(({artTagLink, preferShortName}) => + html.tag('li', + artTagLink.slot('preferShortName', preferShortName)))))), }; diff --git a/src/content/dependencies/generateCoverArtworkArtistDetails.js b/src/content/dependencies/generateCoverArtworkArtistDetails.js index 5b235353..2773c6fc 100644 --- a/src/content/dependencies/generateCoverArtworkArtistDetails.js +++ b/src/content/dependencies/generateCoverArtworkArtistDetails.js @@ -1,10 +1,7 @@ export default { - contentDependencies: ['linkArtistGallery'], - extraDependencies: ['html', 'language'], - - relations: (relation, contributions) => ({ + relations: (relation, artwork) => ({ artistLinks: - contributions + artwork.artistContribs .map(contrib => contrib.artist) .map(artist => relation('linkArtistGallery', artist)), @@ -17,6 +14,8 @@ export default { {class: 'illustrator-details'}, language.$('misc.coverGrid.details.coverArtists', { + [language.onlyIfOptions]: ['artists'], + artists: language.formatConjunctionList(relations.artistLinks), })), diff --git a/src/content/dependencies/generateCoverArtworkOriginDetails.js b/src/content/dependencies/generateCoverArtworkOriginDetails.js new file mode 100644 index 00000000..db18e9e4 --- /dev/null +++ b/src/content/dependencies/generateCoverArtworkOriginDetails.js @@ -0,0 +1,176 @@ +export default { + query: (artwork) => ({ + attachedArtistContribs: + (artwork.attachedArtwork + ? artwork.attachedArtwork.artistContribs + : null) + }), + + relations: (relation, query, artwork) => ({ + credit: + relation('generateArtistCredit', + artwork.artistContribs, + query.attachedArtistContribs ?? []), + + source: + relation('transformContent', artwork.source), + + originDetails: + relation('transformContent', artwork.originDetails), + + albumLink: + (artwork.thing.isAlbum + ? relation('linkAlbum', artwork.thing) + : null), + + datetimestamp: + (artwork.date && artwork.date !== artwork.thing.date + ? relation('generateAbsoluteDatetimestamp', artwork.date) + : null), + }), + + + data: (query, artwork) => ({ + label: + artwork.label, + + forAlbum: + artwork.thing.isAlbum, + + forSingleStyleAlbum: + artwork.thing.isAlbum && + artwork.thing.style === 'single', + + showFilename: + artwork.showFilename, + }), + + generate: (data, relations, {html, language, pagePath}) => + language.encapsulate('misc.coverArtwork', capsule => + html.tag('p', {class: 'image-details'}, + {[html.onlyIfContent]: true}, + + {class: 'origin-details'}, + + (() => { + relations.datetimestamp?.setSlots({ + style: 'year', + tooltip: true, + }); + + const artworkBy = + language.encapsulate(capsule, 'artworkBy', workingCapsule => { + const workingOptions = {}; + + if (data.label) { + workingCapsule += '.customLabel'; + workingOptions.label = data.label; + } + + if (relations.datetimestamp) { + workingCapsule += '.withYear'; + workingOptions.year = relations.datetimestamp; + } + + return relations.credit.slots({ + showAnnotation: true, + showExternalLinks: true, + showChronology: true, + showWikiEdits: true, + + trimAnnotation: false, + + chronologyKind: 'coverArt', + + normalStringKey: workingCapsule, + additionalStringOptions: workingOptions, + }); + }); + + const trackArtFromAlbum = + pagePath[0] === 'track' && + data.forAlbum && + !data.forSingleStyleAlbum && + language.$(capsule, 'trackArtFromAlbum', { + album: + relations.albumLink.slot('color', false), + }); + + const source = + language.encapsulate(capsule, 'source', workingCapsule => { + const workingOptions = { + [language.onlyIfOptions]: ['source'], + source: relations.source.slot('mode', 'inline'), + }; + + if (html.isBlank(artworkBy) && data.label) { + workingCapsule += '.customLabel'; + workingOptions.label = data.label; + } + + if (html.isBlank(artworkBy) && relations.datetimestamp) { + workingCapsule += '.withYear'; + workingOptions.year = relations.datetimestamp; + } + + return language.$(workingCapsule, workingOptions); + }); + + const label = + html.isBlank(artworkBy) && + html.isBlank(source) && + language.encapsulate(capsule, 'customLabel', workingCapsule => { + const workingOptions = { + [language.onlyIfOptions]: ['label'], + label: data.label, + }; + + if (relations.datetimestamp) { + workingCapsule += '.withYear'; + workingOptions.year = relations.datetimestamp; + } + + return language.$(workingCapsule, workingOptions); + }); + + const year = + html.isBlank(artworkBy) && + html.isBlank(source) && + html.isBlank(label) && + language.$(capsule, 'year', { + [language.onlyIfOptions]: ['year'], + year: relations.datetimestamp, + }); + + const originDetailsLine = + html.tag('span', {class: 'origin-details-line'}, + {[html.onlyIfContent]: true}, + + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + })); + + const filenameLine = + html.tag('span', {class: 'filename-line'}, + {[html.onlyIfContent]: true}, + + html.tag('code', {class: 'filename'}, + {[html.onlyIfContent]: true}, + + language.sanitize(data.showFilename))); + + return [ + html.tags([ + artworkBy, + trackArtFromAlbum, + source, + label, + year, + ], {[html.joinChildren]: html.tag('br')}), + + originDetailsLine, + filenameLine, + ]; + })())), +}; diff --git a/src/content/dependencies/generateCoverArtworkReferenceDetails.js b/src/content/dependencies/generateCoverArtworkReferenceDetails.js index 006b2b4b..d4e4e7e4 100644 --- a/src/content/dependencies/generateCoverArtworkReferenceDetails.js +++ b/src/content/dependencies/generateCoverArtworkReferenceDetails.js @@ -1,20 +1,21 @@ export default { - extraDependencies: ['html', 'language'], + relations: (relation, artwork) => ({ + referencedArtworksLink: + relation('linkReferencedArtworks', artwork), - data: (referenced, referencedBy) => ({ + referencingArtworksLink: + relation('linkReferencingArtworks', artwork), + }), + + data: (artwork) => ({ referenced: - referenced.length, + artwork.referencedArtworks.length, referencedBy: - referencedBy.length, + artwork.referencedByArtworks.length, }), - slots: { - referencedLink: {type: 'html', mutable: true}, - referencingLink: {type: 'html', mutable: true}, - }, - - generate: (data, slots, {html, language}) => + generate: (data, relations, {html, language}) => language.encapsulate('releaseInfo', capsule => { const referencedText = language.$(capsule, 'referencesArtworks', { @@ -47,10 +48,10 @@ export default { [ !html.isBlank(referencedText) && - slots.referencedLink.slot('content', referencedText), + relations.referencedArtworksLink.slot('content', referencedText), !html.isBlank(referencingText) && - slots.referencingLink.slot('content', referencingText), + relations.referencingArtworksLink.slot('content', referencingText), ])); }), } diff --git a/src/content/dependencies/generateCoverCarousel.js b/src/content/dependencies/generateCoverCarousel.js index 430f651e..1ffeff8e 100644 --- a/src/content/dependencies/generateCoverCarousel.js +++ b/src/content/dependencies/generateCoverCarousel.js @@ -2,8 +2,6 @@ import {empty, repeat, stitchArrays} from '#sugar'; import {getCarouselLayoutForNumberOfItems} from '#wiki-data'; export default { - extraDependencies: ['html'], - slots: { images: {validate: v => v.strictArrayOf(v.isHTML)}, links: {validate: v => v.strictArrayOf(v.isHTML)}, @@ -19,7 +17,7 @@ export default { }); if (empty(stitched)) { - return; + return html.blank(); } const layout = getCarouselLayoutForNumberOfItems(stitched.length); diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index 1898832f..091833a9 100644 --- a/src/content/dependencies/generateCoverGrid.js +++ b/src/content/dependencies/generateCoverGrid.js @@ -1,20 +1,23 @@ -import {stitchArrays} from '#sugar'; +import {empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: ['generateGridActionLinks'], - extraDependencies: ['html', 'language'], + relations: (relation) => ({ + actionLinks: + relation('generateGridActionLinks'), - relations(relation) { - return { - actionLinks: relation('generateGridActionLinks'), - }; - }, + expando: + relation('generateGridExpando'), + }), slots: { + attributes: {type: 'attributes', mutable: false}, + images: {validate: v => v.strictArrayOf(v.isHTML)}, links: {validate: v => v.strictArrayOf(v.isHTML)}, names: {validate: v => v.strictArrayOf(v.isHTML)}, info: {validate: v => v.strictArrayOf(v.isHTML)}, + tab: {validate: v => v.strictArrayOf(v.isHTML)}, + notFromThisGroup: {validate: v => v.strictArrayOf(v.isBoolean)}, // Differentiating from sparseArrayOf here - this list of classes should // have the same length as the items above, i.e. nulls aren't going to be @@ -29,32 +32,115 @@ export default { v.isString))), }, + itemAttributes: { + validate: v => + v.strictArrayOf( + v.optional(v.isAttributes)), + }, + lazy: {validate: v => v.anyOf(v.isWholeNumber, v.isBoolean)}, actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, + + revealAllWarnings: { + validate: v => v.looseArrayOf(v.isString), + }, + + bottomCaption: { + type: 'html', + mutable: false, + }, + + cutIndex: {validate: v => v.isWholeNumber}, }, - generate(relations, slots, {html, language}) { - return ( - html.tag('div', {class: 'grid-listing'}, [ + generate: (relations, slots, {html, language}) => + html.tag('div', {class: 'grid-listing'}, + slots.attributes, + {[html.onlyIfContent]: true}, + + [ + !empty((slots.revealAllWarnings ?? []).filter(Boolean)) && + language.encapsulate('misc.coverGrid.revealAll', capsule => + html.tag('div', {class: 'reveal-all-container'}, + ((slots.tab ?? []) + .slice(0, 4) + .some(tab => tab && !html.isBlank(tab))) && + + {class: 'has-nearby-tab'}, + + html.tag('p', {class: 'reveal-all'}, [ + html.tag('a', {href: '#'}, [ + html.tag('span', {class: 'reveal-label'}, + language.$(capsule, 'reveal')), + + html.tag('span', {class: 'conceal-label'}, + {style: 'display: none'}, + language.$(capsule, 'conceal')), + ]), + + html.tag('br'), + + html.tag('span', {class: 'warnings'}, + language.$(capsule, 'warnings', { + warnings: + language.formatUnitList( + unique(slots.revealAllWarnings.filter(Boolean)) + .sort() + .map(warning => html.tag('b', warning))), + })), + ]))), + stitchArrays({ classes: slots.classes, + attributes: slots.itemAttributes, image: slots.images, link: slots.links, name: slots.names, info: slots.info, - }).map(({classes, image, link, name, info}, index) => + tab: slots.tab, + + notFromThisGroup: + slots.notFromThisGroup ?? + Array.from(slots.links).fill(null) + }).map(({ + classes, + attributes, + image, + link, + name, + info, + tab, + notFromThisGroup, + }, index) => link.slots({ attributes: [ + link.getSlotValue('attributes'), + {class: ['grid-item', 'box']}, + tab && + !html.isBlank(tab) && + {class: 'has-tab'}, + + attributes, + (classes ? {class: classes} : null), + + slots.cutIndex >= 1 && + index >= slots.cutIndex && + {class: 'hidden-by-expandable-cut'}, ], colorContext: 'image-box', content: [ + html.tag('span', + {[html.onlyIfContent]: true}, + + tab), + image.slots({ thumb: 'medium', square: true, @@ -69,7 +155,15 @@ export default { html.tag('span', {[html.onlyIfContent]: true}, - language.sanitize(name)), + (notFromThisGroup + ? language.encapsulate('misc.coverGrid.details.notFromThisGroup', capsule => + language.$(capsule, { + name, + marker: + html.tag('span', {class: 'grid-name-marker'}, + language.$(capsule, 'marker')), + })) + : language.sanitize(name))), html.tag('span', {[html.onlyIfContent]: true}, @@ -84,6 +178,17 @@ export default { relations.actionLinks .slot('actionLinks', slots.actionLinks), - ])); - }, + + (slots.cutIndex >= 1 && + slots.cutIndex < slots.links.length + ? relations.expando.slots({ + caption: slots.bottomCaption, + }) + + : !html.isBlank(relations.bottomCaption) + ? html.tag('p', {class: 'grid-caption'}, + slots.caption) + + : html.blank()), + ]), }; diff --git a/src/content/dependencies/generateDatetimestampTemplate.js b/src/content/dependencies/generateDatetimestampTemplate.js index a92d15fc..56b2e595 100644 --- a/src/content/dependencies/generateDatetimestampTemplate.js +++ b/src/content/dependencies/generateDatetimestampTemplate.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateTextWithTooltip'], - extraDependencies: ['html'], - relations: (relation) => ({ textWithTooltip: relation('generateTextWithTooltip'), diff --git a/src/content/dependencies/generateDotSwitcherTemplate.js b/src/content/dependencies/generateDotSwitcherTemplate.js index 22205922..561a44bc 100644 --- a/src/content/dependencies/generateDotSwitcherTemplate.js +++ b/src/content/dependencies/generateDotSwitcherTemplate.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { attributes: { type: 'attributes', diff --git a/src/content/dependencies/generateExternalHandle.js b/src/content/dependencies/generateExternalHandle.js index 8c0368a4..8653b177 100644 --- a/src/content/dependencies/generateExternalHandle.js +++ b/src/content/dependencies/generateExternalHandle.js @@ -1,8 +1,6 @@ import {isExternalLinkContext} from '#external-links'; export default { - extraDependencies: ['html', 'language'], - data: (url) => ({url}), slots: { diff --git a/src/content/dependencies/generateExternalIcon.js b/src/content/dependencies/generateExternalIcon.js index 637af658..03af643e 100644 --- a/src/content/dependencies/generateExternalIcon.js +++ b/src/content/dependencies/generateExternalIcon.js @@ -1,8 +1,6 @@ import {isExternalLinkContext} from '#external-links'; export default { - extraDependencies: ['html', 'language', 'to'], - data: (url) => ({url}), slots: { diff --git a/src/content/dependencies/generateExternalPlatform.js b/src/content/dependencies/generateExternalPlatform.js index c4f63ecf..b2822d64 100644 --- a/src/content/dependencies/generateExternalPlatform.js +++ b/src/content/dependencies/generateExternalPlatform.js @@ -1,8 +1,6 @@ import {isExternalLinkContext} from '#external-links'; export default { - extraDependencies: ['html', 'language'], - data: (url) => ({url}), slots: { diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js index 8f174b21..896ee224 100644 --- a/src/content/dependencies/generateFlashActGalleryPage.js +++ b/src/content/dependencies/generateFlashActGalleryPage.js @@ -1,21 +1,6 @@ -import {stitchArrays} from '#sugar'; - import striptags from 'striptags'; export default { - contentDependencies: [ - 'generateCoverGrid', - 'generateFlashActNavAccent', - 'generateFlashActSidebar', - 'generatePageLayout', - 'image', - 'linkFlash', - 'linkFlashAct', - 'linkFlashIndex', - ], - - extraDependencies: ['language'], - relations: (relation, act) => ({ layout: relation('generatePageLayout'), @@ -37,7 +22,7 @@ export default { coverGridImages: act.flashes - .map(_flash => relation('image')), + .map(flash => relation('image', flash.coverArtwork)), flashLinks: act.flashes @@ -50,10 +35,6 @@ export default { flashNames: act.flashes.map(flash => flash.name), - - flashCoverPaths: - act.flashes.map(flash => - ['media.flashArt', flash.directory, flash.coverArtFileExtension]) }), generate: (data, relations, {language}) => @@ -71,15 +52,9 @@ export default { mainContent: [ relations.coverGrid.slots({ links: relations.flashLinks, + images: relations.coverGridImages, names: data.flashNames, lazy: 6, - - images: - stitchArrays({ - image: relations.coverGridImages, - path: data.flashCoverPaths, - }).map(({image, path}) => - image.slot('path', path)), }), ], diff --git a/src/content/dependencies/generateFlashActNavAccent.js b/src/content/dependencies/generateFlashActNavAccent.js index c4ec77b8..7ad46051 100644 --- a/src/content/dependencies/generateFlashActNavAccent.js +++ b/src/content/dependencies/generateFlashActNavAccent.js @@ -1,15 +1,6 @@ import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'generateNextLink', - 'generatePreviousLink', - 'linkFlashAct', - ], - - extraDependencies: ['wikiData'], - sprawl: ({flashActData}) => ({flashActData}), diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js index 1421dde9..0d952077 100644 --- a/src/content/dependencies/generateFlashActSidebar.js +++ b/src/content/dependencies/generateFlashActSidebar.js @@ -1,10 +1,4 @@ export default { - contentDependencies: [ - 'generateFlashActSidebarCurrentActBox', - 'generateFlashActSidebarSideMapBox', - 'generatePageSidebar', - ], - relations: (relation, act, flash) => ({ sidebar: relation('generatePageSidebar'), diff --git a/src/content/dependencies/generateFlashActSidebarCurrentActBox.js b/src/content/dependencies/generateFlashActSidebarCurrentActBox.js index 6d152c7c..e08582fe 100644 --- a/src/content/dependencies/generateFlashActSidebarCurrentActBox.js +++ b/src/content/dependencies/generateFlashActSidebarCurrentActBox.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generatePageSidebarBox', - 'linkFlash', - 'linkFlashAct', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, act, _flash) => ({ box: relation('generatePageSidebarBox'), diff --git a/src/content/dependencies/generateFlashActSidebarSideMapBox.js b/src/content/dependencies/generateFlashActSidebarSideMapBox.js index 7b26ef31..4b97f21d 100644 --- a/src/content/dependencies/generateFlashActSidebarSideMapBox.js +++ b/src/content/dependencies/generateFlashActSidebarSideMapBox.js @@ -1,15 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generatePageSidebarBox', - 'linkFlashAct', - 'linkFlashIndex', - ], - - extraDependencies: ['html', 'wikiData'], - sprawl: ({flashSideData}) => ({flashSideData}), relations: (relation, sprawl, _act, _flash) => ({ diff --git a/src/content/dependencies/generateFlashArtworkColumn.js b/src/content/dependencies/generateFlashArtworkColumn.js new file mode 100644 index 00000000..207c3bf3 --- /dev/null +++ b/src/content/dependencies/generateFlashArtworkColumn.js @@ -0,0 +1,9 @@ +export default { + relations: (relation, flash) => ({ + coverArtwork: + relation('generateCoverArtwork', flash.coverArtwork), + }), + + generate: (relations) => + relations.coverArtwork, +}; diff --git a/src/content/dependencies/generateFlashCoverArtwork.js b/src/content/dependencies/generateFlashCoverArtwork.js deleted file mode 100644 index 4b0e5242..00000000 --- a/src/content/dependencies/generateFlashCoverArtwork.js +++ /dev/null @@ -1,41 +0,0 @@ -export default { - contentDependencies: ['generateCoverArtwork', 'image'], - extraDependencies: ['html', 'language'], - - relations: (relation) => ({ - coverArtwork: - relation('generateCoverArtwork'), - - image: - relation('image'), - }), - - data: (flash) => ({ - path: - ['media.flashArt', flash.directory, flash.coverArtFileExtension], - - color: - flash.color, - - dimensions: - flash.coverArtDimensions, - }), - - slots: { - mode: {type: 'string'}, - }, - - generate: (data, relations, slots, {language}) => - relations.coverArtwork.slots({ - mode: slots.mode, - - image: - relations.image.slots({ - path: data.path, - color: data.color, - alt: language.$('misc.alt.flashArt'), - }), - - dimensions: data.dimensions, - }), -}; diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js index a21bb49e..1fb286c6 100644 --- a/src/content/dependencies/generateFlashIndexPage.js +++ b/src/content/dependencies/generateFlashIndexPage.js @@ -1,17 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateCoverGrid', - 'generatePageLayout', - 'image', - 'linkFlash', - 'linkFlashAct', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({flashActData}) => ({flashActData}), query(sprawl) { @@ -53,7 +42,7 @@ export default { actCoverGridImages: query.flashActs .map(act => act.flashes - .map(() => relation('image'))), + .map(flash => relation('image', flash.coverArtwork))), }), data: (query) => ({ @@ -73,11 +62,6 @@ export default { query.flashActs .map(act => act.flashes .map(flash => flash.name)), - - actCoverGridPaths: - query.flashActs - .map(act => act.flashes - .map(flash => ['media.flashArt', flash.directory, flash.coverArtFileExtension])), }), generate: (data, relations, {html, language}) => @@ -116,7 +100,6 @@ export default { coverGridImages: relations.actCoverGridImages, coverGridLinks: relations.actCoverGridLinks, coverGridNames: data.actCoverGridNames, - coverGridPaths: data.actCoverGridPaths, }).map(({ colorStyle, actLink, @@ -126,7 +109,6 @@ export default { coverGridImages, coverGridLinks, coverGridNames, - coverGridPaths, }, index) => [ html.tag('h2', {id: anchor}, @@ -135,15 +117,9 @@ export default { coverGrid.slots({ links: coverGridLinks, + images: coverGridImages, names: coverGridNames, lazy: index === 0 ? 4 : true, - - images: - stitchArrays({ - image: coverGridImages, - path: coverGridPaths, - }).map(({image, path}) => - image.slot('path', path)), }), ]), ], diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js index 350a0fc5..86ec6648 100644 --- a/src/content/dependencies/generateFlashInfoPage.js +++ b/src/content/dependencies/generateFlashInfoPage.js @@ -1,22 +1,19 @@ import {empty} from '#sugar'; -export default { - contentDependencies: [ - 'generateAdditionalNamesBox', - 'generateCommentaryEntry', - 'generateContentHeading', - 'generateContributionList', - 'generateFlashActSidebar', - 'generateFlashCoverArtwork', - 'generateFlashNavAccent', - 'generatePageLayout', - 'generateTrackList', - 'linkExternal', - 'linkFlashAct', - ], - - extraDependencies: ['html', 'language'], +function checkInterrupted(which, relations, {html}) { + if ( + !html.isBlank(relations.contributorContributionList) || + !html.isBlank(relations.featuredTracksList) + ) return true; + + if (which === 'crediting-sources') { + if (!html.isBlank(relations.artistCommentaryEntries)) return true; + } + + return false; +} +export default { query(flash) { const query = {}; @@ -47,12 +44,18 @@ export default { query.urls .map(url => relation('linkExternal', url)), - cover: - relation('generateFlashCoverArtwork', flash), + artworkColumn: + relation('generateFlashArtworkColumn', flash), contentHeading: relation('generateContentHeading'), + commentaryContentHeading: + relation('generateCommentaryContentHeading', flash), + + readCommentaryLine: + relation('generateReadCommentaryLine', flash), + flashActLink: relation('linkFlashAct', flash.act), @@ -69,9 +72,10 @@ export default { flash.commentary .map(entry => relation('generateCommentaryEntry', entry)), - creditSourceEntries: - flash.commentary - .map(entry => relation('generateCommentaryEntry', entry)), + creditingSourcesSection: + relation('generateCollapsedContentEntrySection', + flash.creditingSources, + flash), }), data: (_query, flash) => ({ @@ -98,7 +102,7 @@ export default { additionalNames: relations.additionalNamesBox, - cover: relations.cover, + artworkColumnContent: relations.artworkColumn, mainContent: [ html.tag('p', @@ -123,21 +127,16 @@ export default { {[html.joinChildren]: html.tag('br')}, language.encapsulate('releaseInfo', capsule => [ - !html.isBlank(relations.artistCommentaryEntries) && - language.encapsulate(capsule, 'readCommentary', capsule => - language.$(capsule, { - link: - html.tag('a', - {href: '#artist-commentary'}, - language.$(capsule, 'link')), - })), + checkInterrupted('commentary', relations, {html}) && + relations.readCommentaryLine, - !html.isBlank(relations.creditSourceEntries) && - language.encapsulate(capsule, 'readCreditSources', capsule => + checkInterrupted('crediting-sources', relations, {html}) && + !html.isBlank(relations.creditingSourcesSection) && + language.encapsulate(capsule, 'readCreditingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#credit-sources'}, + {href: '#crediting-sources'}, language.$(capsule, 'link')), })), ])), @@ -168,24 +167,14 @@ export default { ]), html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'artist-commentary'}, - title: language.$('misc.artistCommentary'), - }), - + relations.commentaryContentHeading, relations.artistCommentaryEntries, ]), - html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'credit-sources'}, - title: language.$('misc.creditSources'), - }), - - relations.creditSourceEntries, - ]), + relations.creditingSourcesSection.slots({ + id: 'crediting-sources', + string: 'misc.creditingSources', + }), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateFlashNavAccent.js b/src/content/dependencies/generateFlashNavAccent.js index 0f5d2d6b..db9d3c1e 100644 --- a/src/content/dependencies/generateFlashNavAccent.js +++ b/src/content/dependencies/generateFlashNavAccent.js @@ -1,15 +1,6 @@ import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'generateNextLink', - 'generatePreviousLink', - 'linkFlash', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({flashActData}) => ({flashActData}), diff --git a/src/content/dependencies/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js index dfd83aef..efa1972a 100644 --- a/src/content/dependencies/generateFooterLocalizationLinks.js +++ b/src/content/dependencies/generateFooterLocalizationLinks.js @@ -2,15 +2,6 @@ import {sortByName} from '#sort'; import {stitchArrays} from '#sugar'; export default { - extraDependencies: [ - 'defaultLanguage', - 'html', - 'language', - 'languages', - 'pagePath', - 'to', - ], - generate({ defaultLanguage, html, diff --git a/src/content/dependencies/generateGridActionLinks.js b/src/content/dependencies/generateGridActionLinks.js index 585a02b9..5b3f9c1e 100644 --- a/src/content/dependencies/generateGridActionLinks.js +++ b/src/content/dependencies/generateGridActionLinks.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, diff --git a/src/content/dependencies/generateGridExpando.js b/src/content/dependencies/generateGridExpando.js new file mode 100644 index 00000000..5a0cbce5 --- /dev/null +++ b/src/content/dependencies/generateGridExpando.js @@ -0,0 +1,37 @@ +export default { + slots: { + caption: {type: 'html', mutable: false}, + }, + + generate: (slots, {html, language}) => + language.encapsulate('misc.coverGrid', capsule => + html.tag('div', {class: 'grid-expando'}, + {[html.onlyIfSiblings]: true}, + + html.tag('p', {class: 'grid-expando-content'}, + {[html.joinChildren]: html.tag('br')}, + + [ + html.tag('span', {class: 'grid-caption'}, + slots.caption), + + !html.isBlank(slots.contentBelowCut) && + language.$(capsule, 'expandCollapseCue', { + cue: + html.tag('a', {class: 'grid-expando-toggle'}, + {href: '#'}, + + {[html.joinChildren]: ''}, + {[html.noEdgeWhitespace]: true}, + + [ + html.tag('span', {class: 'grid-expand-cue'}, + language.$(capsule, 'expand')), + + html.tag('span', {class: 'grid-collapse-cue'}, + {style: 'display: none'}, + language.$(capsule, 'collapse')), + ]), + }), + ]))), +}; diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index 206c495d..e378f8a2 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -1,115 +1,77 @@ import {sortChronologically} from '#sort'; -import {empty, stitchArrays} from '#sugar'; import {filterItemsForCarousel, getTotalDuration} from '#wiki-data'; export default { - contentDependencies: [ - 'generateCoverCarousel', - 'generateCoverGrid', - 'generateGroupNavLinks', - 'generateGroupSecondaryNav', - 'generateGroupSidebar', - 'generatePageLayout', - 'generateQuickDescription', - 'image', - 'linkAlbum', - 'linkListing', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({enableGroupUI: wikiInfo.enableGroupUI}), - relations(relation, sprawl, group) { - const relations = {}; + query(_sprawl, group) { + const query = {}; - const albums = + query.allAlbums = sortChronologically(group.albums.slice(), {latestFirst: true}); - relations.layout = - relation('generatePageLayout'); - - relations.navLinks = - relation('generateGroupNavLinks', group); - - if (sprawl.enableGroupUI) { - relations.secondaryNav = - relation('generateGroupSecondaryNav', group); - - relations.sidebar = - relation('generateGroupSidebar', group); - } - - const carouselAlbums = filterItemsForCarousel(group.featuredAlbums); + query.allTracks = + query.allAlbums.flatMap((album) => album.tracks); - if (!empty(carouselAlbums)) { - relations.coverCarousel = - relation('generateCoverCarousel'); + query.carouselAlbums = + filterItemsForCarousel(group.featuredAlbums); - relations.carouselLinks = - carouselAlbums - .map(album => relation('linkAlbum', album)); + return query; + }, - relations.carouselImages = - carouselAlbums - .map(album => relation('image', album.artTags)); - } + relations: (relation, query, sprawl, group) => ({ + layout: + relation('generatePageLayout'), - relations.quickDescription = - relation('generateQuickDescription', group); + navLinks: + relation('generateGroupNavLinks', group), - relations.coverGrid = - relation('generateCoverGrid'); + secondaryNav: + (sprawl.enableGroupUI + ? relation('generateGroupSecondaryNav', group) + : null), - relations.gridLinks = - albums - .map(album => relation('linkAlbum', album)); + coverCarousel: + relation('generateCoverCarousel'), - relations.gridImages = - albums.map(album => - (album.hasCoverArt - ? relation('image', album.artTags) - : relation('image'))); + carouselLinks: + query.carouselAlbums + .map(album => relation('linkAlbum', album)), - return relations; - }, + carouselImages: + query.carouselAlbums + .map(album => relation('image', album.coverArtworks[0])), - data(sprawl, group) { - const data = {}; + quickDescription: + relation('generateQuickDescription', group), - data.name = group.name; - data.color = group.color; + albumViewSwitcher: + relation('generateIntrapageDotSwitcher'), - const albums = sortChronologically(group.albums.slice(), {latestFirst: true}); - const tracks = albums.flatMap((album) => album.tracks); + albumsBySeriesView: + relation('generateGroupGalleryPageAlbumsBySeriesView', group), - data.numAlbums = albums.length; - data.numTracks = tracks.length; - data.totalDuration = getTotalDuration(tracks, {mainReleasesOnly: true}); + albumsByDateView: + relation('generateGroupGalleryPageAlbumsByDateView', group), + }), - data.gridNames = albums.map(album => album.name); - data.gridDurations = albums.map(album => getTotalDuration(album.tracks)); - data.gridNumTracks = albums.map(album => album.tracks.length); + data: (query, _sprawl, group) => ({ + name: + group.name, - data.gridPaths = - albums.map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)); + color: + group.color, - const carouselAlbums = filterItemsForCarousel(group.featuredAlbums); + numAlbums: + query.allAlbums.length, - if (!empty(group.featuredAlbums)) { - data.carouselPaths = - carouselAlbums.map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)); - } + numTracks: + query.allTracks.length, - return data; - }, + totalDuration: + getTotalDuration(query.allTracks, {mainReleasesOnly: true}), + }), generate: (data, relations, {html, language}) => language.encapsulate('groupGalleryPage', pageCapsule => @@ -121,16 +83,10 @@ export default { mainClasses: ['top-index'], mainContent: [ - relations.coverCarousel - ?.slots({ - links: relations.carouselLinks, - images: - stitchArrays({ - image: relations.carouselImages, - path: data.carouselPaths, - }).map(({image, path}) => - image.slot('path', path)), - }), + relations.coverCarousel.slots({ + links: relations.carouselLinks, + images: relations.carouselImages, + }), relations.quickDescription, @@ -155,49 +111,81 @@ export default { })), })), - relations.coverGrid - .slots({ - links: relations.gridLinks, - names: data.gridNames, - images: - stitchArrays({ - image: relations.gridImages, - path: data.gridPaths, - name: data.gridNames, - }).map(({image, path, name}) => - image.slots({ - path, - missingSourceContent: - language.$('misc.coverGrid.noCoverArt', { - album: name, - }), - })), - info: - stitchArrays({ - numTracks: data.gridNumTracks, - duration: data.gridDurations, - }).map(({numTracks, duration}) => - language.$('misc.coverGrid.details.albumLength', { - tracks: language.countTracks(numTracks, {unit: true}), - time: language.formatDuration(duration), - })), - }), + ([ + !html.isBlank(relations.albumsBySeriesView), + !html.isBlank(relations.albumsByDateView) + ]).filter(Boolean).length > 1 && + + language.encapsulate(pageCapsule, 'albumViewSwitcher', capsule => + html.tag('p', {class: 'gallery-view-switcher'}, + {class: ['drop', 'shiny']}, + + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + [ + language.$(capsule), + + relations.albumViewSwitcher.slots({ + initialOptionIndex: 0, + + titles: [ + !html.isBlank(relations.albumsByDateView) && + language.$(capsule, 'byDate'), + + !html.isBlank(relations.albumsBySeriesView) && + language.$(capsule, 'bySeries'), + ].filter(Boolean), + + targetIDs: [ + !html.isBlank(relations.albumsByDateView) && + 'group-album-gallery-by-date', + + !html.isBlank(relations.albumsBySeriesView) && + 'group-album-gallery-by-series', + ].filter(Boolean), + }), + ])), + + /* + data.trackGridLabels.some(value => value !== null) && + html.tag('p', {class: 'gallery-set-switcher'}, + language.encapsulate(pageCapsule, 'setSwitcher', switcherCapsule => + language.$(switcherCapsule, { + sets: + relations.setSwitcher.slots({ + initialOptionIndex: 0, + + titles: + data.trackGridLabels.map(label => + label ?? + language.$(switcherCapsule, 'unlabeledSet')), + + targetIDs: + data.trackGridIDs, + }), + }))), + */ + + relations.albumsByDateView.slots({ + showTitle: + !html.isBlank(relations.albumsBySeriesView), + }), + + relations.albumsBySeriesView.slots({ + attributes: [ + !html.isBlank(relations.albumsBySeriesView) && + {style: 'display: none'}, + ], + }), ], - leftSidebar: - (relations.sidebar - ? relations.sidebar - .slot('currentExtra', 'gallery') - .content /* TODO: Kludge. */ - : null), - navLinkStyle: 'hierarchical', navLinks: relations.navLinks .slot('currentExtra', 'gallery') .content, - secondaryNav: - relations.secondaryNav ?? null, + secondaryNav: relations.secondaryNav, })), }; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js new file mode 100644 index 00000000..37c1951d --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js @@ -0,0 +1,97 @@ +import {stitchArrays} from '#sugar'; +import {getTotalDuration} from '#wiki-data'; + +export default { + query: (albums, _group) => ({ + artworks: + albums.map(album => + (album.hasCoverArt + ? album.coverArtworks[0] + : null)), + }), + + relations: (relation, query, albums, group) => ({ + coverGrid: + relation('generateCoverGrid'), + + links: + albums + .map(album => relation('linkAlbum', album)), + + images: + query.artworks + .map(artwork => relation('image', artwork)), + + tabs: + albums + .map(album => + relation('generateGroupGalleryPageAlbumGridTab', album, group)), + }), + + data: (query, albums, group) => ({ + names: + albums.map(album => album.name), + + styles: + albums.map(album => album.style), + + tracks: + albums.map(album => album.tracks.length), + + allWarnings: + query.artworks.flatMap(artwork => artwork?.contentWarnings), + + durations: + albums.map(album => + (album.hideDuration + ? null + : getTotalDuration(album.tracks))), + + notFromThisGroup: + albums.map(album => !album.groups.includes(group)), + }), + + generate: (data, relations, {language}) => + language.encapsulate('misc.coverGrid', capsule => + relations.coverGrid.slots({ + links: relations.links, + names: data.names, + notFromThisGroup: data.notFromThisGroup, + + images: + stitchArrays({ + image: relations.images, + name: data.names, + }).map(({image, name}) => + image.slots({ + missingSourceContent: + language.$(capsule, 'noCoverArt', { + album: name, + }), + })), + + itemAttributes: + data.styles.map(style => ({'data-style': style})), + + tab: relations.tabs, + + info: + stitchArrays({ + style: data.styles, + tracks: data.tracks, + duration: data.durations, + }).map(({style, tracks, duration}) => + (style === 'single' && duration + ? language.$(capsule, 'details.albumLength.single', { + time: language.formatDuration(duration), + }) + : duration + ? language.$(capsule, 'details.albumLength', { + tracks: language.countTracks(tracks, {unit: true}), + time: language.formatDuration(duration), + }) + : null)), + + revealAllWarnings: data.allWarnings, + })), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumGridTab.js b/src/content/dependencies/generateGroupGalleryPageAlbumGridTab.js new file mode 100644 index 00000000..c3b860e4 --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumGridTab.js @@ -0,0 +1,79 @@ +import {empty} from '#sugar'; + +export default { + query(album, group) { + if (album.groups.length > 1) { + const contextGroup = group; + + const candidateGroupCategory = + album.groups + .filter(group => !group.excludeFromGalleryTabs) + .find(group => group.category !== contextGroup.category) + ?.category ?? + null; + + const candidateGroups = + album.groups + .filter(group => !group.excludeFromGalleryTabs) + .filter(group => group.category === candidateGroupCategory); + + if (!empty(candidateGroups)) { + return { + mode: 'groups', + notedGroups: candidateGroups, + }; + } + } + + if (!empty(album.artistContribs)) { + if ( + album.artistContribs.length === 1 && + !empty(group.closelyLinkedArtists) && + (album.artistContribs[0].artist.name === + group.closelyLinkedArtists[0].artist.name) + ) { + return {mode: null}; + } + + return { + mode: 'artists', + notedArtistContribs: album.artistContribs, + }; + } + + return {mode: null};; + }, + + relations: (relation, query, _album, _group) => ({ + artistCredit: + (query.mode === 'artists' + ? relation('generateArtistCredit', query.notedArtistContribs, []) + : null), + }), + + data: (query, _album, _group) => ({ + mode: query.mode, + + groupNames: + (query.mode === 'groups' + ? query.notedGroups.map(group => group.name) + : null), + }), + + generate: (data, relations, {language}) => + language.encapsulate('misc.coverGrid.tab', capsule => + (data.mode === 'groups' + ? language.$(capsule, 'groups', { + groups: + language.formatUnitList(data.groupNames), + }) + : data.mode === 'artists' + ? relations.artistCredit.slots({ + normalStringKey: + capsule + '.artists', + + normalFeaturingStringKey: + capsule + '.artists.featuring', + }) + : null)), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js new file mode 100644 index 00000000..75ef1048 --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js @@ -0,0 +1,48 @@ +import {sortChronologically} from '#sort'; + +export default { + query: (group) => ({ + albums: + sortChronologically(group.albums.slice(), {latestFirst: true}), + }), + + relations: (relation, query, group) => ({ + styleSelector: + (group.divideAlbumsByStyle + ? relation('generateGroupGalleryPageStyleSelector', group) + : null), + + albumGrid: + relation('generateGroupGalleryPageAlbumGrid', + query.albums, + group), + }), + + slots: { + showTitle: { + type: 'boolean', + }, + + attributes: { + type: 'attributes', + mutable: false, + }, + }, + + generate: (relations, slots, {html, language}) => + language.encapsulate('groupGalleryPage.albumsByDate', capsule => + html.tag('div', {id: 'group-album-gallery-by-date'}, + slots.attributes, + + {[html.onlyIfContent]: true}, + + html.tag('section', [ + slots.showTitle && + html.tag('h2', + language.$(capsule, 'title')), + + relations.styleSelector, + + relations.albumGrid, + ]))), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js new file mode 100644 index 00000000..68cf249f --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js @@ -0,0 +1,23 @@ +export default { + relations: (relation, group) => ({ + seriesSections: + group.serieses + .map(series => + relation('generateGroupGalleryPageSeriesSection', series)), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + }, + + generate: (relations, slots, {html}) => + html.tag('div', {id: 'group-album-gallery-by-series'}, + slots.attributes, + + {[html.onlyIfContent]: true}, + + relations.seriesSections), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageSeriesSection.js b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js new file mode 100644 index 00000000..1aa835d6 --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js @@ -0,0 +1,138 @@ +import {sortChronologically} from '#sort'; + +export default { + query(series) { + const query = {}; + + query.albums = + sortChronologically(series.albums.slice(), {latestFirst: true}); + + query.allAlbumsDated = + series.albums.every(album => album.date); + + query.anyAlbumNotFromThisGroup = + series.albums.some(album => !album.groups.includes(series.group)); + + query.latestAlbum = + query.albums + .filter(album => album.date) + .at(0) ?? + null; + + query.earliestAlbum = + query.albums + .filter(album => album.date) + .at(-1) ?? + null; + + return query; + }, + + relations: (relation, query, series) => ({ + contentHeading: + relation('generateContentHeading'), + + grid: + relation('generateGroupGalleryPageAlbumGrid', + query.albums, + series.group), + }), + + data: (query, series) => ({ + name: + series.name, + + groupName: + series.group.name, + + albums: + series.albums.length, + + tracks: + series.albums + .flatMap(album => album.tracks) + .length, + + allAlbumsDated: + query.allAlbumsDated, + + anyAlbumNotFromThisGroup: + query.anyAlbumNotFromThisGroup, + + earliestAlbumDate: + (query.earliestAlbum + ? query.earliestAlbum.date + : null), + + latestAlbumDate: + (query.latestAlbum + ? query.latestAlbum.date + : null), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('groupGalleryPage.albumSection', capsule => + html.tags([ + relations.contentHeading.slots({ + tag: 'h2', + title: language.sanitize(data.name), + }), + + relations.grid.slots({ + cutIndex: 4, + + bottomCaption: + language.encapsulate(capsule, 'caption', captionCapsule => + html.tags([ + data.anyAlbumNotFromThisGroup && + language.$(captionCapsule, 'seriesAlbumsNotFromGroup', { + marker: + language.$('misc.coverGrid.details.notFromThisGroup.marker'), + + series: + html.tag('i', data.name), + + group: data.groupName, + }), + + language.encapsulate(captionCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.tracks = + html.tag('b', + language.countTracks(data.tracks, {unit: true})); + + workingOptions.albums = + html.tag('b', + language.countAlbums(data.albums, {unit: true})); + + if (data.allAlbumsDated) { + const earliestDate = data.earliestAlbumDate; + const latestDate = data.latestAlbumDate; + + const earliestYear = earliestDate.getFullYear(); + const latestYear = latestDate.getFullYear(); + + if (earliestYear === latestYear) { + if (data.albums === 1) { + workingCapsule += '.withDate'; + workingOptions.date = + language.formatDate(earliestDate); + } else { + workingCapsule += '.withYear'; + workingOptions.year = + language.formatYear(earliestDate); + } + } else { + workingCapsule += '.withYearRange'; + workingOptions.yearRange = + language.formatYearRange(earliestDate, latestDate); + } + } + + return language.$(workingCapsule, workingOptions); + }), + ], {[html.joinChildren]: html.tag('br')})), + }), + ])), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageStyleSelector.js b/src/content/dependencies/generateGroupGalleryPageStyleSelector.js new file mode 100644 index 00000000..9342e50f --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageStyleSelector.js @@ -0,0 +1,60 @@ +import {unique} from '#sugar'; + +export default { + query: (group) => ({ + styles: + unique(group.albums.map(album => album.style)), + }), + + data: (query, group) => ({ + albums: + group.albums.length, + + styles: + query.styles, + }), + + generate: (data, {html, language}) => + language.encapsulate('groupGalleryPage', pageCapsule => + (data.styles.length <= 1 + ? html.blank() + : html.tag('p', {class: 'gallery-style-selector'}, + {class: ['drop', 'shiny']}, + + language.encapsulate(pageCapsule, 'albumStyleSwitcher', capsule => [ + html.tag('span', + language.$(capsule)), + + html.tag('br'), + + html.tag('span', {class: 'styles'}, + data.styles.map(style => + html.tag('label', {'data-style': style}, [ + html.tag('input', {type: 'checkbox'}, + {checked: true}), + + html.tag('span', + language.$(capsule, style)), + ]))), + + html.tag('br'), + + html.tag('span', {class: ['count', 'all']}, + language.$(capsule, 'count.all', { + total: data.albums, + })), + + html.tag('span', {class: ['count', 'filtered']}, + {style: 'display: none'}, + + language.$(capsule, 'count.filtered', { + count: html.tag('span'), + total: data.albums, + })), + + html.tag('span', {class: ['count', 'none']}, + {style: 'display: none'}, + + language.$(capsule, 'count.none')), + ])))), +}; diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js index 7b9c2afa..0f3093b2 100644 --- a/src/content/dependencies/generateGroupInfoPage.js +++ b/src/content/dependencies/generateGroupInfoPage.js @@ -1,20 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateGroupInfoPageAlbumsSection', - 'generateGroupNavLinks', - 'generateGroupSecondaryNav', - 'generateGroupSidebar', - 'generatePageLayout', - 'linkArtist', - 'linkExternal', - 'transformContent', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({ enableGroupUI: wikiInfo.enableGroupUI, diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js b/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js index df42598d..de55f33a 100644 --- a/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js +++ b/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js @@ -1,10 +1,6 @@ import {sortChronologically} from '#sort'; export default { - contentDependencies: ['generateGroupInfoPageAlbumsListItem'], - - extraDependencies: ['html'], - query: (group) => ({ // Typically, a latestFirst: false (default) chronological sort would be // appropriate here, but navigation between adjacent albums in a group is a diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js b/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js index bcd5d288..f8314d71 100644 --- a/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js +++ b/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js @@ -1,13 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateContentHeading', - 'generateGroupInfoPageAlbumsListItem', - ], - - extraDependencies: ['html', 'language'], - query: (group) => ({ closelyLinkedArtists: group.closelyLinkedArtists diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js b/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js index 99e7e8ff..09b0a542 100644 --- a/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js +++ b/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js @@ -1,16 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateAbsoluteDatetimestamp', - 'generateArtistCredit', - 'generateColorStyleAttribute', - 'linkAlbum', - 'linkGroup', - ], - - extraDependencies: ['html', 'language'], - query: (album, group) => { const otherCategory = album.groups @@ -127,8 +117,7 @@ export default { workingCapsule += '.withArtists'; workingOptions.by = html.tag('span', {class: 'by'}, - html.metatag('chunkwrap', {split: ','}, - html.resolve(artistCredit))); + artistCredit); } return language.$(workingCapsule, workingOptions); diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js index 0b678e9d..4470eb2f 100644 --- a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js +++ b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js @@ -1,14 +1,4 @@ export default { - contentDependencies: [ - 'generateContentHeading', - 'generateGroupInfoPageAlbumsListByDate', - 'generateGroupInfoPageAlbumsListBySeries', - 'generateIntrapageDotSwitcher', - 'linkGroupGallery', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, group) => ({ contentHeading: relation('generateContentHeading'), diff --git a/src/content/dependencies/generateGroupNavAccent.js b/src/content/dependencies/generateGroupNavAccent.js index 0e4ebe8a..18281bf0 100644 --- a/src/content/dependencies/generateGroupNavAccent.js +++ b/src/content/dependencies/generateGroupNavAccent.js @@ -1,14 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'linkGroup', - 'linkGroupGallery', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, group) => ({ switcher: relation('generateInterpageDotSwitcher'), diff --git a/src/content/dependencies/generateGroupNavLinks.js b/src/content/dependencies/generateGroupNavLinks.js index bdc3ee4c..4f13e474 100644 --- a/src/content/dependencies/generateGroupNavLinks.js +++ b/src/content/dependencies/generateGroupNavLinks.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateGroupNavAccent', 'linkGroup'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({groupCategoryData, wikiInfo}) => ({ groupCategoryData, enableGroupUI: wikiInfo.enableGroupUI, diff --git a/src/content/dependencies/generateGroupSecondaryNav.js b/src/content/dependencies/generateGroupSecondaryNav.js index c48f3142..6b4347dd 100644 --- a/src/content/dependencies/generateGroupSecondaryNav.js +++ b/src/content/dependencies/generateGroupSecondaryNav.js @@ -1,9 +1,4 @@ export default { - contentDependencies: [ - 'generateSecondaryNav', - 'generateGroupSecondaryNavCategoryPart', - ], - relations: (relation, group) => ({ secondaryNav: relation('generateSecondaryNav'), diff --git a/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js b/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js index b2adb9f8..df627c99 100644 --- a/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js +++ b/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js @@ -1,15 +1,6 @@ import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateSecondaryNavParentSiblingsPart', - 'linkGroupDynamically', - 'linkListing', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({listingSpec, wikiInfo}) => ({ groupsByCategoryListing: (wikiInfo.enableListings diff --git a/src/content/dependencies/generateGroupSidebar.js b/src/content/dependencies/generateGroupSidebar.js index 0888cbbe..1359eaca 100644 --- a/src/content/dependencies/generateGroupSidebar.js +++ b/src/content/dependencies/generateGroupSidebar.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateGroupSidebarCategoryDetails', - 'generatePageSidebar', - 'generatePageSidebarBox', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({groupCategoryData}) => ({groupCategoryData}), relations: (relation, sprawl, group) => ({ diff --git a/src/content/dependencies/generateGroupSidebarCategoryDetails.js b/src/content/dependencies/generateGroupSidebarCategoryDetails.js index 208ccd07..a7e1f240 100644 --- a/src/content/dependencies/generateGroupSidebarCategoryDetails.js +++ b/src/content/dependencies/generateGroupSidebarCategoryDetails.js @@ -1,14 +1,6 @@ import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'linkGroup', - 'linkGroupGallery', - ], - - extraDependencies: ['html', 'language'], - relations(relation, category) { return { colorStyle: diff --git a/src/content/dependencies/generateImageOverlay.js b/src/content/dependencies/generateImageOverlay.js index cfb78a1b..006cfcce 100644 --- a/src/content/dependencies/generateImageOverlay.js +++ b/src/content/dependencies/generateImageOverlay.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'language'], - generate: ({html, language}) => html.tag('div', {id: 'image-overlay-container'}, html.tag('div', {id: 'image-overlay-content-container'}, [ diff --git a/src/content/dependencies/generateInterpageDotSwitcher.js b/src/content/dependencies/generateInterpageDotSwitcher.js index 5a33444e..ddb7cb37 100644 --- a/src/content/dependencies/generateInterpageDotSwitcher.js +++ b/src/content/dependencies/generateInterpageDotSwitcher.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateDotSwitcherTemplate'], - extraDependencies: ['html', 'language'], - relations: (relation) => ({ template: relation('generateDotSwitcherTemplate'), diff --git a/src/content/dependencies/generateIntrapageDotSwitcher.js b/src/content/dependencies/generateIntrapageDotSwitcher.js index 3f300676..943d862c 100644 --- a/src/content/dependencies/generateIntrapageDotSwitcher.js +++ b/src/content/dependencies/generateIntrapageDotSwitcher.js @@ -1,9 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateDotSwitcherTemplate'], - extraDependencies: ['html', 'language'], - relations: (relation) => ({ template: relation('generateDotSwitcherTemplate'), @@ -39,9 +36,32 @@ export default { stitchArrays({ title: slots.titles, targetID: slots.targetIDs, - }).map(({title, targetID}) => - html.tag('a', {href: '#'}, - {'data-target-id': targetID}, - language.sanitize(title))), + }).map(({title, targetID}) => { + const {content} = html.smush(title); + + const customCue = + content.find(item => + item?.tagName === 'span' && + item.attributes.has('class', 'dot-switcher-interaction-cue')); + + const cue = + (customCue && !html.isBlank(customCue) + ? customCue.content + : language.sanitize(title)); + + const a = + html.tag('a', {href: '#'}, + {'data-target-id': targetID}, + {[html.onlyIfContent]: true}, + + cue); + + if (customCue) { + content.splice(content.indexOf(customCue), 1, a); + return html.tags(content, {[html.joinChildren]: ''}); + } else { + return a; + } + }), }), }; diff --git a/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js b/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js new file mode 100644 index 00000000..e381a745 --- /dev/null +++ b/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js @@ -0,0 +1,19 @@ +export default { + relations: (relation, _album, additionalFiles) => ({ + chunk: + relation('generateListAllAdditionalFilesChunk', additionalFiles), + }), + + slots: { + stringsKey: {type: 'string'}, + }, + + generate: (relations, slots, {language}) => + language.encapsulate('listingPage', slots.stringsKey, pageCapsule => + relations.chunk.slots({ + title: + language.$(pageCapsule, 'albumFiles'), + + stringsKey: slots.stringsKey, + })), +}; diff --git a/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js b/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js new file mode 100644 index 00000000..0f14f12c --- /dev/null +++ b/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js @@ -0,0 +1,42 @@ +export default { + relations: (relation, album, property) => ({ + heading: + relation('generateContentHeading'), + + albumLink: + relation('linkAlbum', album), + + albumChunk: + relation('generateListAllAdditionalFilesAlbumChunk', + album, + album[property] ?? []), + + trackChunks: + album.tracks.map(track => + relation('generateListAllAdditionalFilesTrackChunk', + track, + track[property] ?? [])), + }), + + slots: { + stringsKey: {type: 'string'}, + }, + + generate: (relations, slots, {html}) => + html.tags([ + relations.heading.slots({ + tag: 'h3', + title: relations.albumLink, + }), + + html.tag('dl', + {[html.onlyIfContent]: true}, + + [ + relations.albumChunk.slot('stringsKey', slots.stringsKey), + + relations.trackChunks.map(trackChunk => + trackChunk.slot('stringsKey', slots.stringsKey)), + ]), + ]), +}; diff --git a/src/content/dependencies/generateListAllAdditionalFilesChunk.js b/src/content/dependencies/generateListAllAdditionalFilesChunk.js index deb8c4ea..d68e3bc1 100644 --- a/src/content/dependencies/generateListAllAdditionalFilesChunk.js +++ b/src/content/dependencies/generateListAllAdditionalFilesChunk.js @@ -1,7 +1,22 @@ -import {empty, stitchArrays} from '#sugar'; +import {stitchArrays} from '#sugar'; export default { - extraDependencies: ['html', 'language'], + relations: (relation, additionalFiles) => ({ + links: + additionalFiles + .map(file => file.filenames + .map(filename => relation('linkAdditionalFile', file, filename))), + }), + + data: (additionalFiles) => ({ + titles: + additionalFiles + .map(file => file.title), + + filenames: + additionalFiles + .map(file => file.filenames), + }), slots: { title: { @@ -9,82 +24,73 @@ export default { mutable: false, }, - additionalFileTitles: { - validate: v => v.strictArrayOf(v.isHTML), - }, - - additionalFileLinks: { - validate: v => v.strictArrayOf(v.strictArrayOf(v.isHTML)), - }, - - additionalFileFiles: { - validate: v => v.strictArrayOf(v.strictArrayOf(v.isString)), - }, - stringsKey: {type: 'string'}, }, - generate(slots, {html, language}) { - if (empty(slots.additionalFileLinks)) { - return html.blank(); - } + generate: (data, relations, slots, {html, language}) => + language.encapsulate('listingPage', slots.stringsKey, pageCapsule => + html.tags([ + html.tag('dt', + {[html.onlyIfSiblings]: true}, + slots.title), - return html.tags([ - html.tag('dt', slots.title), - html.tag('dd', - html.tag('ul', - stitchArrays({ - additionalFileTitle: slots.additionalFileTitles, - additionalFileLinks: slots.additionalFileLinks, - additionalFileFiles: slots.additionalFileFiles, - }).map(({ - additionalFileTitle, - additionalFileLinks, - additionalFileFiles, - }) => - language.encapsulate('listingPage', slots.stringsKey, 'file', capsule => - (additionalFileLinks.length === 1 - ? html.tag('li', - additionalFileLinks[0].slots({ - content: - language.$(capsule, { - title: additionalFileTitle, - }), - })) + html.tag('dd', + {[html.onlyIfContent]: true}, - : additionalFileLinks.length === 0 - ? html.tag('li', - language.$(capsule, 'withNoFiles', { - title: additionalFileTitle, - })) + html.tag('ul', + {[html.onlyIfContent]: true}, - : html.tag('li', {class: 'has-details'}, - html.tag('details', [ - html.tag('summary', - html.tag('span', - language.$(capsule, 'withMultipleFiles', { - title: - html.tag('b', additionalFileTitle), + stitchArrays({ + title: data.titles, + links: relations.links, + filenames: data.filenames, + }).map(({ + title, + links, + filenames, + }) => + language.encapsulate(pageCapsule, 'file', capsule => + (links.length === 1 + ? html.tag('li', + links[0].slots({ + content: + language.$(capsule, { + title: title, + }), + })) - files: - language.countAdditionalFiles( - additionalFileLinks.length, - {unit: true}), - }))), + : links.length === 0 + ? html.tag('li', + language.$(capsule, 'withNoFiles', { + title: title, + })) - html.tag('ul', - stitchArrays({ - additionalFileLink: additionalFileLinks, - additionalFileFile: additionalFileFiles, - }).map(({additionalFileLink, additionalFileFile}) => - html.tag('li', - additionalFileLink.slots({ - content: - language.$(capsule, { - title: additionalFileFile, - }), - })))), - ]))))))), - ]); - }, + : html.tag('li', {class: 'has-details'}, + html.tag('details', [ + html.tag('summary', + html.tag('span', + language.$(capsule, 'withMultipleFiles', { + title: + html.tag('b', title), + + files: + language.countAdditionalFiles( + links.length, + {unit: true}), + }))), + + html.tag('ul', + stitchArrays({ + link: links, + filename: filenames, + }).map(({link, filename}) => + html.tag('li', + link.slots({ + content: + language.$(capsule, { + title: filename, + }), + })))), + ]))))))), + ])), }; diff --git a/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js b/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js new file mode 100644 index 00000000..9ac79bb5 --- /dev/null +++ b/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js @@ -0,0 +1,20 @@ +export default { + relations: (relation, track, additionalFiles) => ({ + trackLink: + relation('linkTrack', track), + + chunk: + relation('generateListAllAdditionalFilesChunk', additionalFiles), + }), + + slots: { + stringsKey: {type: 'string'}, + }, + + generate: (relations, slots) => + relations.chunk.slots({ + title: relations.trackLink, + stringsKey: slots.stringsKey, + }), +}; + diff --git a/src/content/dependencies/generateListRandomPageLinksAlbumLink.js b/src/content/dependencies/generateListRandomPageLinksAlbumLink.js index b3560aca..29e7b1c9 100644 --- a/src/content/dependencies/generateListRandomPageLinksAlbumLink.js +++ b/src/content/dependencies/generateListRandomPageLinksAlbumLink.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkAlbum'], - data: (album) => ({directory: album.directory}), diff --git a/src/content/dependencies/generateListingIndexList.js b/src/content/dependencies/generateListingIndexList.js index 78622e6e..db494f37 100644 --- a/src/content/dependencies/generateListingIndexList.js +++ b/src/content/dependencies/generateListingIndexList.js @@ -1,9 +1,6 @@ import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: ['linkListing'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({listingTargetSpec, wikiInfo}) { return {listingTargetSpec, wikiInfo}; }, diff --git a/src/content/dependencies/generateListingPage.js b/src/content/dependencies/generateListingPage.js index 5f9a99a9..987008eb 100644 --- a/src/content/dependencies/generateListingPage.js +++ b/src/content/dependencies/generateListingPage.js @@ -1,67 +1,36 @@ -import {bindOpts, empty, stitchArrays} from '#sugar'; +import {bindOpts, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateContentHeading', - 'generateListingSidebar', - 'generatePageLayout', - 'linkListing', - 'linkListingIndex', - 'linkTemplate', - ], + relations: (relation, listing) => ({ + layout: + relation('generatePageLayout'), - extraDependencies: ['html', 'language', 'wikiData'], + sidebar: + relation('generateListingSidebar', listing), - relations(relation, listing) { - const relations = {}; + listingsIndexLink: + relation('linkListingIndex'), - relations.layout = - relation('generatePageLayout'); + chunkHeading: + relation('generateContentHeading'), - relations.sidebar = - relation('generateListingSidebar', listing); + showSkipToSectionLinkTemplate: + relation('linkTemplate'), - relations.listingsIndexLink = - relation('linkListingIndex'); + sameTargetListingsLine: + (listing.target.listings.length > 1 + ? relation('generateListingPageSameTargetListingsLine', listing) + : null), - relations.chunkHeading = - relation('generateContentHeading'); + seeAlsoLinks: + listing.seeAlso + .map(listing => relation('linkListing', listing)), + }), - relations.showSkipToSectionLinkTemplate = - relation('linkTemplate'); - - if (listing.target.listings.length > 1) { - relations.sameTargetListingLinks = - listing.target.listings - .map(listing => relation('linkListing', listing)); - } else { - relations.sameTargetListingLinks = []; - } - - relations.seeAlsoLinks = - (!empty(listing.seeAlso) - ? listing.seeAlso - .map(listing => relation('linkListing', listing)) - : []); - - return relations; - }, - - data(listing) { - return { - stringsKey: listing.stringsKey, - - targetStringsKey: listing.target.stringsKey, - - sameTargetListingStringsKeys: - listing.target.listings - .map(listing => listing.stringsKey), - - sameTargetListingsCurrentIndex: - listing.target.listings - .indexOf(listing), - }; - }, + data: (listing) => ({ + stringsKey: + listing.stringsKey, + }), slots: { type: { @@ -169,29 +138,7 @@ export default { headingMode: 'sticky', mainContent: [ - html.tag('p', - {[html.onlyIfContent]: true}, - language.$('listingPage.listingsFor', { - [language.onlyIfOptions]: ['listings'], - - target: - language.$('listingPage.target', data.targetStringsKey), - - listings: - language.formatUnitList( - stitchArrays({ - link: relations.sameTargetListingLinks, - stringsKey: data.sameTargetListingStringsKeys, - }).map(({link, stringsKey}, index) => - html.tag('span', - index === data.sameTargetListingsCurrentIndex && - {class: 'current'}, - - link.slots({ - attributes: {class: 'nowrap'}, - content: language.$('listingPage', stringsKey, 'title.short'), - })))), - })), + relations.sameTargetListingsLine, html.tag('p', {[html.onlyIfContent]: true}, diff --git a/src/content/dependencies/generateListingPageSameTargetListingsLine.js b/src/content/dependencies/generateListingPageSameTargetListingsLine.js new file mode 100644 index 00000000..2146b1eb --- /dev/null +++ b/src/content/dependencies/generateListingPageSameTargetListingsLine.js @@ -0,0 +1,46 @@ +import {stitchArrays} from '#sugar'; + +export default { + relations: (relation, listing) => ({ + listingLinks: + listing.target.listings + .map(listing => relation('linkListing', listing)), + }), + + data: (listing) => ({ + targetStringsKey: + listing.target.stringsKey, + + listingStringsKeys: + listing.target.listings.map(listing => listing.stringsKey), + + currentIndex: + listing.target.listings.indexOf(listing), + }), + + generate: (data, relations, {html, language}) => + html.tag('p', + {[html.onlyIfContent]: true}, + + language.$('listingPage.listingsFor', { + [language.onlyIfOptions]: ['listings'], + + target: + language.$('listingPage.target', data.targetStringsKey), + + listings: + language.formatUnitList( + stitchArrays({ + link: relations.listingLinks, + stringsKey: data.listingStringsKeys, + }).map(({link, stringsKey}, index) => + html.tag('span', + index === data.currentIndex && + {class: 'current'}, + + link.slots({ + attributes: {class: 'nowrap'}, + content: language.$('listingPage', stringsKey, 'title.short'), + })))), + })), +}; diff --git a/src/content/dependencies/generateListingSidebar.js b/src/content/dependencies/generateListingSidebar.js index aeac05cf..2d9429cf 100644 --- a/src/content/dependencies/generateListingSidebar.js +++ b/src/content/dependencies/generateListingSidebar.js @@ -1,13 +1,4 @@ export default { - contentDependencies: [ - 'generateListingIndexList', - 'generatePageSidebar', - 'generatePageSidebarBox', - 'linkListingIndex', - ], - - extraDependencies: ['html'], - relations: (relation, currentListing) => ({ sidebar: relation('generatePageSidebar'), diff --git a/src/content/dependencies/generateListingsIndexPage.js b/src/content/dependencies/generateListingsIndexPage.js index b57ebe15..80963d12 100644 --- a/src/content/dependencies/generateListingsIndexPage.js +++ b/src/content/dependencies/generateListingsIndexPage.js @@ -1,20 +1,14 @@ import {getTotalDuration} from '#wiki-data'; export default { - contentDependencies: [ - 'generateListingIndexList', - 'generateListingSidebar', - 'generatePageLayout', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({albumData, trackData, wikiInfo}) { return { wikiName: wikiInfo.name, numTracks: trackData.length, numAlbums: albumData.length, - totalDuration: getTotalDuration(trackData), + totalDuration: + getTotalDuration( + trackData.filter(track => track.countInArtistTotals)), }; }, diff --git a/src/content/dependencies/generateLyricsEntry.js b/src/content/dependencies/generateLyricsEntry.js new file mode 100644 index 00000000..15f84b27 --- /dev/null +++ b/src/content/dependencies/generateLyricsEntry.js @@ -0,0 +1,119 @@ +export default { + relations: (relation, entry) => ({ + content: + relation('transformContent', entry.body), + + artistText: + relation('transformContent', entry.artistText), + + artistLinks: + entry.artists + .filter(artist => artist.name !== 'HSMusic Wiki') // smh + .map(artist => relation('linkArtist', artist)), + + sourceLinks: + entry.sourceURLs + .map(url => relation('linkExternal', url)), + + originDetails: + relation('transformContent', entry.originDetails), + }), + + data: (entry) => ({ + isWikiLyrics: + entry.isWikiLyrics, + + hasSquareBracketAnnotations: + entry.hasSquareBracketAnnotations, + + numStanzas: + 1 + + + (Array.from( + entry.body + .matchAll(/\n\n|<br><br>/g)) + + .length) + + + (entry.body.includes('<br') + ? entry.body.split('\n').length + : 0), + + numLines: + 1 + + + (Array.from( + entry.body + .replaceAll(/(<br>){1,}/g, '\n') + .replaceAll(/\n{2,}/g, '\n') + .matchAll(/\n/g)) + + .length), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('misc.lyrics', capsule => + html.tag('blockquote', {class: 'lyrics-entry'}, + slots.attributes, + + {'data-stanzas': data.numStanzas}, + {'data-lines': data.numLines}, + + (data.numStanzas > 1 || + data.numLines > 8) && + {class: 'long-lyrics'}, + + [ + html.tag('p', {class: 'lyrics-details'}, + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + [ + language.$(capsule, 'source', { + [language.onlyIfOptions]: ['source'], + + source: + language.formatUnitList( + relations.sourceLinks.map(link => + link.slots({ + indicateExternal: true, + tab: 'separate', + }))), + }), + + data.isWikiLyrics && + language.$(capsule, 'contributors', { + [language.onlyIfOptions]: ['contributors'], + + contributors: + (html.isBlank(relations.artistText) + ? language.formatUnitList(relations.artistLinks) + : relations.artistText.slot('mode', 'inline')), + }), + + // This check is doubled up only for clarity: entries are coded + // in data so that `hasSquareBracketAnnotations` is only true + // if `isWikiLyrics` is also true. + data.isWikiLyrics && + data.hasSquareBracketAnnotations && + language.$(capsule, 'squareBracketAnnotations'), + ]), + + html.tag('p', {class: 'origin-details'}, + {[html.onlyIfContent]: true}, + + relations.originDetails.slots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + })), + + relations.content.slot('mode', 'lyrics'), + ])), +}; diff --git a/src/content/dependencies/generateLyricsSection.js b/src/content/dependencies/generateLyricsSection.js new file mode 100644 index 00000000..bbc3a776 --- /dev/null +++ b/src/content/dependencies/generateLyricsSection.js @@ -0,0 +1,85 @@ +import {stitchArrays} from '#sugar'; + +export default { + relations: (relation, entries) => ({ + heading: + relation('generateContentHeading'), + + switcher: + relation('generateIntrapageDotSwitcher'), + + entries: + entries + .map(entry => relation('generateLyricsEntry', entry)), + + annotationParts: + entries + .map(entry => entry.annotationParts + .map(part => relation('transformContent', part))), + }), + + data: (entries) => ({ + ids: + Array.from( + {length: entries.length}, + (_, index) => 'lyrics-entry-' + index), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('releaseInfo.lyrics', capsule => + html.tags([ + relations.heading + .slots({ + attributes: {id: 'lyrics'}, + title: language.$(capsule), + }), + + html.tag('p', {class: 'lyrics-switcher'}, + {[html.onlyIfContent]: true}, + + language.$(capsule, 'switcher', { + [language.onlyIfOptions]: ['entries'], + + entries: + relations.switcher.slots({ + initialOptionIndex: 0, + + titles: + relations.annotationParts + .map(([first, ...rest]) => + language.formatUnitList([ + html.tag('span', + {class: 'dot-switcher-interaction-cue'}, + {[html.onlyIfContent]: true}, + + first?.slots({ + mode: 'inline', + textOnly: true, + })), + + ...rest.map(part => + part.slots({ + mode: 'inline', + textOnly: true, + })), + ])), + + targetIDs: + data.ids, + }), + })), + + stitchArrays({ + entry: relations.entries, + id: data.ids, + }).map(({entry, id}, index) => + entry.slots({ + attributes: [ + {id}, + + index >= 1 && + {style: 'display: none'}, + ], + })), + ])), +}; diff --git a/src/content/dependencies/generateName.js b/src/content/dependencies/generateName.js new file mode 100644 index 00000000..e0d0c6d3 --- /dev/null +++ b/src/content/dependencies/generateName.js @@ -0,0 +1,33 @@ +export default { + contentDependencies: ['transformContent'], + extraDependencies: ['html', 'language'], + + relations: (relation, thing) => ({ + customName: + (thing.nameText + ? relation('transformContent', thing.nameText) + : null), + }), + + data: (thing) => ({ + normalName: + thing.name, + + shortName: + thing.nameShort, + }), + + slots: { + preferShortName: { + type: 'boolean', + default: false, + }, + }, + + generate: (data, relations, slots, {language}) => + (relations.customName + ? relations.customName.slot('mode', 'inline') + : slots.preferShortName && data.shortName + ? language.sanitize(data.shortName) + : language.sanitize(data.normalName)), +}; diff --git a/src/content/dependencies/generateNewsEntryNavAccent.js b/src/content/dependencies/generateNewsEntryNavAccent.js index 5d168e41..05248eb3 100644 --- a/src/content/dependencies/generateNewsEntryNavAccent.js +++ b/src/content/dependencies/generateNewsEntryNavAccent.js @@ -1,11 +1,4 @@ export default { - contentDependencies: [ - 'generateInterpageDotSwitcher', - 'generateNextLink', - 'generatePreviousLink', - 'linkNewsEntry', - ], - relations: (relation, previousEntry, nextEntry) => ({ switcher: relation('generateInterpageDotSwitcher'), diff --git a/src/content/dependencies/generateNewsEntryPage.js b/src/content/dependencies/generateNewsEntryPage.js index 4abd87d1..bbfb886d 100644 --- a/src/content/dependencies/generateNewsEntryPage.js +++ b/src/content/dependencies/generateNewsEntryPage.js @@ -2,16 +2,6 @@ import {sortChronologically} from '#sort'; import {atOffset} from '#sugar'; export default { - contentDependencies: [ - 'generateNewsEntryNavAccent', - 'generateNewsEntryReadAnotherLinks', - 'generatePageLayout', - 'linkNewsIndex', - 'transformContent', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({newsData}) { return {newsData}; }, diff --git a/src/content/dependencies/generateNewsEntryReadAnotherLinks.js b/src/content/dependencies/generateNewsEntryReadAnotherLinks.js index d978b0e4..a985742b 100644 --- a/src/content/dependencies/generateNewsEntryReadAnotherLinks.js +++ b/src/content/dependencies/generateNewsEntryReadAnotherLinks.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateAbsoluteDatetimestamp', - 'generateRelativeDatetimestamp', - 'linkNewsEntry', - ], - - extraDependencies: ['html', 'language'], - relations(relation, currentEntry, previousEntry, nextEntry) { const relations = {}; diff --git a/src/content/dependencies/generateNewsIndexPage.js b/src/content/dependencies/generateNewsIndexPage.js index 02964ce8..d88bfdba 100644 --- a/src/content/dependencies/generateNewsIndexPage.js +++ b/src/content/dependencies/generateNewsIndexPage.js @@ -2,14 +2,6 @@ import {sortChronologically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generatePageLayout', - 'linkNewsEntry', - 'transformContent', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({newsData}) { return {newsData}; }, diff --git a/src/content/dependencies/generateNextLink.js b/src/content/dependencies/generateNextLink.js index 2e48cd2b..2c497e12 100644 --- a/src/content/dependencies/generateNextLink.js +++ b/src/content/dependencies/generateNextLink.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['generatePreviousNextLink'], - relations: (relation) => ({ link: relation('generatePreviousNextLink'), diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 8a073624..23d5932d 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -1,27 +1,9 @@ +import striptags from 'striptags'; + import {openAggregate} from '#aggregate'; -import {empty, repeat} from '#sugar'; +import {atOffset, empty, repeat} from '#sugar'; export default { - contentDependencies: [ - 'generateColorStyleRules', - 'generateFooterLocalizationLinks', - 'generateImageOverlay', - 'generatePageSidebar', - 'generateSearchSidebarBox', - 'generateStickyHeadingContainer', - 'transformContent', - ], - - extraDependencies: [ - 'getColors', - 'html', - 'language', - 'pagePath', - 'pagePathStringFromRoot', - 'to', - 'wikiData', - ], - sprawl: ({wikiInfo}) => ({ enableSearch: wikiInfo.enableSearch, footerContent: wikiInfo.footerContent, @@ -58,8 +40,14 @@ export default { relation('transformContent', sprawl.footerContent); } - relations.colorStyleRules = - relation('generateColorStyleRules'); + relations.colorStyleTag = + relation('generateColorStyleTag'); + + relations.staticURLStyleTag = + relation('generateStaticURLStyleTag'); + + relations.wikiWallpaperStyleTag = + relation('generateWikiWallpaperStyleTag'); relations.imageOverlay = relation('generateImageOverlay'); @@ -93,7 +81,7 @@ export default { mutable: false, }, - cover: { + artworkColumnContent: { type: 'html', mutable: false, }, @@ -107,9 +95,9 @@ export default { color: {validate: v => v.isColor}, - styleRules: { - validate: v => v.sparseArrayOf(v.isHTML), - default: [], + styleTags: { + type: 'html', + mutable: false, }, mainClasses: { @@ -262,15 +250,43 @@ export default { ? data.canonicalBase + pagePathStringFromRoot : null); + const primaryCover = (() => { + const apparentFirst = tag => html.smooth(tag).content[0]; + + const maybeTemplate = + apparentFirst(slots.artworkColumnContent); + + if (!maybeTemplate) return null; + + const maybeTemplateContent = + html.resolve(maybeTemplate, {normalize: 'tag'}); + + const maybeCoverArtwork = + apparentFirst(maybeTemplateContent); + + if (!maybeCoverArtwork) return null; + + if (maybeCoverArtwork.attributes.has('class', 'cover-artwork')) { + return maybeTemplate; + } else { + return null; + } + })(); + const titleContentsHTML = (html.isBlank(slots.title) ? null - : html.isBlank(slots.additionalNames) - ? language.sanitize(slots.title) - : html.tag('a', { + + : (!html.isBlank(slots.additionalNames) && + !html.resolve(slots.additionalNames, {slots: ['alwaysVisible']}) + .getSlotValue('alwaysVisible')) + + ? html.tag('a', { href: '#additional-names-box', title: language.$('misc.additionalNames.tooltip').toString(), - }, language.sanitize(slots.title))); + }, language.sanitize(slots.title)) + + : language.sanitize(slots.title)); const titleHTML = (html.isBlank(slots.title) @@ -279,7 +295,7 @@ export default { ? [ relations.stickyHeadingContainer.slots({ title: titleContentsHTML, - cover: slots.cover, + cover: primaryCover, }), relations.stickyHeadingContainer.clone().slots({ @@ -316,9 +332,11 @@ export default { [ titleHTML, - html.tag('div', {id: 'cover-art-container'}, + html.tag('div', {id: 'artwork-column'}, {[html.onlyIfContent]: true}, - slots.cover), + {class: 'isolate-tooltip-z-indexing'}, + + slots.artworkColumnContent), subtitleHTML, @@ -361,7 +379,7 @@ export default { slots.navLinks ?.filter(Boolean) - ?.map((cur, i) => { + ?.map((cur, i, entries) => { let content; if (cur.html) { @@ -395,25 +413,42 @@ export default { (slots.navLinkStyle === 'hierarchical' && i === slots.navLinks.length - 1); - return ( - html.metatag('blockwrap', - html.tag('span', {class: 'nav-link'}, - showAsCurrent && - {class: 'current'}, - - [ - html.tag('span', {class: 'nav-link-content'}, - content), - - html.tag('span', {class: 'nav-link-accent'}, - {[html.noEdgeWhitespace]: true}, - {[html.onlyIfContent]: true}, - - language.$('misc.navAccent', { - [language.onlyIfOptions]: ['links'], - links: cur.accent, - })), - ]))); + const navLink = + html.tag('span', {class: 'nav-link'}, + showAsCurrent && + {class: 'current'}, + + [ + html.tag('span', {class: 'nav-link-content'}, + content), + + html.tag('span', {class: 'nav-link-accent'}, + {[html.noEdgeWhitespace]: true}, + {[html.onlyIfContent]: true}, + + language.$('misc.navAccent', { + [language.onlyIfOptions]: ['links'], + links: cur.accent, + })), + ]); + + if (slots.navLinkStyle === 'index') { + return navLink; + } + + const prev = + atOffset(entries, i, -1); + + if ( + prev && + prev.releaseRestToWrapTogether !== true && + (prev.releaseRestToWrapTogether === false || + prev.auto === 'home') + ) { + return navLink; + } else { + return html.metatag('blockwrap', navLink); + } })), html.tag('div', {class: 'nav-bottom-row'}, @@ -539,24 +574,33 @@ export default { {id: 'additional-files', string: 'additionalFiles'}, {id: 'commentary', string: 'commentary'}, {id: 'artist-commentary', string: 'artistCommentary'}, - {id: 'credit-sources', string: 'creditSources'}, + {id: 'crediting-sources', string: 'creditingSources'}, + {id: 'referencing-sources', string: 'referencingSources'}, ])), ]); - const styleRulesCSS = - html.resolve(slots.styleRules, {normalize: 'string'}); + const slottedStyleTags = + html.smush(slots.styleTags); + + const slottedWallpaperStyleTag = + slottedStyleTags.content + .find(tag => tag.attributes.has('class', 'wallpaper-style')); - const fallbackBackgroundStyleRule = - (styleRulesCSS.match(/body::before[^}]*background-image:/) - ? '' - : `body::before {\n` + - ` background-image: url("${to('media.path', 'bg.jpg')}");\n` + - `}`); + const fallbackWallpaperStyleTag = + (slottedWallpaperStyleTag + ? html.blank() + : relations.wikiWallpaperStyleTag); + + const usingWallpaperStyleTag = + (slottedWallpaperStyleTag + ? slottedWallpaperStyleTag + : html.resolve(fallbackWallpaperStyleTag, {normalize: 'tag'})); const numWallpaperParts = - html.resolve(slots.styleRules, {normalize: 'string'}) - .match(/\.wallpaper-part:nth-child/g) - ?.length ?? 0; + (usingWallpaperStyleTag && + usingWallpaperStyleTag.attributes.has('data-wallpaper-mode', 'parts') + ? parseInt(usingWallpaperStyleTag.attributes.get('data-num-wallpaper-parts')) + : 0); const wallpaperPartsHTML = html.tag('div', {class: 'wallpaper-parts'}, @@ -612,11 +656,25 @@ export default { language.encapsulate('misc.pageTitle', workingCapsule => { const workingOptions = {}; - workingOptions.title = slots.title; + // Slightly jank: The output of striptags is, of course, a string, + // and as far as language.formatString() is concerned, that means + // it needs to be sanitized - including turning ampersands into + // &'s. But the title is already HTML that has implicitly been + // sanitized, however it got here, and includes HTML entities that + // are properly escaped. Those need to get included as they are, + // so we wrap the title in a tag and pass it off as good to go. + workingOptions.title = + html.tags([ + striptags(slots.title.toString()), + ]); if (!html.isBlank(slots.subtitle)) { + // Same shenanigans here, as far as wrapping striptags goes. workingCapsule += '.withSubtitle'; - workingOptions.subtitle = slots.subtitle; + workingOptions.subtitle = + html.tags([ + striptags(slots.subtitle.toString()), + ]); } const showWikiName = @@ -698,13 +756,14 @@ export default { href: to('staticCSS.path', 'site.css'), }), - html.tag('style', [ - relations.colorStyleRules - .slot('color', slots.color ?? data.wikiColor), + relations.colorStyleTag + .slot('color', slots.color ?? data.wikiColor), - fallbackBackgroundStyleRule, - slots.styleRules, - ]), + relations.staticURLStyleTag, + + fallbackWallpaperStyleTag, + + slottedStyleTags, html.tag('script', { src: to('staticLib.path', 'chroma-js/chroma.min.js'), diff --git a/src/content/dependencies/generatePageSidebar.js b/src/content/dependencies/generatePageSidebar.js index d3b55580..dfe85632 100644 --- a/src/content/dependencies/generatePageSidebar.js +++ b/src/content/dependencies/generatePageSidebar.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { // Attributes to apply to the whole sidebar. This be added to the // containing sidebar-column, arr - specify attributes on each section if diff --git a/src/content/dependencies/generatePageSidebarBox.js b/src/content/dependencies/generatePageSidebarBox.js index 26b30494..3133aa64 100644 --- a/src/content/dependencies/generatePageSidebarBox.js +++ b/src/content/dependencies/generatePageSidebarBox.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { content: { type: 'html', diff --git a/src/content/dependencies/generatePageSidebarConjoinedBox.js b/src/content/dependencies/generatePageSidebarConjoinedBox.js index 7974c707..4ed0ff22 100644 --- a/src/content/dependencies/generatePageSidebarConjoinedBox.js +++ b/src/content/dependencies/generatePageSidebarConjoinedBox.js @@ -4,9 +4,6 @@ // templates' resolved content), take care when slotting into this. export default { - contentDependencies: ['generatePageSidebarBox'], - extraDependencies: ['html'], - relations: (relation) => ({ box: relation('generatePageSidebarBox'), diff --git a/src/content/dependencies/generatePreviousLink.js b/src/content/dependencies/generatePreviousLink.js index 775367f9..29146a21 100644 --- a/src/content/dependencies/generatePreviousLink.js +++ b/src/content/dependencies/generatePreviousLink.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['generatePreviousNextLink'], - relations: (relation) => ({ link: relation('generatePreviousNextLink'), diff --git a/src/content/dependencies/generatePreviousNextLink.js b/src/content/dependencies/generatePreviousNextLink.js index afae1228..1e98358f 100644 --- a/src/content/dependencies/generatePreviousNextLink.js +++ b/src/content/dependencies/generatePreviousNextLink.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html', 'language'], - slots: { link: { type: 'html', diff --git a/src/content/dependencies/generateQuickDescription.js b/src/content/dependencies/generateQuickDescription.js index e144503e..f67f9514 100644 --- a/src/content/dependencies/generateQuickDescription.js +++ b/src/content/dependencies/generateQuickDescription.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['transformContent'], - extraDependencies: ['html', 'language'], - query: (thing) => ({ hasDescription: !!thing.description, diff --git a/src/content/dependencies/generateReadCommentaryLine.js b/src/content/dependencies/generateReadCommentaryLine.js new file mode 100644 index 00000000..05700536 --- /dev/null +++ b/src/content/dependencies/generateReadCommentaryLine.js @@ -0,0 +1,43 @@ +import {empty} from '#sugar'; + +export default { + query: (thing) => ({ + entries: + (thing.isTrack + ? [...thing.commentary, ...thing.commentaryFromMainRelease] + : thing.commentary), + }), + + data: (query, _thing) => ({ + hasWikiEditorCommentary: + query.entries.some(entry => entry.isWikiEditorCommentary), + + onlyWikiEditorCommentary: + !empty(query.entries) && + query.entries.every(entry => entry.isWikiEditorCommentary), + + hasAnyCommentary: + !empty(query.entries), + }), + + generate: (data, {html, language}) => + language.encapsulate('releaseInfo.readCommentary', capsule => + language.$(capsule, { + [language.onlyIfOptions]: ['link'], + + link: + html.tag('a', + {[html.onlyIfContent]: true}, + + {href: '#artist-commentary'}, + + language.encapsulate(capsule, 'link', capsule => + (data.onlyWikiEditorCommentary + ? language.$(capsule, 'onlyWikiCommentary') + : data.hasWikiEditorCommentary + ? language.$(capsule, 'withWikiCommentary') + : data.hasAnyCommentary + ? language.$(capsule) + : html.blank()))), + })), +}; diff --git a/src/content/dependencies/generateReferencedArtworksPage.js b/src/content/dependencies/generateReferencedArtworksPage.js index 3d21b15d..2f47b7a5 100644 --- a/src/content/dependencies/generateReferencedArtworksPage.js +++ b/src/content/dependencies/generateReferencedArtworksPage.js @@ -1,67 +1,45 @@ -import {stitchArrays} from '#sugar'; - export default { - contentDependencies: [ - 'generateCoverGrid', - 'generatePageLayout', - 'image', - 'linkAlbum', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - - relations: (relation, referencedArtworks) => ({ + relations: (relation, artwork) => ({ layout: relation('generatePageLayout'), + cover: + relation('generateCoverArtwork', artwork), + coverGrid: relation('generateCoverGrid'), links: - referencedArtworks.map(({thing}) => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))), + artwork.referencedArtworks.map(({artwork}) => + relation('linkAnythingMan', artwork.thing)), images: - referencedArtworks.map(({thing}) => - relation('image', thing.artTags)), + artwork.referencedArtworks.map(({artwork}) => + relation('image', artwork)), }), - data: (referencedArtworks) => ({ + data: (artwork) => ({ + color: + artwork.thing.color, + count: - referencedArtworks.length, + artwork.referencedArtworks.length, names: - referencedArtworks - .map(({thing}) => thing.name), - - paths: - referencedArtworks - .map(({thing}) => - (thing.album - ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] - : ['media.albumCover', thing.directory, thing.coverArtFileExtension])), - - dimensions: - referencedArtworks - .map(({thing}) => thing.coverArtDimensions), + artwork.referencedArtworks + .map(({artwork}) => artwork.thing.name), coverArtistNames: - referencedArtworks - .map(({thing}) => - thing.coverArtistContribs + artwork.referencedArtworks + .map(({artwork}) => + artwork.artistContribs .map(contrib => contrib.artist.name)), }), slots: { - color: {validate: v => v.isColor}, - - styleRules: {type: 'html', mutable: false}, + styleTags: {type: 'html', mutable: false}, title: {type: 'html', mutable: false}, - cover: {type: 'html', mutable: true}, navLinks: {validate: v => v.isArray}, navBottomRowContent: {type: 'html', mutable: false}, @@ -73,11 +51,13 @@ export default { title: slots.title, subtitle: language.$(pageCapsule, 'subtitle'), - color: slots.color, - styleRules: slots.styleRules, + color: data.color, + styleTags: slots.styleTags, - cover: - slots.cover.slot('details', 'artists'), + artworkColumnContent: + relations.cover.slots({ + showArtistDetails: true, + }), mainClasses: ['top-index'], mainContent: [ @@ -91,19 +71,9 @@ export default { relations.coverGrid.slots({ links: relations.links, + images: relations.images, names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - }).map(({image, path, dimensions}) => - image.slots({ - path, - dimensions, - })), - info: data.coverArtistNames.map(names => language.$('misc.coverGrid.details.coverArtists', { diff --git a/src/content/dependencies/generateReferencingArtworksPage.js b/src/content/dependencies/generateReferencingArtworksPage.js index 2fe2e93d..abb92732 100644 --- a/src/content/dependencies/generateReferencingArtworksPage.js +++ b/src/content/dependencies/generateReferencingArtworksPage.js @@ -1,67 +1,45 @@ -import {stitchArrays} from '#sugar'; - export default { - contentDependencies: [ - 'generateCoverGrid', - 'generatePageLayout', - 'image', - 'linkAlbum', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - - relations: (relation, referencingArtworks) => ({ + relations: (relation, artwork) => ({ layout: relation('generatePageLayout'), + cover: + relation('generateCoverArtwork', artwork), + coverGrid: relation('generateCoverGrid'), links: - referencingArtworks.map(({thing}) => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))), + artwork.referencedByArtworks.map(({artwork}) => + relation('linkAnythingMan', artwork.thing)), images: - referencingArtworks.map(({thing}) => - relation('image', thing.artTags)), + artwork.referencedByArtworks.map(({artwork}) => + relation('image', artwork)), }), - data: (referencingArtworks) => ({ + data: (artwork) => ({ + color: + artwork.thing.color, + count: - referencingArtworks.length, + artwork.referencedByArtworks.length, names: - referencingArtworks - .map(({thing}) => thing.name), - - paths: - referencingArtworks - .map(({thing}) => - (thing.album - ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] - : ['media.albumCover', thing.directory, thing.coverArtFileExtension])), - - dimensions: - referencingArtworks - .map(({thing}) => thing.coverArtDimensions), + artwork.referencedByArtworks + .map(({artwork}) => artwork.thing.name), coverArtistNames: - referencingArtworks - .map(({thing}) => - thing.coverArtistContribs + artwork.referencedByArtworks + .map(({artwork}) => + artwork.artistContribs .map(contrib => contrib.artist.name)), }), slots: { - color: {validate: v => v.isColor}, - - styleRules: {type: 'html', mutable: false}, + styleTags: {type: 'html', mutable: false}, title: {type: 'html', mutable: false}, - cover: {type: 'html', mutable: true}, navLinks: {validate: v => v.isArray}, navBottomRowContent: {type: 'html', mutable: false}, @@ -73,11 +51,13 @@ export default { title: slots.title, subtitle: language.$(pageCapsule, 'subtitle'), - color: slots.color, - styleRules: slots.styleRules, + color: data.color, + styleTags: slots.styleTags, - cover: - slots.cover.slot('details', 'artists'), + artworkColumnContent: + relations.cover.slots({ + showArtistDetails: true, + }), mainClasses: ['top-index'], mainContent: [ @@ -91,19 +71,9 @@ export default { relations.coverGrid.slots({ links: relations.links, + images: relations.images, names: data.names, - images: - stitchArrays({ - image: relations.images, - path: data.paths, - dimensions: data.dimensions, - }).map(({image, path, dimensions}) => - image.slots({ - path, - dimensions, - })), - info: data.coverArtistNames.map(names => language.$('misc.coverGrid.details.coverArtists', { diff --git a/src/content/dependencies/generateRelativeDatetimestamp.js b/src/content/dependencies/generateRelativeDatetimestamp.js index a997de0e..b3fe6239 100644 --- a/src/content/dependencies/generateRelativeDatetimestamp.js +++ b/src/content/dependencies/generateRelativeDatetimestamp.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateAbsoluteDatetimestamp', - 'generateDatetimestampTemplate', - 'generateTooltip', - ], - - extraDependencies: ['html', 'language'], - data: (currentDate, referenceDate) => (currentDate.getTime() === referenceDate.getTime() ? {equal: true, date: currentDate} diff --git a/src/content/dependencies/generateReleaseInfoContributionsLine.js b/src/content/dependencies/generateReleaseInfoContributionsLine.js index 016e0a2c..4353ccf4 100644 --- a/src/content/dependencies/generateReleaseInfoContributionsLine.js +++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js @@ -1,16 +1,15 @@ export default { - contentDependencies: ['generateArtistCredit'], - extraDependencies: ['html'], - - relations: (relation, contributions) => ({ + relations: (relation, contributions, formatText) => ({ credit: - relation('generateArtistCredit', contributions, []), + relation('generateArtistCredit', contributions, [], formatText), }), slots: { stringKey: {type: 'string'}, featuringStringKey: {type: 'string'}, + additionalStringOptions: {validate: v => v.isObject}, + chronologyKind: {type: 'string'}, }, @@ -27,5 +26,6 @@ export default { normalStringKey: slots.stringKey, normalFeaturingStringKey: slots.featuringStringKey, + additionalStringOptions: slots.additionalStringOptions, }), }; diff --git a/src/content/dependencies/generateReleaseInfoListenLine.js b/src/content/dependencies/generateReleaseInfoListenLine.js new file mode 100644 index 00000000..97f248d6 --- /dev/null +++ b/src/content/dependencies/generateReleaseInfoListenLine.js @@ -0,0 +1,156 @@ +import {isExternalLinkContext} from '#external-links'; +import {empty, stitchArrays, unique} from '#sugar'; + +function getReleaseContext(urlString, { + _artistURLs, + albumArtistURLs, +}) { + const composerBandcampDomains = + albumArtistURLs + .filter(url => url.hostname.endsWith('.bandcamp.com')) + .map(url => url.hostname); + + const url = new URL(urlString); + + if (url.hostname === 'homestuck.bandcamp.com') { + return 'officialRelease'; + } + + if (composerBandcampDomains.includes(url.hostname)) { + return 'composerRelease'; + } + + return null; +} + +export default { + query(thing) { + const query = {}; + + query.album = + (thing.album + ? thing.album + : thing); + + query.urls = + (!empty(thing.urls) + ? thing.urls + : thing.album && + thing.album.style === 'single' && + thing.album.tracks[0] === thing + ? thing.album.urls + : []); + + query.artists = + thing.artistContribs + .map(contrib => contrib.artist); + + query.artistGroups = + query.artists + .flatMap(artist => artist.closelyLinkedGroups) + .map(({group}) => group); + + query.albumArtists = + query.album.artistContribs + .map(contrib => contrib.artist); + + query.albumArtistGroups = + query.albumArtists + .flatMap(artist => artist.closelyLinkedGroups) + .map(({group}) => group); + + return query; + }, + + relations: (relation, query, _thing) => ({ + links: + query.urls.map(url => relation('linkExternal', url)), + }), + + data(query, thing) { + const data = {}; + + data.name = thing.name; + + const artistURLs = + unique([ + ...query.artists.flatMap(artist => artist.urls), + ...query.artistGroups.flatMap(group => group.urls), + ]).map(url => new URL(url)); + + const albumArtistURLs = + unique([ + ...query.albumArtists.flatMap(artist => artist.urls), + ...query.albumArtistGroups.flatMap(group => group.urls), + ]).map(url => new URL(url)); + + const boundGetReleaseContext = urlString => + getReleaseContext(urlString, { + artistURLs, + albumArtistURLs, + }); + + let releaseContexts = + query.urls.map(boundGetReleaseContext); + + const albumReleaseContexts = + query.album.urls.map(boundGetReleaseContext); + + const presentReleaseContexts = + unique(releaseContexts.filter(Boolean)); + + const presentAlbumReleaseContexts = + unique(albumReleaseContexts.filter(Boolean)); + + if ( + presentReleaseContexts.length <= 1 && + presentAlbumReleaseContexts.length <= 1 + ) { + releaseContexts = + query.urls.map(() => null); + } + + data.releaseContexts = releaseContexts; + + return data; + }, + + slots: { + visibleWithoutLinks: { + type: 'boolean', + default: false, + }, + + context: { + validate: () => isExternalLinkContext, + default: 'generic', + }, + }, + + generate: (data, relations, slots, {html, language}) => + language.encapsulate('releaseInfo.listenOn', capsule => + (empty(relations.links) && slots.visibleWithoutLinks + ? language.$(capsule, 'noLinks', { + name: + html.tag('i', data.name), + }) + + : language.$('releaseInfo.listenOn', { + [language.onlyIfOptions]: ['links'], + + links: + language.formatDisjunctionList( + stitchArrays({ + link: relations.links, + releaseContext: data.releaseContexts, + }).map(({link, releaseContext}) => + link.slot('context', [ + ... + (Array.isArray(slots.context) + ? slots.context + : [slots.context]), + + releaseContext, + ]))), + }))), +}; diff --git a/src/content/dependencies/generateSearchSidebarBox.js b/src/content/dependencies/generateSearchSidebarBox.js index 188a678f..701a01ac 100644 --- a/src/content/dependencies/generateSearchSidebarBox.js +++ b/src/content/dependencies/generateSearchSidebarBox.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generatePageSidebarBox'], - extraDependencies: ['html', 'language'], - relations: (relation) => ({ sidebarBox: relation('generatePageSidebarBox'), @@ -57,6 +54,43 @@ export default { html.tag('template', {class: 'wiki-search-tag-result-kind-string'}, language.$(capsule, 'artTag')), ]), + + language.encapsulate(capsule, 'resultDisambiguator', capsule => [ + html.tag('template', {class: 'wiki-search-group-result-disambiguator-string'}, + language.$(capsule, 'group', { + disambiguator: html.tag('slot', {name: 'disambiguator'}), + })), + + html.tag('template', {class: 'wiki-search-flash-result-disambiguator-string'}, + language.$(capsule, 'flash', { + disambiguator: html.tag('slot', {name: 'disambiguator'}), + })), + + html.tag('template', {class: 'wiki-search-track-result-disambiguator-string'}, + language.$(capsule, 'track', { + disambiguator: html.tag('slot', {name: 'disambiguator'}), + })), + ]), + + language.encapsulate(capsule, 'resultFilter', capsule => [ + html.tag('template', {class: 'wiki-search-album-result-filter-string'}, + language.$(capsule, 'album')), + + html.tag('template', {class: 'wiki-search-artist-result-filter-string'}, + language.$(capsule, 'artist')), + + html.tag('template', {class: 'wiki-search-flash-result-filter-string'}, + language.$(capsule, 'flash')), + + html.tag('template', {class: 'wiki-search-group-result-filter-string'}, + language.$(capsule, 'group')), + + html.tag('template', {class: 'wiki-search-track-result-filter-string'}, + language.$(capsule, 'track')), + + html.tag('template', {class: 'wiki-search-tag-result-filter-string'}, + language.$(capsule, 'artTag')), + ]), ], })), }; diff --git a/src/content/dependencies/generateSecondaryNav.js b/src/content/dependencies/generateSecondaryNav.js index 9ce7ce9b..63b3839b 100644 --- a/src/content/dependencies/generateSecondaryNav.js +++ b/src/content/dependencies/generateSecondaryNav.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { content: { type: 'html', diff --git a/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js b/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js index f204f1fb..fe7c17ac 100644 --- a/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js +++ b/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js @@ -1,15 +1,4 @@ export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateInterpageDotSwitcher', - 'generateNextLink', - 'generatePreviousLink', - 'linkAlbumDynamically', - 'linkGroup', - ], - - extraDependencies: ['html', 'language'], - relations: (relation) => ({ switcher: relation('generateInterpageDotSwitcher'), diff --git a/src/content/dependencies/generateSocialEmbed.js b/src/content/dependencies/generateSocialEmbed.js index 513ea518..5fa9376c 100644 --- a/src/content/dependencies/generateSocialEmbed.js +++ b/src/content/dependencies/generateSocialEmbed.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['absoluteTo', 'html', 'language', 'wikiData'], - sprawl({wikiInfo}) { return { canonicalBase: wikiInfo.canonicalBase, diff --git a/src/content/dependencies/generateStaticPage.js b/src/content/dependencies/generateStaticPage.js index 226152c7..485b802e 100644 --- a/src/content/dependencies/generateStaticPage.js +++ b/src/content/dependencies/generateStaticPage.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generatePageLayout', 'transformContent'], - extraDependencies: ['html'], - relations(relation, staticPage) { return { layout: relation('generatePageLayout'), @@ -23,17 +20,19 @@ export default { title: data.name, headingMode: 'sticky', - styleRules: - (data.stylesheet - ? [data.stylesheet] - : []), + styleTags: [ + html.tag('style', {class: 'static-page-style'}, + {[html.onlyIfContent]: true}, + data.stylesheet), + ], mainClasses: ['long-content'], mainContent: [ relations.content, - data.script && - html.tag('script', data.script), + html.tag('script', + {[html.onlyIfContent]: true}, + data.script), ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateStaticURLStyleTag.js b/src/content/dependencies/generateStaticURLStyleTag.js new file mode 100644 index 00000000..443a4d08 --- /dev/null +++ b/src/content/dependencies/generateStaticURLStyleTag.js @@ -0,0 +1,20 @@ +export default { + relations: (relation) => ({ + styleTag: + relation('generateStyleTag'), + }), + + generate: (relations, {to}) => + relations.styleTag.slots({ + attributes: {class: 'static-url-style'}, + + rules: [ + { + select: '.image-media-link::after', + declare: [ + `mask-image: url("${to('staticMisc.path', 'image.svg')}");` + ], + }, + ], + }), +}; diff --git a/src/content/dependencies/generateStickyHeadingContainer.js b/src/content/dependencies/generateStickyHeadingContainer.js index ec3062a3..f7388d60 100644 --- a/src/content/dependencies/generateStickyHeadingContainer.js +++ b/src/content/dependencies/generateStickyHeadingContainer.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { rootAttributes: { type: 'attributes', diff --git a/src/content/dependencies/generateStyleTag.js b/src/content/dependencies/generateStyleTag.js new file mode 100644 index 00000000..cdeadcfe --- /dev/null +++ b/src/content/dependencies/generateStyleTag.js @@ -0,0 +1,46 @@ +import {empty} from '#sugar'; + +const indent = text => + text + .split('\n') + .map(line => ' '.repeat(4) + line) + .join('\n'); + +export default { + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + + rules: { + validate: v => + v.looseArrayOf( + v.validateProperties({ + select: v.isString, + declare: v.looseArrayOf(v.isString), + })), + }, + }, + + generate: (slots, {html}) => + html.tag('style', slots.attributes, + {[html.onlyIfContent]: true}, + + slots.rules + .filter(Boolean) + + .map(rule => ({ + select: rule.select, + declare: rule.declare.filter(Boolean), + })) + + .filter(rule => !empty(rule.declare)) + + .map(rule => + `${rule.select} {\n` + + indent(rule.declare.join('\n')) + '\n' + + `}`) + + .join('\n\n')), +}; diff --git a/src/content/dependencies/generateTextWithTooltip.js b/src/content/dependencies/generateTextWithTooltip.js index 49ce1f61..360cfebc 100644 --- a/src/content/dependencies/generateTextWithTooltip.js +++ b/src/content/dependencies/generateTextWithTooltip.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { attributes: { type: 'attributes', diff --git a/src/content/dependencies/generateTooltip.js b/src/content/dependencies/generateTooltip.js index b09ee230..6f23af6d 100644 --- a/src/content/dependencies/generateTooltip.js +++ b/src/content/dependencies/generateTooltip.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { attributes: { type: 'attributes', diff --git a/src/content/dependencies/generateTrackArtistCommentarySection.js b/src/content/dependencies/generateTrackArtistCommentarySection.js index e3041d3a..39a3e145 100644 --- a/src/content/dependencies/generateTrackArtistCommentarySection.js +++ b/src/content/dependencies/generateTrackArtistCommentarySection.js @@ -1,15 +1,6 @@ import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateCommentaryEntry', - 'generateContentHeading', - 'linkAlbum', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - query: (track) => ({ otherSecondaryReleasesWithCommentary: track.otherReleases @@ -18,8 +9,8 @@ export default { }), relations: (relation, query, track) => ({ - contentHeading: - relation('generateContentHeading'), + commentaryContentHeading: + relation('generateCommentaryContentHeading', track), mainReleaseTrackLink: (track.isSecondaryRelease @@ -28,7 +19,7 @@ export default { mainReleaseArtistCommentaryEntries: (track.isSecondaryRelease - ? track.mainReleaseTrack.commentary + ? track.commentaryFromMainRelease .map(entry => relation('generateCommentaryEntry', entry)) : null), @@ -78,54 +69,40 @@ export default { generate: (data, relations, {html, language}) => language.encapsulate('misc.artistCommentary', capsule => html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'artist-commentary'}, - title: language.$('misc.artistCommentary'), - }), + relations.commentaryContentHeading, + relations.artistCommentaryEntries, data.isSecondaryRelease && - html.tags([ - html.tag('p', {class: ['drop', 'commentary-drop']}, - {[html.onlyIfSiblings]: true}, - - language.encapsulate(capsule, 'info.fromMainRelease', workingCapsule => { - const workingOptions = {}; - - workingOptions.album = - relations.mainReleaseTrackLink.slots({ - content: - data.mainReleaseAlbumName, - - color: - data.mainReleaseAlbumColor, - }); - - if (data.name !== data.mainReleaseName) { - workingCapsule += '.namedDifferently'; - workingOptions.name = - html.tag('i', data.mainReleaseName); - } - - return language.$(workingCapsule, workingOptions); - })), - - relations.mainReleaseArtistCommentaryEntries, - ]), - - html.tags([ - data.isSecondaryRelease && - !html.isBlank(relations.mainReleaseArtistCommentaryEntries) && - html.tag('p', {class: ['drop', 'commentary-drop']}, - {[html.onlyIfSiblings]: true}, - - language.$(capsule, 'info.releaseSpecific', { - album: - relations.thisReleaseAlbumLink, - })), - - relations.artistCommentaryEntries, - ]), + html.tag('div', {class: 'inherited-commentary-section'}, + {[html.onlyIfContent]: true}, + + [ + html.tag('p', {class: ['drop', 'commentary-drop']}, + {[html.onlyIfSiblings]: true}, + + language.encapsulate(capsule, 'info.fromMainRelease', workingCapsule => { + const workingOptions = {}; + + workingOptions.album = + relations.mainReleaseTrackLink.slots({ + content: + data.mainReleaseAlbumName, + + color: + data.mainReleaseAlbumColor, + }); + + if (data.name !== data.mainReleaseName) { + workingCapsule += '.namedDifferently'; + workingOptions.name = + html.tag('i', data.mainReleaseName); + } + + return language.$(workingCapsule, workingOptions); + })), + + relations.mainReleaseArtistCommentaryEntries, + ]), html.tag('p', {class: ['drop', 'commentary-drop']}, {[html.onlyIfContent]: true}, diff --git a/src/content/dependencies/generateTrackArtworkColumn.js b/src/content/dependencies/generateTrackArtworkColumn.js new file mode 100644 index 00000000..234586e0 --- /dev/null +++ b/src/content/dependencies/generateTrackArtworkColumn.js @@ -0,0 +1,30 @@ +export default { + relations: (relation, track) => ({ + albumCover: + (!track.hasUniqueCoverArt && track.album.hasCoverArt + ? relation('generateCoverArtwork', track.album.coverArtworks[0]) + : null), + + trackCovers: + (track.hasUniqueCoverArt + ? track.trackArtworks.map(artwork => + relation('generateCoverArtwork', artwork)) + : []), + }), + + generate: (relations, {html}) => + html.tags([ + relations.albumCover?.slots({ + showOriginDetails: true, + showArtTagDetails: true, + showReferenceDetails: true, + }), + + relations.trackCovers.map(cover => + cover.slots({ + showOriginDetails: true, + showArtTagDetails: true, + showReferenceDetails: true, + })), + ]), +}; diff --git a/src/content/dependencies/generateTrackCoverArtwork.js b/src/content/dependencies/generateTrackCoverArtwork.js deleted file mode 100644 index 9153e2fc..00000000 --- a/src/content/dependencies/generateTrackCoverArtwork.js +++ /dev/null @@ -1,143 +0,0 @@ -export default { - contentDependencies: [ - 'generateCoverArtwork', - 'generateCoverArtworkArtTagDetails', - 'generateCoverArtworkArtistDetails', - 'generateCoverArtworkReferenceDetails', - 'image', - 'linkAlbum', - 'linkTrackReferencedArtworks', - 'linkTrackReferencingArtworks', - ], - - extraDependencies: ['html', 'language'], - - query: (track) => ({ - artTags: - (track.hasUniqueCoverArt - ? track.artTags - : track.album.artTags), - - coverArtistContribs: - (track.hasUniqueCoverArt - ? track.coverArtistContribs - : track.album.coverArtistContribs), - }), - - relations: (relation, query, track) => ({ - coverArtwork: - relation('generateCoverArtwork'), - - image: - relation('image'), - - artTagDetails: - relation('generateCoverArtworkArtTagDetails', - query.artTags), - - artistDetails: - relation('generateCoverArtworkArtistDetails', - query.coverArtistContribs), - - referenceDetails: - relation('generateCoverArtworkReferenceDetails', - track.referencedArtworks, - track.referencedByArtworks), - - referencedArtworksLink: - relation('linkTrackReferencedArtworks', track), - - referencingArtworksLink: - relation('linkTrackReferencingArtworks', track), - - albumLink: - relation('linkAlbum', track.album), - }), - - data: (query, track) => ({ - path: - (track.hasUniqueCoverArt - ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] - : ['media.albumCover', track.album.directory, track.album.coverArtFileExtension]), - - color: - track.color, - - dimensions: - (track.hasUniqueCoverArt - ? track.coverArtDimensions - : track.album.coverArtDimensions), - - nonUnique: - !track.hasUniqueCoverArt, - - warnings: - query.artTags - .filter(tag => tag.isContentWarning) - .map(tag => tag.name), - }), - - slots: { - mode: {type: 'string'}, - - details: { - validate: v => v.is('tags', 'artists'), - default: 'tags', - }, - - showReferenceLinks: { - type: 'boolean', - default: false, - }, - - showNonUniqueLine: { - type: 'boolean', - default: false, - }, - }, - - generate: (data, relations, slots, {html, language}) => - relations.coverArtwork.slots({ - mode: slots.mode, - - image: - relations.image.slots({ - path: data.path, - color: data.color, - alt: language.$('misc.alt.trackCover'), - }), - - dimensions: data.dimensions, - warnings: data.warnings, - - details: [ - slots.details === 'tags' && - relations.artTagDetails, - - slots.details === 'artists'&& - relations.artistDetails, - - slots.showReferenceLinks && - relations.referenceDetails.slots({ - referencedLink: - relations.referencedArtworksLink, - - referencingLink: - relations.referencingArtworksLink, - }), - - slots.showNonUniqueLine && - data.nonUnique && - html.tag('p', {class: 'image-details'}, - {class: 'non-unique-details'}, - - language.$('misc.trackArtFromAlbum', { - album: - relations.albumLink.slots({ - color: false, - }), - })), - ], - }), -}; - diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 1c349c2e..92e00a41 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -1,44 +1,46 @@ -export default { - contentDependencies: [ - 'generateAdditionalNamesBox', - 'generateAlbumAdditionalFilesList', - 'generateAlbumNavAccent', - 'generateAlbumSecondaryNav', - 'generateAlbumSidebar', - 'generateAlbumStyleRules', - 'generateCommentaryEntry', - 'generateContentHeading', - 'generateContributionList', - 'generatePageLayout', - 'generateTrackArtistCommentarySection', - 'generateTrackCoverArtwork', - 'generateTrackInfoPageFeaturedByFlashesList', - 'generateTrackInfoPageOtherReleasesList', - 'generateTrackList', - 'generateTrackListDividedByGroups', - 'generateTrackNavLinks', - 'generateTrackReleaseInfo', - 'generateTrackSocialEmbed', - 'linkAlbum', - 'linkTrack', - 'transformContent', - ], - - extraDependencies: ['html', 'language'], +function checkInterrupted(which, relations, {html}) { + if ( + !html.isBlank(relations.additionalFilesList) || + !html.isBlank(relations.contributorContributionList) || + !html.isBlank(relations.flashesThatFeatureList) || + !html.isBlank(relations.lyricsSection) || + !html.isBlank(relations.midiProjectFilesList) || + !html.isBlank(relations.referencedByTracksList) || + !html.isBlank(relations.referencedTracksList) || + !html.isBlank(relations.sampledByTracksList) || + !html.isBlank(relations.sampledTracksList) || + !html.isBlank(relations.sheetMusicFilesList) + ) return true; + + if (which === 'crediting-sources' || which === 'referencing-sources') { + if (!html.isBlank(relations.artistCommentarySection)) return true; + } + + return false; +} +export default { query: (track) => ({ mainReleaseTrack: (track.isMainRelease ? track : track.mainReleaseTrack), + + singleTrackSingle: + track.album.style === 'single' && + track.album.tracks.length === 1, + + firstTrackInSingle: + track.album.style === 'single' && + track === track.album.tracks[0], }), relations: (relation, query, track) => ({ layout: relation('generatePageLayout'), - albumStyleRules: - relation('generateAlbumStyleRules', track.album, track), + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), socialEmbed: relation('generateTrackSocialEmbed', track), @@ -46,6 +48,9 @@ export default { navLinks: relation('generateTrackNavLinks', track), + albumNavLink: + relation('linkAlbum', track.album), + albumNavAccent: relation('generateAlbumNavAccent', track.album, track), @@ -58,17 +63,23 @@ export default { additionalNamesBox: relation('generateAdditionalNamesBox', track.additionalNames), - cover: - (track.hasUniqueCoverArt || track.album.hasCoverArt - ? relation('generateTrackCoverArtwork', track) - : null), + artworkColumn: + (query.firstTrackInSingle + ? relation('generateAlbumArtworkColumn', track.album) + : relation('generateTrackArtworkColumn', track)), contentHeading: relation('generateContentHeading'), + name: + relation('generateName', track), + releaseInfo: relation('generateTrackReleaseInfo', track), + readCommentaryLine: + relation('generateReadCommentaryLine', track), + otherReleasesList: relation('generateTrackInfoPageOtherReleasesList', track), @@ -76,54 +87,68 @@ export default { relation('generateContributionList', track.contributorContribs), referencedTracksList: - relation('generateTrackList', track.referencedTracks), + relation('generateTrackList', track.referencedTracks, track), sampledTracksList: - relation('generateTrackList', track.sampledTracks), + relation('generateTrackList', track.sampledTracks, track), referencedByTracksList: relation('generateTrackListDividedByGroups', - query.mainReleaseTrack.referencedByTracks), + query.mainReleaseTrack.referencedByTracks, + track), sampledByTracksList: relation('generateTrackListDividedByGroups', - query.mainReleaseTrack.sampledByTracks), + query.mainReleaseTrack.sampledByTracks, + track), flashesThatFeatureList: relation('generateTrackInfoPageFeaturedByFlashesList', track), - lyrics: - relation('transformContent', track.lyrics), + lyricsSection: + relation('generateLyricsSection', track.lyrics), sheetMusicFilesList: - relation('generateAlbumAdditionalFilesList', - track.album, - track.sheetMusicFiles), + relation('generateAdditionalFilesList', track.sheetMusicFiles), midiProjectFilesList: - relation('generateAlbumAdditionalFilesList', - track.album, - track.midiProjectFiles), + relation('generateAdditionalFilesList', track.midiProjectFiles), additionalFilesList: - relation('generateAlbumAdditionalFilesList', - track.album, - track.additionalFiles), + relation('generateAdditionalFilesList', track.additionalFiles), artistCommentarySection: relation('generateTrackArtistCommentarySection', track), - creditSourceEntries: - track.creditSources - .map(entry => relation('generateCommentaryEntry', entry)), + creditingSourcesSection: + relation('generateCollapsedContentEntrySection', + track.creditingSources, + track), + + referencingSourcesSection: + relation('generateCollapsedContentEntrySection', + track.referencingSources, + track), }), - data: (_query, track) => ({ + data: (query, track) => ({ name: track.name, color: track.color, + + dateAlbumAddedToWiki: + track.album.dateAddedToWiki, + + needsLyrics: + track.needsLyrics, + + singleTrackSingle: + query.singleTrackSingle, + + firstTrackInSingle: + query.firstTrackInSingle, }), generate: (data, relations, {html, language}) => @@ -131,7 +156,7 @@ export default { relations.layout.slots({ title: language.$(pageCapsule, 'title', { - track: data.name, + track: relations.name, }), headingMode: 'sticky', @@ -139,15 +164,10 @@ export default { additionalNames: relations.additionalNamesBox, color: data.color, - styleRules: [relations.albumStyleRules], + styleTags: relations.albumStyleTags, - cover: - (relations.cover - ? relations.cover.slots({ - showReferenceLinks: true, - showNonUniqueLine: true, - }) - : null), + artworkColumnContent: + relations.artworkColumn, mainContent: [ relations.releaseInfo, @@ -184,21 +204,26 @@ export default { language.$(capsule, 'link')), })), - !html.isBlank(relations.artistCommentarySection) && - language.encapsulate(capsule, 'readCommentary', capsule => + checkInterrupted('commentary', relations, {html}) && + relations.readCommentaryLine, + + !html.isBlank(relations.creditingSourcesSection) && + checkInterrupted('crediting-sources', relations, {html}) && + language.encapsulate(capsule, 'readCreditingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#artist-commentary'}, + {href: '#crediting-sources'}, language.$(capsule, 'link')), })), - !html.isBlank(relations.creditSourceEntries) && - language.encapsulate(capsule, 'readCreditSources', capsule => + !html.isBlank(relations.referencingSourcesSection) && + checkInterrupted('referencing-sources', relations, {html}) && + language.encapsulate(capsule, 'readReferencingSources', capsule => language.$(capsule, { link: html.tag('a', - {href: '#credit-sources'}, + {href: '#referencing-sources'}, language.$(capsule, 'link')), })), ])), @@ -315,17 +340,26 @@ export default { relations.flashesThatFeatureList, ]), - html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'lyrics'}, - title: language.$('releaseInfo.lyrics'), - }), - - html.tag('blockquote', + data.firstTrackInSingle && + html.tag('p', {[html.onlyIfContent]: true}, - relations.lyrics.slot('mode', 'lyrics')), - ]), + + language.$('releaseInfo.addedToWiki', { + [language.onlyIfOptions]: ['date'], + date: language.formatDate(data.dateAlbumAddedToWiki), + })), + + data.firstTrackInSingle && + (!html.isBlank(relations.lyricsSection) || + !html.isBlank(relations.artistCommentarySection)) && + html.tag('hr', {class: 'main-separator'}), + + data.needsLyrics && + html.isBlank(relations.lyricsSection) && + html.tag('p', + language.$(pageCapsule, 'needsLyrics')), + + relations.lyricsSection, html.tags([ relations.contentHeading.clone() @@ -359,29 +393,40 @@ export default { relations.artistCommentarySection, - html.tags([ - relations.contentHeading.clone() - .slots({ - attributes: {id: 'credit-sources'}, - title: language.$('misc.creditSources'), - }), + relations.creditingSourcesSection.slots({ + id: 'crediting-sources', + string: 'misc.creditingSources', + }), - relations.creditSourceEntries, - ]), + relations.referencingSourcesSection.slots({ + id: 'referencing-sources', + string: 'misc.referencingSources', + }), ], navLinkStyle: 'hierarchical', - navLinks: html.resolve(relations.navLinks), + navLinks: + (data.singleTrackSingle + ? [ + {auto: 'home'}, + { + html: relations.albumNavLink, + accent: language.$(pageCapsule, 'nav.singleAccent'), + }, + ] + : html.resolve(relations.navLinks)), navBottomRowContent: - relations.albumNavAccent.slots({ - showTrackNavigation: true, - showExtraLinks: false, - }), + (data.singleTrackSingle + ? null + : relations.albumNavAccent.slots({ + showTrackNavigation: true, + showExtraLinks: false, + })), secondaryNav: relations.secondaryNav - .slot('mode', 'track'), + .slot('mode', data.firstTrackInSingle ? 'album' : 'track'), leftSidebar: relations.sidebar, diff --git a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js index 61654512..cd7bb014 100644 --- a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js +++ b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js @@ -2,9 +2,6 @@ import {sortFlashesChronologically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['linkFlash', 'linkTrack'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({ enableFlashesAndGames: wikiInfo.enableFlashesAndGames, diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js index ebd76577..ca6c3fb7 100644 --- a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js +++ b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js @@ -1,22 +1,39 @@ -import {stitchArrays} from '#sugar'; +import {onlyItem, stitchArrays} from '#sugar'; export default { - contentDependencies: ['linkTrack'], - extraDependencies: ['html', 'language'], + query(track) { + const query = {}; + + query.singleSingle = + onlyItem( + track.otherReleases.filter(track => track.album.style === 'single')); + + query.regularReleases = + (query.singleSingle + ? track.otherReleases.filter(track => track !== query.singleSingle) + : track.otherReleases); + + return query; + }, + + relations: (relation, query, _track) => ({ + singleLink: + (query.singleSingle + ? relation('linkTrack', query.singleSingle) + : null), - relations: (relation, track) => ({ trackLinks: - track.otherReleases + query.regularReleases .map(track => relation('linkTrack', track)), }), - data: (track) => ({ + data: (query, _track) => ({ albumNames: - track.otherReleases + query.regularReleases .map(track => track.album.name), albumColors: - track.otherReleases + query.regularReleases .map(track => track.album.color), }), @@ -24,19 +41,43 @@ export default { html.tag('p', {[html.onlyIfContent]: true}, - language.$('releaseInfo.alsoReleasedOn', { - [language.onlyIfOptions]: ['albums'], - - albums: - language.formatConjunctionList( - stitchArrays({ - trackLink: relations.trackLinks, - albumName: data.albumNames, - albumColor: data.albumColors, - }).map(({trackLink, albumName, albumColor}) => - trackLink.slots({ - content: language.sanitize(albumName), - color: albumColor, - }))), - })), + language.encapsulate('releaseInfo.alsoReleased', capsule => + language.encapsulate(capsule, workingCapsule => { + const workingOptions = {}; + + let any = false; + + const albumList = + language.formatConjunctionList( + stitchArrays({ + trackLink: relations.trackLinks, + albumName: data.albumNames, + albumColor: data.albumColors, + }).map(({trackLink, albumName, albumColor}) => + trackLink.slots({ + content: language.sanitize(albumName), + color: albumColor, + }))); + + if (!html.isBlank(albumList)) { + any = true; + workingCapsule += '.onAlbums'; + workingOptions.albums = albumList; + } + + if (relations.singleLink) { + any = true; + workingCapsule += '.asSingle'; + workingOptions.single = + relations.singleLink.slots({ + content: language.$(capsule, 'single'), + }); + } + + if (any) { + return language.$(workingCapsule, workingOptions); + } else { + return html.blank(); + } + }))), }; diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js index 53a32536..e30feb23 100644 --- a/src/content/dependencies/generateTrackList.js +++ b/src/content/dependencies/generateTrackList.js @@ -1,10 +1,16 @@ export default { - contentDependencies: ['generateTrackListItem'], - extraDependencies: ['html'], + query: (tracks, contextTrack) => ({ + presentedTracks: + (contextTrack + ? tracks.map(track => + track.otherReleases.find(({album}) => album === contextTrack.album) ?? + track) + : tracks), + }), - relations: (relation, tracks) => ({ + relations: (relation, query, _tracks, _contextTrack) => ({ items: - tracks + query.presentedTracks .map(track => relation('generateTrackListItem', track, [])), }), diff --git a/src/content/dependencies/generateTrackListDividedByGroups.js b/src/content/dependencies/generateTrackListDividedByGroups.js index 230868d6..d7342891 100644 --- a/src/content/dependencies/generateTrackListDividedByGroups.js +++ b/src/content/dependencies/generateTrackListDividedByGroups.js @@ -1,20 +1,12 @@ import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateContentHeading', - 'generateTrackList', - 'linkGroup', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({wikiInfo}) => ({ divideTrackListsByGroups: wikiInfo.divideTrackListsByGroups, }), - query(sprawl, tracks) { + query(sprawl, tracks, _contextTrack) { const dividingGroups = sprawl.divideTrackListsByGroups; const groupings = new Map(); @@ -50,10 +42,10 @@ export default { return {groups, groupedTracks, ungroupedTracks}; }, - relations: (relation, query, sprawl, tracks) => ({ + relations: (relation, query, sprawl, tracks, contextTrack) => ({ flatList: (empty(sprawl.divideTrackListsByGroups) - ? relation('generateTrackList', tracks) + ? relation('generateTrackList', tracks, contextTrack) : null), contentHeading: @@ -65,12 +57,12 @@ export default { groupedTrackLists: query.groupedTracks - .map(tracks => relation('generateTrackList', tracks)), + .map(tracks => relation('generateTrackList', tracks, contextTrack)), ungroupedTrackList: (empty(query.ungroupedTracks) ? null - : relation('generateTrackList', query.ungroupedTracks)), + : relation('generateTrackList', query.ungroupedTracks, contextTrack)), }), data: (query, _sprawl, _tracks) => ({ diff --git a/src/content/dependencies/generateTrackListItem.js b/src/content/dependencies/generateTrackListItem.js index 887b6f03..9de9c3a6 100644 --- a/src/content/dependencies/generateTrackListItem.js +++ b/src/content/dependencies/generateTrackListItem.js @@ -1,13 +1,4 @@ export default { - contentDependencies: [ - 'generateArtistCredit', - 'generateColorStyleAttribute', - 'generateTrackListMissingDuration', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, track, contextContributions) => ({ trackLink: relation('linkTrack', track), @@ -15,7 +6,8 @@ export default { credit: relation('generateArtistCredit', track.artistContribs, - contextContributions), + contextContributions, + track.artistText), colorStyle: relation('generateColorStyleAttribute', track.color), @@ -97,8 +89,7 @@ export default { workingCapsule += '.withArtists'; workingOptions.by = html.tag('span', {class: 'by'}, - html.metatag('chunkwrap', {split: ','}, - html.resolve(relations.credit))); + relations.credit); } return language.$(workingCapsule, workingOptions); diff --git a/src/content/dependencies/generateTrackListMissingDuration.js b/src/content/dependencies/generateTrackListMissingDuration.js index b5917982..da3113a2 100644 --- a/src/content/dependencies/generateTrackListMissingDuration.js +++ b/src/content/dependencies/generateTrackListMissingDuration.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateTextWithTooltip', 'generateTooltip'], - extraDependencies: ['html', 'language'], - relations: (relation) => ({ textWithTooltip: relation('generateTextWithTooltip'), diff --git a/src/content/dependencies/generateTrackNavLinks.js b/src/content/dependencies/generateTrackNavLinks.js index e01653f0..d18e6cad 100644 --- a/src/content/dependencies/generateTrackNavLinks.js +++ b/src/content/dependencies/generateTrackNavLinks.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkAlbum', 'linkTrack'], - extraDependencies: ['html', 'language'], - relations: (relation, track) => ({ albumLink: relation('linkAlbum', track.album), @@ -11,11 +8,14 @@ export default { }), data: (track) => ({ + albumStyle: + track.album.style, + hasTrackNumbers: track.album.hasTrackNumbers, trackNumber: - track.album.tracks.indexOf(track) + 1, + track.trackNumber, }), slots: { @@ -28,7 +28,13 @@ export default { language.encapsulate('trackPage.nav', navCapsule => [ {auto: 'home'}, - {html: relations.albumLink.slot('color', false)}, + { + html: relations.albumLink.slot('color', false), + accent: + (data.albumStyle === 'single' + ? language.$(navCapsule, 'singleAccent') + : null), + }, { html: diff --git a/src/content/dependencies/generateTrackReferencedArtworksPage.js b/src/content/dependencies/generateTrackReferencedArtworksPage.js index ac81e525..a2612067 100644 --- a/src/content/dependencies/generateTrackReferencedArtworksPage.js +++ b/src/content/dependencies/generateTrackReferencedArtworksPage.js @@ -1,37 +1,21 @@ export default { - contentDependencies: [ - 'generateAlbumStyleRules', - 'generateBackToTrackLink', - 'generateReferencedArtworksPage', - 'generateTrackCoverArtwork', - 'generateTrackNavLinks', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, track) => ({ page: - relation('generateReferencedArtworksPage', track.referencedArtworks), + relation('generateReferencedArtworksPage', track.trackArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', track.album, track), + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), navLinks: relation('generateTrackNavLinks', track), backToTrackLink: relation('generateBackToTrackLink', track), - - cover: - relation('generateTrackCoverArtwork', track), }), data: (track) => ({ name: track.name, - - color: - track.color, }), generate: (data, relations, {html, language}) => @@ -42,10 +26,7 @@ export default { data.name, }), - color: data.color, - styleRules: [relations.albumStyleRules], - - cover: relations.cover, + styleTags: relations.albumStyleTags, navLinks: html.resolve( diff --git a/src/content/dependencies/generateTrackReferencingArtworksPage.js b/src/content/dependencies/generateTrackReferencingArtworksPage.js index 097ee929..be13dd79 100644 --- a/src/content/dependencies/generateTrackReferencingArtworksPage.js +++ b/src/content/dependencies/generateTrackReferencingArtworksPage.js @@ -1,37 +1,21 @@ export default { - contentDependencies: [ - 'generateAlbumStyleRules', - 'generateBackToTrackLink', - 'generateReferencingArtworksPage', - 'generateTrackCoverArtwork', - 'generateTrackNavLinks', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, track) => ({ page: - relation('generateReferencingArtworksPage', track.referencedByArtworks), + relation('generateReferencingArtworksPage', track.trackArtworks[0]), - albumStyleRules: - relation('generateAlbumStyleRules', track.album, track), + albumStyleTags: + relation('generateAlbumStyleTags', track.album, track), navLinks: relation('generateTrackNavLinks', track), backToTrackLink: relation('generateBackToTrackLink', track), - - cover: - relation('generateTrackCoverArtwork', track), }), data: (track) => ({ name: track.name, - - color: - track.color, }), generate: (data, relations, {html, language}) => @@ -42,10 +26,7 @@ export default { data.name, }), - color: data.color, - styleRules: [relations.albumStyleRules], - - cover: relations.cover, + styleTags: relations.albumStyleTags, navLinks: html.resolve( diff --git a/src/content/dependencies/generateTrackReleaseBox.js b/src/content/dependencies/generateTrackReleaseBox.js index ef02e2b9..c880fe63 100644 --- a/src/content/dependencies/generateTrackReleaseBox.js +++ b/src/content/dependencies/generateTrackReleaseBox.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generatePageSidebarBox', - 'linkTrack', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, track) => ({ box: relation('generatePageSidebarBox'), diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js index 38b8383f..0207e574 100644 --- a/src/content/dependencies/generateTrackReleaseInfo.js +++ b/src/content/dependencies/generateTrackReleaseInfo.js @@ -1,29 +1,19 @@ -import {empty} from '#sugar'; +import {compareArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateReleaseInfoContributionsLine', - 'linkExternal', - ], - - extraDependencies: ['html', 'language'], - relations(relation, track) { const relations = {}; - relations.artistContributionLinks = - relation('generateReleaseInfoContributionsLine', track.artistContribs); + relations.artistContributionsLine = + relation('generateReleaseInfoContributionsLine', + track.artistContribs, + track.artistText); - if (track.hasUniqueCoverArt) { - relations.coverArtistContributionsLine = - relation('generateReleaseInfoContributionsLine', track.coverArtistContribs); - } + relations.listenLine = + relation('generateReleaseInfoListenLine', track); - if (!empty(track.urls)) { - relations.externalLinks = - track.urls.map(url => - relation('linkExternal', url)); - } + relations.albumLink = + relation('linkAlbum', track.album); return relations; }, @@ -35,9 +25,18 @@ export default { data.date = track.date; data.duration = track.duration; + const {album} = track; + + data.showAlbum = + album.showAlbumInTracksWithoutArtists && + track.artistContribs.every(({annotation}) => !annotation) && + compareArrays( + track.artistContribs.map(({artist}) => artist), + album.artistContribs.map(({artist}) => artist), + {checkOrder: true}); + if ( track.hasUniqueCoverArt && - track.coverArtDate && +track.coverArtDate !== +track.date ) { data.coverArtDate = track.coverArtDate; @@ -54,15 +53,21 @@ export default { {[html.joinChildren]: html.tag('br')}, [ - relations.artistContributionLinks.slots({ - stringKey: capsule + '.by', - featuringStringKey: capsule + '.by.featuring', - chronologyKind: 'track', - }), + language.encapsulate(capsule, 'by', capsule => { + const withAlbum = + (data.showAlbum ? '.withAlbum' : ''); - relations.coverArtistContributionsLine?.slots({ - stringKey: capsule + '.coverArtBy', - chronologyKind: 'trackArt', + const albumOptions = + (data.showAlbum ? {album: relations.albumLink} : {}); + + return relations.artistContributionsLine.slots({ + stringKey: capsule + withAlbum, + featuringStringKey: capsule + '.featuring' + withAlbum, + + additionalStringOptions: albumOptions, + + chronologyKind: 'track', + }); }), language.$(capsule, 'released', { @@ -70,11 +75,6 @@ export default { date: language.formatDate(data.date), }), - language.$(capsule, 'artReleased', { - [language.onlyIfOptions]: ['date'], - date: language.formatDate(data.coverArtDate), - }), - language.$(capsule, 'duration', { [language.onlyIfOptions]: ['duration'], duration: language.formatDuration(data.duration), @@ -82,17 +82,9 @@ export default { ]), html.tag('p', - language.encapsulate(capsule, 'listenOn', capsule => - (relations.externalLinks - ? language.$(capsule, { - links: - language.formatDisjunctionList( - relations.externalLinks - .map(link => link.slot('context', 'track'))), - }) - : language.$(capsule, 'noLinks', { - name: - html.tag('i', data.name), - })))), + relations.listenLine.slots({ + visibleWithoutLinks: true, + context: ['track'], + })), ])), }; diff --git a/src/content/dependencies/generateTrackSocialEmbed.js b/src/content/dependencies/generateTrackSocialEmbed.js index 7cb37af2..94453f7d 100644 --- a/src/content/dependencies/generateTrackSocialEmbed.js +++ b/src/content/dependencies/generateTrackSocialEmbed.js @@ -1,11 +1,4 @@ export default { - contentDependencies: [ - 'generateSocialEmbed', - 'generateTrackSocialEmbedDescription', - ], - - extraDependencies: ['absoluteTo', 'language'], - relations(relation, track) { return { socialEmbed: @@ -26,14 +19,12 @@ export default { data.trackDirectory = track.directory; data.albumDirectory = album.directory; + data.hasImage = track.hasUniqueCoverArt || album.hasCoverArt; + if (track.hasUniqueCoverArt) { - data.imageSource = 'track'; - data.coverArtFileExtension = track.coverArtFileExtension; + data.imagePath = track.trackArtworks[0].path; } else if (album.hasCoverArt) { - data.imageSource = 'album'; - data.coverArtFileExtension = album.coverArtFileExtension; - } else { - data.imageSource = 'none'; + data.imagePath = album.coverArtworks[0].path; } return data; @@ -59,10 +50,8 @@ export default { absoluteTo('localized.album', data.albumDirectory), imagePath: - (data.imageSource === 'album' - ? ['media.albumCover', data.albumDirectory, data.coverArtFileExtension] - : data.imageSource === 'track' - ? ['media.trackCover', data.albumDirectory, data.trackDirectory, data.coverArtFileExtension] + (data.hasImage + ? data.imagePath : null), })), }; diff --git a/src/content/dependencies/generateTrackSocialEmbedDescription.js b/src/content/dependencies/generateTrackSocialEmbedDescription.js index 4706aa26..97a4017f 100644 --- a/src/content/dependencies/generateTrackSocialEmbedDescription.js +++ b/src/content/dependencies/generateTrackSocialEmbedDescription.js @@ -1,8 +1,6 @@ import {empty} from '#sugar'; export default { - extraDependencies: ['html', 'language'], - data: (track) => ({ artistNames: track.artistContribs diff --git a/src/content/dependencies/generateUnsafeMunchy.js b/src/content/dependencies/generateUnsafeMunchy.js index c11aadc7..df8231ef 100644 --- a/src/content/dependencies/generateUnsafeMunchy.js +++ b/src/content/dependencies/generateUnsafeMunchy.js @@ -1,6 +1,4 @@ export default { - extraDependencies: ['html'], - slots: { contentSource: {type: 'string'}, }, diff --git a/src/content/dependencies/generateWallpaperStyleTag.js b/src/content/dependencies/generateWallpaperStyleTag.js new file mode 100644 index 00000000..b89f01c2 --- /dev/null +++ b/src/content/dependencies/generateWallpaperStyleTag.js @@ -0,0 +1,77 @@ +import {empty, stitchArrays} from '#sugar'; + +export default { + relations: (relation) => ({ + styleTag: + relation('generateStyleTag'), + }), + + slots: { + singleWallpaperPath: { + validate: v => v.strictArrayOf(v.isString), + }, + + singleWallpaperStyle: { + validate: v => v.isString, + }, + + wallpaperPartPaths: { + validate: v => + v.strictArrayOf(v.optional(v.strictArrayOf(v.isString))), + }, + + wallpaperPartStyles: { + validate: v => + v.strictArrayOf(v.optional(v.isString)), + }, + }, + + generate(relations, slots, {html, to}) { + const attributes = html.attributes(); + const rules = []; + + attributes.add('class', 'wallpaper-style'); + + if (empty(slots.wallpaperPartPaths)) { + attributes.set('data-wallpaper-mode', 'one'); + + rules.push({ + select: 'body::before', + declare: [ + `background-image: url("${to(...slots.singleWallpaperPath)}");`, + slots.singleWallpaperStyle, + ], + }); + } else { + attributes.set('data-wallpaper-mode', 'parts'); + attributes.set('data-num-wallpaper-parts', slots.wallpaperPartPaths.length); + + stitchArrays({ + path: slots.wallpaperPartPaths, + style: slots.wallpaperPartStyles, + }).forEach(({path, style}, index) => { + rules.push({ + select: `.wallpaper-part:nth-child(${index + 1})`, + declare: [ + path && `background-image: url("${to(...path)}");`, + style, + ], + }); + }); + + rules.push({ + select: 'body::before', + declare: [ + 'display: none;', + ], + }); + } + + relations.styleTag.setSlots({ + attributes, + rules, + }); + + return relations.styleTag; + }, +}; diff --git a/src/content/dependencies/generateWikiHomepageActionsRow.js b/src/content/dependencies/generateWikiHomepageActionsRow.js index 9f501099..5e3ff381 100644 --- a/src/content/dependencies/generateWikiHomepageActionsRow.js +++ b/src/content/dependencies/generateWikiHomepageActionsRow.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['generateGridActionLinks', 'transformContent'], - relations: (relation, row) => ({ template: relation('generateGridActionLinks'), diff --git a/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js b/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js index 3068d951..8f4b3400 100644 --- a/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js +++ b/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js @@ -1,8 +1,4 @@ -import {stitchArrays} from '#sugar'; - export default { - contentDependencies: ['generateCoverCarousel', 'image', 'linkAlbum'], - relations: (relation, row) => ({ coverCarousel: relation('generateCoverCarousel'), @@ -13,27 +9,12 @@ export default { images: row.albums - .map(album => relation('image', album.artTags)), + .map(album => relation('image', album.coverArtworks[0])), }), - data: (row) => ({ - paths: - row.albums.map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)), - }), - - generate: (data, relations) => + generate: (relations) => relations.coverCarousel.slots({ - links: - relations.links, - - images: - stitchArrays({ - image: relations.images, - path: data.paths, - }).map(({image, path}) => - image.slot('path', path)), + links: relations.links, + images: relations.images, }), }; diff --git a/src/content/dependencies/generateWikiHomepageAlbumGridRow.js b/src/content/dependencies/generateWikiHomepageAlbumGridRow.js index c1d2c79d..eb3417d7 100644 --- a/src/content/dependencies/generateWikiHomepageAlbumGridRow.js +++ b/src/content/dependencies/generateWikiHomepageAlbumGridRow.js @@ -2,9 +2,6 @@ import {empty, stitchArrays} from '#sugar'; import {getNewAdditions, getNewReleases} from '#wiki-data'; export default { - contentDependencies: ['generateCoverGrid', 'image', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}, row) { const sprawl = {}; @@ -21,8 +18,7 @@ export default { sprawl.albums = (row.sourceGroup ? row.sourceGroup.albums - .slice() - .reverse() + .toReversed() .filter(album => album.isListedOnHomepage) .slice(0, row.countAlbumsFromGroup) : []); @@ -45,20 +41,17 @@ export default { images: sprawl.albums - .map(album => relation('image', album.artTags)), + .map(album => + relation('image', + (album.hasCoverArt + ? album.coverArtworks[0] + : null))), }), data: (sprawl, _row) => ({ names: sprawl.albums .map(album => album.name), - - paths: - sprawl.albums - .map(album => - (album.hasCoverArt - ? ['media.albumCover', album.directory, album.coverArtFileExtension] - : null)), }), generate: (data, relations, {language}) => @@ -69,11 +62,9 @@ export default { images: stitchArrays({ image: relations.images, - path: data.paths, name: data.names, - }).map(({image, path, name}) => + }).map(({image, name}) => image.slots({ - path, missingSourceContent: language.$('misc.coverGrid.noCoverArt', { album: name, diff --git a/src/content/dependencies/generateWikiHomepageNewsBox.js b/src/content/dependencies/generateWikiHomepageNewsBox.js index 83a27695..3a06a7c3 100644 --- a/src/content/dependencies/generateWikiHomepageNewsBox.js +++ b/src/content/dependencies/generateWikiHomepageNewsBox.js @@ -1,14 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generatePageSidebarBox', - 'linkNewsEntry', - 'transformContent', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({newsData}) => ({ entries: newsData.slice(0, 3), diff --git a/src/content/dependencies/generateWikiHomepagePage.js b/src/content/dependencies/generateWikiHomepagePage.js index 8c09a007..9029131b 100644 --- a/src/content/dependencies/generateWikiHomepagePage.js +++ b/src/content/dependencies/generateWikiHomepagePage.js @@ -1,15 +1,4 @@ export default { - contentDependencies: [ - 'generatePageLayout', - 'generatePageSidebar', - 'generatePageSidebarBox', - 'generateWikiHomepageNewsBox', - 'generateWikiHomepageSection', - 'transformContent', - ], - - extraDependencies: ['wikiData'], - sprawl: ({wikiInfo}) => ({ wikiName: wikiInfo.name, diff --git a/src/content/dependencies/generateWikiHomepageSection.js b/src/content/dependencies/generateWikiHomepageSection.js index 49a474da..5fc0c76f 100644 --- a/src/content/dependencies/generateWikiHomepageSection.js +++ b/src/content/dependencies/generateWikiHomepageSection.js @@ -1,13 +1,4 @@ export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateWikiHomepageActionsRow', - 'generateWikiHomepageAlbumCarouselRow', - 'generateWikiHomepageAlbumGridRow', - ], - - extraDependencies: ['html'], - relations: (relation, homepageSection) => ({ colorStyle: relation('generateColorStyleAttribute', homepageSection.color), diff --git a/src/content/dependencies/generateWikiWallpaperStyleTag.js b/src/content/dependencies/generateWikiWallpaperStyleTag.js new file mode 100644 index 00000000..be52bcc1 --- /dev/null +++ b/src/content/dependencies/generateWikiWallpaperStyleTag.js @@ -0,0 +1,35 @@ +export default { + sprawl: ({wikiInfo}) => ({wikiInfo}), + + relations: (relation) => ({ + wallpaperStyleTag: + relation('generateWallpaperStyleTag'), + }), + + data: ({wikiInfo}) => ({ + singleWallpaperPath: [ + 'media.path', + 'bg.' + wikiInfo.wikiWallpaperFileExtension, + ], + + singleWallpaperStyle: + wikiInfo.wikiWallpaperStyle, + + wallpaperPartPaths: + wikiInfo.wikiWallpaperParts.map(part => + (part.asset + ? ['media.path', part.asset] + : null)), + + wallpaperPartStyles: + wikiInfo.wikiWallpaperParts.map(part => part.style), + }), + + generate: (data, relations) => + relations.wallpaperStyleTag.slots({ + singleWallpaperPath: data.singleWallpaperPath, + singleWallpaperStyle: data.singleWallpaperStyle, + wallpaperPartPaths: data.wallpaperPartPaths, + wallpaperPartStyles: data.wallpaperPartStyles, + }), +}; diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index bc268ec1..1b6b08dd 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -2,82 +2,83 @@ import {logWarn} from '#cli'; import {empty} from '#sugar'; export default { - extraDependencies: [ - 'checkIfImagePathHasCachedThumbnails', - 'getDimensionsOfImagePath', - 'getSizeOfMediaFile', - 'getThumbnailEqualOrSmaller', - 'getThumbnailsAvailableForDimensions', - 'html', - 'language', - 'missingImagePaths', - 'to', - ], - - contentDependencies: ['generateColorStyleAttribute'], - - relations: (relation) => ({ + relations: (relation, _artwork) => ({ colorStyle: relation('generateColorStyleAttribute'), }), - data(artTags) { - const data = {}; - - if (artTags) { - data.contentWarnings = - artTags - .filter(artTag => artTag.isContentWarning) - .map(artTag => artTag.name); - } else { - data.contentWarnings = null; - } - - return data; - }, + data: (artwork) => ({ + path: + (artwork + ? artwork.path + : null), + + warnings: + (artwork + ? artwork.artTags + .filter(artTag => artTag.isContentWarning) + .map(artTag => artTag.name) + : null), + + dimensions: + (artwork + ? artwork.dimensions + : null), + }), slots: { - src: {type: 'string'}, - - path: { - validate: v => v.validateArrayItems(v.isString), - }, - thumb: {type: 'string'}, + reveal: {type: 'boolean', default: true}, + lazy: {type: 'boolean', default: false}, + square: {type: 'boolean', default: false}, + link: { validate: v => v.anyOf(v.isBoolean, v.isString), default: false, }, - color: { - validate: v => v.isColor, - }, + color: {validate: v => v.isColor}, - warnings: { - validate: v => v.looseArrayOf(v.isString), + // Added to the .image-container. + attributes: { + type: 'attributes', + mutable: false, }, - reveal: {type: 'boolean', default: true}, - lazy: {type: 'boolean', default: false}, - - square: {type: 'boolean', default: false}, - - dimensions: { - validate: v => v.isDimensions, + // Added to the <img>. + imgAttributes: { + type: 'attributes', + mutable: false, }, + // Added to the <img> itself. alt: {type: 'string'}, - attributes: { - type: 'attributes', - mutable: false, + // Specify 'src' or 'path', or the path will be used from the artwork. + // If none of the above is present, the message in missingSourceContent + // will be displayed instead. + + src: {type: 'string'}, + + path: { + validate: v => v.validateArrayItems(v.isString), }, missingSourceContent: { type: 'html', mutable: false, }, + + // These will also be used from the artwork if not specified as slots. + + warnings: { + validate: v => v.looseArrayOf(v.isString), + }, + + dimensions: { + validate: v => v.isDimensions, + }, }, generate(data, relations, slots, { @@ -91,15 +92,14 @@ export default { missingImagePaths, to, }) { - let originalSrc; - - if (slots.src) { - originalSrc = slots.src; - } else if (!empty(slots.path)) { - originalSrc = to(...slots.path); - } else { - originalSrc = ''; - } + const originalSrc = + (slots.src + ? slots.src + : slots.path + ? to(...slots.path) + : data.path + ? to(...data.path) + : ''); // TODO: This feels janky. It's necessary to deal with static content that // includes strings like <img src="media/misc/foo.png">, but processing the @@ -121,29 +121,29 @@ export default { !isMissingImageFile && (typeof slots.link === 'string' || slots.link); - const contentWarnings = - slots.warnings ?? - data.contentWarnings; + const warnings = slots.warnings ?? data.warnings; + const dimensions = slots.dimensions ?? data.dimensions; const willReveal = slots.reveal && originalSrc && !isMissingImageFile && - !empty(contentWarnings); - - const willSquare = - slots.square; + !empty(warnings); const imgAttributes = html.attributes([ {class: 'image'}, + slots.imgAttributes, + slots.alt && {alt: slots.alt}, - slots.dimensions?.[0] && - {width: slots.dimensions[0]}, + dimensions && + dimensions[0] && + {width: dimensions[0]}, - slots.dimensions?.[1] && - {height: slots.dimensions[1]}, + dimensions && + dimensions[1] && + {height: dimensions[1]}, ]); const isPlaceholder = @@ -169,7 +169,7 @@ export default { html.tag('span', {class: 'reveal-warnings'}, language.$('misc.contentWarnings.warnings', { - warnings: language.formatUnitList(contentWarnings), + warnings: language.formatUnitList(warnings), })), html.tag('br'), @@ -323,14 +323,14 @@ export default { wrapped = html.tag('div', {class: 'image-outer-area'}, - willSquare && + slots.square && {class: 'square-content'}, wrapped); wrapped = html.tag('div', {class: 'image-container'}, - willSquare && + slots.square && {class: 'square'}, typeof slots.link === 'string' && diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js index a5009804..cfa6346c 100644 --- a/src/content/dependencies/index.js +++ b/src/content/dependencies/index.js @@ -11,6 +11,11 @@ import {colors, logWarn} from '#cli'; import contentFunction, {ContentFunctionSpecError} from '#content-function'; import {annotateFunction} from '#sugar'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const codeSrcPath = path.resolve(__dirname, '..'); +const codeRootPath = path.resolve(codeSrcPath, '..'); + function cachebust(filePath) { if (filePath in cachebust.cache) { cachebust.cache[filePath] += 1; @@ -42,7 +47,9 @@ export function watchContentDependencies({ close, }); - const eslint = new ESLint(); + const eslint = new ESLint({ + cwd: codeRootPath, + }); const metaPath = fileURLToPath(import.meta.url); const metaDirname = path.dirname(metaPath); @@ -87,6 +94,8 @@ export function watchContentDependencies({ const filePaths = files.map(file => path.join(watchPath, file)); for (const filePath of filePaths) { if (filePath === metaPath) continue; + if (filePath.endsWith('.DS_Store')) continue; + const functionName = getFunctionName(filePath); if (!isMocked(functionName)) { contentDependencies[functionName] = null; @@ -98,8 +107,9 @@ export function watchContentDependencies({ watcher.on('all', (event, filePath) => { if (!['add', 'change'].includes(event)) return; if (filePath === metaPath) return; - handlePathUpdated(filePath); + if (filePath.endsWith('.DS_Store')) return; + handlePathUpdated(filePath); }); watcher.on('unlink', (filePath) => { @@ -108,6 +118,8 @@ export function watchContentDependencies({ return; } + if (filePath.endsWith('.DS_Store')) return; + handlePathRemoved(filePath); }); diff --git a/src/content/dependencies/linkAdditionalFile.js b/src/content/dependencies/linkAdditionalFile.js new file mode 100644 index 00000000..1b5e650f --- /dev/null +++ b/src/content/dependencies/linkAdditionalFile.js @@ -0,0 +1,27 @@ +export default { + query: (file, filename) => ({ + index: + file.filenames.indexOf(filename), + }), + + relations: (relation, _query, _file, _filename) => ({ + linkTemplate: + relation('linkTemplate'), + }), + + data: (query, file, filename) => ({ + filename, + + // Kinda jank, but eh. + path: + (query.index >= 0 + ? file.paths.at(query.index) + : null), + }), + + generate: (data, relations) => + relations.linkTemplate.slots({ + path: data.path, + content: data.filename, + }), +}; diff --git a/src/content/dependencies/linkAlbum.js b/src/content/dependencies/linkAlbum.js index 36b0d13a..085d5f62 100644 --- a/src/content/dependencies/linkAlbum.js +++ b/src/content/dependencies/linkAlbum.js @@ -1,8 +1,18 @@ export default { - contentDependencies: ['linkThing'], + relations: (relation, album) => ({ + link: + (album.style === 'single' + ? relation('linkTrack', album.tracks[0]) + : relation('linkThing', 'localized.album', album)), + }), - relations: (relation, album) => - ({link: relation('linkThing', 'localized.album', album)}), + data: (album) => ({ + style: album.style, + name: album.name, + }), - generate: (relations) => relations.link, + generate: (data, relations, {language}) => + (data.style === 'single' + ? relations.link.slot('content', language.sanitize(data.name)) + : relations.link), }; diff --git a/src/content/dependencies/linkAlbumAdditionalFile.js b/src/content/dependencies/linkAlbumAdditionalFile.js deleted file mode 100644 index 39e7111e..00000000 --- a/src/content/dependencies/linkAlbumAdditionalFile.js +++ /dev/null @@ -1,24 +0,0 @@ -export default { - contentDependencies: ['linkTemplate'], - - relations(relation) { - return { - linkTemplate: relation('linkTemplate'), - }; - }, - - data(album, file) { - return { - albumDirectory: album.directory, - file, - }; - }, - - generate(data, relations) { - return relations.linkTemplate - .slots({ - path: ['media.albumAdditionalFile', data.albumDirectory, data.file], - content: data.file, - }); - }, -}; diff --git a/src/content/dependencies/linkAlbumCommentary.js b/src/content/dependencies/linkAlbumCommentary.js index ab519fd6..f1917345 100644 --- a/src/content/dependencies/linkAlbumCommentary.js +++ b/src/content/dependencies/linkAlbumCommentary.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, album) => ({link: relation('linkThing', 'localized.albumCommentary', album)}), diff --git a/src/content/dependencies/linkAlbumDynamically.js b/src/content/dependencies/linkAlbumDynamically.js index 45f8c2a9..ba572c8d 100644 --- a/src/content/dependencies/linkAlbumDynamically.js +++ b/src/content/dependencies/linkAlbumDynamically.js @@ -1,14 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'linkAlbumCommentary', - 'linkAlbumGallery', - 'linkAlbum', - ], - - extraDependencies: ['html', 'pagePath'], - relations: (relation, album) => ({ galleryLink: relation('linkAlbumGallery', album), diff --git a/src/content/dependencies/linkAlbumGallery.js b/src/content/dependencies/linkAlbumGallery.js index e3f30a29..efba66d1 100644 --- a/src/content/dependencies/linkAlbumGallery.js +++ b/src/content/dependencies/linkAlbumGallery.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, album) => ({link: relation('linkThing', 'localized.albumGallery', album)}), diff --git a/src/content/dependencies/linkAlbumReferencedArtworks.js b/src/content/dependencies/linkAlbumReferencedArtworks.js index ba51b5e3..411bd2ab 100644 --- a/src/content/dependencies/linkAlbumReferencedArtworks.js +++ b/src/content/dependencies/linkAlbumReferencedArtworks.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, album) => ({link: relation('linkThing', 'localized.albumReferencedArtworks', album)}), diff --git a/src/content/dependencies/linkAlbumReferencingArtworks.js b/src/content/dependencies/linkAlbumReferencingArtworks.js index 4d5e799d..3aee9a4b 100644 --- a/src/content/dependencies/linkAlbumReferencingArtworks.js +++ b/src/content/dependencies/linkAlbumReferencingArtworks.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, album) => ({link: relation('linkThing', 'localized.albumReferencingArtworks', album)}), diff --git a/src/content/dependencies/linkAnythingMan.js b/src/content/dependencies/linkAnythingMan.js index d4697403..cb22baee 100644 --- a/src/content/dependencies/linkAnythingMan.js +++ b/src/content/dependencies/linkAnythingMan.js @@ -1,21 +1,13 @@ export default { - contentDependencies: [ - 'linkAlbum', - 'linkFlash', - 'linkTrack', - ], - - query: (thing) => ({ - referenceType: thing.constructor[Symbol.for('Thing.referenceType')], - }), - - relations: (relation, query, thing) => ({ + relations: (relation, thing) => ({ link: - (query.referenceType === 'album' + (thing.isAlbum ? relation('linkAlbum', thing) - : query.referenceType === 'flash' + : thing.isArtwork + ? relation('linkArtwork', thing) + : thing.isFlash ? relation('linkFlash', thing) - : query.referenceType === 'track' + : thing.isTrack ? relation('linkTrack', thing) : null), }), diff --git a/src/content/dependencies/linkArtTagDynamically.js b/src/content/dependencies/linkArtTagDynamically.js index 964258e1..4514b7c1 100644 --- a/src/content/dependencies/linkArtTagDynamically.js +++ b/src/content/dependencies/linkArtTagDynamically.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkArtTagGallery', 'linkArtTagInfo'], - extraDependencies: ['pagePath'], - relations: (relation, artTag) => ({ galleryLink: relation('linkArtTagGallery', artTag), infoLink: relation('linkArtTagInfo', artTag), diff --git a/src/content/dependencies/linkArtTagGallery.js b/src/content/dependencies/linkArtTagGallery.js index a92b69c1..92ab1ed3 100644 --- a/src/content/dependencies/linkArtTagGallery.js +++ b/src/content/dependencies/linkArtTagGallery.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, artTag) => ({link: relation('linkThing', 'localized.artTagGallery', artTag)}), diff --git a/src/content/dependencies/linkArtTagInfo.js b/src/content/dependencies/linkArtTagInfo.js index 409cb3c0..5eb2ac56 100644 --- a/src/content/dependencies/linkArtTagInfo.js +++ b/src/content/dependencies/linkArtTagInfo.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, artTag) => ({link: relation('linkThing', 'localized.artTagInfo', artTag)}), diff --git a/src/content/dependencies/linkArtist.js b/src/content/dependencies/linkArtist.js index 718ee6fa..917ae6b6 100644 --- a/src/content/dependencies/linkArtist.js +++ b/src/content/dependencies/linkArtist.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, artist) => ({link: relation('linkThing', 'localized.artist', artist)}), diff --git a/src/content/dependencies/linkArtistGallery.js b/src/content/dependencies/linkArtistGallery.js index 66dc172d..001eec1f 100644 --- a/src/content/dependencies/linkArtistGallery.js +++ b/src/content/dependencies/linkArtistGallery.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, artist) => ({link: relation('linkThing', 'localized.artistGallery', artist)}), diff --git a/src/content/dependencies/linkArtistRollingWindow.js b/src/content/dependencies/linkArtistRollingWindow.js new file mode 100644 index 00000000..6ab516ac --- /dev/null +++ b/src/content/dependencies/linkArtistRollingWindow.js @@ -0,0 +1,6 @@ +export default { + relations: (relation, artist) => + ({link: relation('linkThing', 'localized.artistRollingWindow', artist)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkArtwork.js b/src/content/dependencies/linkArtwork.js new file mode 100644 index 00000000..fce89229 --- /dev/null +++ b/src/content/dependencies/linkArtwork.js @@ -0,0 +1,13 @@ +export default { + relations: (relation, artwork) => ({ + link: + (artwork.thing.isAlbum + ? relation('linkAlbum', artwork.thing) + : artwork.thing.isTrack + ? relation('linkTrack', artwork.thing) + : null), + }), + + generate: (relations) => + relations.link, +}; diff --git a/src/content/dependencies/linkCommentaryIndex.js b/src/content/dependencies/linkCommentaryIndex.js index 5568ff84..e59b3641 100644 --- a/src/content/dependencies/linkCommentaryIndex.js +++ b/src/content/dependencies/linkCommentaryIndex.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkStationaryIndex'], - relations: (relation) => ({link: relation( diff --git a/src/content/dependencies/linkContribution.js b/src/content/dependencies/linkContribution.js index c658d461..aa9bdef9 100644 --- a/src/content/dependencies/linkContribution.js +++ b/src/content/dependencies/linkContribution.js @@ -1,12 +1,4 @@ export default { - contentDependencies: [ - 'generateContributionTooltip', - 'generateTextWithTooltip', - 'linkArtist', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, contribution) => ({ artistLink: relation('linkArtist', contribution.artist), @@ -24,13 +16,15 @@ export default { }), slots: { + content: {type: 'html', mutable: false}, + showAnnotation: {type: 'boolean', default: false}, showExternalLinks: {type: 'boolean', default: false}, showChronology: {type: 'boolean', default: false}, trimAnnotation: {type: 'boolean', default: false}, - preventWrapping: {type: 'boolean', default: true}, + preventWrapping: {type: 'boolean', default: false}, preventTooltip: {type: 'boolean', default: false}, chronologyKind: {type: 'string'}, @@ -46,6 +40,10 @@ export default { language.encapsulate('misc.artistLink', workingCapsule => { const workingOptions = {}; + if (!html.isBlank(slots.content)) { + relations.artistLink.setSlot('content', slots.content); + } + // Filling slots early is necessary to actually give the tooltip // content. Otherwise, the coming-up html.isBlank() always reports // the tooltip as blank! diff --git a/src/content/dependencies/linkExternal.js b/src/content/dependencies/linkExternal.js index 073c821e..ad8d4f23 100644 --- a/src/content/dependencies/linkExternal.js +++ b/src/content/dependencies/linkExternal.js @@ -1,9 +1,20 @@ import {isExternalLinkContext, isExternalLinkStyle} from '#external-links'; export default { - extraDependencies: ['html', 'language', 'wikiData'], + sprawl: ({wikiInfo}) => ({ + canonicalBase: + wikiInfo.canonicalBase, - data: (url) => ({url}), + canonicalMediaBase: + wikiInfo.canonicalMediaBase, + }), + + data: (sprawl, url) => ({ + url, + + canonicalBase: + sprawl.canonicalBase, + }), slots: { content: { @@ -39,25 +50,44 @@ export default { default: false, }, + disableBrowserTooltip: { + type: 'boolean', + default: false, + }, + tab: { validate: v => v.is('default', 'separate'), default: 'default', }, }, - generate(data, slots, {html, language}) { + generate(data, slots, {html, language, to}) { + const {url} = data; + let urlIsValid; try { - new URL(data.url); + new URL(url); urlIsValid = true; - } catch (error) { + } catch { urlIsValid = false; } + let href; + if (urlIsValid) { + const {canonicalBase, canonicalMediaBase} = data; + if (canonicalMediaBase && url.startsWith(canonicalMediaBase)) { + href = to('media.path', url.slice(canonicalMediaBase.length)); + } else if (canonicalBase && url.startsWith(canonicalBase)) { + href = to('shared.path', url.slice(canonicalBase.length)); + } else { + href = url; + } + } + let formattedLink; if (urlIsValid) { formattedLink = - language.formatExternalLink(data.url, { + language.formatExternalLink(url, { style: slots.style, context: slots.context, }); @@ -65,7 +95,7 @@ export default { // Fall back to platform if nothing matched the desired style. if (html.isBlank(formattedLink) && slots.style !== 'platform') { formattedLink = - language.formatExternalLink(data.url, { + language.formatExternalLink(url, { style: 'platform', context: slots.context, }); @@ -80,7 +110,7 @@ export default { let linkContent; if (urlIsValid) { - linkAttributes.set('href', data.url); + linkAttributes.set('href', href); if (html.isBlank(slots.content)) { linkContent = formattedLink; @@ -111,7 +141,9 @@ export default { linkAttributes.add('class', 'indicate-external'); let titleText; - if (slots.tab === 'separate') { + if (slots.disableBrowserTooltip) { + titleText = null; + } else if (slots.tab === 'separate') { if (html.isBlank(slots.content)) { titleText = language.$('misc.external.opensInNewTab.annotation'); diff --git a/src/content/dependencies/linkFlash.js b/src/content/dependencies/linkFlash.js index 93dd5a28..cfc01079 100644 --- a/src/content/dependencies/linkFlash.js +++ b/src/content/dependencies/linkFlash.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, flash) => ({link: relation('linkThing', 'localized.flash', flash)}), diff --git a/src/content/dependencies/linkFlashAct.js b/src/content/dependencies/linkFlashAct.js index 82c23325..069bedf4 100644 --- a/src/content/dependencies/linkFlashAct.js +++ b/src/content/dependencies/linkFlashAct.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['generateUnsafeMunchy', 'linkThing'], - relations: (relation, flashAct) => ({ unsafeMunchy: relation('generateUnsafeMunchy'), diff --git a/src/content/dependencies/linkFlashIndex.js b/src/content/dependencies/linkFlashIndex.js index 6dd0710e..9c1b076e 100644 --- a/src/content/dependencies/linkFlashIndex.js +++ b/src/content/dependencies/linkFlashIndex.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkStationaryIndex'], - relations: (relation) => ({link: relation( diff --git a/src/content/dependencies/linkFlashSide.js b/src/content/dependencies/linkFlashSide.js index b77ca65a..6407ef25 100644 --- a/src/content/dependencies/linkFlashSide.js +++ b/src/content/dependencies/linkFlashSide.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkFlashAct'], - relations: (relation, flashSide) => ({ link: relation('linkFlashAct', flashSide.acts[0]), diff --git a/src/content/dependencies/linkGroup.js b/src/content/dependencies/linkGroup.js index ebab1b5b..10bec2fb 100644 --- a/src/content/dependencies/linkGroup.js +++ b/src/content/dependencies/linkGroup.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, group) => ({link: relation('linkThing', 'localized.groupInfo', group)}), diff --git a/src/content/dependencies/linkGroupDynamically.js b/src/content/dependencies/linkGroupDynamically.js index 90303ed1..0b5bd85c 100644 --- a/src/content/dependencies/linkGroupDynamically.js +++ b/src/content/dependencies/linkGroupDynamically.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkGroupGallery', 'linkGroup'], - extraDependencies: ['pagePath'], - relations: (relation, group) => ({ galleryLink: relation('linkGroupGallery', group), infoLink: relation('linkGroup', group), diff --git a/src/content/dependencies/linkGroupExtra.js b/src/content/dependencies/linkGroupExtra.js index bc3c0580..1a6161c1 100644 --- a/src/content/dependencies/linkGroupExtra.js +++ b/src/content/dependencies/linkGroupExtra.js @@ -1,13 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: [ - 'linkGroup', - 'linkGroupGallery', - ], - - extraDependencies: ['html'], - relations(relation, group) { const relations = {}; diff --git a/src/content/dependencies/linkGroupGallery.js b/src/content/dependencies/linkGroupGallery.js index 86c4a0f3..957756d8 100644 --- a/src/content/dependencies/linkGroupGallery.js +++ b/src/content/dependencies/linkGroupGallery.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, group) => ({link: relation('linkThing', 'localized.groupGallery', group)}), diff --git a/src/content/dependencies/linkListing.js b/src/content/dependencies/linkListing.js index ac66919a..4eb2dce6 100644 --- a/src/content/dependencies/linkListing.js +++ b/src/content/dependencies/linkListing.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkThing'], - extraDependencies: ['language'], - relations: (relation, listing) => ({link: relation('linkThing', 'localized.listing', listing)}), diff --git a/src/content/dependencies/linkListingIndex.js b/src/content/dependencies/linkListingIndex.js index 1bfaf46e..209066a9 100644 --- a/src/content/dependencies/linkListingIndex.js +++ b/src/content/dependencies/linkListingIndex.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkStationaryIndex'], - relations: (relation) => ({link: relation( diff --git a/src/content/dependencies/linkNewsEntry.js b/src/content/dependencies/linkNewsEntry.js index 1fb32dd9..9ef7ac0e 100644 --- a/src/content/dependencies/linkNewsEntry.js +++ b/src/content/dependencies/linkNewsEntry.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, newsEntry) => ({link: relation('linkThing', 'localized.newsEntry', newsEntry)}), diff --git a/src/content/dependencies/linkNewsIndex.js b/src/content/dependencies/linkNewsIndex.js index e911a384..4414afc6 100644 --- a/src/content/dependencies/linkNewsIndex.js +++ b/src/content/dependencies/linkNewsIndex.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkStationaryIndex'], - relations: (relation) => ({link: relation( diff --git a/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js b/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js index ec856631..5a16256e 100644 --- a/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js +++ b/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js @@ -3,9 +3,6 @@ import {sortAlbumsTracksChronologically, sortContributionsChronologically} import {chunkArtistTrackContributions} from '#wiki-data'; export default { - contentDependencies: ['generateColorStyleAttribute'], - extraDependencies: ['html', 'language'], - query(track, artist) { const relevantInfoPageChunkingContributions = track.allReleases diff --git a/src/content/dependencies/linkPathFromMedia.js b/src/content/dependencies/linkPathFromMedia.js index d71c69f8..344b7d2c 100644 --- a/src/content/dependencies/linkPathFromMedia.js +++ b/src/content/dependencies/linkPathFromMedia.js @@ -1,17 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: ['linkTemplate'], - - extraDependencies: [ - 'checkIfImagePathHasCachedThumbnails', - 'getDimensionsOfImagePath', - 'getSizeOfMediaFile', - 'getThumbnailsAvailableForDimensions', - 'html', - 'to', - ], - relations: (relation) => ({link: relation('linkTemplate')}), diff --git a/src/content/dependencies/linkPathFromRoot.js b/src/content/dependencies/linkPathFromRoot.js index dab3ac1f..b4a90c07 100644 --- a/src/content/dependencies/linkPathFromRoot.js +++ b/src/content/dependencies/linkPathFromRoot.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkTemplate'], - relations: (relation) => ({link: relation('linkTemplate')}), diff --git a/src/content/dependencies/linkPathFromSite.js b/src/content/dependencies/linkPathFromSite.js index 64676465..67a43059 100644 --- a/src/content/dependencies/linkPathFromSite.js +++ b/src/content/dependencies/linkPathFromSite.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkTemplate'], - relations: (relation) => ({link: relation('linkTemplate')}), diff --git a/src/content/dependencies/linkReferencedArtworks.js b/src/content/dependencies/linkReferencedArtworks.js new file mode 100644 index 00000000..f8b3f3c8 --- /dev/null +++ b/src/content/dependencies/linkReferencedArtworks.js @@ -0,0 +1,12 @@ +export default { + relations: (relation, artwork) => ({ + link: + (artwork.thing.isAlbum + ? relation('linkAlbumReferencedArtworks', artwork.thing) + : artwork.thing.isTrack + ? relation('linkTrackReferencedArtworks', artwork.thing) + : null), + }), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkReferencingArtworks.js b/src/content/dependencies/linkReferencingArtworks.js new file mode 100644 index 00000000..6b7e4f9a --- /dev/null +++ b/src/content/dependencies/linkReferencingArtworks.js @@ -0,0 +1,12 @@ +export default { + relations: (relation, artwork) => ({ + link: + (artwork.thing.isAlbum + ? relation('linkAlbumReferencingArtworks', artwork.thing) + : artwork.thing.isTrack + ? relation('linkTrackReferencingArtworks', artwork.thing) + : null), + }), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkStaticPage.js b/src/content/dependencies/linkStaticPage.js index 032af6c9..c3ac69fa 100644 --- a/src/content/dependencies/linkStaticPage.js +++ b/src/content/dependencies/linkStaticPage.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, staticPage) => ({link: relation('linkThing', 'localized.staticPage', staticPage)}), diff --git a/src/content/dependencies/linkStationaryIndex.js b/src/content/dependencies/linkStationaryIndex.js index d5506e60..10f8ba44 100644 --- a/src/content/dependencies/linkStationaryIndex.js +++ b/src/content/dependencies/linkStationaryIndex.js @@ -1,9 +1,6 @@ // Not to be confused with "html.Stationery". export default { - contentDependencies: ['linkTemplate'], - extraDependencies: ['language'], - relations(relation) { return { linkTemplate: relation('linkTemplate'), diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index 4f853dc4..10466b43 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -3,13 +3,6 @@ import {empty} from '#sugar'; import striptags from 'striptags'; export default { - extraDependencies: [ - 'appendIndexHTML', - 'html', - 'language', - 'to', - ], - slots: { href: {type: 'string'}, path: {validate: v => v.validateArrayItems(v.isString)}, diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js index 3902f380..7784afe7 100644 --- a/src/content/dependencies/linkThing.js +++ b/src/content/dependencies/linkThing.js @@ -1,13 +1,4 @@ export default { - contentDependencies: [ - 'generateColorStyleAttribute', - 'generateTextWithTooltip', - 'generateTooltip', - 'linkTemplate', - ], - - extraDependencies: ['html', 'language'], - relations: (relation, _pathKey, thing) => ({ linkTemplate: relation('linkTemplate'), @@ -20,11 +11,15 @@ export default { tooltip: relation('generateTooltip'), + + name: + relation('generateName', thing), }), data: (pathKey, thing) => ({ name: thing.name, nameShort: thing.nameShort ?? thing.shortName, + nameText: thing.nameText, path: (pathKey @@ -75,7 +70,7 @@ export default { hash: {type: 'string'}, }, - generate(data, relations, slots, {html, language}) { + generate(data, relations, slots, {html}) { const path = slots.path ?? data.path; @@ -83,14 +78,12 @@ export default { const wrapperAttributes = html.attributes(); const showShortName = - (slots.preferShortName - ? data.nameShort && data.nameShort !== data.name - : false); + slots.preferShortName && + !data.nameText && + data.nameShort && + data.nameShort !== data.name; - const name = - (showShortName - ? data.nameShort - : data.name); + const name = relations.name; const showWikiTooltip = (slots.tooltipStyle === 'auto' @@ -114,7 +107,7 @@ export default { const content = (html.isBlank(slots.content) - ? language.sanitize(name) + ? name : slots.content); if (slots.color !== false) { diff --git a/src/content/dependencies/linkTrack.js b/src/content/dependencies/linkTrack.js index d5d96726..8ee715f0 100644 --- a/src/content/dependencies/linkTrack.js +++ b/src/content/dependencies/linkTrack.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, track) => ({link: relation('linkThing', 'localized.track', track)}), diff --git a/src/content/dependencies/linkTrackDynamically.js b/src/content/dependencies/linkTrackDynamically.js index bbcf1c34..088bbe09 100644 --- a/src/content/dependencies/linkTrackDynamically.js +++ b/src/content/dependencies/linkTrackDynamically.js @@ -1,9 +1,6 @@ import {empty} from '#sugar'; export default { - contentDependencies: ['linkTrack'], - extraDependencies: ['pagePath'], - relations: (relation, track) => ({ infoLink: relation('linkTrack', track), }), diff --git a/src/content/dependencies/linkTrackReferencedArtworks.js b/src/content/dependencies/linkTrackReferencedArtworks.js index b4cb08fe..6da6504e 100644 --- a/src/content/dependencies/linkTrackReferencedArtworks.js +++ b/src/content/dependencies/linkTrackReferencedArtworks.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, track) => ({link: relation('linkThing', 'localized.trackReferencedArtworks', track)}), diff --git a/src/content/dependencies/linkTrackReferencingArtworks.js b/src/content/dependencies/linkTrackReferencingArtworks.js index c9c9f4d1..4d113ba7 100644 --- a/src/content/dependencies/linkTrackReferencingArtworks.js +++ b/src/content/dependencies/linkTrackReferencingArtworks.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['linkThing'], - relations: (relation, track) => ({link: relation('linkThing', 'localized.trackReferencingArtworks', track)}), diff --git a/src/content/dependencies/linkWikiHomepage.js b/src/content/dependencies/linkWikiHomepage.js index d8d3d0a0..91fbe410 100644 --- a/src/content/dependencies/linkWikiHomepage.js +++ b/src/content/dependencies/linkWikiHomepage.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['linkTemplate'], - extraDependencies: ['wikiData'], - sprawl({wikiInfo}) { return {wikiShortName: wikiInfo.nameShort}; }, diff --git a/src/content/dependencies/listAlbumsByDate.js b/src/content/dependencies/listAlbumsByDate.js index c83ffc97..eaf9eecf 100644 --- a/src/content/dependencies/listAlbumsByDate.js +++ b/src/content/dependencies/listAlbumsByDate.js @@ -2,9 +2,6 @@ import {sortChronologically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listAlbumsByDateAdded.js b/src/content/dependencies/listAlbumsByDateAdded.js index d462ad46..940da67d 100644 --- a/src/content/dependencies/listAlbumsByDateAdded.js +++ b/src/content/dependencies/listAlbumsByDateAdded.js @@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort'; import {chunkByProperties} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listAlbumsByDuration.js b/src/content/dependencies/listAlbumsByDuration.js index c60685ab..8de2bb84 100644 --- a/src/content/dependencies/listAlbumsByDuration.js +++ b/src/content/dependencies/listAlbumsByDuration.js @@ -3,16 +3,17 @@ import {filterByCount, stitchArrays} from '#sugar'; import {getTotalDuration} from '#wiki-data'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, query({albumData}, spec) { - const albums = sortAlphabetically(albumData.slice()); - const durations = albums.map(album => getTotalDuration(album.tracks)); + const albums = + sortAlphabetically( + albumData.filter(album => !album.hideDuration)); + + const durations = + albums.map(album => getTotalDuration(album.tracks)); filterByCount(albums, durations); sortByCount(albums, durations, {greatestFirst: true}); diff --git a/src/content/dependencies/listAlbumsByName.js b/src/content/dependencies/listAlbumsByName.js index 21419537..a7939292 100644 --- a/src/content/dependencies/listAlbumsByName.js +++ b/src/content/dependencies/listAlbumsByName.js @@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listAlbumsByTracks.js b/src/content/dependencies/listAlbumsByTracks.js index 798e6c2e..b1f62a82 100644 --- a/src/content/dependencies/listAlbumsByTracks.js +++ b/src/content/dependencies/listAlbumsByTracks.js @@ -2,21 +2,25 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, query({albumData}, spec) { - const albums = sortAlphabetically(albumData.slice()); - const counts = albums.map(album => album.tracks.length); + const albums = + sortAlphabetically( + albumData.filter(album => !album.hideDuration)); + + const counts = + albums.map(album => album.tracks.length); filterByCount(albums, counts); sortByCount(albums, counts, {greatestFirst: true}); - return {spec, albums, counts}; + const styles = + albums.map(album => album.style); + + return {spec, albums, counts, styles}; }, relations(relation, query) { @@ -32,6 +36,7 @@ export default { data(query) { return { counts: query.counts, + styles: query.styles, }; }, @@ -42,10 +47,19 @@ export default { stitchArrays({ link: relations.albumLinks, count: data.counts, - }).map(({link, count}) => ({ - album: link, - tracks: language.countTracks(count, {unit: true}), - })), + style: data.styles, + }).map(({link, count, style}) => { + const row = { + album: link, + tracks: language.countTracks(count, {unit: true}), + }; + + if (style === 'single') { + row.stringsKey = 'single'; + } + + return row; + }), }); }, }; diff --git a/src/content/dependencies/listAllAdditionalFiles.js b/src/content/dependencies/listAllAdditionalFiles.js index a6e34b9a..2d338916 100644 --- a/src/content/dependencies/listAllAdditionalFiles.js +++ b/src/content/dependencies/listAllAdditionalFiles.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['listAllAdditionalFilesTemplate'], - relations: (relation, spec) => ({page: relation('listAllAdditionalFilesTemplate', spec, 'additionalFiles')}), diff --git a/src/content/dependencies/listAllAdditionalFilesTemplate.js b/src/content/dependencies/listAllAdditionalFilesTemplate.js index e33ad7b5..f298233c 100644 --- a/src/content/dependencies/listAllAdditionalFilesTemplate.js +++ b/src/content/dependencies/listAllAdditionalFilesTemplate.js @@ -1,209 +1,37 @@ import {sortChronologically} from '#sort'; -import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateListingPage', - 'generateListAllAdditionalFilesChunk', - 'linkAlbum', - 'linkTrack', - 'linkAlbumAdditionalFile', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({albumData}) => ({albumData}), - query(sprawl, spec, property) { - const albums = - sortChronologically(sprawl.albumData.slice()); - - const tracks = - albums - .map(album => album.tracks.slice()); - - // Get additional file objects from albums and their tracks. - // There's a possibility that albums and tracks don't both implement - // the same additional file fields - in this case, just treat them - // as though they do implement those fields, but don't have any - // additional files of that type. - - const albumAdditionalFileObjects = - albums - .map(album => album[property] ?? []); - - const trackAdditionalFileObjects = - tracks - .map(byAlbum => byAlbum - .map(track => track[property] ?? [])); - - // Filter out tracks that don't have any additional files. - - stitchArrays({tracks, trackAdditionalFileObjects}) - .forEach(({tracks, trackAdditionalFileObjects}) => { - filterMultipleArrays(tracks, trackAdditionalFileObjects, - (track, trackAdditionalFileObjects) => !empty(trackAdditionalFileObjects)); - }); - - // Filter out albums that don't have any tracks, - // nor any additional files of their own. - - filterMultipleArrays(albums, albumAdditionalFileObjects, tracks, trackAdditionalFileObjects, - (album, albumAdditionalFileObjects, tracks, trackAdditionalFileObjects) => - !empty(albumAdditionalFileObjects) || - !empty(trackAdditionalFileObjects)); - - // Map additional file objects into titles and lists of file names. - - const albumAdditionalFileTitles = - albumAdditionalFileObjects - .map(byAlbum => byAlbum - .map(({title}) => title)); - - const albumAdditionalFileFiles = - albumAdditionalFileObjects - .map(byAlbum => byAlbum - .map(({files}) => files ?? [])); - - const trackAdditionalFileTitles = - trackAdditionalFileObjects - .map(byAlbum => byAlbum - .map(byTrack => byTrack - .map(({title}) => title))); + query: (sprawl, spec, property) => ({ + spec, + property, - const trackAdditionalFileFiles = - trackAdditionalFileObjects - .map(byAlbum => byAlbum - .map(byTrack => byTrack - .map(({files}) => files ?? []))); - - return { - spec, - albums, - tracks, - albumAdditionalFileTitles, - albumAdditionalFileFiles, - trackAdditionalFileTitles, - trackAdditionalFileFiles, - }; - }, + albums: + sortChronologically(sprawl.albumData.slice()), + }), relations: (relation, query) => ({ page: relation('generateListingPage', query.spec), - albumLinks: - query.albums - .map(album => relation('linkAlbum', album)), - - trackLinks: - query.tracks - .map(byAlbum => byAlbum - .map(track => relation('linkTrack', track))), - - albumChunks: - query.albums - .map(() => relation('generateListAllAdditionalFilesChunk')), - - trackChunks: - query.tracks - .map(byAlbum => byAlbum - .map(() => relation('generateListAllAdditionalFilesChunk'))), - - albumAdditionalFileLinks: - stitchArrays({ - album: query.albums, - files: query.albumAdditionalFileFiles, - }).map(({album, files: byAlbum}) => - byAlbum.map(files => files - .map(file => - relation('linkAlbumAdditionalFile', album, file)))), - - trackAdditionalFileLinks: - stitchArrays({ - album: query.albums, - files: query.trackAdditionalFileFiles, - }).map(({album, files: byAlbum}) => - byAlbum - .map(byTrack => byTrack - .map(files => files - .map(file => relation('linkAlbumAdditionalFile', album, file))))), - }), - - data: (query) => ({ - albumAdditionalFileTitles: query.albumAdditionalFileTitles, - trackAdditionalFileTitles: query.trackAdditionalFileTitles, - albumAdditionalFileFiles: query.albumAdditionalFileFiles, - trackAdditionalFileFiles: query.trackAdditionalFileFiles, + albumSections: + query.albums.map(album => + relation('generateListAllAdditionalFilesAlbumSection', + album, + query.property)), }), slots: { stringsKey: {type: 'string'}, }, - generate: (data, relations, slots, {html, language}) => + generate: (relations, slots) => relations.page.slots({ type: 'custom', content: - stitchArrays({ - albumLink: relations.albumLinks, - trackLinks: relations.trackLinks, - albumChunk: relations.albumChunks, - trackChunks: relations.trackChunks, - albumAdditionalFileTitles: data.albumAdditionalFileTitles, - trackAdditionalFileTitles: data.trackAdditionalFileTitles, - albumAdditionalFileLinks: relations.albumAdditionalFileLinks, - trackAdditionalFileLinks: relations.trackAdditionalFileLinks, - albumAdditionalFileFiles: data.albumAdditionalFileFiles, - trackAdditionalFileFiles: data.trackAdditionalFileFiles, - }).map(({ - albumLink, - trackLinks, - albumChunk, - trackChunks, - albumAdditionalFileTitles, - trackAdditionalFileTitles, - albumAdditionalFileLinks, - trackAdditionalFileLinks, - albumAdditionalFileFiles, - trackAdditionalFileFiles, - }) => [ - html.tag('h3', {class: 'content-heading'}, albumLink), - - html.tag('dl', [ - albumChunk.slots({ - title: - language.$('listingPage', slots.stringsKey, 'albumFiles'), - - additionalFileTitles: albumAdditionalFileTitles, - additionalFileLinks: albumAdditionalFileLinks, - additionalFileFiles: albumAdditionalFileFiles, - - stringsKey: slots.stringsKey, - }), - - stitchArrays({ - trackLink: trackLinks, - trackChunk: trackChunks, - trackAdditionalFileTitles, - trackAdditionalFileLinks, - trackAdditionalFileFiles, - }).map(({ - trackLink, - trackChunk, - trackAdditionalFileTitles, - trackAdditionalFileLinks, - trackAdditionalFileFiles, - }) => - trackChunk.slots({ - title: trackLink, - additionalFileTitles: trackAdditionalFileTitles, - additionalFileLinks: trackAdditionalFileLinks, - additionalFileFiles: trackAdditionalFileFiles, - stringsKey: slots.stringsKey, - })), - ]), - ]), + relations.albumSections.map(section => + section.slot('stringsKey', slots.stringsKey)), }), }; diff --git a/src/content/dependencies/listAllMidiProjectFiles.js b/src/content/dependencies/listAllMidiProjectFiles.js index 31a70ef0..109cf2e7 100644 --- a/src/content/dependencies/listAllMidiProjectFiles.js +++ b/src/content/dependencies/listAllMidiProjectFiles.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['listAllAdditionalFilesTemplate'], - relations: (relation, spec) => ({page: relation('listAllAdditionalFilesTemplate', spec, 'midiProjectFiles')}), diff --git a/src/content/dependencies/listAllSheetMusicFiles.js b/src/content/dependencies/listAllSheetMusicFiles.js index 166b2068..4f3bdb96 100644 --- a/src/content/dependencies/listAllSheetMusicFiles.js +++ b/src/content/dependencies/listAllSheetMusicFiles.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['listAllAdditionalFilesTemplate'], - relations: (relation, spec) => ({page: relation('listAllAdditionalFilesTemplate', spec, 'sheetMusicFiles')}), diff --git a/src/content/dependencies/listArtTagNetwork.js b/src/content/dependencies/listArtTagNetwork.js index 5386dcdc..98f81019 100644 --- a/src/content/dependencies/listArtTagNetwork.js +++ b/src/content/dependencies/listArtTagNetwork.js @@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort'; import {empty, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtTagInfo'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({artTagData}) { return {artTagData}; }, @@ -29,21 +26,21 @@ export default { const getStats = (artTag) => ({ directUses: - artTag.directlyTaggedInThings.length, + artTag.directlyFeaturedInArtworks.length, // Not currently displayed directAndIndirectUses: unique([ - ...artTag.indirectlyTaggedInThings, - ...artTag.directlyTaggedInThings, + ...artTag.indirectlyFeaturedInArtworks, + ...artTag.directlyFeaturedInArtworks, ]).length, totalUses: [ - ...artTag.directlyTaggedInThings, + ...artTag.directlyFeaturedInArtworks, ... artTag.allDescendantArtTags - .flatMap(artTag => artTag.directlyTaggedInThings), + .flatMap(artTag => artTag.directlyFeaturedInArtworks), ].length, descendants: diff --git a/src/content/dependencies/listArtTagsByName.js b/src/content/dependencies/listArtTagsByName.js index 31856478..10e9e873 100644 --- a/src/content/dependencies/listArtTagsByName.js +++ b/src/content/dependencies/listArtTagsByName.js @@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort'; import {stitchArrays, unique} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtTagGallery'], - extraDependencies: ['language', 'wikiData'], - sprawl({artTagData}) { return {artTagData}; }, @@ -35,8 +32,8 @@ export default { counts: query.artTags.map(artTag => unique([ - ...artTag.indirectlyTaggedInThings, - ...artTag.directlyTaggedInThings, + ...artTag.indirectlyFeaturedInArtworks, + ...artTag.directlyFeaturedInArtworks, ]).length), }; }, diff --git a/src/content/dependencies/listArtTagsByUses.js b/src/content/dependencies/listArtTagsByUses.js index fcd324f7..5131580f 100644 --- a/src/content/dependencies/listArtTagsByUses.js +++ b/src/content/dependencies/listArtTagsByUses.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays, unique} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtTagGallery'], - extraDependencies: ['language', 'wikiData'], - sprawl: ({artTagData}) => ({artTagData}), @@ -17,8 +14,8 @@ export default { const counts = artTags.map(artTag => unique([ - ...artTag.directlyTaggedInThings, - ...artTag.indirectlyTaggedInThings, + ...artTag.directlyFeaturedInArtworks, + ...artTag.indirectlyFeaturedInArtworks, ]).length); filterByCount(artTags, counts); diff --git a/src/content/dependencies/listArtistsByCommentaryEntries.js b/src/content/dependencies/listArtistsByCommentaryEntries.js index eff2dba3..ab7bde6c 100644 --- a/src/content/dependencies/listArtistsByCommentaryEntries.js +++ b/src/content/dependencies/listArtistsByCommentaryEntries.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtist'], - extraDependencies: ['language', 'wikiData'], - sprawl({artistData}) { return {artistData}; }, diff --git a/src/content/dependencies/listArtistsByContributions.js b/src/content/dependencies/listArtistsByContributions.js index 41944959..2f8d6391 100644 --- a/src/content/dependencies/listArtistsByContributions.js +++ b/src/content/dependencies/listArtistsByContributions.js @@ -1,18 +1,8 @@ import {sortAlphabetically, sortByCount} from '#sort'; - -import { - accumulateSum, - empty, - filterByCount, - filterMultipleArrays, - stitchArrays, - unique, -} from '#sugar'; +import {empty, filterByCount, filterMultipleArrays, stitchArrays} + from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtist'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({artistData, wikiInfo}) { return { artistData, @@ -41,37 +31,46 @@ export default { query[countsKey] = counts; }; + const countContributions = (artist, keys) => { + const contribs = + keys + .flatMap(key => artist[key]) + .filter(contrib => contrib.countInContributionTotals); + + const things = + new Set(contribs.map(contrib => contrib.thing)); + + return things.size; + }; + queryContributionInfo( 'artistsByTrackContributions', 'countsByTrackContributions', artist => - (unique( - ([ - artist.trackArtistContributions, - artist.trackContributorContributions, - ]).flat() - .map(({thing}) => thing) - )).length); + countContributions(artist, [ + 'trackArtistContributions', + 'trackContributorContributions', + ])); queryContributionInfo( 'artistsByArtworkContributions', 'countsByArtworkContributions', artist => - accumulateSum( - [ - artist.albumCoverArtistContributions, - artist.albumWallpaperArtistContributions, - artist.albumBannerArtistContributions, - artist.trackCoverArtistContributions, - ], - contribs => contribs.length)); + countContributions(artist, [ + 'albumCoverArtistContributions', + 'albumWallpaperArtistContributions', + 'albumBannerArtistContributions', + 'trackCoverArtistContributions', + ])); if (sprawl.enableFlashesAndGames) { queryContributionInfo( 'artistsByFlashContributions', 'countsByFlashContributions', artist => - artist.flashContributorContributions.length); + countContributions(artist, [ + 'flashContributorContributions', + ])); } return query; diff --git a/src/content/dependencies/listArtistsByDuration.js b/src/content/dependencies/listArtistsByDuration.js index 6b2a18a0..1d550b26 100644 --- a/src/content/dependencies/listArtistsByDuration.js +++ b/src/content/dependencies/listArtistsByDuration.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtist'], - extraDependencies: ['language', 'wikiData'], - sprawl({artistData}) { return {artistData}; }, diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js index 0bf9dd2d..44564b4b 100644 --- a/src/content/dependencies/listArtistsByGroup.js +++ b/src/content/dependencies/listArtistsByGroup.js @@ -10,9 +10,6 @@ import { } from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'], - extraDependencies: ['language', 'wikiData'], - sprawl({artistData, wikiInfo}) { return {artistData, wikiInfo}; }, @@ -37,20 +34,25 @@ export default { ([ (unique( ([ - artist.albumArtistContributions, - artist.albumCoverArtistContributions, - artist.albumWallpaperArtistContributions, - artist.albumBannerArtistContributions, + artist.albumArtistContributions + .map(contrib => contrib.thing), + artist.albumCoverArtistContributions + .map(contrib => contrib.thing.thing), + artist.albumWallpaperArtistContributions + .map(contrib => contrib.thing.thing), + artist.albumBannerArtistContributions + .map(contrib => contrib.thing.thing), ]).flat() - .map(({thing}) => thing) )).map(album => album.groups), (unique( ([ - artist.trackArtistContributions, - artist.trackContributorContributions, - artist.trackCoverArtistContributions, + artist.trackArtistContributions + .map(contrib => contrib.thing), + artist.trackContributorContributions + .map(contrib => contrib.thing), + artist.trackCoverArtistContributions + .map(contrib => contrib.thing.thing), ]).flat() - .map(({thing}) => thing) )).map(track => track.album.groups), ]).flat() .map(groups => groups diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js index 27a2faa3..dc7341cf 100644 --- a/src/content/dependencies/listArtistsByLatestContribution.js +++ b/src/content/dependencies/listArtistsByLatestContribution.js @@ -11,15 +11,6 @@ import { const {Album, Flash} = T; export default { - contentDependencies: [ - 'generateListingPage', - 'linkAlbum', - 'linkArtist', - 'linkFlash', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({albumData, artistData, flashData, trackData, wikiInfo}) => ({albumData, artistData, flashData, trackData, enableFlashesAndGames: wikiInfo.enableFlashesAndGames}), @@ -98,13 +89,16 @@ export default { ])) { // Might combine later with 'track' of the same album and date. considerDate(artist, album.coverArtDate ?? album.date, album, 'artwork'); + // '?? album.date' is kept here because wallpaper and banner may + // technically be present for an album w/o cover art, therefore + // also no cover art date. } } for (const track of tracksLatestFirst) { for (const artist of getArtists(track, 'coverArtistContribs')) { // No special effect if artist already has 'artwork' for the same album and date. - considerDate(artist, track.coverArtDate ?? track.date, track.album, 'artwork'); + considerDate(artist, track.coverArtDate, track.album, 'artwork'); } for (const artist of new Set([ diff --git a/src/content/dependencies/listArtistsByName.js b/src/content/dependencies/listArtistsByName.js index 93218492..8bee4947 100644 --- a/src/content/dependencies/listArtistsByName.js +++ b/src/content/dependencies/listArtistsByName.js @@ -3,9 +3,6 @@ import {stitchArrays} from '#sugar'; import {getArtistNumContributions} from '#wiki-data'; export default { - contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'], - extraDependencies: ['language', 'wikiData'], - sprawl: ({artistData, wikiInfo}) => ({artistData, wikiInfo}), diff --git a/src/content/dependencies/listGroupsByAlbums.js b/src/content/dependencies/listGroupsByAlbums.js index 4adfb6d9..64814640 100644 --- a/src/content/dependencies/listGroupsByAlbums.js +++ b/src/content/dependencies/listGroupsByAlbums.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkGroup'], - extraDependencies: ['language', 'wikiData'], - sprawl({groupData}) { return {groupData}; }, diff --git a/src/content/dependencies/listGroupsByCategory.js b/src/content/dependencies/listGroupsByCategory.js index 43919bef..4c10a1e4 100644 --- a/src/content/dependencies/listGroupsByCategory.js +++ b/src/content/dependencies/listGroupsByCategory.js @@ -1,9 +1,6 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkGroup', 'linkGroupGallery'], - extraDependencies: ['language', 'wikiData'], - sprawl({groupCategoryData}) { return {groupCategoryData}; }, diff --git a/src/content/dependencies/listGroupsByDuration.js b/src/content/dependencies/listGroupsByDuration.js index c79e1bc4..089915c2 100644 --- a/src/content/dependencies/listGroupsByDuration.js +++ b/src/content/dependencies/listGroupsByDuration.js @@ -3,9 +3,6 @@ import {filterByCount, stitchArrays} from '#sugar'; import {getTotalDuration} from '#wiki-data'; export default { - contentDependencies: ['generateListingPage', 'linkGroup'], - extraDependencies: ['language', 'wikiData'], - sprawl({groupData}) { return {groupData}; }, diff --git a/src/content/dependencies/listGroupsByLatestAlbum.js b/src/content/dependencies/listGroupsByLatestAlbum.js index 48319314..2d83a354 100644 --- a/src/content/dependencies/listGroupsByLatestAlbum.js +++ b/src/content/dependencies/listGroupsByLatestAlbum.js @@ -2,15 +2,6 @@ import {compareDates, sortChronologically} from '#sort'; import {filterMultipleArrays, sortMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: [ - 'generateListingPage', - 'linkAlbum', - 'linkGroup', - 'linkGroupGallery', - ], - - extraDependencies: ['language', 'wikiData'], - sprawl({groupData}) { return {groupData}; }, diff --git a/src/content/dependencies/listGroupsByName.js b/src/content/dependencies/listGroupsByName.js index 696a49bd..e3308158 100644 --- a/src/content/dependencies/listGroupsByName.js +++ b/src/content/dependencies/listGroupsByName.js @@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort'; import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkGroup', 'linkGroupGallery'], - extraDependencies: ['language', 'wikiData'], - sprawl({groupData}) { return {groupData}; }, diff --git a/src/content/dependencies/listGroupsByTracks.js b/src/content/dependencies/listGroupsByTracks.js index 0b5e4e97..c9d97614 100644 --- a/src/content/dependencies/listGroupsByTracks.js +++ b/src/content/dependencies/listGroupsByTracks.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {accumulateSum, filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkGroup'], - extraDependencies: ['language', 'wikiData'], - sprawl({groupData}) { return {groupData}; }, diff --git a/src/content/dependencies/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js index 79bba441..81eca274 100644 --- a/src/content/dependencies/listRandomPageLinks.js +++ b/src/content/dependencies/listRandomPageLinks.js @@ -2,14 +2,6 @@ import {sortChronologically} from '#sort'; import {empty} from '#sugar'; export default { - contentDependencies: [ - 'generateListingPage', - 'generateListRandomPageLinksAlbumLink', - 'linkGroup', - ], - - extraDependencies: ['html', 'language', 'wikiData'], - sprawl: ({albumData, wikiInfo}) => ({albumData, wikiInfo}), query(sprawl, spec) { diff --git a/src/content/dependencies/listTracksByAlbum.js b/src/content/dependencies/listTracksByAlbum.js index b2405034..f6858ada 100644 --- a/src/content/dependencies/listTracksByAlbum.js +++ b/src/content/dependencies/listTracksByAlbum.js @@ -1,7 +1,4 @@ export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listTracksByDate.js b/src/content/dependencies/listTracksByDate.js index dcfaeaf0..9d63f19b 100644 --- a/src/content/dependencies/listTracksByDate.js +++ b/src/content/dependencies/listTracksByDate.js @@ -2,9 +2,6 @@ import {sortAlbumsTracksChronologically} from '#sort'; import {chunkByProperties, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl: ({trackData}) => ({trackData}), query({trackData}, spec) { diff --git a/src/content/dependencies/listTracksByDuration.js b/src/content/dependencies/listTracksByDuration.js index 64feb4f1..95fd28b2 100644 --- a/src/content/dependencies/listTracksByDuration.js +++ b/src/content/dependencies/listTracksByDuration.js @@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl({trackData}) { return {trackData}; }, diff --git a/src/content/dependencies/listTracksByDurationInAlbum.js b/src/content/dependencies/listTracksByDurationInAlbum.js index c1ea32a1..ad44c7b2 100644 --- a/src/content/dependencies/listTracksByDurationInAlbum.js +++ b/src/content/dependencies/listTracksByDurationInAlbum.js @@ -2,9 +2,6 @@ import {sortByCount, sortChronologically} from '#sort'; import {filterByCount, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listTracksByName.js b/src/content/dependencies/listTracksByName.js index 773b0473..a9c2c504 100644 --- a/src/content/dependencies/listTracksByName.js +++ b/src/content/dependencies/listTracksByName.js @@ -1,9 +1,6 @@ import {sortAlphabetically} from '#sort'; export default { - contentDependencies: ['generateListingPage', 'linkTrack'], - extraDependencies: ['wikiData'], - sprawl({trackData}) { return {trackData}; }, diff --git a/src/content/dependencies/listTracksByTimesReferenced.js b/src/content/dependencies/listTracksByTimesReferenced.js index 5838ded0..8a57e1a6 100644 --- a/src/content/dependencies/listTracksByTimesReferenced.js +++ b/src/content/dependencies/listTracksByTimesReferenced.js @@ -2,9 +2,6 @@ import {sortAlbumsTracksChronologically, sortByCount} from '#sort'; import {filterByCount, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl({trackData}) { return {trackData}; }, diff --git a/src/content/dependencies/listTracksInFlashesByAlbum.js b/src/content/dependencies/listTracksInFlashesByAlbum.js index 8ca0d993..db5472db 100644 --- a/src/content/dependencies/listTracksInFlashesByAlbum.js +++ b/src/content/dependencies/listTracksInFlashesByAlbum.js @@ -2,9 +2,6 @@ import {sortChronologically} from '#sort'; import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkFlash', 'linkTrack'], - extraDependencies: ['language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listTracksInFlashesByFlash.js b/src/content/dependencies/listTracksInFlashesByFlash.js index 6ab954ed..325b3cb5 100644 --- a/src/content/dependencies/listTracksInFlashesByFlash.js +++ b/src/content/dependencies/listTracksInFlashesByFlash.js @@ -2,9 +2,6 @@ import {sortFlashesChronologically} from '#sort'; import {empty, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkFlash', 'linkTrack'], - extraDependencies: ['wikiData'], - sprawl({flashData}) { return {flashData}; }, diff --git a/src/content/dependencies/listTracksNeedingLyrics.js b/src/content/dependencies/listTracksNeedingLyrics.js new file mode 100644 index 00000000..d21fcd06 --- /dev/null +++ b/src/content/dependencies/listTracksNeedingLyrics.js @@ -0,0 +1,7 @@ +export default { + relations: (relation, spec) => + ({page: relation('listTracksWithExtra', spec, 'needsLyrics', 'truthy')}), + + generate: (relations) => + relations.page, +}; diff --git a/src/content/dependencies/listTracksWithExtra.js b/src/content/dependencies/listTracksWithExtra.js index c7f42f9d..09d8ee21 100644 --- a/src/content/dependencies/listTracksWithExtra.js +++ b/src/content/dependencies/listTracksWithExtra.js @@ -2,9 +2,6 @@ import {sortChronologically} from '#sort'; import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], - extraDependencies: ['html', 'language', 'wikiData'], - sprawl({albumData}) { return {albumData}; }, diff --git a/src/content/dependencies/listTracksWithLyrics.js b/src/content/dependencies/listTracksWithLyrics.js index a13a76f0..79d76bf3 100644 --- a/src/content/dependencies/listTracksWithLyrics.js +++ b/src/content/dependencies/listTracksWithLyrics.js @@ -1,8 +1,6 @@ export default { - contentDependencies: ['listTracksWithExtra'], - relations: (relation, spec) => - ({page: relation('listTracksWithExtra', spec, 'lyrics', 'truthy')}), + ({page: relation('listTracksWithExtra', spec, 'lyrics', 'array')}), generate: (relations) => relations.page, diff --git a/src/content/dependencies/listTracksWithMidiProjectFiles.js b/src/content/dependencies/listTracksWithMidiProjectFiles.js index 418af4c2..9a48f6ae 100644 --- a/src/content/dependencies/listTracksWithMidiProjectFiles.js +++ b/src/content/dependencies/listTracksWithMidiProjectFiles.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['listTracksWithExtra'], - relations: (relation, spec) => ({page: relation('listTracksWithExtra', spec, 'midiProjectFiles', 'array')}), diff --git a/src/content/dependencies/listTracksWithSheetMusicFiles.js b/src/content/dependencies/listTracksWithSheetMusicFiles.js index 0c6761eb..f0ba4196 100644 --- a/src/content/dependencies/listTracksWithSheetMusicFiles.js +++ b/src/content/dependencies/listTracksWithSheetMusicFiles.js @@ -1,6 +1,4 @@ export default { - contentDependencies: ['listTracksWithExtra'], - relations: (relation, spec) => ({page: relation('listTracksWithExtra', spec, 'sheetMusicFiles', 'array')}), diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index f56a1da9..73452cfa 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -1,7 +1,11 @@ +import {basename} from 'node:path'; + +import {logWarn} from '#cli'; import {bindFind} from '#find'; -import {replacerSpec, parseInput} from '#replacer'; +import {replacerSpec, parseContentNodes} from '#replacer'; import {Marked} from 'marked'; +import striptags from 'striptags'; const commonMarkedOptions = { headerIds: false, @@ -45,26 +49,47 @@ function getPlaceholder(node, content) { return {type: 'text', data: content.slice(node.i, node.iEnd)}; } -export default { - contentDependencies: [ - ...( - Object.values(replacerSpec) - .map(description => description.link) - .filter(Boolean)), - 'image', - 'linkExternal', - ], - - extraDependencies: ['html', 'language', 'to', 'wikiData'], +function getArg(node, argKey) { + return ( + node.data.args + ?.find(({key}) => key.data === argKey) + ?.value ?? + null); +} +export default { sprawl(wikiData, content) { - const find = bindFind(wikiData); + const find = + bindFind(wikiData, { + mode: 'quiet', + fuzz: { + capitalization: true, + kebab: true, + }, + }); - const parsedNodes = parseInput(content ?? ''); + const {result: parsedNodes, error} = + parseContentNodes(content ?? '', {errorMode: 'return'}); return { + error, + nodes: parsedNodes .map(node => { + if (node.type === 'tooltip') { + return { + i: node.i, + iEnd: node.iEnd, + type: 'tooltip', + data: { + // No recursion yet. Sorry! + tooltip: node.data.content[0].data, + label: node.data.label[0].data, + link: null, + }, + }; + } + if (node.type !== 'tag') { return node; } @@ -85,7 +110,7 @@ export default { } if (spec.link) { - let data = {link: spec.link}; + let data = {link: spec.link, replacerKey, replacerValue}; determineData: { // No value at all: this is an index link. @@ -124,15 +149,46 @@ export default { data.label = enteredLabel ?? - (transformName && data.thing.name - ? transformName(data.thing.name, node, content) - : null); + + (transformName && data.thing.name && + replacerKeyImplied && replacerValue === data.thing.name + + ? transformName(data.thing.name, node, content) + : null) ?? + + (replacerKeyImplied + ? replacerValue + : null); data.hash = enteredHash ?? null; return {i: node.i, iEnd: node.iEnd, type: 'internal-link', data}; } + if (replacerKey === 'tooltip') { + // TODO: Again, no recursive nodes. Sorry! + // const enteredLabel = node.data.label && transformNode(node.data.label, opts); + const enteredLabel = node.data.label?.data; + + return { + i: node.i, + iEnd: node.iEnd, + type: 'tooltip', + data: { + tooltip: + replacerValue ?? '(empty tooltip...)', + + label: + enteredLabel ?? '(tooltip without label)', + + link: + (getArg(node, 'link') + ? getArg(node, 'link')[0].data + : null), + }, + }; + } + // This will be another {type: 'tag'} node which gets processed in // generate. Extract replacerKey and replacerValue now, since it'd // be a pain to deal with later. @@ -140,8 +196,8 @@ export default { ...node, data: { ...node.data, - replacerKey: node.data.replacerKey.data, - replacerValue: node.data.replacerValue[0].data, + replacerKey, + replacerValue, }, }; }), @@ -152,25 +208,11 @@ export default { return { content, + error: + sprawl.error, + nodes: - sprawl.nodes - .map(node => { - switch (node.type) { - // Replace internal link nodes with a stub. It'll be replaced - // (by position) with an item from relations. - // - // TODO: This should be where label and hash get passed through, - // rather than in relations... (in which case there's no need to - // handle it specially here, and we can really just return - // data.nodes = sprawl.nodes) - case 'internal-link': - return {type: 'internal-link'}; - - // Other nodes will get processed in generate. - default: - return node; - } - }), + sprawl.nodes, }; }, @@ -184,10 +226,18 @@ export default { link: relation(name, arg), label: node.data.label, hash: node.data.hash, + name: arg?.name, + shortName: arg?.shortName ?? arg?.nameShort, } : getPlaceholder(node, content)); return { + textWithTooltip: + relation('generateTextWithTooltip'), + + tooltip: + relation('generateTooltip'), + internalLinks: nodes .filter(({type}) => type === 'internal-link') @@ -206,11 +256,15 @@ export default { externalLinks: nodes .filter(({type}) => type === 'external-link') - .map(node => { - const {href} = node.data; + .map(({data: {href}}) => + relation('linkExternal', href)), - return relation('linkExternal', href); - }), + externalLinksForTooltipNodes: + nodes + .filter(({type}) => type === 'tooltip') + .filter(({data}) => data.link) + .map(({data: {link: href}}) => + relation('linkExternal', href)), images: nodes @@ -241,19 +295,63 @@ export default { default: true, }, + textOnly: { + type: 'boolean', + default: false, + }, + thumb: { validate: v => v.is('small', 'medium', 'large'), default: 'large', }, + + substitute: { + validate: v => + v.strictArrayOf( + v.validateProperties({ + match: v.validateProperties({ + replacerKey: v.isString, + replacerValue: v.isString, + }), + + substitute: v.isHTML, + + apply: v.optional(v.isFunction), + })), + }, }, - generate(data, relations, slots, {html, language, to}) { + generate(data, relations, slots, {html, language, niceShowAggregate, to}) { + if (data.error) { + logWarn`Error in content text.`; + niceShowAggregate(data.error); + } + let imageIndex = 0; let internalLinkIndex = 0; let externalLinkIndex = 0; + let externalLinkForTooltipNodeIndex = 0; let offsetTextNode = 0; + const substitutions = + (slots.substitute + ? slots.substitute.slice() + : []); + + const pickSubstitution = node => { + const index = + substitutions.findIndex(({match}) => + match.replacerKey === node.data.replacerKey && + match.replacerValue === node.data.replacerValue); + + if (index === -1) { + return null; + } + + return substitutions.splice(index, 1).at(0); + }; + const contentFromNodes = data.nodes.map((node, index) => { const nextNode = data.nodes[index + 1]; @@ -272,6 +370,25 @@ export default { } }; + const substitution = pickSubstitution(node); + + if (substitution) { + const source = + substitution.substitute; + + let substitute = source; + + if (substitution.apply) { + const result = substitution.apply(source, node); + + if (result !== undefined) { + substitute = result; + } + } + + return {type: 'substitution', data: substitute}; + } + switch (node.type) { case 'text': { const text = node.data.slice(offsetTextNode); @@ -305,9 +422,8 @@ export default { height && {height}, style && {style}, - align === 'center' && - !link && - {class: 'align-center'}, + align && !link && + {class: 'align-' + align}, pixelate && {class: 'pixelate'}); @@ -318,8 +434,8 @@ export default { {href: link}, {target: '_blank'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, {title: language.encapsulate('misc.external.opensInNewTab', capsule => @@ -369,8 +485,8 @@ export default { inline: false, data: html.tag('div', {class: 'content-image-container'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, image), }; @@ -382,22 +498,31 @@ export default { ? to('media.path', node.src.slice('media/'.length)) : node.src); - const {width, height, align, pixelate} = node; + const {width, height, align, inline, pixelate} = node; - const content = - html.tag('div', {class: 'content-video-container'}, - align === 'center' && - {class: 'align-center'}, + const video = + html.tag('video', + src && {src}, + width && {width}, + height && {height}, - html.tag('video', - src && {src}, - width && {width}, - height && {height}, + {controls: true}, - {controls: true}, + align && inline && + {class: 'align-' + align}, + + pixelate && + {class: 'pixelate'}); + + const content = + (inline + ? video + : html.tag('div', {class: 'content-video-container'}, + align && + {class: 'align-' + align}, + + video)); - pixelate && - {class: 'pixelate'})); return { type: 'processed-video', @@ -411,15 +536,14 @@ export default { ? to('media.path', node.src.slice('media/'.length)) : node.src); - const {align, inline} = node; + const {align, inline, nameless} = node; const audio = html.tag('audio', src && {src}, - align === 'center' && - inline && - {class: 'align-center'}, + align && inline && + {class: 'align-' + align}, {controls: true}); @@ -427,10 +551,17 @@ export default { (inline ? audio : html.tag('div', {class: 'content-audio-container'}, - align === 'center' && - {class: 'align-center'}, + align && + {class: 'align-' + align}, + + [ + !nameless && + html.tag('a', {class: 'filename'}, + src && {href: src}, + language.sanitize(basename(node.src))), - audio)); + audio, + ])); return { type: 'processed-audio', @@ -452,7 +583,17 @@ export default { nodeFromRelations.link, {slots: ['content', 'hash']}); - const {label, hash} = nodeFromRelations; + const {label, hash, shortName, name} = nodeFromRelations; + + if (slots.textOnly) { + if (label) { + return {type: 'text', data: label}; + } else if (slots.preferShortLinkNames) { + return {type: 'text', data: shortName ?? name}; + } else { + return {type: 'text', data: name}; + } + } // These are removed from the typical combined slots({})-style // because we don't want to override slots that were already set @@ -466,7 +607,7 @@ export default { try { link.getSlotDescription('preferShortName'); hasPreferShortNameSlot = true; - } catch (error) { + } catch { hasPreferShortNameSlot = false; } @@ -479,7 +620,7 @@ export default { try { link.getSlotDescription('tooltipStyle'); hasTooltipStyleSlot = true; - } catch (error) { + } catch { hasTooltipStyleSlot = false; } @@ -503,9 +644,16 @@ export default { } case 'external-link': { - const {label} = node.data; const externalLink = relations.externalLinks[externalLinkIndex++]; + const label = + node.data.label ?? + node.data.href.replace(/^https?:\/\//, ''); + + if (slots.textOnly) { + return {type: 'text', data: label}; + } + externalLink.setSlots({ content: label, fromContent: true, @@ -526,6 +674,52 @@ export default { return {type: 'processed-external-link', data: externalLink}; } + case 'tooltip': { + const {label, link, tooltip: tooltipContent} = node.data; + + const externalLink = + (link + ? relations.externalLinksForTooltipNodes + .at(externalLinkForTooltipNodeIndex++) + : null); + + if (externalLink) { + externalLink.setSlots({ + content: label, + fromContent: true, + }); + + if (slots.indicateExternalLinks) { + externalLink.setSlots({ + indicateExternal: true, + disableBrowserTooltip: true, + tab: 'separate', + style: 'platform', + }); + } + } + + const textWithTooltip = relations.textWithTooltip.clone(); + const tooltip = relations.tooltip.clone(); + + tooltip.setSlots({ + attributes: {class: 'content-tooltip'}, + content: tooltipContent, // Not sanitized! + }); + + textWithTooltip.setSlots({ + attributes: [ + {class: 'content-tooltip-guy'}, + externalLink && {class: 'has-link'}, + ], + + text: externalLink ?? label, + tooltip, + }); + + return {type: 'processed-tooltip', data: textWithTooltip}; + } + case 'tag': { const {replacerKey, replacerValue} = node.data; @@ -542,12 +736,19 @@ export default { ? valueFn(replacerValue) : replacerValue); - const contents = + const content = (htmlFn ? htmlFn(value, {html, language}) : value); - return {type: 'text', data: contents.toString()}; + const contentText = + html.resolve(content, {normalize: 'string'}); + + if (slots.textOnly) { + return {type: 'text', data: striptags(contentText)}; + } else { + return {type: 'text', data: contentText}; + } } default: @@ -670,8 +871,8 @@ export default { extractNonTextNodes() // Compress multiple line breaks into single line breaks, // except when they're preceding or following indented - // text (by at least two spaces). - .replace(/(?<! .*)\n{2,}(?!^ )/gm, '\n') /* eslint-disable-line no-regex-spaces */ + // text (by at least two spaces) or blockquotes. + .replace(/(?<!^ .*|^>.*)\n{2,}(?!^ |^>)/gm, '\n') /* eslint-disable-line no-regex-spaces */ // Expand line breaks which don't follow a list, quote, // or <br> / " ", and which don't precede or follow // indented text (by at least two spaces). diff --git a/src/data/cacheable-object.js b/src/data/cacheable-object.js index 087f7825..3f70af30 100644 --- a/src/data/cacheable-object.js +++ b/src/data/cacheable-object.js @@ -12,9 +12,10 @@ export default class CacheableObject { static propertyDependants = Symbol.for('CacheableObject.propertyDependants'); static cacheValid = Symbol.for('CacheableObject.cacheValid'); + static cachedValue = Symbol.for('CacheableObject.cachedValue'); static updateValue = Symbol.for('CacheableObject.updateValues'); - constructor() { + constructor({seal = true} = {}) { this[CacheableObject.updateValue] = Object.create(null); this[CacheableObject.cachedValue] = Object.create(null); this[CacheableObject.cacheValid] = Object.create(null); @@ -34,6 +35,10 @@ export default class CacheableObject { this[property] = null; } } + + if (seal) { + Object.seal(this); + } } static finalizeCacheableObjectPrototype() { @@ -239,13 +244,13 @@ export class CacheableObjectPropertyValueError extends Error { try { inspectOldValue = inspect(oldValue); - } catch (error) { + } catch { inspectOldValue = colors.red(`(couldn't inspect)`); } try { inspectNewValue = inspect(newValue); - } catch (error) { + } catch { inspectNewValue = colors.red(`(couldn't inspect)`); } diff --git a/src/data/checks.js b/src/data/checks.js index 10261e4f..e68b2ed0 100644 --- a/src/data/checks.js +++ b/src/data/checks.js @@ -4,14 +4,14 @@ import {inspect as nodeInspect} from 'node:util'; import {colors, ENABLE_COLOR} from '#cli'; import CacheableObject from '#cacheable-object'; -import {replacerSpec, parseInput} from '#replacer'; +import {replacerSpec, parseContentNodes} from '#replacer'; import {compareArrays, cut, cutStart, empty, getNestedProp, iterateMultiline} from '#sugar'; import Thing from '#thing'; import thingConstructors from '#things'; -import {combineWikiDataArrays, commentaryRegexCaseSensitive} from '#wiki-data'; import { + annotateError, annotateErrorWithIndex, conditionallySuppressError, decorateErrorWithIndex, @@ -50,7 +50,7 @@ export function reportDirectoryErrors(wikiData, { if (!thingData) continue; for (const thing of thingData) { - if (findSpec.include && !findSpec.include(thing)) { + if (findSpec.include && !findSpec.include(thing, thingConstructors)) { continue; } @@ -166,6 +166,69 @@ function getFieldPropertyMessage(yamlDocumentSpec, property) { return fieldPropertyMessage; } +function decoAnnotateFindErrors(findFn) { + function annotateMultipleNameMatchesIncludingUnfortunatelyUnsecondary(error) { + const matches = error[Symbol.for('hsmusic.find.multipleNameMatches')]; + if (!matches) return; + + const notSoSecondary = + matches + .map(match => match.thing ?? match) + .filter(match => + match.isTrack && + match.isMainRelease && + CacheableObject.getUpdateValue(match, 'mainRelease')); + + if (empty(notSoSecondary)) return; + + let {message} = error; + message += (message.includes('\n') ? '\n\n' : '\n'); + message += colors.bright(colors.yellow('<!>')) + ' '; + message += colors.yellow(`Some of these tracks are meant to be secondary releases,`) + '\n'; + message += ' '.repeat(4); + message += colors.yellow(`but another error is keeping that from processing correctly!`) + '\n'; + message += ' '.repeat(4); + message += colors.yellow(`Probably look for an error to do with "Main Release", first.`); + Object.assign(error, {message}); + } + + return (...args) => { + try { + return findFn(...args); + } catch (caughtError) { + throw annotateError(caughtError, ...[ + annotateMultipleNameMatchesIncludingUnfortunatelyUnsecondary, + ]); + } + }; +} + +function decoSuppressFindErrors(findFn, {property}) { + void property; + + return conditionallySuppressError(_error => { + // We're not suppressing any errors at the moment. + // An old suppression is kept below for reference. + + /* + if (property === 'sampledTracks') { + // Suppress "didn't match anything" errors in particular, just for samples. + // In hsmusic-data we have a lot of "stub" sample data which don't have + // corresponding tracks yet, so it won't be useful to report such reference + // errors until we take the time to address that. But other errors, like + // malformed reference strings or miscapitalized existing tracks, should + // still be reported, as samples of existing tracks *do* display on the + // website! + if (error.message.includes(`Didn't match anything`)) { + return true; + } + } + */ + + return false; + }, findFn); +} + // Warn about references across data which don't match anything. This involves // using the find() functions on all references, setting it to 'error' mode, and // collecting everything in a structured logged (which gets logged if there are @@ -185,15 +248,21 @@ export function filterReferenceErrors(wikiData, { groups: 'group', artTags: '_artTag', referencedArtworks: '_artwork', - commentary: '_commentary', + commentary: '_content', + creditingSources: '_content', }], ['artTagData', { directDescendantArtTags: 'artTag', }], + ['artworkData', { + referencedArtworks: '_artwork', + }], + ['flashData', { - commentary: '_commentary', + commentary: '_content', + creditingSources: '_content', }], ['groupCategoryData', { @@ -220,8 +289,8 @@ export function filterReferenceErrors(wikiData, { flashes: 'flash', }], - ['groupData', { - serieses: '_serieses', + ['seriesData', { + albums: 'album', }], ['trackData', { @@ -232,8 +301,11 @@ export function filterReferenceErrors(wikiData, { sampledTracks: '_trackMainReleasesOnly', artTags: '_artTag', referencedArtworks: '_artwork', - mainReleaseTrack: '_trackMainReleasesOnly', - commentary: '_commentary', + mainRelease: '_mainRelease', + commentary: '_content', + creditingSources: '_content', + referencingSources: '_content', + lyrics: '_content', }], ['wikiInfo', { @@ -268,12 +340,12 @@ export function filterReferenceErrors(wikiData, { let writeProperty = true; switch (findFnKey) { - case '_commentary': + case '_content': if (value) { value = - Array.from(value.matchAll(commentaryRegexCaseSensitive)) - .map(({groups}) => groups.artistReferences) - .map(text => text.split(',').map(text => text.trim())); + value.map(entry => + CacheableObject.getUpdateValue(entry, 'artists') ?? + []); } writeProperty = false; @@ -287,15 +359,6 @@ export function filterReferenceErrors(wikiData, { // need writing, humm...) writeProperty = false; break; - - case '_serieses': - if (value) { - // Doesn't report on which series has the error, but... - value = value.flatMap(series => series.albums); - } - - writeProperty = false; - break; } if (value === undefined) { @@ -313,15 +376,12 @@ export function filterReferenceErrors(wikiData, { case '_artwork': { const mixed = find.mixed({ - album: find.albumWithArtwork, - track: find.trackWithArtwork, + album: find.albumPrimaryArtwork, + track: find.trackPrimaryArtwork, }); const data = - combineWikiDataArrays([ - wikiData.albumData, - wikiData.trackData, - ]); + wikiData.artworkData; findFn = ref => mixed(ref.reference, data, {mode: 'error'}); @@ -332,7 +392,7 @@ export function filterReferenceErrors(wikiData, { findFn = boundFind.artTag; break; - case '_commentary': + case '_content': findFn = findArtistOrAlias; break; @@ -350,8 +410,94 @@ export function filterReferenceErrors(wikiData, { }; break; - case '_serieses': - findFn = boundFind.album; + case '_mainRelease': + findFn = ref => { + // Mocking what's going on in `withMainRelease`. + + if (ref === 'same name single') { + // Accessing the current thing here. + try { + return boundFind.albumSinglesOnly(thing.name, { + fuzz: { + capitalization: true, + kebab: true, + }, + }); + } catch (caughtError) { + throw new Error( + `Didn't match a single with the same name`, + {cause: caughtError}); + } + } + + let track, trackError; + let album, albumError; + + try { + track = boundFind.trackMainReleasesOnly(ref); + } catch (caughtError) { + trackError = new Error( + `Didn't match a track`, {cause: caughtError}); + } + + try { + album = boundFind.album(ref); + } catch (caughtError) { + albumError = new Error( + `Didn't match an album`, {cause: caughtError}); + } + + if (track && album) { + if (album.tracks.includes(track)) { + return track; + } else { + throw new Error( + `Unrelated album and track matched for reference "${ref}". Please resolve:\n` + + `- ${inspect(track)}\n` + + `- ${inspect(album)}\n` + + `Returning null for this reference.`); + } + } + + if (track) { + return track; + } + + if (album) { + // At this point verification depends on the thing itself, + // which is currently in lexical scope, but if this code + // gets refactored, there might be trouble here... + + if (thing.mainReleaseTrack === null) { + if (album === thing.album) { + throw new Error( + `Matched album for reference "${ref}":\n` + + `- ` + inspect(album) + `\n` + + `...but this is the album that includes this secondary release, itself.\n` + + `Please resolve by pointing to aonther album here, or by removing this\n` + + `Main Release field, if this track is meant to be the main release.`); + } else { + throw new Error( + `Matched album for reference "${ref}":\n` + + `- ` + inspect(album) + `\n` + + `...but none of its tracks automatically match this secondary release.\n` + + `Please resolve by specifying the track here, instead of the album.`); + } + } else { + return album; + } + } + + const aggregateCause = + new AggregateError([albumError, trackError]); + + aggregateCause[Symbol.for('hsmusic.aggregate.translucent')] = true; + + throw new Error(`Trouble matching "${ref}"`, { + cause: aggregateCause, + }); + } + break; case '_trackArtwork': @@ -360,9 +506,16 @@ export function filterReferenceErrors(wikiData, { case '_trackMainReleasesOnly': findFn = trackRef => { - const track = boundFind.track(trackRef); - const mainRef = track && CacheableObject.getUpdateValue(track, 'mainReleaseTrack'); + let track = boundFind.trackMainReleasesOnly(trackRef, {mode: 'quiet'}); + if (track) { + return track; + } + + // Will error normally, if this can't unambiguously resolve + // or doesn't match any track. + track = boundFind.track(trackRef); + const mainRef = CacheableObject.getUpdateValue(track, 'mainRelease'); if (mainRef) { // It's possible for the main release to not actually exist, in this case. // It should still be reported since the 'Main Release' field was present. @@ -393,27 +546,8 @@ export function filterReferenceErrors(wikiData, { break; } - const suppress = fn => conditionallySuppressError(error => { - // We're not suppressing any errors at the moment. - // An old suppression is kept below for reference. - - /* - if (property === 'sampledTracks') { - // Suppress "didn't match anything" errors in particular, just for samples. - // In hsmusic-data we have a lot of "stub" sample data which don't have - // corresponding tracks yet, so it won't be useful to report such reference - // errors until we take the time to address that. But other errors, like - // malformed reference strings or miscapitalized existing tracks, should - // still be reported, as samples of existing tracks *do* display on the - // website! - if (error.message.includes(`Didn't match anything`)) { - return true; - } - } - */ - - return false; - }, fn); + findFn = decoSuppressFindErrors(findFn, {property}); + findFn = decoAnnotateFindErrors(findFn); const fieldPropertyMessage = getFieldPropertyMessage( @@ -464,15 +598,15 @@ export function filterReferenceErrors(wikiData, { } } - if (findFnKey === '_commentary') { + if (findFnKey === '_content') { filter( value, {message: errorMessage}, decorateErrorWithIndex(refs => (refs.length === 1 - ? suppress(findFn)(refs[0]) + ? findFn(refs[0]) : filterAggregate( refs, {message: `Errors in entry's artist references`}, - decorateErrorWithIndex(suppress(findFn))) + decorateErrorWithIndex(findFn)) .aggregate .close()))); @@ -484,19 +618,18 @@ export function filterReferenceErrors(wikiData, { if (Array.isArray(value)) { newPropertyValue = filter( value, {message: errorMessage}, - decorateErrorWithIndex(suppress(findFn))); + decorateErrorWithIndex(findFn)); break determineNewPropertyValue; } - nest({message: errorMessage}, - suppress(({call}) => { - try { - call(findFn, value); - } catch (error) { - newPropertyValue = null; - throw error; - } - })); + nest({message: errorMessage}, ({call}) => { + try { + call(findFn, value); + } catch (error) { + newPropertyValue = null; + throw error; + } + }); } if (writeProperty) { @@ -520,7 +653,11 @@ export class ContentNodeError extends Error { message, }) { const headingLine = - `(${where}) ${message}`; + (message.includes('\n\n') + ? `(${where})\n\n` + message + '\n' + : message.includes('\n') + ? `(${where})\n` + message + : `(${where}) ${message}`); const textUpToNode = containingLine.slice(0, columnNumber); @@ -565,16 +702,29 @@ export function reportContentTextErrors(wikiData, { description: 'description', }; + const artworkShape = { + source: 'artwork source', + originDetails: 'artwork origin details', + }; + const commentaryShape = { body: 'commentary body', - artistDisplayText: 'commentary artist display text', + artistText: 'commentary artist text', annotation: 'commentary annotation', }; + const lyricsShape = { + body: 'lyrics body', + artistText: 'lyrics artist text', + annotation: 'lyrics annotation', + }; + const contentTextSpec = [ ['albumData', { additionalFiles: additionalFileShape, commentary: commentaryShape, + creditingSources: commentaryShape, + coverArtworks: artworkShape, }], ['artTagData', { @@ -587,6 +737,8 @@ export function reportContentTextErrors(wikiData, { ['flashData', { commentary: commentaryShape, + creditingSources: commentaryShape, + coverArtwork: artworkShape, }], ['flashActData', { @@ -616,10 +768,12 @@ export function reportContentTextErrors(wikiData, { ['trackData', { additionalFiles: additionalFileShape, commentary: commentaryShape, - creditSources: commentaryShape, - lyrics: '_content', + creditingSources: commentaryShape, + referencingSources: commentaryShape, + lyrics: lyricsShape, midiProjectFiles: additionalFileShape, sheetMusicFiles: additionalFileShape, + trackArtworks: artworkShape, }], ['wikiInfo', { @@ -628,11 +782,19 @@ export function reportContentTextErrors(wikiData, { }], ]; - const boundFind = bindFind(wikiData, {mode: 'error'}); + const boundFind = + bindFind(wikiData, { + mode: 'error', + fuzz: { + capitalization: true, + kebab: true, + }, + }); + const findArtistOrAlias = bindFindArtistOrAlias(boundFind); function* processContent(input) { - const nodes = parseInput(input); + const nodes = parseContentNodes(input); for (const node of nodes) { const index = node.i; @@ -669,6 +831,9 @@ export function reportContentTextErrors(wikiData, { break; } + findFn = decoSuppressFindErrors(findFn, {property: null}); + findFn = decoAnnotateFindErrors(findFn); + const findRef = (replacerKeyImplied ? replacerValue @@ -689,7 +854,7 @@ export function reportContentTextErrors(wikiData, { } else if (node.type === 'external-link') { try { new URL(node.data.href); - } catch (error) { + } catch { yield { index, length, message: @@ -740,8 +905,8 @@ export function reportContentTextErrors(wikiData, { for (const thing of things) { nest({message: `Content text errors in ${inspect(thing)}`}, ({nest, push}) => { - for (const [property, shape] of Object.entries(propSpec)) { - const value = thing[property]; + for (let [property, shape] of Object.entries(propSpec)) { + let value = thing[property]; if (value === undefined) { push(new TypeError(`Property ${colors.red(property)} isn't valid for ${colors.green(thing.constructor.name)}`)); @@ -760,6 +925,31 @@ export function reportContentTextErrors(wikiData, { const topMessage = `Content text errors` + fieldPropertyMessage; + const checkShapeEntries = (entry, callProcessContentOpts) => { + for (const [key, annotation] of Object.entries(shape)) { + const value = entry[key]; + + // TODO: This should be an undefined/null check, like above, + // but it's not, because sometimes the stuff we're checking + // here isn't actually coded as a Thing - so the properties + // might really be undefined instead of null. Terrifying and + // awful. And most of all, citation needed. + if (!value) continue; + + callProcessContent({ + ...callProcessContentOpts, + + // TODO: `nest` isn't provided by `callProcessContentOpts` + //`but `push` is - this is to match the old code, but + // what's the deal here? + nest, + + value, + message: `Error in ${colors.green(annotation)}`, + }); + } + }; + if (shape === '_content') { callProcessContent({ nest, @@ -767,26 +957,18 @@ export function reportContentTextErrors(wikiData, { value, message: topMessage, }); - } else { + } else if (Array.isArray(value)) { nest({message: topMessage}, ({push}) => { for (const [index, entry] of value.entries()) { - for (const [key, annotation] of Object.entries(shape)) { - const value = entry[key]; - - // TODO: Should this check undefined/null similar to above? - if (!value) continue; - - callProcessContent({ - nest, - push, - value, - message: `Error in ${colors.green(annotation)}`, - annotateError: error => - annotateErrorWithIndex(error, index), - }); - } + checkShapeEntries(entry, { + push, + annotateError: error => + annotateErrorWithIndex(error, index), + }); } }); + } else { + checkShapeEntries(value, {push}); } } }); @@ -795,3 +977,49 @@ export function reportContentTextErrors(wikiData, { } }); } + +export function reportOrphanedArtworks(wikiData) { + const aggregate = + openAggregate({message: `Artwork objects are orphaned`}); + + const assess = ({ + message, + filterThing, + filterContribs, + link, + }) => { + aggregate.nest({message: `Orphaned ${message}`}, ({push}) => { + const ostensibleArtworks = + wikiData.artworkData + .filter(artwork => + artwork.thing instanceof filterThing && + artwork.artistContribsFromThingProperty === filterContribs); + + const orphanedArtworks = + ostensibleArtworks + .filter(artwork => !artwork.thing[link].includes(artwork)); + + for (const artwork of orphanedArtworks) { + push(new Error(`Orphaned: ${inspect(artwork)}`)); + } + }); + }; + + const {Album, Track} = thingConstructors; + + assess({ + message: `album cover artworks`, + filterThing: Album, + filterContribs: 'coverArtistContribs', + link: 'coverArtworks', + }); + + assess({ + message: `track artworks`, + filterThing: Track, + filterContribs: 'coverArtistContribs', + link: 'trackArtworks', + }); + + aggregate.close(); +} diff --git a/src/data/composite.js b/src/data/composite.js index f31c4069..e5873cf5 100644 --- a/src/data/composite.js +++ b/src/data/composite.js @@ -1416,7 +1416,7 @@ export function compositeFrom(description) { export function displayCompositeCacheAnalysis() { const showTimes = (cache, key) => { - const times = cache.times[key].slice().sort(); + const times = cache.times[key].toSorted(); const all = times; const worst10pc = times.slice(-times.length / 10); diff --git a/src/data/composite/control-flow/flipFilter.js b/src/data/composite/control-flow/flipFilter.js new file mode 100644 index 00000000..995bacad --- /dev/null +++ b/src/data/composite/control-flow/flipFilter.js @@ -0,0 +1,36 @@ +// Flips a filter, so that each true item becomes false, and vice versa. +// Overwrites the provided dependency. +// +// See also: +// - withAvailabilityFilter + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `flipFilter`, + + inputs: { + filter: input({type: 'array'}), + }, + + outputs: ({ + [input.staticDependency('filter')]: filterDependency, + }) => [filterDependency ?? '#flippedFilter'], + + steps: () => [ + { + dependencies: [ + input('filter'), + input.staticDependency('filter'), + ], + + compute: (continuation, { + [input('filter')]: filter, + [input.staticDependency('filter')]: filterDependency, + }) => continuation({ + [filterDependency ?? '#flippedFilter']: + filter.map(item => !item), + }), + }, + ], +}); diff --git a/src/data/composite/control-flow/index.js b/src/data/composite/control-flow/index.js index 7e137a14..778dc66b 100644 --- a/src/data/composite/control-flow/index.js +++ b/src/data/composite/control-flow/index.js @@ -10,6 +10,7 @@ export {default as exposeDependency} from './exposeDependency.js'; export {default as exposeDependencyOrContinue} from './exposeDependencyOrContinue.js'; export {default as exposeUpdateValueOrContinue} from './exposeUpdateValueOrContinue.js'; export {default as exposeWhetherDependencyAvailable} from './exposeWhetherDependencyAvailable.js'; +export {default as flipFilter} from './flipFilter.js'; export {default as raiseOutputWithoutDependency} from './raiseOutputWithoutDependency.js'; export {default as raiseOutputWithoutUpdateValue} from './raiseOutputWithoutUpdateValue.js'; export {default as withAvailabilityFilter} from './withAvailabilityFilter.js'; diff --git a/src/data/composite/control-flow/withAvailabilityFilter.js b/src/data/composite/control-flow/withAvailabilityFilter.js index cfea998e..fd93af71 100644 --- a/src/data/composite/control-flow/withAvailabilityFilter.js +++ b/src/data/composite/control-flow/withAvailabilityFilter.js @@ -4,6 +4,7 @@ // Accepts the same mode options as withResultOfAvailabilityCheck. // // See also: +// - flipFilter // - withFilteredList // - withResultOfAvailabilityCheck // diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js index 46a3dc81..05b59445 100644 --- a/src/data/composite/data/index.js +++ b/src/data/composite/data/index.js @@ -20,6 +20,7 @@ export {default as withMappedList} from './withMappedList.js'; export {default as withSortedList} from './withSortedList.js'; export {default as withStretchedList} from './withStretchedList.js'; +export {default as withLengthOfList} from './withLengthOfList.js'; export {default as withPropertyFromList} from './withPropertyFromList.js'; export {default as withPropertiesFromList} from './withPropertiesFromList.js'; diff --git a/src/data/composite/data/withFilteredList.js b/src/data/composite/data/withFilteredList.js index 44c1661d..15ee3373 100644 --- a/src/data/composite/data/withFilteredList.js +++ b/src/data/composite/data/withFilteredList.js @@ -2,9 +2,6 @@ // corresponding items in a list. Items which correspond to a truthy value // are kept, and the rest are excluded from the output list. // -// If the flip option is set, only items corresponding with a *falsy* value in -// the filter are kept. -// // TODO: There should be two outputs - one for the items included according to // the filter, and one for the items excluded. // @@ -22,28 +19,19 @@ export default templateCompositeFrom({ inputs: { list: input({type: 'array'}), filter: input({type: 'array'}), - - flip: input({ - type: 'boolean', - defaultValue: false, - }), }, outputs: ['#filteredList'], steps: () => [ { - dependencies: [input('list'), input('filter'), input('flip')], + dependencies: [input('list'), input('filter')], compute: (continuation, { [input('list')]: list, [input('filter')]: filter, - [input('flip')]: flip, }) => continuation({ '#filteredList': - list.filter((_item, index) => - (flip - ? !filter[index] - : filter[index])), + list.filter((_item, index) => filter[index]), }), }, ], diff --git a/src/data/composite/data/withLengthOfList.js b/src/data/composite/data/withLengthOfList.js new file mode 100644 index 00000000..e67aa887 --- /dev/null +++ b/src/data/composite/data/withLengthOfList.js @@ -0,0 +1,54 @@ +import {input, templateCompositeFrom} from '#composite'; + +function getOutputName({ + [input.staticDependency('list')]: list, +}) { + if (list && list.startsWith('#')) { + return `${list}.length`; + } else if (list) { + return `#${list}.length`; + } else { + return '#length'; + } +} + +export default templateCompositeFrom({ + annotation: `withMappedList`, + + inputs: { + list: input({type: 'array'}), + }, + + outputs: inputs => [getOutputName(inputs)], + + steps: () => [ + { + dependencies: [input.staticDependency('list')], + compute: (continuation, inputs) => + continuation({'#output': getOutputName(inputs)}), + }, + + { + dependencies: [input('list')], + compute: (continuation, { + [input('list')]: list, + }) => continuation({ + ['#value']: + (list === null + ? null + : list.length), + }), + }, + + { + dependencies: ['#output', '#value'], + + compute: (continuation, { + ['#output']: output, + ['#value']: value, + }) => continuation({ + [output]: value, + }), + }, + ], +}); diff --git a/src/data/composite/data/withNearbyItemFromList.js b/src/data/composite/data/withNearbyItemFromList.js index 83a8cc21..5e165219 100644 --- a/src/data/composite/data/withNearbyItemFromList.js +++ b/src/data/composite/data/withNearbyItemFromList.js @@ -9,6 +9,10 @@ // - If the 'valuePastEdge' input is provided, that value will be output // instead of null. // +// - If the 'filter' input is provided, corresponding items will be skipped, +// and only (repeating `offset`) the next included in the filter will be +// returned. +// // Both the list and item must be provided. // // See also: @@ -16,7 +20,6 @@ // import {input, templateCompositeFrom} from '#composite'; -import {atOffset} from '#sugar'; import {raiseOutputWithoutDependency} from '#composite/control-flow'; @@ -28,9 +31,12 @@ export default templateCompositeFrom({ inputs: { list: input({acceptsNull: false, type: 'array'}), item: input({acceptsNull: false}), - offset: input({type: 'number'}), + wrap: input({type: 'boolean', defaultValue: false}), + valuePastEdge: input({defaultValue: null}), + + filter: input({defaultValue: null, type: 'array'}), }, outputs: ['#nearbyItem'], @@ -45,29 +51,55 @@ export default templateCompositeFrom({ dependency: '#index', mode: input.value('index'), - output: input.value({ - ['#nearbyItem']: - null, - }), + output: input.value({'#nearbyItem': null}), }), { dependencies: [ input('list'), input('offset'), + input('wrap'), + input('valuePastEdge'), + + input('filter'), + '#index', ], compute: (continuation, { [input('list')]: list, [input('offset')]: offset, + [input('wrap')]: wrap, + [input('valuePastEdge')]: valuePastEdge, + + [input('filter')]: filter, + ['#index']: index, - }) => continuation({ - ['#nearbyItem']: - atOffset(list, index, offset, {wrap}), - }), + }) => { + const startIndex = index; + + do { + index += offset; + + if (wrap) { + index = index % list.length; + } else if (index < 0) { + return continuation({'#nearbyItem': valuePastEdge}); + } else if (index >= list.length) { + return continuation({'#nearbyItem': valuePastEdge}); + } + + if (filter && !filter[index]) { + continue; + } + + return continuation({'#nearbyItem': list[index]}); + } while (index !== startIndex); + + return continuation({'#nearbyItem': null}); + }, }, ], }); diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js index 65ebf77b..760095c2 100644 --- a/src/data/composite/data/withPropertyFromList.js +++ b/src/data/composite/data/withPropertyFromList.js @@ -5,11 +5,15 @@ // original list are kept null here. Objects which don't have the specified // property are retained in-place as null. // +// If the `internal` input is true, this reads the CacheableObject update value +// of each object rather than its exposed value. +// // See also: // - withPropertiesFromList // - withPropertyFromObject // +import CacheableObject from '#cacheable-object'; import {input, templateCompositeFrom} from '#composite'; function getOutputName({list, property, prefix}) { @@ -26,6 +30,7 @@ export default templateCompositeFrom({ list: input({type: 'array'}), property: input({type: 'string'}), prefix: input.staticValue({type: 'string', defaultValue: null}), + internal: input({type: 'boolean', defaultValue: false}), }, outputs: ({ @@ -37,13 +42,26 @@ export default templateCompositeFrom({ steps: () => [ { - dependencies: [input('list'), input('property')], + dependencies: [ + input('list'), + input('property'), + input('internal'), + ], + compute: (continuation, { [input('list')]: list, [input('property')]: property, + [input('internal')]: internal, }) => continuation({ ['#values']: - list.map(item => item[property] ?? null), + list.map(item => + (item === null + ? null + : internal + ? CacheableObject.getUpdateValue(item, property) + ?? null + : item[property] + ?? null)), }), }, diff --git a/src/data/composite/data/withPropertyFromObject.js b/src/data/composite/data/withPropertyFromObject.js index 4f240506..7b452b99 100644 --- a/src/data/composite/data/withPropertyFromObject.js +++ b/src/data/composite/data/withPropertyFromObject.js @@ -13,6 +13,21 @@ import CacheableObject from '#cacheable-object'; import {input, templateCompositeFrom} from '#composite'; +function getOutputName({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, +}) { + if (object && property) { + if (object.startsWith('#')) { + return `${object}.${property}`; + } else { + return `#${object}.${property}`; + } + } else { + return '#value'; + } +} + export default templateCompositeFrom({ annotation: `withPropertyFromObject`, @@ -22,15 +37,7 @@ export default templateCompositeFrom({ internal: input({type: 'boolean', defaultValue: false}), }, - outputs: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => - (object && property - ? (object.startsWith('#') - ? [`${object}.${property}`] - : [`#${object}.${property}`]) - : ['#value']), + outputs: inputs => [getOutputName(inputs)], steps: () => [ { @@ -39,17 +46,8 @@ export default templateCompositeFrom({ input.staticValue('property'), ], - compute: (continuation, { - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => continuation({ - '#output': - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value'), - }), + compute: (continuation, inputs) => + continuation({'#output': getOutputName(inputs)}), }, { diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js index 8b5098f0..de1d37c3 100644 --- a/src/data/composite/things/album/index.js +++ b/src/data/composite/things/album/index.js @@ -1 +1,2 @@ +export {default as withCoverArtDate} from './withCoverArtDate.js'; export {default as withTracks} from './withTracks.js'; diff --git a/src/data/composite/things/album/withCoverArtDate.js b/src/data/composite/things/album/withCoverArtDate.js new file mode 100644 index 00000000..978f566a --- /dev/null +++ b/src/data/composite/things/album/withCoverArtDate.js @@ -0,0 +1,50 @@ +import {input, templateCompositeFrom} from '#composite'; +import {isDate} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withHasArtwork} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withCoverArtDate`, + + inputs: { + from: input({ + validate: isDate, + defaultDependency: 'coverArtDate', + acceptsNull: true, + }), + }, + + outputs: ['#coverArtDate'], + + steps: () => [ + withHasArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + }), + + raiseOutputWithoutDependency({ + dependency: '#hasArtwork', + mode: input.value('falsy'), + output: input.value({'#coverArtDate': null}), + }), + + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: from, + }) => + (from + ? continuation.raiseOutput({'#coverArtDate': from}) + : continuation()), + }, + + { + dependencies: ['date'], + compute: (continuation, {date}) => + (date + ? continuation({'#coverArtDate': date}) + : continuation({'#coverArtDate': null})), + }, + ], +}); diff --git a/src/data/composite/things/artwork/index.js b/src/data/composite/things/artwork/index.js new file mode 100644 index 00000000..b5e5e167 --- /dev/null +++ b/src/data/composite/things/artwork/index.js @@ -0,0 +1,7 @@ +export {default as withArtTags} from './withArtTags.js'; +export {default as withAttachedArtwork} from './withAttachedArtwork.js'; +export {default as withContainingArtworkList} from './withContainingArtworkList.js'; +export {default as withContentWarningArtTags} from './withContentWarningArtTags.js'; +export {default as withContribsFromAttachedArtwork} from './withContribsFromAttachedArtwork.js'; +export {default as withDate} from './withDate.js'; +export {default as withPropertyFromAttachedArtwork} from './withPropertyFromAttachedArtwork.js'; diff --git a/src/data/composite/things/artwork/withArtTags.js b/src/data/composite/things/artwork/withArtTags.js new file mode 100644 index 00000000..1fed3c31 --- /dev/null +++ b/src/data/composite/things/artwork/withArtTags.js @@ -0,0 +1,99 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; +import {withResolvedReferenceList} from '#composite/wiki-data'; +import {soupyFind} from '#composite/wiki-properties'; + +import withPropertyFromAttachedArtwork + from './withPropertyFromAttachedArtwork.js'; + +export default templateCompositeFrom({ + annotation: `withArtTags`, + + inputs: { + from: input({ + type: 'array', + acceptsNull: true, + defaultDependency: 'artTags', + }), + }, + + outputs: ['#artTags'], + + steps: () => [ + withResolvedReferenceList({ + list: input('from'), + find: soupyFind.input('artTag'), + }), + + withResultOfAvailabilityCheck({ + from: '#resolvedReferenceList', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability', '#resolvedReferenceList'], + compute: (continuation, { + ['#availability']: availability, + ['#resolvedReferenceList']: resolvedReferenceList, + }) => + (availability + ? continuation.raiseOutput({ + '#artTags': resolvedReferenceList, + }) + : continuation()), + }, + + withPropertyFromAttachedArtwork({ + property: input.value('artTags'), + }), + + withResultOfAvailabilityCheck({ + from: '#attachedArtwork.artTags', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability', '#attachedArtwork.artTags'], + compute: (continuation, { + ['#availability']: availability, + ['#attachedArtwork.artTags']: attachedArtworkArtTags, + }) => + (availability + ? continuation.raiseOutput({ + '#artTags': attachedArtworkArtTags, + }) + : continuation()), + }, + + raiseOutputWithoutDependency({ + dependency: 'artTagsFromThingProperty', + output: input.value({'#artTags': []}), + }), + + withPropertyFromObject({ + object: 'thing', + property: 'artTagsFromThingProperty', + }).outputs({ + ['#value']: '#thing.artTags', + }), + + withResultOfAvailabilityCheck({ + from: '#thing.artTags', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability', '#thing.artTags'], + compute: (continuation, { + ['#availability']: availability, + ['#thing.artTags']: thingArtTags, + }) => + (availability + ? continuation({'#artTags': thingArtTags}) + : continuation({'#artTags': []})), + }, + ], +}); diff --git a/src/data/composite/things/artwork/withAttachedArtwork.js b/src/data/composite/things/artwork/withAttachedArtwork.js new file mode 100644 index 00000000..d7c0d87b --- /dev/null +++ b/src/data/composite/things/artwork/withAttachedArtwork.js @@ -0,0 +1,43 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {flipFilter, raiseOutputWithoutDependency} + from '#composite/control-flow'; +import {withNearbyItemFromList, withPropertyFromList} from '#composite/data'; + +import withContainingArtworkList from './withContainingArtworkList.js'; + +export default templateCompositeFrom({ + annotaion: `withContribsFromMainArtwork`, + + outputs: ['#attachedArtwork'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'attachAbove', + mode: input.value('falsy'), + output: input.value({'#attachedArtwork': null}), + }), + + withContainingArtworkList(), + + withPropertyFromList({ + list: '#containingArtworkList', + property: input.value('attachAbove'), + }), + + flipFilter({ + filter: '#containingArtworkList.attachAbove', + }).outputs({ + '#containingArtworkList.attachAbove': '#filterNotAttached', + }), + + withNearbyItemFromList({ + list: '#containingArtworkList', + item: input.myself(), + offset: input.value(-1), + filter: '#filterNotAttached', + }).outputs({ + '#nearbyItem': '#attachedArtwork', + }), + ], +}); diff --git a/src/data/composite/things/artwork/withContainingArtworkList.js b/src/data/composite/things/artwork/withContainingArtworkList.js new file mode 100644 index 00000000..9c928ffd --- /dev/null +++ b/src/data/composite/things/artwork/withContainingArtworkList.js @@ -0,0 +1,46 @@ +// Gets the list of artworks which contains this one, which is functionally +// equivalent to `this.thing[this.thingProperty]`. If the exposed value is not +// a list at all (i.e. the property holds a single artwork), this composition +// outputs null. + +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withContainingArtworkList`, + + outputs: ['#containingArtworkList'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'thing', + output: input.value({'#containingArtworkList': null}), + }), + + raiseOutputWithoutDependency({ + dependency: 'thingProperty', + output: input.value({'#containingArtworkList': null}), + }), + + withPropertyFromObject({ + object: 'thing', + property: 'thingProperty', + }).outputs({ + '#value': '#containingValue', + }), + + { + dependencies: ['#containingValue'], + compute: (continuation, { + ['#containingValue']: containingValue, + }) => continuation({ + ['#containingArtworkList']: + (Array.isArray(containingValue) + ? containingValue + : null), + }), + }, + ], +}); diff --git a/src/data/composite/things/artwork/withContentWarningArtTags.js b/src/data/composite/things/artwork/withContentWarningArtTags.js new file mode 100644 index 00000000..4c07e837 --- /dev/null +++ b/src/data/composite/things/artwork/withContentWarningArtTags.js @@ -0,0 +1,27 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {withFilteredList, withPropertyFromList} from '#composite/data'; + +import withArtTags from './withArtTags.js'; + +export default templateCompositeFrom({ + annotation: `withContentWarningArtTags`, + + outputs: ['#contentWarningArtTags'], + + steps: () => [ + withArtTags(), + + withPropertyFromList({ + list: '#artTags', + property: input.value('isContentWarning'), + }), + + withFilteredList({ + list: '#artTags', + filter: '#artTags.isContentWarning', + }).outputs({ + '#filteredList': '#contentWarningArtTags', + }), + ], +}); diff --git a/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js new file mode 100644 index 00000000..e9425c95 --- /dev/null +++ b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js @@ -0,0 +1,27 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withRecontextualizedContributionList} from '#composite/wiki-data'; + +import withPropertyFromAttachedArtwork from './withPropertyFromAttachedArtwork.js'; + +export default templateCompositeFrom({ + annotaion: `withContribsFromAttachedArtwork`, + + outputs: ['#attachedArtwork.artistContribs'], + + steps: () => [ + withPropertyFromAttachedArtwork({ + property: input.value('artistContribs'), + }), + + raiseOutputWithoutDependency({ + dependency: '#attachedArtwork.artistContribs', + output: input.value({'#attachedArtwork.artistContribs': null}), + }), + + withRecontextualizedContributionList({ + list: '#attachedArtwork.artistContribs', + }), + ], +}); diff --git a/src/data/composite/things/artwork/withDate.js b/src/data/composite/things/artwork/withDate.js new file mode 100644 index 00000000..5e05b814 --- /dev/null +++ b/src/data/composite/things/artwork/withDate.js @@ -0,0 +1,41 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withDate`, + + inputs: { + from: input({ + defaultDependency: 'date', + acceptsNull: true, + }), + }, + + outputs: ['#date'], + + steps: () => [ + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: date, + }) => + (date + ? continuation.raiseOutput({'#date': date}) + : continuation()), + }, + + raiseOutputWithoutDependency({ + dependency: 'dateFromThingProperty', + output: input.value({'#date': null}), + }), + + withPropertyFromObject({ + object: 'thing', + property: 'dateFromThingProperty', + }).outputs({ + ['#value']: '#date', + }), + ], +}) diff --git a/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js b/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js new file mode 100644 index 00000000..a2f954b9 --- /dev/null +++ b/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js @@ -0,0 +1,65 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {withResultOfAvailabilityCheck} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +import withAttachedArtwork from './withAttachedArtwork.js'; + +function getOutputName({ + [input.staticValue('property')]: property, +}) { + if (property) { + return `#attachedArtwork.${property}`; + } else { + return '#value'; + } +} + +export default templateCompositeFrom({ + annotation: `withPropertyFromAttachedArtwork`, + + inputs: { + property: input({type: 'string'}), + }, + + outputs: inputs => [getOutputName(inputs)], + + steps: () => [ + { + dependencies: [input.staticValue('property')], + compute: (continuation, inputs) => + continuation({'#output': getOutputName(inputs)}), + }, + + withAttachedArtwork(), + + withResultOfAvailabilityCheck({ + from: '#attachedArtwork', + }), + + { + dependencies: ['#availability', '#output'], + compute: (continuation, { + ['#availability']: availability, + ['#output']: output, + }) => + (availability + ? continuation() + : continuation.raiseOutput({[output]: null})), + }, + + withPropertyFromObject({ + object: '#attachedArtwork', + property: input('property'), + }), + + { + dependencies: ['#value', '#output'], + compute: (continuation, { + ['#value']: value, + ['#output']: output, + }) => + continuation.raiseOutput({[output]: value}), + }, + ], +}); diff --git a/src/data/composite/things/content/contentArtists.js b/src/data/composite/things/content/contentArtists.js new file mode 100644 index 00000000..8d5db5a5 --- /dev/null +++ b/src/data/composite/things/content/contentArtists.js @@ -0,0 +1,40 @@ +import {input, templateCompositeFrom} from '#composite'; +import {validateReferenceList} from '#validators'; + +import {exitWithoutDependency, exposeDependency} + from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; +import {soupyFind} from '#composite/wiki-properties'; + +import withExpressedOrImplicitArtistReferences + from './helpers/withExpressedOrImplicitArtistReferences.js'; + +export default templateCompositeFrom({ + annotation: `contentArtists`, + + compose: false, + + update: { + validate: validateReferenceList('artist'), + }, + + steps: () => [ + withExpressedOrImplicitArtistReferences({ + from: input.updateValue(), + }), + + exitWithoutDependency({ + dependency: '#artistReferences', + value: input.value([]), + }), + + withResolvedReferenceList({ + list: '#artistReferences', + find: soupyFind.input('artist'), + }), + + exposeDependency({ + dependency: '#resolvedReferenceList', + }), + ], +}); diff --git a/src/data/composite/things/content/hasAnnotationPart.js b/src/data/composite/things/content/hasAnnotationPart.js new file mode 100644 index 00000000..83d175e3 --- /dev/null +++ b/src/data/composite/things/content/hasAnnotationPart.js @@ -0,0 +1,25 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exposeDependency} from '#composite/control-flow'; + +import withHasAnnotationPart from './withHasAnnotationPart.js'; + +export default templateCompositeFrom({ + annotation: `hasAnnotationPart`, + + compose: false, + + inputs: { + part: input({type: 'string'}), + }, + + steps: () => [ + withHasAnnotationPart({ + part: input('part'), + }), + + exposeDependency({ + dependency: '#hasAnnotationPart', + }), + ], +}); diff --git a/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js b/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js new file mode 100644 index 00000000..69da8c75 --- /dev/null +++ b/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js @@ -0,0 +1,61 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withFilteredList, withMappedList} from '#composite/data'; +import {withContentNodes} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withExpressedOrImplicitArtistReferences`, + + inputs: { + from: input({type: 'array', acceptsNull: true}), + }, + + outputs: ['#artistReferences'], + + steps: () => [ + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: expressedArtistReferences, + }) => + (expressedArtistReferences + ? continuation.raiseOutput({'#artistReferences': expressedArtistReferences}) + : continuation()), + }, + + raiseOutputWithoutDependency({ + dependency: 'artistText', + output: input.value({'#artistReferences': null}), + }), + + withContentNodes({ + from: 'artistText', + }), + + withMappedList({ + list: '#contentNodes', + map: input.value(node => + node.type === 'tag' && + node.data.replacerKey?.data === 'artist'), + }).outputs({ + '#mappedList': '#artistTagFilter', + }), + + withFilteredList({ + list: '#contentNodes', + filter: '#artistTagFilter', + }).outputs({ + '#filteredList': '#artistTags', + }), + + withMappedList({ + list: '#artistTags', + map: input.value(node => + 'artist:' + + node.data.replacerValue[0].data), + }).outputs({ + '#mappedList': '#artistReferences', + }), + ], +}); diff --git a/src/data/composite/things/content/index.js b/src/data/composite/things/content/index.js new file mode 100644 index 00000000..4176337d --- /dev/null +++ b/src/data/composite/things/content/index.js @@ -0,0 +1,7 @@ +export {default as contentArtists} from './contentArtists.js'; +export {default as hasAnnotationPart} from './hasAnnotationPart.js'; +export {default as withAnnotationParts} from './withAnnotationParts.js'; +export {default as withHasAnnotationPart} from './withHasAnnotationPart.js'; +export {default as withSourceText} from './withSourceText.js'; +export {default as withSourceURLs} from './withSourceURLs.js'; +export {default as withWebArchiveDate} from './withWebArchiveDate.js'; diff --git a/src/data/composite/things/content/withAnnotationParts.js b/src/data/composite/things/content/withAnnotationParts.js new file mode 100644 index 00000000..0c5a0294 --- /dev/null +++ b/src/data/composite/things/content/withAnnotationParts.js @@ -0,0 +1,103 @@ +import {input, templateCompositeFrom} from '#composite'; +import {empty, transposeArrays} from '#sugar'; +import {is} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromList} from '#composite/data'; +import {splitContentNodesAround, withContentNodes} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withAnnotationParts`, + + inputs: { + mode: input({ + validate: is('strings', 'nodes'), + }), + }, + + outputs: ['#annotationParts'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'annotation', + output: input.value({'#annotationParts': []}), + }), + + withContentNodes({ + from: 'annotation', + }), + + splitContentNodesAround({ + nodes: '#contentNodes', + around: input.value(/, */g), + }), + + { + dependencies: ['#contentNodeLists'], + compute: (continuation, { + ['#contentNodeLists']: nodeLists, + }) => continuation({ + ['#contentNodeLists']: + nodeLists.filter(list => !empty(list)), + }), + }, + + { + dependencies: ['#contentNodeLists', input('mode')], + compute: (continuation, { + ['#contentNodeLists']: nodeLists, + [input('mode')]: mode, + }) => + (mode === 'nodes' + ? continuation.raiseOutput({'#annotationParts': nodeLists}) + : continuation()), + }, + + { + dependencies: ['#contentNodeLists'], + + compute: (continuation, { + ['#contentNodeLists']: nodeLists, + }) => continuation({ + ['#firstNodes']: + nodeLists.map(list => list.at(0)), + + ['#lastNodes']: + nodeLists.map(list => list.at(-1)), + }), + }, + + withPropertyFromList({ + list: '#firstNodes', + property: input.value('i'), + }).outputs({ + '#firstNodes.i': '#startIndices', + }), + + withPropertyFromList({ + list: '#lastNodes', + property: input.value('iEnd'), + }).outputs({ + '#lastNodes.iEnd': '#endIndices', + }), + + { + dependencies: [ + 'annotation', + '#startIndices', + '#endIndices', + ], + + compute: (continuation, { + ['annotation']: annotation, + ['#startIndices']: startIndices, + ['#endIndices']: endIndices, + }) => continuation({ + ['#annotationParts']: + transposeArrays([startIndices, endIndices]) + .map(([start, end]) => + annotation.slice(start, end)), + }), + }, + ], +}); diff --git a/src/data/composite/things/content/withHasAnnotationPart.js b/src/data/composite/things/content/withHasAnnotationPart.js new file mode 100644 index 00000000..4af554f3 --- /dev/null +++ b/src/data/composite/things/content/withHasAnnotationPart.js @@ -0,0 +1,43 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withAnnotationParts from './withAnnotationParts.js'; + +export default templateCompositeFrom({ + annotation: `withHasAnnotationPart`, + + inputs: { + part: input({type: 'string'}), + }, + + outputs: ['#hasAnnotationPart'], + + steps: () => [ + withAnnotationParts({ + mode: input.value('strings'), + }), + + raiseOutputWithoutDependency({ + dependency: '#annotationParts', + output: input.value({'#hasAnnotationPart': false}), + }), + + { + dependencies: [ + input('part'), + '#annotationParts', + ], + + compute: (continuation, { + [input('part')]: search, + ['#annotationParts']: parts, + }) => continuation({ + ['#hasAnnotationPart']: + parts.some(part => + part.toLowerCase() === + search.toLowerCase()), + }), + }, + ], +}); diff --git a/src/data/composite/things/content/withSourceText.js b/src/data/composite/things/content/withSourceText.js new file mode 100644 index 00000000..292306b7 --- /dev/null +++ b/src/data/composite/things/content/withSourceText.js @@ -0,0 +1,53 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withAnnotationParts from './withAnnotationParts.js'; + +export default templateCompositeFrom({ + annotation: `withSourceText`, + + outputs: ['#sourceText'], + + steps: () => [ + withAnnotationParts({ + mode: input.value('nodes'), + }), + + raiseOutputWithoutDependency({ + dependency: '#annotationParts', + output: input.value({'#sourceText': null}), + }), + + { + dependencies: ['#annotationParts'], + compute: (continuation, { + ['#annotationParts']: annotationParts, + }) => continuation({ + ['#firstPartWithExternalLink']: + annotationParts + .find(nodes => nodes + .some(node => node.type === 'external-link')) ?? + null, + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#firstPartWithExternalLink', + output: input.value({'#sourceText': null}), + }), + + { + dependencies: ['annotation', '#firstPartWithExternalLink'], + compute: (continuation, { + ['annotation']: annotation, + ['#firstPartWithExternalLink']: nodes, + }) => continuation({ + ['#sourceText']: + annotation.slice( + nodes.at(0).i, + nodes.at(-1).iEnd), + }), + }, + ], +}); diff --git a/src/data/composite/things/content/withSourceURLs.js b/src/data/composite/things/content/withSourceURLs.js new file mode 100644 index 00000000..f85ff9ea --- /dev/null +++ b/src/data/composite/things/content/withSourceURLs.js @@ -0,0 +1,62 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withFilteredList, withMappedList} from '#composite/data'; + +import withAnnotationParts from './withAnnotationParts.js'; + +export default templateCompositeFrom({ + annotation: `withSourceURLs`, + + outputs: ['#sourceURLs'], + + steps: () => [ + withAnnotationParts({ + mode: input.value('nodes'), + }), + + raiseOutputWithoutDependency({ + dependency: '#annotationParts', + output: input.value({'#sourceURLs': []}), + }), + + { + dependencies: ['#annotationParts'], + compute: (continuation, { + ['#annotationParts']: annotationParts, + }) => continuation({ + ['#firstPartWithExternalLink']: + annotationParts + .find(nodes => nodes + .some(node => node.type === 'external-link')) ?? + null, + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#firstPartWithExternalLink', + output: input.value({'#sourceURLs': []}), + }), + + withMappedList({ + list: '#firstPartWithExternalLink', + map: input.value(node => node.type === 'external-link'), + }).outputs({ + '#mappedList': '#externalLinkFilter', + }), + + withFilteredList({ + list: '#firstPartWithExternalLink', + filter: '#externalLinkFilter', + }).outputs({ + '#filteredList': '#externalLinks', + }), + + withMappedList({ + list: '#externalLinks', + map: input.value(node => node.data.href), + }).outputs({ + '#mappedList': '#sourceURLs', + }), + ], +}); diff --git a/src/data/composite/things/content/withWebArchiveDate.js b/src/data/composite/things/content/withWebArchiveDate.js new file mode 100644 index 00000000..3aaa4f64 --- /dev/null +++ b/src/data/composite/things/content/withWebArchiveDate.js @@ -0,0 +1,41 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `withWebArchiveDate`, + + outputs: ['#webArchiveDate'], + + steps: () => [ + { + dependencies: ['annotation'], + + compute: (continuation, {annotation}) => + continuation({ + ['#dateText']: + annotation + ?.match(/https?:\/\/web.archive.org\/web\/([0-9]{8,8})[0-9]*\//) + ?.[1] ?? + null, + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#dateText', + output: input.value({['#webArchiveDate']: null}), + }), + + { + dependencies: ['#dateText'], + compute: (continuation, {['#dateText']: dateText}) => + continuation({ + ['#webArchiveDate']: + new Date( + dateText.slice(0, 4) + '/' + + dateText.slice(4, 6) + '/' + + dateText.slice(6, 8)), + }), + }, + ], +}); diff --git a/src/data/composite/things/contribution/index.js b/src/data/composite/things/contribution/index.js index 9b22be2e..31d86b8b 100644 --- a/src/data/composite/things/contribution/index.js +++ b/src/data/composite/things/contribution/index.js @@ -1,6 +1,4 @@ export {default as inheritFromContributionPresets} from './inheritFromContributionPresets.js'; -export {default as thingPropertyMatches} from './thingPropertyMatches.js'; -export {default as thingReferenceTypeMatches} from './thingReferenceTypeMatches.js'; export {default as withContainingReverseContributionList} from './withContainingReverseContributionList.js'; export {default as withContributionArtist} from './withContributionArtist.js'; export {default as withContributionContext} from './withContributionContext.js'; diff --git a/src/data/composite/things/contribution/thingPropertyMatches.js b/src/data/composite/things/contribution/thingPropertyMatches.js deleted file mode 100644 index 4a37f2cf..00000000 --- a/src/data/composite/things/contribution/thingPropertyMatches.js +++ /dev/null @@ -1,33 +0,0 @@ -import {input, templateCompositeFrom} from '#composite'; - -import {exitWithoutDependency} from '#composite/control-flow'; - -export default templateCompositeFrom({ - annotation: `thingPropertyMatches`, - - compose: false, - - inputs: { - value: input({type: 'string'}), - }, - - steps: () => [ - exitWithoutDependency({ - dependency: 'thingProperty', - value: input.value(false), - }), - - { - dependencies: [ - 'thingProperty', - input('value'), - ], - - compute: ({ - ['thingProperty']: thingProperty, - [input('value')]: value, - }) => - thingProperty === value, - }, - ], -}); diff --git a/src/data/composite/things/contribution/thingReferenceTypeMatches.js b/src/data/composite/things/contribution/thingReferenceTypeMatches.js deleted file mode 100644 index 2ee811af..00000000 --- a/src/data/composite/things/contribution/thingReferenceTypeMatches.js +++ /dev/null @@ -1,39 +0,0 @@ -import {input, templateCompositeFrom} from '#composite'; - -import {exitWithoutDependency} from '#composite/control-flow'; -import {withPropertyFromObject} from '#composite/data'; - -export default templateCompositeFrom({ - annotation: `thingReferenceTypeMatches`, - - compose: false, - - inputs: { - value: input({type: 'string'}), - }, - - steps: () => [ - exitWithoutDependency({ - dependency: 'thing', - value: input.value(false), - }), - - withPropertyFromObject({ - object: 'thing', - property: input.value('constructor'), - }), - - { - dependencies: [ - '#thing.constructor', - input('value'), - ], - - compute: ({ - ['#thing.constructor']: constructor, - [input('value')]: value, - }) => - constructor[Symbol.for('Thing.referenceType')] === value, - }, - ], -}); diff --git a/src/data/composite/things/track-section/index.js b/src/data/composite/things/track-section/index.js index 3202ed49..f11a2ab5 100644 --- a/src/data/composite/things/track-section/index.js +++ b/src/data/composite/things/track-section/index.js @@ -1 +1,3 @@ export {default as withAlbum} from './withAlbum.js'; +export {default as withContinueCountingFrom} from './withContinueCountingFrom.js'; +export {default as withStartCountingFrom} from './withStartCountingFrom.js'; diff --git a/src/data/composite/things/track-section/withContinueCountingFrom.js b/src/data/composite/things/track-section/withContinueCountingFrom.js new file mode 100644 index 00000000..0ca52b6c --- /dev/null +++ b/src/data/composite/things/track-section/withContinueCountingFrom.js @@ -0,0 +1,25 @@ +import {templateCompositeFrom} from '#composite'; + +import withStartCountingFrom from './withStartCountingFrom.js'; + +export default templateCompositeFrom({ + annotation: `withContinueCountingFrom`, + + outputs: ['#continueCountingFrom'], + + steps: () => [ + withStartCountingFrom(), + + { + dependencies: ['#startCountingFrom', 'tracks'], + compute: (continuation, { + ['#startCountingFrom']: startCountingFrom, + ['tracks']: tracks, + }) => continuation({ + ['#continueCountingFrom']: + startCountingFrom + + tracks.length, + }), + }, + ], +}); diff --git a/src/data/composite/things/track-section/withStartCountingFrom.js b/src/data/composite/things/track-section/withStartCountingFrom.js new file mode 100644 index 00000000..ef345327 --- /dev/null +++ b/src/data/composite/things/track-section/withStartCountingFrom.js @@ -0,0 +1,64 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withNearbyItemFromList, withPropertyFromObject} from '#composite/data'; + +import withAlbum from './withAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withStartCountingFrom`, + + inputs: { + from: input({ + type: 'number', + defaultDependency: 'startCountingFrom', + acceptsNull: true, + }), + }, + + outputs: ['#startCountingFrom'], + + steps: () => [ + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: from, + }) => + (from === null + ? continuation() + : continuation.raiseOutput({'#startCountingFrom': from})), + }, + + withAlbum(), + + raiseOutputWithoutDependency({ + dependency: '#album', + output: input.value({'#startCountingFrom': 1}), + }), + + withPropertyFromObject({ + object: '#album', + property: input.value('trackSections'), + }), + + withNearbyItemFromList({ + list: '#album.trackSections', + item: input.myself(), + offset: input.value(-1), + }).outputs({ + '#nearbyItem': '#previousTrackSection', + }), + + raiseOutputWithoutDependency({ + dependency: '#previousTrackSection', + output: input.value({'#startCountingFrom': 1}), + }), + + withPropertyFromObject({ + object: '#previousTrackSection', + property: input.value('continueCountingFrom'), + }).outputs({ + '#previousTrackSection.continueCountingFrom': '#startCountingFrom', + }), + ], +}); diff --git a/src/data/composite/things/track/alwaysReferenceByDirectory.js b/src/data/composite/things/track/alwaysReferenceByDirectory.js new file mode 100644 index 00000000..a342d38b --- /dev/null +++ b/src/data/composite/things/track/alwaysReferenceByDirectory.js @@ -0,0 +1,69 @@ +// Controls how find.track works - it'll never be matched by a reference +// just to the track's name, which means you don't have to always reference +// some *other* (much more commonly referenced) track by directory instead +// of more naturally by name. + +import {input, templateCompositeFrom} from '#composite'; +import {isBoolean} from '#validators'; +import {getKebabCase} from '#wiki-data'; + +import {withPropertyFromObject} from '#composite/data'; + +import { + exitWithoutDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + +import withMainReleaseTrack from './withMainReleaseTrack.js'; +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: `alwaysReferenceByDirectory`, + + compose: false, + + steps: () => [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withPropertyFromAlbum({ + property: input.value('alwaysReferenceTracksByDirectory'), + }), + + // Falsy mode means this exposes true if the album's property is true, + // but continues if the property is false (which is also the default). + exposeDependencyOrContinue({ + dependency: '#album.alwaysReferenceTracksByDirectory', + mode: input.value('falsy'), + }), + + exitWithoutDependency({ + dependency: 'mainRelease', + value: input.value(false), + }), + + withMainReleaseTrack(), + + exitWithoutDependency({ + dependency: '#mainReleaseTrack', + value: input.value(false), + }), + + withPropertyFromObject({ + object: '#mainReleaseTrack', + property: input.value('name'), + }), + + { + dependencies: ['name', '#mainReleaseTrack.name'], + compute: ({ + ['name']: name, + ['#mainReleaseTrack.name']: mainReleaseName, + }) => + getKebabCase(name) === + getKebabCase(mainReleaseName), + }, + ], +}); diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js index beb8c6ab..1c203cd9 100644 --- a/src/data/composite/things/track/index.js +++ b/src/data/composite/things/track/index.js @@ -1,16 +1,18 @@ +export {default as alwaysReferenceByDirectory} from './alwaysReferenceByDirectory.js'; export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.js'; export {default as inheritContributionListFromMainRelease} from './inheritContributionListFromMainRelease.js'; export {default as inheritFromMainRelease} from './inheritFromMainRelease.js'; -export {default as withAlbum} from './withAlbum.js'; export {default as withAllReleases} from './withAllReleases.js'; -export {default as withAlwaysReferenceByDirectory} from './withAlwaysReferenceByDirectory.js'; export {default as withContainingTrackSection} from './withContainingTrackSection.js'; +export {default as withCoverArtistContribs} from './withCoverArtistContribs.js'; export {default as withDate} from './withDate.js'; export {default as withDirectorySuffix} from './withDirectorySuffix.js'; export {default as withHasUniqueCoverArt} from './withHasUniqueCoverArt.js'; export {default as withMainRelease} from './withMainRelease.js'; +export {default as withMainReleaseTrack} from './withMainReleaseTrack.js'; export {default as withOtherReleases} from './withOtherReleases.js'; export {default as withPropertyFromAlbum} from './withPropertyFromAlbum.js'; export {default as withPropertyFromMainRelease} from './withPropertyFromMainRelease.js'; export {default as withSuffixDirectoryFromAlbum} from './withSuffixDirectoryFromAlbum.js'; export {default as withTrackArtDate} from './withTrackArtDate.js'; +export {default as withTrackNumber} from './withTrackNumber.js'; diff --git a/src/data/composite/things/track/trackAdditionalNameList.js b/src/data/composite/things/track/trackAdditionalNameList.js deleted file mode 100644 index 65a2263d..00000000 --- a/src/data/composite/things/track/trackAdditionalNameList.js +++ /dev/null @@ -1,38 +0,0 @@ -// Compiles additional names from various sources. - -import {input, templateCompositeFrom} from '#composite'; -import {isAdditionalNameList} from '#validators'; - -import withInferredAdditionalNames from './withInferredAdditionalNames.js'; -import withSharedAdditionalNames from './withSharedAdditionalNames.js'; - -export default templateCompositeFrom({ - annotation: `trackAdditionalNameList`, - - compose: false, - - update: {validate: isAdditionalNameList}, - - steps: () => [ - withInferredAdditionalNames(), - withSharedAdditionalNames(), - - { - dependencies: [ - '#inferredAdditionalNames', - '#sharedAdditionalNames', - input.updateValue(), - ], - - compute: ({ - ['#inferredAdditionalNames']: inferredAdditionalNames, - ['#sharedAdditionalNames']: sharedAdditionalNames, - [input.updateValue()]: providedAdditionalNames, - }) => [ - ...providedAdditionalNames ?? [], - ...sharedAdditionalNames, - ...inferredAdditionalNames, - ], - }, - ], -}); diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js deleted file mode 100644 index 4c55e1f4..00000000 --- a/src/data/composite/things/track/withAlbum.js +++ /dev/null @@ -1,22 +0,0 @@ -// Gets the track's album. This will early exit if albumData is missing. -// If there's no album whose list of tracks includes this track, the output -// dependency will be null. - -import {templateCompositeFrom} from '#composite'; - -import {withUniqueReferencingThing} from '#composite/wiki-data'; -import {soupyReverse} from '#composite/wiki-properties'; - -export default templateCompositeFrom({ - annotation: `withAlbum`, - - outputs: ['#album'], - - steps: () => [ - withUniqueReferencingThing({ - reverse: soupyReverse.input('albumsWhoseTracksInclude'), - }).outputs({ - ['#uniqueReferencingThing']: '#album', - }), - ], -}); diff --git a/src/data/composite/things/track/withAllReleases.js b/src/data/composite/things/track/withAllReleases.js index b93bf753..bd54384f 100644 --- a/src/data/composite/things/track/withAllReleases.js +++ b/src/data/composite/things/track/withAllReleases.js @@ -8,10 +8,9 @@ import {input, templateCompositeFrom} from '#composite'; import {sortByDate} from '#sort'; -import {exitWithoutDependency} from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; -import withMainRelease from './withMainRelease.js'; +import withMainReleaseTrack from './withMainReleaseTrack.js'; export default templateCompositeFrom({ annotation: `withAllReleases`, @@ -19,7 +18,7 @@ export default templateCompositeFrom({ outputs: ['#allReleases'], steps: () => [ - withMainRelease({ + withMainReleaseTrack({ selfIfMain: input.value(true), notFoundValue: input.value([]), }), @@ -29,18 +28,22 @@ export default templateCompositeFrom({ // `this.secondaryReleases` from within a data composition. // Oooooooooooooooooooooooooooooooooooooooooooooooo withPropertyFromObject({ - object: '#mainRelease', + object: '#mainReleaseTrack', property: input.value('secondaryReleases'), }), { - dependencies: ['#mainRelease', '#mainRelease.secondaryReleases'], + dependencies: [ + '#mainReleaseTrack', + '#mainReleaseTrack.secondaryReleases', + ], + compute: (continuation, { - ['#mainRelease']: mainRelease, - ['#mainRelease.secondaryReleases']: secondaryReleases, + ['#mainReleaseTrack']: mainReleaseTrack, + ['#mainReleaseTrack.secondaryReleases']: secondaryReleases, }) => continuation({ ['#allReleases']: - sortByDate([mainRelease, ...secondaryReleases]), + sortByDate([mainReleaseTrack, ...secondaryReleases]), }), }, ], diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js deleted file mode 100644 index aebcf793..00000000 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ /dev/null @@ -1,106 +0,0 @@ -// Controls how find.track works - it'll never be matched by a reference -// just to the track's name, which means you don't have to always reference -// some *other* (much more commonly referenced) track by directory instead -// of more naturally by name. - -import {input, templateCompositeFrom} from '#composite'; -import find from '#find'; -import {isBoolean} from '#validators'; - -import {withPropertyFromObject} from '#composite/data'; -import {withResolvedReference} from '#composite/wiki-data'; -import {soupyFind} from '#composite/wiki-properties'; - -import { - exitWithoutDependency, - exposeDependencyOrContinue, - exposeUpdateValueOrContinue, -} from '#composite/control-flow'; - -export default templateCompositeFrom({ - annotation: `withAlwaysReferenceByDirectory`, - - outputs: ['#alwaysReferenceByDirectory'], - - steps: () => [ - exposeUpdateValueOrContinue({ - validate: input.value(isBoolean), - }), - - // withAlwaysReferenceByDirectory is sort of a fragile area - we can't - // find the track's album the normal way because albums' track lists - // recurse back into alwaysReferenceByDirectory! - withResolvedReference({ - ref: 'dataSourceAlbum', - find: soupyFind.input('album'), - }).outputs({ - '#resolvedReference': '#album', - }), - - withPropertyFromObject({ - object: '#album', - property: input.value('alwaysReferenceTracksByDirectory'), - }), - - // Falsy mode means this exposes true if the album's property is true, - // but continues if the property is false (which is also the default). - exposeDependencyOrContinue({ - dependency: '#album.alwaysReferenceTracksByDirectory', - mode: input.value('falsy'), - }), - - // Remaining code is for defaulting to true if this track is a rerelease of - // another with the same name, so everything further depends on access to - // trackData as well as mainReleaseTrack. - - exitWithoutDependency({ - dependency: 'trackData', - mode: input.value('empty'), - value: input.value(false), - }), - - exitWithoutDependency({ - dependency: 'mainReleaseTrack', - value: input.value(false), - }), - - // It's necessary to use the custom trackMainReleasesOnly find function - // here, so as to avoid recursion issues - the find.track() function depends - // on accessing each track's alwaysReferenceByDirectory, which means it'll - // hit *this track* - and thus this step - and end up recursing infinitely. - // By definition, find.trackMainReleasesOnly excludes tracks which have - // an mainReleaseTrack update value set, which means even though it does - // still access each of tracks' `alwaysReferenceByDirectory` property, it - // won't access that of *this* track - it will never proceed past the - // `exitWithoutDependency` step directly above, so there's no opportunity - // for recursion. - withResolvedReference({ - ref: 'mainReleaseTrack', - data: 'trackData', - find: input.value(find.trackMainReleasesOnly), - }).outputs({ - '#resolvedReference': '#mainRelease', - }), - - exitWithoutDependency({ - dependency: '#mainRelease', - value: input.value(false), - }), - - withPropertyFromObject({ - object: '#mainRelease', - property: input.value('name'), - }), - - { - dependencies: ['name', '#mainRelease.name'], - compute: (continuation, { - name, - ['#mainRelease.name']: mainReleaseName, - }) => continuation({ - ['#alwaysReferenceByDirectory']: - name === mainReleaseName, - }), - }, - ], -}); diff --git a/src/data/composite/things/track/withCoverArtistContribs.js b/src/data/composite/things/track/withCoverArtistContribs.js new file mode 100644 index 00000000..9057cfeb --- /dev/null +++ b/src/data/composite/things/track/withCoverArtistContribs.js @@ -0,0 +1,73 @@ +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {exposeDependencyOrContinue} from '#composite/control-flow'; + +import { + withRecontextualizedContributionList, + withRedatedContributionList, + withResolvedContribs, +} from '#composite/wiki-data'; + +import exitWithoutUniqueCoverArt from './exitWithoutUniqueCoverArt.js'; +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; +import withTrackArtDate from './withTrackArtDate.js'; + +export default templateCompositeFrom({ + annotation: `withCoverArtistContribs`, + + inputs: { + from: input({ + defaultDependency: 'coverArtistContribs', + validate: isContributionList, + acceptsNull: true, + }), + }, + + outputs: ['#coverArtistContribs'], + + steps: () => [ + exitWithoutUniqueCoverArt({ + value: input.value([]), + }), + + withTrackArtDate(), + + withResolvedContribs({ + from: input('from'), + thingProperty: input.value('coverArtistContribs'), + artistProperty: input.value('trackCoverArtistContributions'), + date: '#trackArtDate', + }).outputs({ + '#resolvedContribs': '#coverArtistContribs', + }), + + exposeDependencyOrContinue({ + dependency: '#coverArtistContribs', + mode: input.value('empty'), + }), + + withPropertyFromAlbum({ + property: input.value('trackCoverArtistContribs'), + }), + + withRecontextualizedContributionList({ + list: '#album.trackCoverArtistContribs', + artistProperty: input.value('trackCoverArtistContributions'), + }), + + withRedatedContributionList({ + list: '#album.trackCoverArtistContribs', + date: '#trackArtDate', + }), + + { + dependencies: ['#album.trackCoverArtistContribs'], + compute: (continuation, { + ['#album.trackCoverArtistContribs']: coverArtistContribs, + }) => continuation({ + ['#coverArtistContribs']: coverArtistContribs, + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withDate.js b/src/data/composite/things/track/withDate.js index b5a770e9..1851c0d2 100644 --- a/src/data/composite/things/track/withDate.js +++ b/src/data/composite/things/track/withDate.js @@ -12,6 +12,14 @@ export default templateCompositeFrom({ steps: () => [ { + dependencies: ['disableDate'], + compute: (continuation, {disableDate}) => + (disableDate + ? continuation.raiseOutput({'#date': null}) + : continuation()), + }, + + { dependencies: ['dateFirstReleased'], compute: (continuation, {dateFirstReleased}) => (dateFirstReleased diff --git a/src/data/composite/things/track/withDirectorySuffix.js b/src/data/composite/things/track/withDirectorySuffix.js index c063e158..c3651491 100644 --- a/src/data/composite/things/track/withDirectorySuffix.js +++ b/src/data/composite/things/track/withDirectorySuffix.js @@ -1,8 +1,9 @@ import {input, templateCompositeFrom} from '#composite'; import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; -import withPropertyFromAlbum from './withPropertyFromAlbum.js'; +import withContainingTrackSection from './withContainingTrackSection.js'; import withSuffixDirectoryFromAlbum from './withSuffixDirectoryFromAlbum.js'; export default templateCompositeFrom({ @@ -16,21 +17,16 @@ export default templateCompositeFrom({ raiseOutputWithoutDependency({ dependency: '#suffixDirectoryFromAlbum', mode: input.value('falsy'), - output: input.value({['#directorySuffix']: null}), + output: input.value({'#directorySuffix': null}), }), - withPropertyFromAlbum({ + withContainingTrackSection(), + + withPropertyFromObject({ + object: '#trackSection', property: input.value('directorySuffix'), + }).outputs({ + '#trackSection.directorySuffix': '#directorySuffix', }), - - { - dependencies: ['#album.directorySuffix'], - compute: (continuation, { - ['#album.directorySuffix']: directorySuffix, - }) => continuation({ - ['#directorySuffix']: - directorySuffix, - }), - }, ], }); diff --git a/src/data/composite/things/track/withHasUniqueCoverArt.js b/src/data/composite/things/track/withHasUniqueCoverArt.js index f7e65f25..85d3b92a 100644 --- a/src/data/composite/things/track/withHasUniqueCoverArt.js +++ b/src/data/composite/things/track/withHasUniqueCoverArt.js @@ -5,11 +5,18 @@ // or a placeholder. (This property is named hasUniqueCoverArt instead of // the usual hasCoverArt to emphasize that it does not inherit from the // album.) +// +// withHasUniqueCoverArt is based only around the presence of *specified* +// cover artist contributions, not whether the references to artists on those +// contributions actually resolve to anything. It completely evades interacting +// with find/replace. import {input, templateCompositeFrom} from '#composite'; -import {empty} from '#sugar'; -import {withResolvedContribs} from '#composite/wiki-data'; +import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {fillMissingListItems, withFlattenedList, withPropertyFromList} + from '#composite/data'; import withPropertyFromAlbum from './withPropertyFromAlbum.js'; @@ -29,36 +36,73 @@ export default templateCompositeFrom({ : continuation()), }, - withResolvedContribs({ + withResultOfAvailabilityCheck({ from: 'coverArtistContribs', - date: input.value(null), + mode: input.value('empty'), }), { - dependencies: ['#resolvedContribs'], + dependencies: ['#availability'], compute: (continuation, { - ['#resolvedContribs']: contribsFromTrack, + ['#availability']: availability, }) => - (empty(contribsFromTrack) - ? continuation() - : continuation.raiseOutput({ + (availability + ? continuation.raiseOutput({ ['#hasUniqueCoverArt']: true, - })), + }) + : continuation()), }, withPropertyFromAlbum({ property: input.value('trackCoverArtistContribs'), + internal: input.value(true), + }), + + withResultOfAvailabilityCheck({ + from: '#album.trackCoverArtistContribs', + mode: input.value('empty'), }), { - dependencies: ['#album.trackCoverArtistContribs'], + dependencies: ['#availability'], compute: (continuation, { - ['#album.trackCoverArtistContribs']: contribsFromAlbum, + ['#availability']: availability, }) => - continuation.raiseOutput({ - ['#hasUniqueCoverArt']: - !empty(contribsFromAlbum), - }), + (availability + ? continuation.raiseOutput({ + ['#hasUniqueCoverArt']: true, + }) + : continuation()), }, + + raiseOutputWithoutDependency({ + dependency: 'trackArtworks', + mode: input.value('empty'), + output: input.value({'#hasUniqueCoverArt': false}), + }), + + withPropertyFromList({ + list: 'trackArtworks', + property: input.value('artistContribs'), + internal: input.value(true), + }), + + // Since we're getting the update value for each artwork's artistContribs, + // it may not be set at all, and in that case won't be exposing as []. + fillMissingListItems({ + list: '#trackArtworks.artistContribs', + fill: input.value([]), + }), + + withFlattenedList({ + list: '#trackArtworks.artistContribs', + }), + + withResultOfAvailabilityCheck({ + from: '#flattenedList', + mode: input.value('empty'), + }).outputs({ + '#availability': '#hasUniqueCoverArt', + }), ], }); diff --git a/src/data/composite/things/track/withMainRelease.js b/src/data/composite/things/track/withMainRelease.js index 3a91edae..67a312ae 100644 --- a/src/data/composite/things/track/withMainRelease.js +++ b/src/data/composite/things/track/withMainRelease.js @@ -1,13 +1,15 @@ -// Just includes the main release of this track as a dependency. -// If this track isn't a secondary release, then it'll provide null, unless -// the {selfIfMain} option is set, in which case it'll provide this track -// itself. This will early exit (with notFoundValue) if the main release -// is specified by reference and that reference doesn't resolve to anything. +// Resolves this track's `mainRelease` reference, using weird-ass atypical +// machinery that operates on soupyFind and does not operate on findMixed, +// let alone a prim and proper standalone find spec. +// +// Raises null only if there is no `mainRelease` reference provided at all. +// This will early exit (with notFoundValue) if the reference doesn't resolve. +// import {input, templateCompositeFrom} from '#composite'; -import {exitWithoutDependency, withResultOfAvailabilityCheck} - from '#composite/control-flow'; +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; import {withResolvedReference} from '#composite/wiki-data'; import {soupyFind} from '#composite/wiki-properties'; @@ -15,56 +17,121 @@ export default templateCompositeFrom({ annotation: `withMainRelease`, inputs: { - selfIfMain: input({type: 'boolean', defaultValue: false}), + from: input({ + defaultDependency: 'mainRelease', + acceptsNull: true, + }), + notFoundValue: input({defaultValue: null}), }, outputs: ['#mainRelease'], steps: () => [ - withResultOfAvailabilityCheck({ - from: 'mainReleaseTrack', + raiseOutputWithoutDependency({ + dependency: input('from'), + output: input.value({'#mainRelease': null}), }), { + dependencies: [input('from'), 'name'], + compute: (continuation, { + [input('from')]: ref, + ['name']: ownName, + }) => + (ref === 'same name single' + ? continuation({ + ['#albumOrTrackReference']: null, + ['#sameNameSingleReference']: ownName, + }) + : continuation({ + ['#albumOrTrackReference']: ref, + ['#sameNameSingleReference']: null, + })), + }, + + withResolvedReference({ + ref: '#albumOrTrackReference', + find: soupyFind.input('trackMainReleasesOnly'), + }).outputs({ + '#resolvedReference': '#matchingTrack', + }), + + withResolvedReference({ + ref: '#albumOrTrackReference', + find: soupyFind.input('album'), + }).outputs({ + '#resolvedReference': '#matchingAlbum', + }), + + withResolvedReference({ + ref: '#sameNameSingleReference', + find: soupyFind.input('albumSinglesOnly'), + findOptions: input.value({ + fuzz: { + capitalization: true, + kebab: true, + }, + }), + }).outputs({ + '#resolvedReference': '#sameNameSingle', + }), + + { + dependencies: ['#sameNameSingle'], + compute: (continuation, { + ['#sameNameSingle']: sameNameSingle, + }) => + (sameNameSingle + ? continuation.raiseOutput({ + ['#mainRelease']: + sameNameSingle, + }) + : continuation()), + }, + + { dependencies: [ - input.myself(), - input('selfIfMain'), - '#availability', + '#matchingTrack', + '#matchingAlbum', + input('notFoundValue'), ], compute: (continuation, { - [input.myself()]: track, - [input('selfIfMain')]: selfIfMain, - '#availability': availability, + ['#matchingTrack']: matchingTrack, + ['#matchingAlbum']: matchingAlbum, + [input('notFoundValue')]: notFoundValue, }) => - (availability + (matchingTrack && matchingAlbum ? continuation() - : continuation.raiseOutput({ + : matchingTrack ?? matchingAlbum + ? continuation.raiseOutput({ ['#mainRelease']: - (selfIfMain ? track : null), - })), + matchingTrack ?? matchingAlbum, + }) + : continuation.exit(notFoundValue)), }, - withResolvedReference({ - ref: 'mainReleaseTrack', - find: soupyFind.input('track'), - }), - - exitWithoutDependency({ - dependency: '#resolvedReference', - value: input('notFoundValue'), + withPropertyFromObject({ + object: '#matchingAlbum', + property: input.value('tracks'), }), { - dependencies: ['#resolvedReference'], + dependencies: [ + '#matchingAlbum.tracks', + '#matchingTrack', + input('notFoundValue'), + ], compute: (continuation, { - ['#resolvedReference']: resolvedReference, + ['#matchingAlbum.tracks']: matchingAlbumTracks, + ['#matchingTrack']: matchingTrack, + [input('notFoundValue')]: notFoundValue, }) => - continuation({ - ['#mainRelease']: resolvedReference, - }), + (matchingAlbumTracks.includes(matchingTrack) + ? continuation.raiseOutput({'#mainRelease': matchingTrack}) + : continuation.exit(notFoundValue)), }, ], }); diff --git a/src/data/composite/things/track/withMainReleaseTrack.js b/src/data/composite/things/track/withMainReleaseTrack.js new file mode 100644 index 00000000..6371e895 --- /dev/null +++ b/src/data/composite/things/track/withMainReleaseTrack.js @@ -0,0 +1,248 @@ +// Just provides the main release of this track as a dependency. +// If this track isn't a secondary release, then it'll provide null, unless +// the {selfIfMain} option is set, in which case it'll provide this track +// itself. This will early exit (with notFoundValue) if the main release +// is specified by reference and that reference doesn't resolve to anything. + +import {input, templateCompositeFrom} from '#composite'; +import {onlyItem} from '#sugar'; +import {getKebabCase} from '#wiki-data'; + +import { + exitWithoutDependency, + withAvailabilityFilter, + withResultOfAvailabilityCheck, +} from '#composite/control-flow'; + +import { + withFilteredList, + withMappedList, + withPropertyFromList, + withPropertyFromObject, +} from '#composite/data'; + +import withMainRelease from './withMainRelease.js'; + +export default templateCompositeFrom({ + annotation: `withMainReleaseTrack`, + + inputs: { + selfIfMain: input({type: 'boolean', defaultValue: false}), + notFoundValue: input({defaultValue: null}), + }, + + outputs: ['#mainReleaseTrack'], + + steps: () => [ + withResultOfAvailabilityCheck({ + from: 'mainRelease', + }), + + { + dependencies: [ + input.myself(), + input('selfIfMain'), + '#availability', + ], + + compute: (continuation, { + [input.myself()]: track, + [input('selfIfMain')]: selfIfMain, + '#availability': availability, + }) => + (availability + ? continuation() + : continuation.raiseOutput({ + ['#mainReleaseTrack']: + (selfIfMain ? track : null), + })), + }, + + withMainRelease(), + + exitWithoutDependency({ + dependency: '#mainRelease', + value: input('notFoundValue'), + }), + + withPropertyFromObject({ + object: '#mainRelease', + property: input.value('isTrack'), + }), + + { + dependencies: ['#mainRelease', '#mainRelease.isTrack'], + + compute: (continuation, { + ['#mainRelease']: mainRelease, + ['#mainRelease.isTrack']: mainReleaseIsTrack, + }) => + (mainReleaseIsTrack + ? continuation.raiseOutput({ + ['#mainReleaseTrack']: mainRelease, + }) + : continuation()), + }, + + { + dependencies: ['name', 'directory'], + compute: (continuation, { + ['name']: ownName, + ['directory']: ownDirectory, + }) => { + const ownNameKebabed = getKebabCase(ownName); + + return continuation({ + ['#mapItsNameLikeName']: + name => getKebabCase(name) === ownNameKebabed, + + ['#mapItsDirectoryLikeDirectory']: + (ownDirectory + ? directory => directory === ownDirectory + : () => false), + + ['#mapItsNameLikeDirectory']: + (ownDirectory + ? name => getKebabCase(name) === ownDirectory + : () => false), + + ['#mapItsDirectoryLikeName']: + directory => directory === ownNameKebabed, + }); + }, + }, + + withPropertyFromObject({ + object: '#mainRelease', + property: input.value('tracks'), + }), + + withPropertyFromList({ + list: '#mainRelease.tracks', + property: input.value('mainRelease'), + internal: input.value(true), + }), + + withAvailabilityFilter({ + from: '#mainRelease.tracks.mainRelease', + }), + + withMappedList({ + list: '#availabilityFilter', + map: input.value(item => !item), + }).outputs({ + '#mappedList': '#availabilityFilter', + }), + + withFilteredList({ + list: '#mainRelease.tracks', + filter: '#availabilityFilter', + }).outputs({ + '#filteredList': '#mainRelease.tracks', + }), + + withPropertyFromList({ + list: '#mainRelease.tracks', + property: input.value('name'), + }), + + withPropertyFromList({ + list: '#mainRelease.tracks', + property: input.value('directory'), + internal: input.value(true), + }), + + withMappedList({ + list: '#mainRelease.tracks.name', + map: '#mapItsNameLikeName', + }).outputs({ + '#mappedList': '#filterItsNameLikeName', + }), + + withMappedList({ + list: '#mainRelease.tracks.directory', + map: '#mapItsDirectoryLikeDirectory', + }).outputs({ + '#mappedList': '#filterItsDirectoryLikeDirectory', + }), + + withMappedList({ + list: '#mainRelease.tracks.name', + map: '#mapItsNameLikeDirectory', + }).outputs({ + '#mappedList': '#filterItsNameLikeDirectory', + }), + + withMappedList({ + list: '#mainRelease.tracks.directory', + map: '#mapItsDirectoryLikeName', + }).outputs({ + '#mappedList': '#filterItsDirectoryLikeName', + }), + + withFilteredList({ + list: '#mainRelease.tracks', + filter: '#filterItsNameLikeName', + }).outputs({ + '#filteredList': '#matchingItsNameLikeName', + }), + + withFilteredList({ + list: '#mainRelease.tracks', + filter: '#filterItsDirectoryLikeDirectory', + }).outputs({ + '#filteredList': '#matchingItsDirectoryLikeDirectory', + }), + + withFilteredList({ + list: '#mainRelease.tracks', + filter: '#filterItsNameLikeDirectory', + }).outputs({ + '#filteredList': '#matchingItsNameLikeDirectory', + }), + + withFilteredList({ + list: '#mainRelease.tracks', + filter: '#filterItsDirectoryLikeName', + }).outputs({ + '#filteredList': '#matchingItsDirectoryLikeName', + }), + + { + dependencies: [ + '#matchingItsNameLikeName', + '#matchingItsDirectoryLikeDirectory', + '#matchingItsNameLikeDirectory', + '#matchingItsDirectoryLikeName', + ], + + compute: (continuation, { + ['#matchingItsNameLikeName']: NLN, + ['#matchingItsDirectoryLikeDirectory']: DLD, + ['#matchingItsNameLikeDirectory']: NLD, + ['#matchingItsDirectoryLikeName']: DLN, + }) => continuation({ + ['#mainReleaseTrack']: + onlyItem(DLD) ?? + onlyItem(NLN) ?? + onlyItem(DLN) ?? + onlyItem(NLD) ?? + null, + }), + }, + + { + dependencies: ['#mainReleaseTrack', input.myself()], + + compute: (continuation, { + ['#mainReleaseTrack']: mainReleaseTrack, + [input.myself()]: thisTrack, + }) => continuation({ + ['#mainReleaseTrack']: + (mainReleaseTrack === thisTrack + ? null + : mainReleaseTrack), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withOtherReleases.js b/src/data/composite/things/track/withOtherReleases.js index 0639742f..bb3e8983 100644 --- a/src/data/composite/things/track/withOtherReleases.js +++ b/src/data/composite/things/track/withOtherReleases.js @@ -3,9 +3,6 @@ import {input, templateCompositeFrom} from '#composite'; -import {exitWithoutDependency} from '#composite/control-flow'; -import {withPropertyFromObject} from '#composite/data'; - import withAllReleases from './withAllReleases.js'; export default templateCompositeFrom({ diff --git a/src/data/composite/things/track/withPropertyFromAlbum.js b/src/data/composite/things/track/withPropertyFromAlbum.js index e9c5b56e..a203c2e7 100644 --- a/src/data/composite/things/track/withPropertyFromAlbum.js +++ b/src/data/composite/things/track/withPropertyFromAlbum.js @@ -5,13 +5,12 @@ import {input, templateCompositeFrom} from '#composite'; import {withPropertyFromObject} from '#composite/data'; -import withAlbum from './withAlbum.js'; - export default templateCompositeFrom({ annotation: `withPropertyFromAlbum`, inputs: { property: input.staticValue({type: 'string'}), + internal: input({type: 'boolean', defaultValue: false}), }, outputs: ({ @@ -19,11 +18,21 @@ export default templateCompositeFrom({ }) => ['#album.' + property], steps: () => [ - withAlbum(), + // XXX: This is a ridiculous hack considering `defaultValue` above. + // If we were certain what was up, we'd just get around to fixing it LOL + { + dependencies: [input('internal')], + compute: (continuation, { + [input('internal')]: internal, + }) => continuation({ + ['#internal']: internal ?? false, + }), + }, withPropertyFromObject({ - object: '#album', + object: 'album', property: input('property'), + internal: '#internal', }), { diff --git a/src/data/composite/things/track/withPropertyFromMainRelease.js b/src/data/composite/things/track/withPropertyFromMainRelease.js index 393a4c63..c6f65653 100644 --- a/src/data/composite/things/track/withPropertyFromMainRelease.js +++ b/src/data/composite/things/track/withPropertyFromMainRelease.js @@ -10,10 +10,10 @@ import {input, templateCompositeFrom} from '#composite'; import {withResultOfAvailabilityCheck} from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; -import withMainRelease from './withMainRelease.js'; +import withMainReleaseTrack from './withMainReleaseTrack.js'; export default templateCompositeFrom({ - annotation: `inheritFromMainRelease`, + annotation: `withPropertyFromMainRelease`, inputs: { property: input({type: 'string'}), @@ -32,12 +32,12 @@ export default templateCompositeFrom({ : ['#mainReleaseValue'])), steps: () => [ - withMainRelease({ + withMainReleaseTrack({ notFoundValue: input('notFoundValue'), }), withResultOfAvailabilityCheck({ - from: '#mainRelease', + from: '#mainReleaseTrack', }), { @@ -61,7 +61,7 @@ export default templateCompositeFrom({ }, withPropertyFromObject({ - object: '#mainRelease', + object: '#mainReleaseTrack', property: input('property'), }), diff --git a/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js index 7159a3f4..30c777b6 100644 --- a/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js +++ b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js @@ -1,8 +1,9 @@ import {input, templateCompositeFrom} from '#composite'; import {withResultOfAvailabilityCheck} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; -import withPropertyFromAlbum from './withPropertyFromAlbum.js'; +import withContainingTrackSection from './withContainingTrackSection.js'; export default templateCompositeFrom({ annotation: `withSuffixDirectoryFromAlbum`, @@ -36,18 +37,13 @@ export default templateCompositeFrom({ : continuation()), }, - withPropertyFromAlbum({ + withContainingTrackSection(), + + withPropertyFromObject({ + object: '#trackSection', property: input.value('suffixTrackDirectories'), + }).outputs({ + '#trackSection.suffixTrackDirectories': '#suffixDirectoryFromAlbum', }), - - { - dependencies: ['#album.suffixTrackDirectories'], - compute: (continuation, { - ['#album.suffixTrackDirectories']: suffixTrackDirectories, - }) => continuation({ - ['#suffixDirectoryFromAlbum']: - suffixTrackDirectories, - }), - }, ], }); diff --git a/src/data/composite/things/track/withTrackArtDate.js b/src/data/composite/things/track/withTrackArtDate.js index e2c4d8bc..9b7b61c7 100644 --- a/src/data/composite/things/track/withTrackArtDate.js +++ b/src/data/composite/things/track/withTrackArtDate.js @@ -1,11 +1,3 @@ -// Gets the date of cover art release. This represents only the track's own -// unique cover artwork, if any. -// -// If the 'fallback' option is false (the default), this will only output -// the track's own coverArtDate or its album's trackArtDate. If 'fallback' -// is set, and neither of these is available, it'll output the track's own -// date instead. - import {input, templateCompositeFrom} from '#composite'; import {isDate} from '#validators'; @@ -24,11 +16,6 @@ export default templateCompositeFrom({ defaultDependency: 'coverArtDate', acceptsNull: true, }), - - fallback: input({ - type: 'boolean', - defaultValue: false, - }), }, outputs: ['#trackArtDate'], @@ -57,20 +44,13 @@ export default templateCompositeFrom({ }), { - dependencies: [ - '#album.trackArtDate', - input('fallback'), - ], - + dependencies: ['#album.trackArtDate'], compute: (continuation, { ['#album.trackArtDate']: albumTrackArtDate, - [input('fallback')]: fallback, }) => (albumTrackArtDate ? continuation.raiseOutput({'#trackArtDate': albumTrackArtDate}) - : fallback - ? continuation() - : continuation.raiseOutput({'#trackArtDate': null})), + : continuation()), }, withDate().outputs({ diff --git a/src/data/composite/things/track/withTrackNumber.js b/src/data/composite/things/track/withTrackNumber.js new file mode 100644 index 00000000..61428e8c --- /dev/null +++ b/src/data/composite/things/track/withTrackNumber.js @@ -0,0 +1,50 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withIndexInList, withPropertiesFromObject} from '#composite/data'; + +import withContainingTrackSection from './withContainingTrackSection.js'; + +export default templateCompositeFrom({ + annotation: `withTrackNumber`, + + outputs: ['#trackNumber'], + + steps: () => [ + withContainingTrackSection(), + + // Zero is the fallback, not one, but in most albums the first track + // (and its intended output by this composition) will be one. + raiseOutputWithoutDependency({ + dependency: '#trackSection', + output: input.value({'#trackNumber': 0}), + }), + + withPropertiesFromObject({ + object: '#trackSection', + properties: input.value(['tracks', 'startCountingFrom']), + }), + + withIndexInList({ + list: '#trackSection.tracks', + item: input.myself(), + }), + + raiseOutputWithoutDependency({ + dependency: '#index', + output: input.value({'#trackNumber': 0}), + }), + + { + dependencies: ['#trackSection.startCountingFrom', '#index'], + compute: (continuation, { + ['#trackSection.startCountingFrom']: startCountingFrom, + ['#index']: index, + }) => continuation({ + ['#trackNumber']: + startCountingFrom + + index, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/exitWithoutArtwork.js b/src/data/composite/wiki-data/exitWithoutArtwork.js new file mode 100644 index 00000000..8e799fda --- /dev/null +++ b/src/data/composite/wiki-data/exitWithoutArtwork.js @@ -0,0 +1,45 @@ +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList, isThing, strictArrayOf} from '#validators'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +import withHasArtwork from './withHasArtwork.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutArtwork`, + + inputs: { + contribs: input({ + validate: isContributionList, + defaultValue: null, + }), + + artwork: input({ + validate: isThing, + defaultValue: null, + }), + + artworks: input({ + validate: strictArrayOf(isThing), + defaultValue: null, + }), + + value: input({ + defaultValue: null, + }), + }, + + steps: () => [ + withHasArtwork({ + contribs: input('contribs'), + artwork: input('artwork'), + artworks: input('artworks'), + }), + + exitWithoutDependency({ + dependency: '#hasArtwork', + mode: input.value('falsy'), + value: input('value'), + }), + ], +}); diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js index be83e4c9..d70d7c56 100644 --- a/src/data/composite/wiki-data/index.js +++ b/src/data/composite/wiki-data/index.js @@ -5,24 +5,27 @@ // export {default as exitWithoutContribs} from './exitWithoutContribs.js'; +export {default as exitWithoutArtwork} from './exitWithoutArtwork.js'; export {default as gobbleSoupyFind} from './gobbleSoupyFind.js'; export {default as gobbleSoupyReverse} from './gobbleSoupyReverse.js'; +export {default as inputFindOptions} from './inputFindOptions.js'; export {default as inputNotFoundMode} from './inputNotFoundMode.js'; export {default as inputSoupyFind} from './inputSoupyFind.js'; export {default as inputSoupyReverse} from './inputSoupyReverse.js'; export {default as inputWikiData} from './inputWikiData.js'; +export {default as splitContentNodesAround} from './splitContentNodesAround.js'; export {default as withClonedThings} from './withClonedThings.js'; +export {default as withConstitutedArtwork} from './withConstitutedArtwork.js'; +export {default as withContentNodes} from './withContentNodes.js'; export {default as withContributionListSums} from './withContributionListSums.js'; -export {default as withCoverArtDate} from './withCoverArtDate.js'; export {default as withDirectory} from './withDirectory.js'; -export {default as withParsedCommentaryEntries} from './withParsedCommentaryEntries.js'; +export {default as withHasArtwork} from './withHasArtwork.js'; export {default as withRecontextualizedContributionList} from './withRecontextualizedContributionList.js'; export {default as withRedatedContributionList} from './withRedatedContributionList.js'; export {default as withResolvedAnnotatedReferenceList} from './withResolvedAnnotatedReferenceList.js'; export {default as withResolvedContribs} from './withResolvedContribs.js'; export {default as withResolvedReference} from './withResolvedReference.js'; export {default as withResolvedReferenceList} from './withResolvedReferenceList.js'; -export {default as withResolvedSeriesList} from './withResolvedSeriesList.js'; export {default as withReverseReferenceList} from './withReverseReferenceList.js'; export {default as withThingsSortedAlphabetically} from './withThingsSortedAlphabetically.js'; export {default as withUniqueReferencingThing} from './withUniqueReferencingThing.js'; diff --git a/src/data/composite/wiki-data/inputFindOptions.js b/src/data/composite/wiki-data/inputFindOptions.js new file mode 100644 index 00000000..07ed4bce --- /dev/null +++ b/src/data/composite/wiki-data/inputFindOptions.js @@ -0,0 +1,5 @@ +import {input} from '#composite'; + +export default function inputFindOptions() { + return input({type: 'object', defaultValue: null}); +} diff --git a/src/data/composite/wiki-data/splitContentNodesAround.js b/src/data/composite/wiki-data/splitContentNodesAround.js new file mode 100644 index 00000000..6648d8e1 --- /dev/null +++ b/src/data/composite/wiki-data/splitContentNodesAround.js @@ -0,0 +1,87 @@ +import {input, templateCompositeFrom} from '#composite'; +import {splitContentNodesAround} from '#replacer'; +import {anyOf, isFunction, validateInstanceOf} from '#validators'; + +import {withFilteredList, withMappedList, withUnflattenedList} + from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `splitContentNodesAround`, + + inputs: { + nodes: input({type: 'array'}), + + around: input({ + validate: + anyOf(isFunction, validateInstanceOf(RegExp)), + }), + }, + + outputs: ['#contentNodeLists'], + + steps: () => [ + { + dependencies: [input('nodes'), input('around')], + + compute: (continuation, { + [input('nodes')]: nodes, + [input('around')]: splitter, + }) => continuation({ + ['#nodes']: + Array.from(splitContentNodesAround(nodes, splitter)), + }), + }, + + withMappedList({ + list: '#nodes', + map: input.value(node => node.type === 'separator'), + }).outputs({ + '#mappedList': '#separatorFilter', + }), + + withMappedList({ + list: '#separatorFilter', + filter: '#separatorFilter', + map: input.value((_node, index) => index), + }), + + withFilteredList({ + list: '#mappedList', + filter: '#separatorFilter', + }).outputs({ + '#filteredList': '#separatorIndices', + }), + + { + dependencies: ['#nodes', '#separatorFilter'], + + compute: (continuation, { + ['#nodes']: nodes, + ['#separatorFilter']: separatorFilter, + }) => continuation({ + ['#nodes']: + nodes.map((node, index) => + (separatorFilter[index] + ? null + : node)), + }), + }, + + { + dependencies: ['#separatorIndices'], + compute: (continuation, { + ['#separatorIndices']: separatorIndices, + }) => continuation({ + ['#unflattenIndices']: + [0, ...separatorIndices], + }), + }, + + withUnflattenedList({ + list: '#nodes', + indices: '#unflattenIndices', + }).outputs({ + '#unflattenedList': '#contentNodeLists', + }), + ], +}); diff --git a/src/data/composite/wiki-data/withConstitutedArtwork.js b/src/data/composite/wiki-data/withConstitutedArtwork.js new file mode 100644 index 00000000..28d719e2 --- /dev/null +++ b/src/data/composite/wiki-data/withConstitutedArtwork.js @@ -0,0 +1,60 @@ +import {input, templateCompositeFrom} from '#composite'; +import thingConstructors from '#things'; + +export default templateCompositeFrom({ + annotation: `withConstitutedArtwork`, + + inputs: { + thingProperty: input({type: 'string', acceptsNull: true}), + dimensionsFromThingProperty: input({type: 'string', acceptsNull: true}), + fileExtensionFromThingProperty: input({type: 'string', acceptsNull: true}), + dateFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsArtistProperty: input({type: 'string', acceptsNull: true}), + artTagsFromThingProperty: input({type: 'string', acceptsNull: true}), + referencedArtworksFromThingProperty: input({type: 'string', acceptsNull: true}), + }, + + outputs: ['#constitutedArtwork'], + + steps: () => [ + { + dependencies: [ + input.myself(), + input('thingProperty'), + input('dimensionsFromThingProperty'), + input('fileExtensionFromThingProperty'), + input('dateFromThingProperty'), + input('artistContribsFromThingProperty'), + input('artistContribsArtistProperty'), + input('artTagsFromThingProperty'), + input('referencedArtworksFromThingProperty'), + ], + + compute: (continuation, { + [input.myself()]: myself, + [input('thingProperty')]: thingProperty, + [input('dimensionsFromThingProperty')]: dimensionsFromThingProperty, + [input('fileExtensionFromThingProperty')]: fileExtensionFromThingProperty, + [input('dateFromThingProperty')]: dateFromThingProperty, + [input('artistContribsFromThingProperty')]: artistContribsFromThingProperty, + [input('artistContribsArtistProperty')]: artistContribsArtistProperty, + [input('artTagsFromThingProperty')]: artTagsFromThingProperty, + [input('referencedArtworksFromThingProperty')]: referencedArtworksFromThingProperty, + }) => continuation({ + ['#constitutedArtwork']: + Object.assign(new thingConstructors.Artwork, { + thing: myself, + thingProperty, + dimensionsFromThingProperty, + fileExtensionFromThingProperty, + artistContribsFromThingProperty, + artistContribsArtistProperty, + artTagsFromThingProperty, + dateFromThingProperty, + referencedArtworksFromThingProperty, + }), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withContentNodes.js b/src/data/composite/wiki-data/withContentNodes.js new file mode 100644 index 00000000..d014d43b --- /dev/null +++ b/src/data/composite/wiki-data/withContentNodes.js @@ -0,0 +1,25 @@ +import {input, templateCompositeFrom} from '#composite'; +import {parseContentNodes} from '#replacer'; + +export default templateCompositeFrom({ + annotation: `withContentNodes`, + + inputs: { + from: input({type: 'string', acceptsNull: false}), + }, + + outputs: ['#contentNodes'], + + steps: () => [ + { + dependencies: [input('from')], + + compute: (continuation, { + [input('from')]: string, + }) => continuation({ + ['#contentNodes']: + parseContentNodes(string), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withCoverArtDate.js b/src/data/composite/wiki-data/withCoverArtDate.js deleted file mode 100644 index 0c644c77..00000000 --- a/src/data/composite/wiki-data/withCoverArtDate.js +++ /dev/null @@ -1,70 +0,0 @@ -// Gets the current thing's coverArtDate, or, if the 'fallback' option is set, -// the thing's date. This is always null if the thing doesn't actually have -// any coverArtistContribs. - -import {input, templateCompositeFrom} from '#composite'; -import {isDate} from '#validators'; - -import {raiseOutputWithoutDependency} from '#composite/control-flow'; - -import withResolvedContribs from './withResolvedContribs.js'; - -export default templateCompositeFrom({ - annotation: `withCoverArtDate`, - - inputs: { - from: input({ - validate: isDate, - defaultDependency: 'coverArtDate', - acceptsNull: true, - }), - - fallback: input({ - type: 'boolean', - defaultValue: false, - }), - }, - - outputs: ['#coverArtDate'], - - steps: () => [ - withResolvedContribs({ - from: 'coverArtistContribs', - date: input.value(null), - }), - - raiseOutputWithoutDependency({ - dependency: '#resolvedContribs', - mode: input.value('empty'), - output: input.value({'#coverArtDate': null}), - }), - - { - dependencies: [input('from')], - compute: (continuation, { - [input('from')]: from, - }) => - (from - ? continuation.raiseOutput({'#coverArtDate': from}) - : continuation()), - }, - - { - dependencies: [input('fallback')], - compute: (continuation, { - [input('fallback')]: fallback, - }) => - (fallback - ? continuation() - : continuation.raiseOutput({'#coverArtDate': null})), - }, - - { - dependencies: ['date'], - compute: (continuation, {date}) => - (date - ? continuation.raiseOutput({'#coverArtDate': date}) - : continuation.raiseOutput({'#coverArtDate': null})), - }, - ], -}); diff --git a/src/data/composite/wiki-data/withHasArtwork.js b/src/data/composite/wiki-data/withHasArtwork.js new file mode 100644 index 00000000..9c22f439 --- /dev/null +++ b/src/data/composite/wiki-data/withHasArtwork.js @@ -0,0 +1,97 @@ +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList, isThing, strictArrayOf} from '#validators'; + +import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {fillMissingListItems, withFlattenedList, withPropertyFromList} + from '#composite/data'; + +export default templateCompositeFrom({ + annotation: 'withHasArtwork', + + inputs: { + contribs: input({ + validate: isContributionList, + defaultValue: null, + }), + + artwork: input({ + validate: isThing, + defaultValue: null, + }), + + artworks: input({ + validate: strictArrayOf(isThing), + defaultValue: null, + }), + }, + + outputs: ['#hasArtwork'], + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('contribs'), + mode: input.value('empty'), + }), + + { + dependencies: ['#availability'], + compute: (continuation, { + ['#availability']: availability, + }) => + (availability + ? continuation.raiseOutput({ + ['#hasArtwork']: true, + }) + : continuation()), + }, + + { + dependencies: [input('artwork'), input('artworks')], + compute: (continuation, { + [input('artwork')]: artwork, + [input('artworks')]: artworks, + }) => + continuation({ + ['#artworks']: + (artwork && artworks + ? [artwork, ...artworks] + : artwork + ? [artwork] + : artworks + ? artworks + : []), + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#artworks', + mode: input.value('empty'), + output: input.value({'#hasArtwork': false}), + }), + + withPropertyFromList({ + list: '#artworks', + property: input.value('artistContribs'), + internal: input.value(true), + }), + + // Since we're getting the update value for each artwork's artistContribs, + // it may not be set at all, and in that case won't be exposing as []. + fillMissingListItems({ + list: '#artworks.artistContribs', + fill: input.value([]), + }), + + withFlattenedList({ + list: '#artworks.artistContribs', + }), + + withResultOfAvailabilityCheck({ + from: '#flattenedList', + mode: input.value('empty'), + }).outputs({ + '#availability': '#hasArtwork', + }), + ], +}); diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js deleted file mode 100644 index 9bf4278c..00000000 --- a/src/data/composite/wiki-data/withParsedCommentaryEntries.js +++ /dev/null @@ -1,260 +0,0 @@ -import {input, templateCompositeFrom} from '#composite'; -import {stitchArrays} from '#sugar'; -import {isCommentary} from '#validators'; -import {commentaryRegexCaseSensitive} from '#wiki-data'; - -import { - fillMissingListItems, - withFlattenedList, - withPropertiesFromList, - withUnflattenedList, -} from '#composite/data'; - -import inputSoupyFind from './inputSoupyFind.js'; -import withResolvedReferenceList from './withResolvedReferenceList.js'; - -export default templateCompositeFrom({ - annotation: `withParsedCommentaryEntries`, - - inputs: { - from: input({validate: isCommentary}), - }, - - outputs: ['#parsedCommentaryEntries'], - - steps: () => [ - { - dependencies: [input('from')], - - compute: (continuation, { - [input('from')]: commentaryText, - }) => continuation({ - ['#rawMatches']: - Array.from(commentaryText.matchAll(commentaryRegexCaseSensitive)), - }), - }, - - withPropertiesFromList({ - list: '#rawMatches', - properties: input.value([ - '0', // The entire match as a string. - 'groups', - 'index', - ]), - }).outputs({ - '#rawMatches.0': '#rawMatches.text', - '#rawMatches.groups': '#rawMatches.groups', - '#rawMatches.index': '#rawMatches.startIndex', - }), - - { - dependencies: [ - '#rawMatches.text', - '#rawMatches.startIndex', - ], - - compute: (continuation, { - ['#rawMatches.text']: text, - ['#rawMatches.startIndex']: startIndex, - }) => continuation({ - ['#rawMatches.endIndex']: - stitchArrays({text, startIndex}) - .map(({text, startIndex}) => startIndex + text.length), - }), - }, - - { - dependencies: [ - input('from'), - '#rawMatches.startIndex', - '#rawMatches.endIndex', - ], - - compute: (continuation, { - [input('from')]: commentaryText, - ['#rawMatches.startIndex']: startIndex, - ['#rawMatches.endIndex']: endIndex, - }) => continuation({ - ['#entries.body']: - stitchArrays({startIndex, endIndex}) - .map(({endIndex}, index, stitched) => - (index === stitched.length - 1 - ? commentaryText.slice(endIndex) - : commentaryText.slice( - endIndex, - stitched[index + 1].startIndex))) - .map(body => body.trim()), - }), - }, - - withPropertiesFromList({ - list: '#rawMatches.groups', - prefix: input.value('#entries'), - properties: input.value([ - 'artistReferences', - 'artistDisplayText', - 'annotation', - 'date', - 'secondDate', - 'dateKind', - 'accessDate', - 'accessKind', - ]), - }), - - // The artistReferences group will always have a value, since it's required - // for the line to match in the first place. - - { - dependencies: ['#entries.artistReferences'], - compute: (continuation, { - ['#entries.artistReferences']: artistReferenceTexts, - }) => continuation({ - ['#entries.artistReferences']: - artistReferenceTexts - .map(text => text.split(',').map(ref => ref.trim())), - }), - }, - - withFlattenedList({ - list: '#entries.artistReferences', - }), - - withResolvedReferenceList({ - list: '#flattenedList', - find: inputSoupyFind.input('artist'), - notFoundMode: input.value('null'), - }), - - withUnflattenedList({ - list: '#resolvedReferenceList', - }).outputs({ - '#unflattenedList': '#entries.artists', - }), - - fillMissingListItems({ - list: '#entries.artistDisplayText', - fill: input.value(null), - }), - - fillMissingListItems({ - list: '#entries.annotation', - fill: input.value(null), - }), - - { - dependencies: ['#entries.annotation'], - compute: (continuation, { - ['#entries.annotation']: annotation, - }) => continuation({ - ['#entries.webArchiveDate']: - annotation - .map(text => text?.match(/https?:\/\/web.archive.org\/web\/([0-9]{8,8})[0-9]*\//)) - .map(match => match?.[1]) - .map(dateText => - (dateText - ? dateText.slice(0, 4) + '/' + - dateText.slice(4, 6) + '/' + - dateText.slice(6, 8) - : null)), - }), - }, - - { - dependencies: ['#entries.date'], - compute: (continuation, { - ['#entries.date']: date, - }) => continuation({ - ['#entries.date']: - date - .map(date => date ? new Date(date) : null), - }), - }, - - { - dependencies: ['#entries.secondDate'], - compute: (continuation, { - ['#entries.secondDate']: secondDate, - }) => continuation({ - ['#entries.secondDate']: - secondDate - .map(date => date ? new Date(date) : null), - }), - }, - - fillMissingListItems({ - list: '#entries.dateKind', - fill: input.value(null), - }), - - { - dependencies: ['#entries.accessDate', '#entries.webArchiveDate'], - compute: (continuation, { - ['#entries.accessDate']: accessDate, - ['#entries.webArchiveDate']: webArchiveDate, - }) => continuation({ - ['#entries.accessDate']: - stitchArrays({accessDate, webArchiveDate}) - .map(({accessDate, webArchiveDate}) => - accessDate ?? - webArchiveDate ?? - null) - .map(date => date ? new Date(date) : date), - }), - }, - - { - dependencies: ['#entries.accessKind', '#entries.webArchiveDate'], - compute: (continuation, { - ['#entries.accessKind']: accessKind, - ['#entries.webArchiveDate']: webArchiveDate, - }) => continuation({ - ['#entries.accessKind']: - stitchArrays({accessKind, webArchiveDate}) - .map(({accessKind, webArchiveDate}) => - accessKind ?? - (webArchiveDate && 'captured') ?? - null), - }), - }, - - { - dependencies: [ - '#entries.artists', - '#entries.artistDisplayText', - '#entries.annotation', - '#entries.date', - '#entries.secondDate', - '#entries.dateKind', - '#entries.accessDate', - '#entries.accessKind', - '#entries.body', - ], - - compute: (continuation, { - ['#entries.artists']: artists, - ['#entries.artistDisplayText']: artistDisplayText, - ['#entries.annotation']: annotation, - ['#entries.date']: date, - ['#entries.secondDate']: secondDate, - ['#entries.dateKind']: dateKind, - ['#entries.accessDate']: accessDate, - ['#entries.accessKind']: accessKind, - ['#entries.body']: body, - }) => continuation({ - ['#parsedCommentaryEntries']: - stitchArrays({ - artists, - artistDisplayText, - annotation, - date, - secondDate, - dateKind, - accessDate, - accessKind, - body, - }), - }), - }, - ], -}); diff --git a/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js b/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js index c9a7c058..670dc422 100644 --- a/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js +++ b/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js @@ -1,12 +1,13 @@ import {input, templateCompositeFrom} from '#composite'; import {stitchArrays} from '#sugar'; -import {isDate, isObject, validateArrayItems} from '#validators'; +import {isObject, validateArrayItems} from '#validators'; import {withPropertyFromList} from '#composite/data'; import {raiseOutputWithoutDependency, withAvailabilityFilter} from '#composite/control-flow'; +import inputFindOptions from './inputFindOptions.js'; import inputSoupyFind from './inputSoupyFind.js'; import inputNotFoundMode from './inputNotFoundMode.js'; import inputWikiData from './inputWikiData.js'; @@ -22,17 +23,13 @@ export default templateCompositeFrom({ acceptsNull: true, }), - date: input({ - validate: isDate, - acceptsNull: true, - }), - reference: input({type: 'string', defaultValue: 'reference'}), annotation: input({type: 'string', defaultValue: 'annotation'}), thing: input({type: 'string', defaultValue: 'thing'}), data: inputWikiData({allowMixedTypes: true}), find: inputSoupyFind(), + findOptions: inputFindOptions(), notFoundMode: inputNotFoundMode(), }, @@ -66,6 +63,7 @@ export default templateCompositeFrom({ list: '#references', data: input('data'), find: input('find'), + findOptions: input('findOptions'), notFoundMode: input.value('null'), }), @@ -91,17 +89,6 @@ export default templateCompositeFrom({ }), }, - { - dependencies: ['#matches', input('date')], - compute: (continuation, { - ['#matches']: matches, - [input('date')]: date, - }) => continuation({ - ['#matches']: - matches.map(match => ({...match, date})), - }), - }, - withAvailabilityFilter({ from: '#resolvedReferenceList', }), diff --git a/src/data/composite/wiki-data/withResolvedReference.js b/src/data/composite/wiki-data/withResolvedReference.js index 6f422194..d9a05367 100644 --- a/src/data/composite/wiki-data/withResolvedReference.js +++ b/src/data/composite/wiki-data/withResolvedReference.js @@ -8,6 +8,7 @@ import {input, templateCompositeFrom} from '#composite'; import {raiseOutputWithoutDependency} from '#composite/control-flow'; import gobbleSoupyFind from './gobbleSoupyFind.js'; +import inputFindOptions from './inputFindOptions.js'; import inputSoupyFind from './inputSoupyFind.js'; import inputWikiData from './inputWikiData.js'; @@ -17,8 +18,9 @@ export default templateCompositeFrom({ inputs: { ref: input({type: 'string', acceptsNull: true}), - data: inputWikiData({allowMixedTypes: false}), + data: inputWikiData({allowMixedTypes: true}), find: inputSoupyFind(), + findOptions: inputFindOptions(), }, outputs: ['#resolvedReference'], @@ -36,21 +38,35 @@ export default templateCompositeFrom({ }), { + dependencies: [input('findOptions')], + compute: (continuation, { + [input('findOptions')]: findOptions, + }) => continuation({ + ['#findOptions']: + (findOptions + ? {...findOptions, mode: 'quiet'} + : {mode: 'quiet'}), + }), + }, + + { dependencies: [ input('ref'), input('data'), '#find', + '#findOptions', ], compute: (continuation, { [input('ref')]: ref, [input('data')]: data, ['#find']: findFunction, + ['#findOptions']: findOptions, }) => continuation({ ['#resolvedReference']: (data - ? findFunction(ref, data, {mode: 'quiet'}) ?? null - : findFunction(ref, {mode: 'quiet'}) ?? null), + ? findFunction(ref, data, findOptions) ?? null + : findFunction(ref, findOptions) ?? null), }), }, ], diff --git a/src/data/composite/wiki-data/withResolvedReferenceList.js b/src/data/composite/wiki-data/withResolvedReferenceList.js index 9dc960dd..14ce6919 100644 --- a/src/data/composite/wiki-data/withResolvedReferenceList.js +++ b/src/data/composite/wiki-data/withResolvedReferenceList.js @@ -11,6 +11,7 @@ import {raiseOutputWithoutDependency, withAvailabilityFilter} import {withMappedList} from '#composite/data'; import gobbleSoupyFind from './gobbleSoupyFind.js'; +import inputFindOptions from './inputFindOptions.js'; import inputNotFoundMode from './inputNotFoundMode.js'; import inputSoupyFind from './inputSoupyFind.js'; import inputWikiData from './inputWikiData.js'; @@ -27,6 +28,7 @@ export default templateCompositeFrom({ data: inputWikiData({allowMixedTypes: true}), find: inputSoupyFind(), + findOptions: inputFindOptions(), notFoundMode: inputNotFoundMode(), }, @@ -47,15 +49,28 @@ export default templateCompositeFrom({ }), { - dependencies: [input('data'), '#find'], + dependencies: [input('findOptions')], + compute: (continuation, { + [input('findOptions')]: findOptions, + }) => continuation({ + ['#findOptions']: + (findOptions + ? {...findOptions, mode: 'quiet'} + : {mode: 'quiet'}), + }), + }, + + { + dependencies: [input('data'), '#find', '#findOptions'], compute: (continuation, { [input('data')]: data, ['#find']: findFunction, + ['#findOptions']: findOptions, }) => continuation({ ['#map']: (data - ? ref => findFunction(ref, data, {mode: 'quiet'}) - : ref => findFunction(ref, {mode: 'quiet'})), + ? ref => findFunction(ref, data, findOptions) + : ref => findFunction(ref, findOptions)), }), }, diff --git a/src/data/composite/wiki-data/withResolvedSeriesList.js b/src/data/composite/wiki-data/withResolvedSeriesList.js deleted file mode 100644 index deaab466..00000000 --- a/src/data/composite/wiki-data/withResolvedSeriesList.js +++ /dev/null @@ -1,130 +0,0 @@ -import {input, templateCompositeFrom} from '#composite'; -import {stitchArrays} from '#sugar'; -import {isSeriesList, validateThing} from '#validators'; - -import {raiseOutputWithoutDependency} from '#composite/control-flow'; - -import { - fillMissingListItems, - withFlattenedList, - withUnflattenedList, - withPropertiesFromList, -} from '#composite/data'; - -import inputSoupyFind from './inputSoupyFind.js'; -import withResolvedReferenceList from './withResolvedReferenceList.js'; - -export default templateCompositeFrom({ - annotation: `withResolvedSeriesList`, - - inputs: { - group: input({ - validate: validateThing({referenceType: 'group'}), - }), - - list: input({ - validate: isSeriesList, - acceptsNull: true, - }), - }, - - outputs: ['#resolvedSeriesList'], - - steps: () => [ - raiseOutputWithoutDependency({ - dependency: input('list'), - mode: input.value('empty'), - output: input.value({ - ['#resolvedSeriesList']: [], - }), - }), - - withPropertiesFromList({ - list: input('list'), - prefix: input.value('#serieses'), - properties: input.value([ - 'name', - 'description', - 'albums', - - 'showAlbumArtists', - ]), - }), - - fillMissingListItems({ - list: '#serieses.albums', - fill: input.value([]), - }), - - withFlattenedList({ - list: '#serieses.albums', - }), - - withResolvedReferenceList({ - list: '#flattenedList', - find: inputSoupyFind.input('album'), - notFoundMode: input.value('null'), - }), - - withUnflattenedList({ - list: '#resolvedReferenceList', - }).outputs({ - '#unflattenedList': '#serieses.albums', - }), - - fillMissingListItems({ - list: '#serieses.description', - fill: input.value(null), - }), - - fillMissingListItems({ - list: '#serieses.showAlbumArtists', - fill: input.value(null), - }), - - { - dependencies: [ - '#serieses.name', - '#serieses.description', - '#serieses.albums', - - '#serieses.showAlbumArtists', - ], - - compute: (continuation, { - ['#serieses.name']: name, - ['#serieses.description']: description, - ['#serieses.albums']: albums, - - ['#serieses.showAlbumArtists']: showAlbumArtists, - }) => continuation({ - ['#seriesProperties']: - stitchArrays({ - name, - description, - albums, - - showAlbumArtists, - }).map(properties => ({ - ...properties, - group: input - })) - }), - }, - - { - dependencies: ['#seriesProperties', input('group')], - compute: (continuation, { - ['#seriesProperties']: seriesProperties, - [input('group')]: group, - }) => continuation({ - ['#resolvedSeriesList']: - seriesProperties - .map(properties => ({ - ...properties, - group, - })), - }), - }, - ], -}); diff --git a/src/data/composite/wiki-properties/additionalFiles.js b/src/data/composite/wiki-properties/additionalFiles.js deleted file mode 100644 index 6760527a..00000000 --- a/src/data/composite/wiki-properties/additionalFiles.js +++ /dev/null @@ -1,30 +0,0 @@ -// 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: [...]}, -// ... -// ] -// - -import {isAdditionalFileList} from '#validators'; - -// TODO: Not templateCompositeFrom. - -export default function() { - return { - flags: {update: true, expose: true}, - update: {validate: isAdditionalFileList}, - expose: { - transform: (additionalFiles) => - additionalFiles ?? [], - }, - }; -} diff --git a/src/data/composite/wiki-properties/additionalNameList.js b/src/data/composite/wiki-properties/additionalNameList.js deleted file mode 100644 index c5971d4a..00000000 --- a/src/data/composite/wiki-properties/additionalNameList.js +++ /dev/null @@ -1,14 +0,0 @@ -// A list of additional names! These can be used for a variety of purposes, -// e.g. providing extra searchable titles, localizations, romanizations or -// original titles, and so on. Each item has a name and, optionally, a -// descriptive annotation. - -import {isAdditionalNameList} from '#validators'; - -export default function() { - return { - flags: {update: true, expose: true}, - update: {validate: isAdditionalNameList}, - expose: {transform: value => value ?? []}, - }; -} diff --git a/src/data/composite/wiki-properties/annotatedReferenceList.js b/src/data/composite/wiki-properties/annotatedReferenceList.js index bb6875f1..aea0f22c 100644 --- a/src/data/composite/wiki-properties/annotatedReferenceList.js +++ b/src/data/composite/wiki-properties/annotatedReferenceList.js @@ -2,7 +2,6 @@ import {input, templateCompositeFrom} from '#composite'; import { isContentString, - isDate, optional, validateArrayItems, validateProperties, @@ -10,8 +9,13 @@ import { } from '#validators'; import {exposeDependency} from '#composite/control-flow'; -import {inputSoupyFind, inputWikiData, withResolvedAnnotatedReferenceList} - from '#composite/wiki-data'; + +import { + inputFindOptions, + inputSoupyFind, + inputWikiData, + withResolvedAnnotatedReferenceList, +} from '#composite/wiki-data'; import {referenceListInputDescriptions, referenceListUpdateDescription} from './helpers/reference-list-helpers.js'; @@ -26,11 +30,7 @@ export default templateCompositeFrom({ data: inputWikiData({allowMixedTypes: true}), find: inputSoupyFind(), - - date: input({ - validate: isDate, - acceptsNull: true, - }), + findOptions: inputFindOptions(), reference: input.staticValue({type: 'string', defaultValue: 'reference'}), annotation: input.staticValue({type: 'string', defaultValue: 'annotation'}), @@ -57,14 +57,13 @@ export default templateCompositeFrom({ withResolvedAnnotatedReferenceList({ list: input.updateValue(), - date: input('date'), - reference: input('reference'), annotation: input('annotation'), thing: input('thing'), data: input('data'), find: input('find'), + findOptions: input('findOptions'), }), exposeDependency({dependency: '#resolvedAnnotatedReferenceList'}), diff --git a/src/data/composite/wiki-properties/canonicalBase.js b/src/data/composite/wiki-properties/canonicalBase.js new file mode 100644 index 00000000..81740d6c --- /dev/null +++ b/src/data/composite/wiki-properties/canonicalBase.js @@ -0,0 +1,16 @@ +import {isURL} from '#validators'; + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isURL}, + expose: { + transform: (value) => + (value === null + ? null + : value.endsWith('/') + ? value + : value + '/'), + }, + }; +} diff --git a/src/data/composite/wiki-properties/commentary.js b/src/data/composite/wiki-properties/commentary.js deleted file mode 100644 index 9625278d..00000000 --- a/src/data/composite/wiki-properties/commentary.js +++ /dev/null @@ -1,30 +0,0 @@ -// Artist commentary! Generally present on tracks and albums. - -import {input, templateCompositeFrom} from '#composite'; -import {isCommentary} from '#validators'; - -import {exitWithoutDependency, exposeDependency} - from '#composite/control-flow'; -import {withParsedCommentaryEntries} from '#composite/wiki-data'; - -export default templateCompositeFrom({ - annotation: `commentary`, - - compose: false, - - steps: () => [ - exitWithoutDependency({ - dependency: input.updateValue({validate: isCommentary}), - mode: input.value('falsy'), - value: input.value([]), - }), - - withParsedCommentaryEntries({ - from: input.updateValue(), - }), - - exposeDependency({ - dependency: '#parsedCommentaryEntries', - }), - ], -}); diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js index c5c14769..54d3e1a5 100644 --- a/src/data/composite/wiki-properties/commentatorArtists.js +++ b/src/data/composite/wiki-properties/commentatorArtists.js @@ -7,7 +7,6 @@ import {exitWithoutDependency, exposeDependency} from '#composite/control-flow'; import {withFlattenedList, withPropertyFromList, withUniqueItemsOnly} from '#composite/data'; -import {withParsedCommentaryEntries} from '#composite/wiki-data'; export default templateCompositeFrom({ annotation: `commentatorArtists`, @@ -21,19 +20,13 @@ export default templateCompositeFrom({ value: input.value([]), }), - withParsedCommentaryEntries({ - from: 'commentary', - }), - withPropertyFromList({ - list: '#parsedCommentaryEntries', + list: 'commentary', property: input.value('artists'), - }).outputs({ - '#parsedCommentaryEntries.artists': '#artistLists', }), withFlattenedList({ - list: '#artistLists', + list: '#commentary.artists', }).outputs({ '#flattenedList': '#artists', }), diff --git a/src/data/composite/wiki-properties/constitutibleArtwork.js b/src/data/composite/wiki-properties/constitutibleArtwork.js new file mode 100644 index 00000000..48f4211a --- /dev/null +++ b/src/data/composite/wiki-properties/constitutibleArtwork.js @@ -0,0 +1,70 @@ +// This composition does not actually inspect the values of any properties +// specified, so it's not responsible for determining whether a constituted +// artwork should exist at all. + +import {input, templateCompositeFrom} from '#composite'; +import {withEntries} from '#sugar'; +import Thing from '#thing'; +import {validateThing} from '#validators'; + +import {exposeDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {withConstitutedArtwork} from '#composite/wiki-data'; + +const template = templateCompositeFrom({ + annotation: `constitutibleArtwork`, + + compose: false, + + inputs: { + thingProperty: input({type: 'string', acceptsNull: true}), + dimensionsFromThingProperty: input({type: 'string', acceptsNull: true}), + fileExtensionFromThingProperty: input({type: 'string', acceptsNull: true}), + dateFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsArtistProperty: input({type: 'string', acceptsNull: true}), + artTagsFromThingProperty: input({type: 'string', acceptsNull: true}), + referencedArtworksFromThingProperty: input({type: 'string', acceptsNull: true}), + }, + + steps: () => [ + exposeUpdateValueOrContinue({ + validate: input.value( + validateThing({ + referenceType: 'artwork', + })), + }), + + withConstitutedArtwork({ + thingProperty: input('thingProperty'), + dimensionsFromThingProperty: input('dimensionsFromThingProperty'), + fileExtensionFromThingProperty: input('fileExtensionFromThingProperty'), + dateFromThingProperty: input('dateFromThingProperty'), + artistContribsFromThingProperty: input('artistContribsFromThingProperty'), + artistContribsArtistProperty: input('artistContribsArtistProperty'), + artTagsFromThingProperty: input('artTagsFromThingProperty'), + referencedArtworksFromThingProperty: input('referencedArtworksFromThingProperty'), + }), + + exposeDependency({ + dependency: '#constitutedArtwork', + }), + ], +}); + +template.fromYAMLFieldSpec = function(field) { + const {[Thing.yamlDocumentSpec]: documentSpec} = this; + + const {provide} = documentSpec.fields[field].transform; + + const inputs = + withEntries(provide, entries => + entries.map(([property, value]) => [ + property, + input.value(value), + ])); + + return template(inputs); +}; + +export default template; diff --git a/src/data/composite/wiki-properties/constitutibleArtworkList.js b/src/data/composite/wiki-properties/constitutibleArtworkList.js new file mode 100644 index 00000000..dad3a957 --- /dev/null +++ b/src/data/composite/wiki-properties/constitutibleArtworkList.js @@ -0,0 +1,72 @@ +// This composition does not actually inspect the values of any properties +// specified, so it's not responsible for determining whether a constituted +// artwork should exist at all. + +import {input, templateCompositeFrom} from '#composite'; +import {withEntries} from '#sugar'; +import Thing from '#thing'; +import {validateWikiData} from '#validators'; + +import {exposeUpdateValueOrContinue} from '#composite/control-flow'; +import {withConstitutedArtwork} from '#composite/wiki-data'; + +const template = templateCompositeFrom({ + annotation: `constitutibleArtworkList`, + + compose: false, + + inputs: { + thingProperty: input({type: 'string', acceptsNull: true}), + dimensionsFromThingProperty: input({type: 'string', acceptsNull: true}), + fileExtensionFromThingProperty: input({type: 'string', acceptsNull: true}), + dateFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsArtistProperty: input({type: 'string', acceptsNull: true}), + artTagsFromThingProperty: input({type: 'string', acceptsNull: true}), + referencedArtworksFromThingProperty: input({type: 'string', acceptsNull: true}), + }, + + steps: () => [ + exposeUpdateValueOrContinue({ + validate: input.value( + validateWikiData({ + referenceType: 'artwork', + })), + }), + + withConstitutedArtwork({ + thingProperty: input('thingProperty'), + dimensionsFromThingProperty: input('dimensionsFromThingProperty'), + fileExtensionFromThingProperty: input('fileExtensionFromThingProperty'), + dateFromThingProperty: input('dateFromThingProperty'), + artistContribsFromThingProperty: input('artistContribsFromThingProperty'), + artistContribsArtistProperty: input('artistContribsArtistProperty'), + artTagsFromThingProperty: input('artTagsFromThingProperty'), + referencedArtworksFromThingProperty: input('referencedArtworksFromThingProperty'), + }), + + { + dependencies: ['#constitutedArtwork'], + compute: ({ + ['#constitutedArtwork']: constitutedArtwork, + }) => [constitutedArtwork], + }, + ], +}); + +template.fromYAMLFieldSpec = function(field) { + const {[Thing.yamlDocumentSpec]: documentSpec} = this; + + const {provide} = documentSpec.fields[field].transform; + + const inputs = + withEntries(provide, entries => + entries.map(([property, value]) => [ + property, + input.value(value), + ])); + + return template(inputs); +}; + +export default template; diff --git a/src/data/composite/wiki-properties/directory.js b/src/data/composite/wiki-properties/directory.js index 9ca2a204..1756a8e5 100644 --- a/src/data/composite/wiki-properties/directory.js +++ b/src/data/composite/wiki-properties/directory.js @@ -18,6 +18,7 @@ export default templateCompositeFrom({ name: input({ validate: isName, defaultDependency: 'name', + acceptsNull: true, }), suffix: input({ diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js index 4aaaeb72..57a2b8f2 100644 --- a/src/data/composite/wiki-properties/index.js +++ b/src/data/composite/wiki-properties/index.js @@ -3,12 +3,12 @@ // Entries here may depend on entries in #composite/control-flow, // #composite/data, and #composite/wiki-data. -export {default as additionalFiles} from './additionalFiles.js'; -export {default as additionalNameList} from './additionalNameList.js'; export {default as annotatedReferenceList} from './annotatedReferenceList.js'; +export {default as canonicalBase} from './canonicalBase.js'; export {default as color} from './color.js'; -export {default as commentary} from './commentary.js'; export {default as commentatorArtists} from './commentatorArtists.js'; +export {default as constitutibleArtwork} from './constitutibleArtwork.js'; +export {default as constitutibleArtworkList} from './constitutibleArtworkList.js'; export {default as contentString} from './contentString.js'; export {default as contribsPresent} from './contribsPresent.js'; export {default as contributionList} from './contributionList.js'; @@ -22,7 +22,6 @@ export {default as name} from './name.js'; export {default as referenceList} from './referenceList.js'; export {default as referencedArtworkList} from './referencedArtworkList.js'; export {default as reverseReferenceList} from './reverseReferenceList.js'; -export {default as seriesList} from './seriesList.js'; export {default as simpleDate} from './simpleDate.js'; export {default as simpleString} from './simpleString.js'; export {default as singleReference} from './singleReference.js'; diff --git a/src/data/composite/wiki-properties/referenceList.js b/src/data/composite/wiki-properties/referenceList.js index 4f8207b5..663349ee 100644 --- a/src/data/composite/wiki-properties/referenceList.js +++ b/src/data/composite/wiki-properties/referenceList.js @@ -11,8 +11,13 @@ import {input, templateCompositeFrom} from '#composite'; import {validateReferenceList} from '#validators'; import {exposeDependency} from '#composite/control-flow'; -import {inputSoupyFind, inputWikiData, withResolvedReferenceList} - from '#composite/wiki-data'; + +import { + inputFindOptions, + inputSoupyFind, + inputWikiData, + withResolvedReferenceList, +} from '#composite/wiki-data'; import {referenceListInputDescriptions, referenceListUpdateDescription} from './helpers/reference-list-helpers.js'; @@ -27,6 +32,7 @@ export default templateCompositeFrom({ data: inputWikiData({allowMixedTypes: true}), find: inputSoupyFind(), + findOptions: inputFindOptions(), }, update: @@ -39,6 +45,7 @@ export default templateCompositeFrom({ list: input.updateValue(), data: input('data'), find: input('find'), + findOptions: input('findOptions'), }), exposeDependency({dependency: '#resolvedReferenceList'}), diff --git a/src/data/composite/wiki-properties/referencedArtworkList.js b/src/data/composite/wiki-properties/referencedArtworkList.js index 819b2f43..4f243493 100644 --- a/src/data/composite/wiki-properties/referencedArtworkList.js +++ b/src/data/composite/wiki-properties/referencedArtworkList.js @@ -1,7 +1,5 @@ import {input, templateCompositeFrom} from '#composite'; import find from '#find'; -import {isDate} from '#validators'; -import {combineWikiDataArrays} from '#wiki-data'; import annotatedReferenceList from './annotatedReferenceList.js'; @@ -10,47 +8,24 @@ export default templateCompositeFrom({ compose: false, - inputs: { - date: input({ - validate: isDate, - acceptsNull: true, - }), - }, - steps: () => [ { - dependencies: [ - 'albumData', - 'trackData', - ], - - compute: (continuation, { - albumData, - trackData, - }) => continuation({ - ['#data']: - combineWikiDataArrays([ - albumData, - trackData, - ]), - }), - }, - - { compute: (continuation) => continuation({ ['#find']: find.mixed({ - track: find.trackWithArtwork, - album: find.albumWithArtwork, + track: find.trackPrimaryArtwork, + album: find.albumPrimaryArtwork, }), }), }, annotatedReferenceList({ referenceType: input.value(['album', 'track']), - data: '#data', + + data: 'artworkData', find: '#find', - date: input('date'), + + thing: input.value('artwork'), }), ], }); diff --git a/src/data/composite/wiki-properties/seriesList.js b/src/data/composite/wiki-properties/seriesList.js deleted file mode 100644 index 2a101b45..00000000 --- a/src/data/composite/wiki-properties/seriesList.js +++ /dev/null @@ -1,31 +0,0 @@ -import {input, templateCompositeFrom} from '#composite'; -import {isSeriesList, validateThing} from '#validators'; - -import {exposeDependency} from '#composite/control-flow'; -import {withResolvedSeriesList} from '#composite/wiki-data'; - -export default templateCompositeFrom({ - annotation: `seriesList`, - - compose: false, - - inputs: { - group: input({ - validate: validateThing({referenceType: 'group'}), - }), - }, - - steps: () => [ - withResolvedSeriesList({ - group: input('group'), - - list: input.updateValue({ - validate: isSeriesList, - }), - }), - - exposeDependency({ - dependency: '#resolvedSeriesList', - }), - ], -}); diff --git a/src/data/composite/wiki-properties/singleReference.js b/src/data/composite/wiki-properties/singleReference.js index f532ebbe..25b97907 100644 --- a/src/data/composite/wiki-properties/singleReference.js +++ b/src/data/composite/wiki-properties/singleReference.js @@ -8,11 +8,19 @@ // import {input, templateCompositeFrom} from '#composite'; -import {isThingClass, validateReference} from '#validators'; +import {validateReference} from '#validators'; import {exposeDependency} from '#composite/control-flow'; -import {inputSoupyFind, inputWikiData, withResolvedReference} - from '#composite/wiki-data'; + +import { + inputFindOptions, + inputSoupyFind, + inputWikiData, + withResolvedReference, +} from '#composite/wiki-data'; + +import {referenceListInputDescriptions, referenceListUpdateDescription} + from './helpers/reference-list-helpers.js'; export default templateCompositeFrom({ annotation: `singleReference`, @@ -20,25 +28,24 @@ export default templateCompositeFrom({ compose: false, inputs: { - class: input.staticValue({validate: isThingClass}), + ...referenceListInputDescriptions(), + data: inputWikiData({allowMixedTypes: true}), find: inputSoupyFind(), - data: inputWikiData({allowMixedTypes: false}), + findOptions: inputFindOptions(), }, - update: ({ - [input.staticValue('class')]: thingClass, - }) => ({ - validate: - validateReference( - thingClass[Symbol.for('Thing.referenceType')]), - }), + update: + referenceListUpdateDescription({ + validateReferenceList: validateReference, + }), steps: () => [ withResolvedReference({ ref: input.updateValue(), data: input('data'), find: input('find'), + findOptions: input('findOptions'), }), exposeDependency({dependency: '#resolvedReference'}), diff --git a/src/data/composite/wiki-properties/soupyReverse.js b/src/data/composite/wiki-properties/soupyReverse.js index 269ccd6f..784a66b4 100644 --- a/src/data/composite/wiki-properties/soupyReverse.js +++ b/src/data/composite/wiki-properties/soupyReverse.js @@ -19,4 +19,19 @@ soupyReverse.contributionsBy = referenced: contrib => [contrib.artist], }); +soupyReverse.artworkContributionsBy = + (bindTo, artworkProperty, {single = false} = {}) => ({ + bindTo, + + referencing: thing => + (single + ? (thing[artworkProperty] + ? thing[artworkProperty].artistContribs + : []) + : thing[artworkProperty] + .flatMap(artwork => artwork.artistContribs)), + + referenced: contrib => [contrib.artist], + }); + export default soupyReverse; diff --git a/src/data/thing.js b/src/data/thing.js index 90453c15..f719224d 100644 --- a/src/data/thing.js +++ b/src/data/thing.js @@ -25,6 +25,10 @@ export default class Thing extends CacheableObject { static yamlSourceDocument = Symbol.for('Thing.yamlSourceDocument'); static yamlSourceDocumentPlacement = Symbol.for('Thing.yamlSourceDocumentPlacement'); + [Symbol.for('Thing.yamlSourceFilename')] = null; + [Symbol.for('Thing.yamlSourceDocument')] = null; + [Symbol.for('Thing.yamlSourceDocumentPlacement')] = null; + static isThingConstructor = Symbol.for('Thing.isThingConstructor'); static isThing = Symbol.for('Thing.isThing'); @@ -33,11 +37,13 @@ export default class Thing extends CacheableObject { static [Symbol.for('Thing.isThingConstructor')] = NaN; constructor() { - super(); + super({seal: false}); // To detect: // Object.hasOwn(object, Symbol.for('Thing.isThing')) this[Symbol.for('Thing.isThing')] = NaN; + + Object.seal(this); } static [Symbol.for('Thing.selectAll')] = _wikiData => []; @@ -54,7 +60,7 @@ export default class Thing extends CacheableObject { if (this.name) { name = colors.green(`"${this.name}"`); } - } catch (error) { + } catch { name = colors.yellow(`couldn't get name`); } @@ -63,7 +69,7 @@ export default class Thing extends CacheableObject { if (this.directory) { reference = colors.blue(Thing.getReference(this)); } - } catch (error) { + } catch { reference = colors.yellow(`couldn't get reference`); } diff --git a/src/data/things/additional-file.js b/src/data/things/additional-file.js new file mode 100644 index 00000000..b15f62e0 --- /dev/null +++ b/src/data/things/additional-file.js @@ -0,0 +1,54 @@ +import {input} from '#composite'; +import Thing from '#thing'; +import {isString, validateArrayItems} from '#validators'; + +import {exposeConstant, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {contentString, simpleString, thing} from '#composite/wiki-properties'; + +export class AdditionalFile extends Thing { + static [Thing.getPropertyDescriptors] = () => ({ + // Update & expose + + thing: thing(), + + title: simpleString(), + + description: contentString(), + + filenames: [ + exposeUpdateValueOrContinue({ + validate: input.value(validateArrayItems(isString)), + }), + + exposeConstant({ + value: input.value([]), + }), + ], + + // Expose only + + isAdditionalFile: [ + exposeConstant({ + value: input.value(true), + }), + ], + }); + + static [Thing.yamlDocumentSpec] = { + fields: { + 'Title': {property: 'title'}, + 'Description': {property: 'description'}, + 'Files': {property: 'filenames'}, + }, + }; + + get paths() { + if (!this.thing) return null; + if (!this.thing.getOwnAdditionalFilePath) return null; + + return ( + this.filenames.map(filename => + this.thing.getOwnAdditionalFilePath(this, filename))); + } +} diff --git a/src/data/things/additional-name.js b/src/data/things/additional-name.js new file mode 100644 index 00000000..99f3ee46 --- /dev/null +++ b/src/data/things/additional-name.js @@ -0,0 +1,31 @@ +import {input} from '#composite'; +import Thing from '#thing'; + +import {exposeConstant} from '#composite/control-flow'; +import {contentString, thing} from '#composite/wiki-properties'; + +export class AdditionalName extends Thing { + static [Thing.getPropertyDescriptors] = () => ({ + // Update & expose + + thing: thing(), + + name: contentString(), + annotation: contentString(), + + // Expose only + + isAdditionalName: [ + exposeConstant({ + value: input.value(true), + }), + ], + }); + + static [Thing.yamlDocumentSpec] = { + fields: { + 'Name': {property: 'name'}, + 'Annotation': {property: 'annotation'}, + }, + }; +} diff --git a/src/data/things/album.js b/src/data/things/album.js index 3eb6fc60..58d5253c 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -3,37 +3,59 @@ export const DATA_ALBUM_DIRECTORY = 'album'; import * as path from 'node:path'; import {inspect} from 'node:util'; +import CacheableObject from '#cacheable-object'; import {colors} from '#cli'; import {input} from '#composite'; import {traverse} from '#node-utils'; import {sortAlbumsTracksChronologically, sortChronologically} from '#sort'; -import {accumulateSum, empty} from '#sugar'; +import {empty} from '#sugar'; import Thing from '#thing'; -import {isColor, isDate, isDirectory} from '#validators'; + +import { + is, + isBoolean, + isColor, + isContributionList, + isDate, + isDirectory, + isNumber, +} from '#validators'; import { parseAdditionalFiles, parseAdditionalNames, parseAnnotatedReferences, + parseArtwork, + parseCommentary, parseContributors, + parseCreditingSources, parseDate, parseDimensions, parseWallpaperParts, } from '#yaml'; -import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue} - from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; -import {exitWithoutContribs, withDirectory, withCoverArtDate} - from '#composite/wiki-data'; +import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + +import { + exitWithoutArtwork, + withDirectory, + withHasArtwork, + withResolvedContribs, +} from '#composite/wiki-data'; import { - additionalFiles, - additionalNameList, - commentary, color, commentatorArtists, + constitutibleArtwork, + constitutibleArtworkList, contentString, contribsPresent, contributionList, @@ -44,7 +66,6 @@ import { name, referencedArtworkList, referenceList, - reverseReferenceList, simpleDate, simpleString, soupyFind, @@ -56,20 +77,31 @@ import { wikiData, } from '#composite/wiki-properties'; -import {withTracks} from '#composite/things/album'; -import {withAlbum} from '#composite/things/track-section'; +import {withCoverArtDate, withTracks} from '#composite/things/album'; +import {withAlbum, withContinueCountingFrom, withStartCountingFrom} + from '#composite/things/track-section'; export class Album extends Thing { static [Thing.referenceType] = 'album'; static [Thing.getPropertyDescriptors] = ({ + AdditionalFile, + AdditionalName, ArtTag, + Artwork, + CommentaryEntry, + CreditingSourcesEntry, Group, - Track, TrackSection, WikiInfo, }) => ({ - // Update & expose + // > Update & expose - Internal relationships + + trackSections: thingList({ + class: input.value(TrackSection), + }), + + // > Update & expose - Identifying metadata name: name('Unnamed Album'), directory: directory(), @@ -90,104 +122,158 @@ export class Album extends Thing { alwaysReferenceTracksByDirectory: flag(false), suffixTrackDirectories: flag(false), - color: color(), - urls: urls(), + style: [ + exposeUpdateValueOrContinue({ + validate: input.value(is(...[ + 'album', + 'single', + ])), + }), - additionalNames: additionalNameList(), + exposeConstant({ + value: input.value('album'), + }), + ], bandcampAlbumIdentifier: simpleString(), bandcampArtworkIdentifier: simpleString(), + additionalNames: thingList({ + class: input.value(AdditionalName), + }), + date: simpleDate(), - trackArtDate: simpleDate(), dateAddedToWiki: simpleDate(), - coverArtDate: [ - // ~~TODO: Why does this fall back, but Track.coverArtDate doesn't?~~ - // TODO: OK so it's because tracks don't *store* dates just like that. - // Really instead of fallback being a flag, it should be a date value, - // if this option is worth existing at all. - withCoverArtDate({ - from: input.updateValue({ - validate: isDate, - }), + // > Update & expose - Credits and contributors + + artistContribs: contributionList({ + date: 'date', + artistProperty: input.value('albumArtistContributions'), + }), - fallback: input.value(true), + trackArtistText: contentString(), + + trackArtistContribs: [ + withResolvedContribs({ + from: input.updateValue({validate: isContributionList}), + thingProperty: input.thisProperty(), + artistProperty: input.value('albumTrackArtistContributions'), + date: 'date', + }).outputs({ + '#resolvedContribs': '#trackArtistContribs', }), - exposeDependency({dependency: '#coverArtDate'}), - ], + exposeDependencyOrContinue({ + dependency: '#trackArtistContribs', + mode: input.value('empty'), + }), - coverArtFileExtension: [ - exitWithoutContribs({contribs: 'coverArtistContribs'}), - fileExtension('jpg'), + withResolvedContribs({ + from: 'artistContribs', + thingProperty: input.thisProperty(), + artistProperty: input.value('albumTrackArtistContributions'), + date: 'date', + }).outputs({ + '#resolvedContribs': '#trackArtistContribs', + }), + + exposeDependency({dependency: '#trackArtistContribs'}), ], - trackCoverArtFileExtension: fileExtension('jpg'), + // > Update & expose - General configuration - wallpaperFileExtension: [ - exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), - fileExtension('jpg'), - ], + countTracksInArtistTotals: flag(true), - bannerFileExtension: [ - exitWithoutContribs({contribs: 'bannerArtistContribs'}), - fileExtension('jpg'), - ], + showAlbumInTracksWithoutArtists: flag(false), - wallpaperStyle: [ - exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), - simpleString(), - ], + hasTrackNumbers: flag(true), + isListedOnHomepage: flag(true), + isListedInGalleries: flag(true), - wallpaperParts: [ - exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), - wallpaperParts(), - ], + hideDuration: flag(false), - bannerStyle: [ - exitWithoutContribs({contribs: 'bannerArtistContribs'}), - simpleString(), + // > Update & expose - General metadata + + color: color(), + + urls: urls(), + + // > Update & expose - Artworks + + coverArtworks: [ + // This works, lol, because this array describes `expose.transform` for + // the coverArtworks property, and compositions generally access the + // update value, not what's exposed by property access out in the open. + // There's no recursion going on here. + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + value: input.value([]), + }), + + constitutibleArtworkList.fromYAMLFieldSpec + .call(this, 'Cover Artwork'), ], - coverArtDimensions: [ - exitWithoutContribs({contribs: 'coverArtistContribs'}), - dimensions(), + coverArtistContribs: [ + withCoverArtDate(), + + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumCoverArtistContributions'), + }), ], - trackDimensions: dimensions(), + coverArtDate: [ + withCoverArtDate({ + from: input.updateValue({ + validate: isDate, + }), + }), - bannerDimensions: [ - exitWithoutContribs({contribs: 'bannerArtistContribs'}), - dimensions(), + exposeDependency({dependency: '#coverArtDate'}), ], - hasTrackNumbers: flag(true), - isListedOnHomepage: flag(true), - isListedInGalleries: flag(true), + coverArtFileExtension: [ + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + }), - commentary: commentary(), - creditSources: commentary(), - additionalFiles: additionalFiles(), + fileExtension('jpg'), + ], - trackSections: thingList({ - class: input.value(TrackSection), - }), + coverArtDimensions: [ + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + }), - artistContribs: contributionList({ - date: 'date', - artistProperty: input.value('albumArtistContributions'), - }), + dimensions(), + ], - coverArtistContribs: [ - withCoverArtDate({ - fallback: input.value(true), + artTags: [ + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + value: input.value([]), }), - contributionList({ - date: '#coverArtDate', - artistProperty: input.value('albumCoverArtistContributions'), + referenceList({ + class: input.value(ArtTag), + find: soupyFind.input('artTag'), + }), + ], + + referencedArtworks: [ + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + value: input.value([]), }), + + referencedArtworkList(), ], trackCoverArtistContribs: contributionList({ @@ -200,80 +286,139 @@ export class Album extends Thing { artistProperty: input.value('trackCoverArtistContributions'), }), - wallpaperArtistContribs: [ - withCoverArtDate({ - fallback: input.value(true), + trackArtDate: simpleDate(), + + trackCoverArtFileExtension: fileExtension('jpg'), + + trackDimensions: dimensions(), + + wallpaperArtwork: [ + exitWithoutDependency({ + dependency: 'wallpaperArtistContribs', + mode: input.value('empty'), + value: input.value(null), }), + constitutibleArtwork.fromYAMLFieldSpec + .call(this, 'Wallpaper Artwork'), + ], + + wallpaperArtistContribs: [ + withCoverArtDate(), + contributionList({ date: '#coverArtDate', artistProperty: input.value('albumWallpaperArtistContributions'), }), ], - bannerArtistContribs: [ - withCoverArtDate({ - fallback: input.value(true), + wallpaperFileExtension: [ + exitWithoutArtwork({ + contribs: 'wallpaperArtistContribs', + artwork: 'wallpaperArtwork', }), + fileExtension('jpg'), + ], + + wallpaperStyle: [ + exitWithoutArtwork({ + contribs: 'wallpaperArtistContribs', + artwork: 'wallpaperArtwork', + }), + + simpleString(), + ], + + wallpaperParts: [ + // kinda nonsensical or at least unlikely lol, but y'know + exitWithoutArtwork({ + contribs: 'wallpaperArtistContribs', + artwork: 'wallpaperArtwork', + value: input.value([]), + }), + + wallpaperParts(), + ], + + bannerArtwork: [ + exitWithoutDependency({ + dependency: 'bannerArtistContribs', + mode: input.value('empty'), + value: input.value(null), + }), + + constitutibleArtwork.fromYAMLFieldSpec + .call(this, 'Banner Artwork'), + ], + + bannerArtistContribs: [ + withCoverArtDate(), + contributionList({ date: '#coverArtDate', artistProperty: input.value('albumBannerArtistContributions'), }), ], - groups: referenceList({ - class: input.value(Group), - find: soupyFind.input('group'), - }), - - artTags: [ - exitWithoutContribs({ - contribs: 'coverArtistContribs', - value: input.value([]), + bannerFileExtension: [ + exitWithoutArtwork({ + contribs: 'bannerArtistContribs', + artwork: 'bannerArtwork', }), - referenceList({ - class: input.value(ArtTag), - find: soupyFind.input('artTag'), - }), + fileExtension('jpg'), ], - referencedArtworks: [ - exitWithoutContribs({ - contribs: 'coverArtistContribs', - value: input.value([]), + bannerDimensions: [ + exitWithoutArtwork({ + contribs: 'bannerArtistContribs', + artwork: 'bannerArtwork', }), - { - dependencies: ['coverArtDate', 'date'], - compute: (continuation, { - coverArtDate, - date, - }) => continuation({ - ['#date']: - coverArtDate ?? date, - }), - }, + dimensions(), + ], - referencedArtworkList({ - date: '#date', + bannerStyle: [ + exitWithoutArtwork({ + contribs: 'bannerArtistContribs', + artwork: 'bannerArtwork', }), + + simpleString(), ], - // Update only + // > Update & expose - Groups - find: soupyFind(), - reverse: soupyReverse(), + groups: referenceList({ + class: input.value(Group), + find: soupyFind.input('group'), + }), - // used for referencedArtworkList (mixedFind) - albumData: wikiData({ - class: input.value(Album), + // > Update & expose - Content entries + + commentary: thingList({ + class: input.value(CommentaryEntry), + }), + + creditingSources: thingList({ + class: input.value(CreditingSourcesEntry), + }), + + // > Update & expose - Additional files + + additionalFiles: thingList({ + class: input.value(AdditionalFile), }), + // > Update only + + find: soupyFind(), + reverse: soupyReverse(), + // used for referencedArtworkList (mixedFind) - trackData: wikiData({ - class: input.value(Track), + artworkData: wikiData({ + class: input.value(Artwork), }), // used for withMatchingContributionPresets (indirectly by Contribution) @@ -281,11 +426,25 @@ export class Album extends Thing { class: input.value(WikiInfo), }), - // Expose only + // > Expose only + + isAlbum: [ + exposeConstant({ + value: input.value(true), + }), + ], commentatorArtists: commentatorArtists(), - hasCoverArt: contribsPresent({contribs: 'coverArtistContribs'}), + hasCoverArt: [ + withHasArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + }), + + exposeDependency({dependency: '#hasArtwork'}), + ], + hasWallpaperArt: contribsPresent({contribs: 'wallpaperArtistContribs'}), hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}), @@ -293,17 +452,6 @@ export class Album extends Thing { withTracks(), exposeDependency({dependency: '#tracks'}), ], - - referencedByArtworks: [ - exitWithoutContribs({ - contribs: 'coverArtistContribs', - value: input.value([]), - }), - - reverseReferenceList({ - reverse: soupyReverse.input('artworksWhichReference'), - }), - ], }); static [Thing.getSerializeDescriptors] = ({ @@ -362,6 +510,20 @@ export class Album extends Thing { : [album.name]), }, + albumSinglesOnly: { + referencing: ['album'], + + bindTo: 'albumData', + + incldue: album => + album.style === 'single', + + getMatchableNames: album => + (album.alwaysReferenceByDirectory + ? [] + : [album.name]), + }, + albumWithArtwork: { referenceTypes: [ 'album', @@ -375,9 +537,34 @@ export class Album extends Thing { album.hasCoverArt, getMatchableNames: album => - (album.alwaysReferenceByDirectory - ? [] + (album.alwaysReferenceByDirectory + ? [] + : [album.name]), + }, + + albumPrimaryArtwork: { + [Thing.findThisThingOnly]: false, + + referenceTypes: [ + 'album', + 'album-referencing-artworks', + 'album-referenced-artworks', + ], + + bindTo: 'artworkData', + + include: (artwork, {Artwork, Album}) => + artwork instanceof Artwork && + artwork.thing instanceof Album && + artwork === artwork.thing.coverArtworks[0], + + getMatchableNames: ({thing: album}) => + (album.alwaysReferenceByDirectory + ? [] : [album.name]), + + getMatchableDirectories: ({thing: album}) => + [album.directory], }, }; @@ -413,14 +600,17 @@ export class Album extends Thing { albumArtistContributionsBy: soupyReverse.contributionsBy('albumData', 'artistContribs'), + albumTrackArtistContributionsBy: + soupyReverse.contributionsBy('albumData', 'trackArtistContribs'), + albumCoverArtistContributionsBy: - soupyReverse.contributionsBy('albumData', 'coverArtistContribs'), + soupyReverse.artworkContributionsBy('albumData', 'coverArtworks'), albumWallpaperArtistContributionsBy: - soupyReverse.contributionsBy('albumData', 'wallpaperArtistContribs'), + soupyReverse.artworkContributionsBy('albumData', 'wallpaperArtwork', {single: true}), albumBannerArtistContributionsBy: - soupyReverse.contributionsBy('albumData', 'bannerArtistContribs'), + soupyReverse.artworkContributionsBy('albumData', 'bannerArtwork', {single: true}), albumsWithCommentaryBy: { bindTo: 'albumData', @@ -432,21 +622,15 @@ export class Album extends Thing { static [Thing.yamlDocumentSpec] = { fields: { - 'Album': {property: 'name'}, + // Identifying metadata + 'Album': {property: 'name'}, 'Directory': {property: 'directory'}, 'Directory Suffix': {property: 'directorySuffix'}, 'Suffix Track Directories': {property: 'suffixTrackDirectories'}, - 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, - 'Always Reference Tracks By Directory': { - property: 'alwaysReferenceTracksByDirectory', - }, - - 'Additional Names': { - property: 'additionalNames', - transform: parseAdditionalNames, - }, + 'Always Reference Tracks By Directory': {property: 'alwaysReferenceTracksByDirectory'}, + 'Style': {property: 'style'}, 'Bandcamp Album ID': { property: 'bandcampAlbumIdentifier', @@ -458,41 +642,129 @@ export class Album extends Thing { transform: String, }, + 'Additional Names': { + property: 'additionalNames', + transform: parseAdditionalNames, + }, + 'Date': { property: 'date', transform: parseDate, }, - 'Color': {property: 'color'}, - 'URLs': {property: 'urls'}, + 'Date Added': { + property: 'dateAddedToWiki', + transform: parseDate, + }, + + // Credits and contributors + + 'Artists': { + property: 'artistContribs', + transform: parseContributors, + }, + + 'Track Artist Text': { + property: 'trackArtistText', + }, + + 'Track Artists': { + property: 'trackArtistContribs', + transform: parseContributors, + }, + + // General configuration + + 'Count Tracks In Artist Totals': {property: 'countTracksInArtistTotals'}, + + 'Show Album In Tracks Without Artists': { + property: 'showAlbumInTracksWithoutArtists', + }, 'Has Track Numbers': {property: 'hasTrackNumbers'}, 'Listed on Homepage': {property: 'isListedOnHomepage'}, 'Listed in Galleries': {property: 'isListedInGalleries'}, - 'Cover Art Date': { - property: 'coverArtDate', - transform: parseDate, + 'Hide Duration': {property: 'hideDuration'}, + + // General metadata + + 'Color': {property: 'color'}, + + 'URLs': {property: 'urls'}, + + // Artworks + // (Note - this YAML section is deliberately ordered differently + // than the corresponding property descriptors.) + + 'Cover Artwork': { + property: 'coverArtworks', + transform: + parseArtwork({ + thingProperty: 'coverArtworks', + dimensionsFromThingProperty: 'coverArtDimensions', + fileExtensionFromThingProperty: 'coverArtFileExtension', + dateFromThingProperty: 'coverArtDate', + artistContribsFromThingProperty: 'coverArtistContribs', + artistContribsArtistProperty: 'albumCoverArtistContributions', + artTagsFromThingProperty: 'artTags', + referencedArtworksFromThingProperty: 'referencedArtworks', + }), }, - 'Default Track Cover Art Date': { - property: 'trackArtDate', - transform: parseDate, + 'Banner Artwork': { + property: 'bannerArtwork', + transform: + parseArtwork({ + single: true, + thingProperty: 'bannerArtwork', + dimensionsFromThingProperty: 'bannerDimensions', + fileExtensionFromThingProperty: 'bannerFileExtension', + dateFromThingProperty: 'date', + artistContribsFromThingProperty: 'bannerArtistContribs', + artistContribsArtistProperty: 'albumBannerArtistContributions', + }), }, - 'Date Added': { - property: 'dateAddedToWiki', - transform: parseDate, + 'Wallpaper Artwork': { + property: 'wallpaperArtwork', + transform: + parseArtwork({ + single: true, + thingProperty: 'wallpaperArtwork', + dimensionsFromThingProperty: null, + fileExtensionFromThingProperty: 'wallpaperFileExtension', + dateFromThingProperty: 'date', + artistContribsFromThingProperty: 'wallpaperArtistContribs', + artistContribsArtistProperty: 'albumWallpaperArtistContributions', + }), }, - 'Cover Art File Extension': {property: 'coverArtFileExtension'}, - 'Track Art File Extension': {property: 'trackCoverArtFileExtension'}, + 'Cover Artists': { + property: 'coverArtistContribs', + transform: parseContributors, + }, + + 'Cover Art Date': { + property: 'coverArtDate', + transform: parseDate, + }, 'Cover Art Dimensions': { property: 'coverArtDimensions', transform: parseDimensions, }, + 'Default Track Cover Artists': { + property: 'trackCoverArtistContribs', + transform: parseContributors, + }, + + 'Default Track Cover Art Date': { + property: 'trackArtDate', + transform: parseDate, + }, + 'Default Track Dimensions': { property: 'trackDimensions', transform: parseDimensions, @@ -504,7 +776,6 @@ export class Album extends Thing { }, 'Wallpaper Style': {property: 'wallpaperStyle'}, - 'Wallpaper File Extension': {property: 'wallpaperFileExtension'}, 'Wallpaper Parts': { property: 'wallpaperParts', @@ -516,51 +787,70 @@ export class Album extends Thing { transform: parseContributors, }, - 'Banner Style': {property: 'bannerStyle'}, - 'Banner File Extension': {property: 'bannerFileExtension'}, - 'Banner Dimensions': { property: 'bannerDimensions', transform: parseDimensions, }, - 'Commentary': {property: 'commentary'}, - 'Credit Sources': {property: 'creditSources'}, + 'Banner Style': {property: 'bannerStyle'}, - 'Additional Files': { - property: 'additionalFiles', - transform: parseAdditionalFiles, - }, + 'Cover Art File Extension': {property: 'coverArtFileExtension'}, + 'Track Art File Extension': {property: 'trackCoverArtFileExtension'}, + 'Wallpaper File Extension': {property: 'wallpaperFileExtension'}, + 'Banner File Extension': {property: 'bannerFileExtension'}, + + 'Art Tags': {property: 'artTags'}, 'Referenced Artworks': { property: 'referencedArtworks', transform: parseAnnotatedReferences, }, - 'Franchises': {ignore: true}, + // Groups - 'Artists': { - property: 'artistContribs', - transform: parseContributors, + 'Groups': {property: 'groups'}, + + // Content entries + + 'Commentary': { + property: 'commentary', + transform: parseCommentary, }, - 'Cover Artists': { - property: 'coverArtistContribs', - transform: parseContributors, + 'Crediting Sources': { + property: 'creditingSources', + transform: parseCreditingSources, }, - 'Default Track Cover Artists': { - property: 'trackCoverArtistContribs', - transform: parseContributors, + // Additional files + + 'Additional Files': { + property: 'additionalFiles', + transform: parseAdditionalFiles, }, - 'Groups': {property: 'groups'}, - 'Art Tags': {property: 'artTags'}, + // Shenanigans + 'Franchises': {ignore: true}, 'Review Points': {ignore: true}, }, invalidFieldCombinations: [ + {message: `Move commentary on singles to the track`, fields: [ + ['Style', 'single'], + 'Commentary', + ]}, + + {message: `Move crediting sources on singles to the track`, fields: [ + ['Style', 'single'], + 'Crediting Sources', + ]}, + + {message: `Move additional names on singles to the track`, fields: [ + ['Style', 'single'], + 'Additional Names', + ]}, + {message: `Specify one wallpaper style or multiple wallpaper parts, not both`, fields: [ 'Wallpaper Parts', 'Wallpaper Style', @@ -597,6 +887,12 @@ export class Album extends Thing { const trackSectionData = []; const trackData = []; + const artworkData = []; + const commentaryData = []; + const creditingSourceData = []; + const referencingSourceData = []; + const lyricsData = []; + for (const {header: album, entries} of results) { const trackSections = []; @@ -608,8 +904,6 @@ export class Album extends Thing { isDefaultTrackSection: true, }); - const albumRef = Thing.getReference(album); - const closeCurrentTrackSection = () => { if ( currentTrackSection.isDefaultTrackSection && @@ -636,17 +930,53 @@ export class Album extends Thing { currentTrackSectionTracks.push(entry); trackData.push(entry); - entry.dataSourceAlbum = albumRef; + // Set the track's album before accessing its list of artworks. + // The existence of its artwork objects may depend on access to + // its album's 'Default Track Cover Artists'. + entry.album = album; + + artworkData.push(...entry.trackArtworks); + commentaryData.push(...entry.commentary); + creditingSourceData.push(...entry.creditingSources); + referencingSourceData.push(...entry.referencingSources); + + // TODO: As exposed, Track.lyrics tries to inherit from the main + // release, which is impossible before the data's been linked. + // We just use the update value here. But it's icky! + lyricsData.push(...CacheableObject.getUpdateValue(entry, 'lyrics') ?? []); } closeCurrentTrackSection(); albumData.push(album); + artworkData.push(...album.coverArtworks); + + if (album.bannerArtwork) { + artworkData.push(album.bannerArtwork); + } + + if (album.wallpaperArtwork) { + artworkData.push(album.wallpaperArtwork); + } + + commentaryData.push(...album.commentary); + creditingSourceData.push(...album.creditingSources); + album.trackSections = trackSections; } - return {albumData, trackSectionData, trackData}; + return { + albumData, + trackSectionData, + trackData, + + artworkData, + commentaryData, + creditingSourceData, + referencingSourceData, + lyricsData, + }; }, sort({albumData, trackData}) { @@ -654,19 +984,101 @@ export class Album extends Thing { sortAlbumsTracksChronologically(trackData); }, }); + + getOwnAdditionalFilePath(_file, filename) { + return [ + 'media.albumAdditionalFile', + this.directory, + filename, + ]; + } + + getOwnArtworkPath(artwork) { + if (artwork === this.bannerArtwork) { + return [ + 'media.albumBanner', + this.directory, + artwork.fileExtension, + ]; + } + + if (artwork === this.wallpaperArtwork) { + if (!empty(this.wallpaperParts)) { + return null; + } + + return [ + 'media.albumWallpaper', + this.directory, + artwork.fileExtension, + ]; + } + + // TODO: using trackCover here is obviously, badly wrong + // but we ought to refactor banners and wallpapers similarly + // (i.e. depend on those intrinsic artwork paths rather than + // accessing media.{albumBanner,albumWallpaper} from content + // or other code directly) + return [ + 'media.trackCover', + this.directory, + + (artwork.unqualifiedDirectory + ? 'cover-' + artwork.unqualifiedDirectory + : 'cover'), + + artwork.fileExtension, + ]; + } + + // As of writing, albums don't even have a `duration` property... + // so this function will never be called... but the message stands... + countOwnContributionInDurationTotals(_contrib) { + return false; + } } export class TrackSection extends Thing { static [Thing.friendlyName] = `Track Section`; static [Thing.referenceType] = `track-section`; - static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ + static [Thing.getPropertyDescriptors] = ({Track}) => ({ // Update & expose name: name('Unnamed Track Section'), unqualifiedDirectory: directory(), + directorySuffix: [ + exposeUpdateValueOrContinue({ + validate: input.value(isDirectory), + }), + + withAlbum(), + + withPropertyFromObject({ + object: '#album', + property: input.value('directorySuffix'), + }), + + exposeDependency({dependency: '#album.directorySuffix'}), + ], + + suffixTrackDirectories: [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withAlbum(), + + withPropertyFromObject({ + object: '#album', + property: input.value('suffixTrackDirectories'), + }), + + exposeDependency({dependency: '#album.suffixTrackDirectories'}), + ], + color: [ exposeUpdateValueOrContinue({ validate: input.value(isColor), @@ -682,8 +1094,31 @@ export class TrackSection extends Thing { exposeDependency({dependency: '#album.color'}), ], + startCountingFrom: [ + withStartCountingFrom({ + from: input.updateValue({validate: isNumber}), + }), + + exposeDependency({dependency: '#startCountingFrom'}), + ], + dateOriginallyReleased: simpleDate(), + countTracksInArtistTotals: [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withAlbum(), + + withPropertyFromObject({ + object: '#album', + property: input.value('countTracksInArtistTotals'), + }), + + exposeDependency({dependency: '#album.countTracksInArtistTotals'}), + ], + isDefaultTrackSection: flag(false), description: contentString(), @@ -703,6 +1138,12 @@ export class TrackSection extends Thing { // Expose only + isTrackSection: [ + exposeConstant({ + value: input.value(true), + }), + ], + directory: [ withAlbum(), @@ -731,42 +1172,10 @@ export class TrackSection extends Thing { }, ], - startIndex: [ - withAlbum(), - - withPropertyFromObject({ - object: '#album', - property: input.value('trackSections'), - }), - - { - dependencies: ['#album.trackSections', input.myself()], - compute: (continuation, { - ['#album.trackSections']: trackSections, - [input.myself()]: myself, - }) => continuation({ - ['#index']: - trackSections.indexOf(myself), - }), - }, + continueCountingFrom: [ + withContinueCountingFrom(), - exitWithoutDependency({ - dependency: '#index', - mode: input.value('index'), - value: input.value(0), - }), - - { - dependencies: ['#album.trackSections', '#index'], - compute: ({ - ['#album.trackSections']: trackSections, - ['#index']: index, - }) => - accumulateSum( - trackSections - .slice(0, index) - .map(section => section.tracks.length)), - }, + exposeDependency({dependency: '#continueCountingFrom'}), ], }); @@ -796,13 +1205,19 @@ export class TrackSection extends Thing { static [Thing.yamlDocumentSpec] = { fields: { 'Section': {property: 'name'}, + 'Directory Suffix': {property: 'directorySuffix'}, + 'Suffix Track Directories': {property: 'suffixTrackDirectories'}, + 'Color': {property: 'color'}, + 'Start Counting From': {property: 'startCountingFrom'}, 'Date Originally Released': { property: 'dateOriginallyReleased', transform: parseDate, }, + 'Count Tracks In Artist Totals': {property: 'countTracksInArtistTotals'}, + 'Description': {property: 'description'}, }, }; @@ -812,38 +1227,38 @@ export class TrackSection extends Thing { parts.push(Thing.prototype[inspect.custom].apply(this)); - if (depth >= 0) { + if (depth >= 0) showAlbum: { let album = null; try { album = this.album; - } catch {} + } catch { + break showAlbum; + } let first = null; try { - first = this.startIndex; + first = this.tracks.at(0).trackNumber; } catch {} - let length = null; + let last = null; try { - length = this.tracks.length; + last = this.tracks.at(-1).trackNumber; } catch {} - if (album) { - const albumName = album.name; - const albumIndex = album.trackSections.indexOf(this); + const albumName = album.name; + const albumIndex = album.trackSections.indexOf(this); - const num = - (albumIndex === -1 - ? 'indeterminate position' - : `#${albumIndex + 1}`); + const num = + (albumIndex === -1 + ? 'indeterminate position' + : `#${albumIndex + 1}`); - const range = - (albumIndex >= 0 && first !== null && length !== null - ? `: ${first + 1}-${first + length}` - : ''); + const range = + (albumIndex >= 0 && first !== null && last !== null + ? `: ${first}-${last}` + : ''); - parts.push(` (${colors.yellow(num + range)} in ${colors.green(albumName)})`); - } + parts.push(` (${colors.yellow(num + range)} in ${colors.green(`"${albumName}"`)})`); } return parts.join(''); diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index c88fcdc2..fff724cb 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,18 +1,25 @@ +export const DATA_ART_TAGS_DIRECTORY = 'art-tags'; export const ART_TAG_DATA_FILE = 'tags.yaml'; +import {readFile} from 'node:fs/promises'; +import * as path from 'node:path'; + import {input} from '#composite'; -import find from '#find'; -import {sortAlphabetically, sortAlbumsTracksChronologically} from '#sort'; +import {traverse} from '#node-utils'; +import {sortAlphabetically} from '#sort'; import Thing from '#thing'; import {unique} from '#sugar'; import {isName} from '#validators'; import {parseAdditionalNames, parseAnnotatedReferences} from '#yaml'; -import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue} - from '#composite/control-flow'; +import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; import { - additionalNameList, annotatedReferenceList, color, contentString, @@ -23,8 +30,8 @@ import { name, soupyFind, soupyReverse, + thingList, urls, - wikiData, } from '#composite/wiki-properties'; import {withAllDescendantArtTags, withAncestorArtTagBaobabTree} @@ -34,7 +41,7 @@ export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; static [Thing.friendlyName] = `Art Tag`; - static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ + static [Thing.getPropertyDescriptors] = ({AdditionalName}) => ({ // Update & expose name: name('Unnamed Art Tag'), @@ -55,7 +62,9 @@ export class ArtTag extends Thing { }, ], - additionalNames: additionalNameList(), + additionalNames: thingList({ + class: input.value(AdditionalName), + }), description: contentString(), @@ -68,8 +77,6 @@ export class ArtTag extends Thing { class: input.value(ArtTag), find: soupyFind.input('artTag'), - date: input.value(null), - reference: input.value('artTag'), thing: input.value('artTag'), }), @@ -81,6 +88,12 @@ export class ArtTag extends Thing { // Expose only + isArtTag: [ + exposeConstant({ + value: input.value(true), + }), + ], + descriptionShort: [ exitWithoutDependency({ dependency: 'description', @@ -94,22 +107,11 @@ export class ArtTag extends Thing { }, ], - directlyTaggedInThings: { - flags: {expose: true}, - - expose: { - dependencies: ['this', 'reverse'], - compute: ({this: artTag, reverse}) => - sortAlbumsTracksChronologically( - [ - ...reverse.albumsWhoseArtworksFeature(artTag), - ...reverse.tracksWhoseArtworksFeature(artTag), - ], - {getDate: thing => thing.coverArtDate ?? thing.date}), - }, - }, + directlyFeaturedInArtworks: reverseReferenceList({ + reverse: soupyReverse.input('artworksWhichFeature'), + }), - indirectlyTaggedInThings: [ + indirectlyFeaturedInArtworks: [ withAllDescendantArtTags(), { @@ -117,7 +119,7 @@ export class ArtTag extends Thing { compute: ({'#allDescendantArtTags': allDescendantArtTags}) => unique( allDescendantArtTags - .flatMap(artTag => artTag.directlyTaggedInThings)), + .flatMap(artTag => artTag.directlyFeaturedInArtworks)), }, ], @@ -187,13 +189,25 @@ export class ArtTag extends Thing { }; static [Thing.getYamlLoadingSpec] = ({ - documentModes: {allInOne}, + documentModes: {allTogether}, thingConstructors: {ArtTag}, }) => ({ title: `Process art tags file`, - file: ART_TAG_DATA_FILE, - documentMode: allInOne, + files: dataPath => + Promise.allSettled([ + readFile(path.join(dataPath, ART_TAG_DATA_FILE)) + .then(() => [ART_TAG_DATA_FILE]), + + traverse(path.join(dataPath, DATA_ART_TAGS_DIRECTORY), { + filterFile: name => path.extname(name) === '.yaml', + prefixPath: DATA_ART_TAGS_DIRECTORY, + }), + ]).then(results => results + .filter(({status}) => status === 'fulfilled') + .flatMap(({value}) => value)), + + documentMode: allTogether, documentThing: ArtTag, save: (results) => ({artTagData: results}), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 7ed99a8e..2905d893 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -5,13 +5,24 @@ import {inspect} from 'node:util'; import CacheableObject from '#cacheable-object'; import {colors} from '#cli'; import {input} from '#composite'; -import {sortAlphabetically} from '#sort'; import {stitchArrays} from '#sugar'; import Thing from '#thing'; import {isName, validateArrayItems} from '#validators'; import {getKebabCase} from '#wiki-data'; +import {parseArtwork} from '#yaml'; import { + sortAlbumsTracksChronologically, + sortArtworksChronologically, + sortAlphabetically, + sortContributionsChronologically, +} from '#sort'; + +import {exitWithoutDependency, exposeConstant} from '#composite/control-flow'; +import {withReverseReferenceList} from '#composite/wiki-data'; + +import { + constitutibleArtwork, contentString, directory, fileExtension, @@ -22,7 +33,6 @@ import { soupyFind, soupyReverse, urls, - wikiData, } from '#composite/wiki-properties'; import {artistTotalDuration} from '#composite/things/artist'; @@ -31,7 +41,7 @@ export class Artist extends Thing { static [Thing.referenceType] = 'artist'; static [Thing.wikiDataArray] = 'artistData'; - static [Thing.getPropertyDescriptors] = ({Album, Flash, Group, Track}) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose name: name('Unnamed Artist'), @@ -43,6 +53,16 @@ export class Artist extends Thing { hasAvatar: flag(false), avatarFileExtension: fileExtension('jpg'), + avatarArtwork: [ + exitWithoutDependency({ + dependency: 'hasAvatar', + value: input.value(null), + }), + + constitutibleArtwork.fromYAMLFieldSpec + .call(this, 'Avatar Artwork'), + ], + aliasNames: { flags: {update: true, expose: true}, update: {validate: validateArrayItems(isName)}, @@ -63,6 +83,12 @@ export class Artist extends Thing { // Expose only + isArtist: [ + exposeConstant({ + value: input.value(true), + }), + ], + trackArtistContributions: reverseReferenceList({ reverse: soupyReverse.input('trackArtistContributionsBy'), }), @@ -83,6 +109,10 @@ export class Artist extends Thing { reverse: soupyReverse.input('albumArtistContributionsBy'), }), + albumTrackArtistContributions: reverseReferenceList({ + reverse: soupyReverse.input('albumTrackArtistContributionsBy'), + }), + albumCoverArtistContributions: reverseReferenceList({ reverse: soupyReverse.input('albumCoverArtistContributionsBy'), }), @@ -111,6 +141,102 @@ export class Artist extends Thing { reverse: soupyReverse.input('groupsCloselyLinkedTo'), }), + musicContributions: [ + withReverseReferenceList({ + reverse: soupyReverse.input('trackArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#trackArtistContribs', + }), + + withReverseReferenceList({ + reverse: soupyReverse.input('trackContributorContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#trackContributorContribs', + }), + + { + dependencies: [ + '#trackArtistContribs', + '#trackContributorContribs', + ], + + compute: (continuation, { + ['#trackArtistContribs']: trackArtistContribs, + ['#trackContributorContribs']: trackContributorContribs, + }) => continuation({ + ['#contributions']: [ + ...trackArtistContribs, + ...trackContributorContribs, + ], + }), + }, + + { + dependencies: ['#contributions'], + compute: ({'#contributions': contributions}) => + sortContributionsChronologically( + contributions, + sortAlbumsTracksChronologically), + }, + ], + + artworkContributions: [ + withReverseReferenceList({ + reverse: soupyReverse.input('trackCoverArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#trackCoverArtistContribs', + }), + + withReverseReferenceList({ + reverse: soupyReverse.input('albumCoverArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#albumCoverArtistContribs', + }), + + withReverseReferenceList({ + reverse: soupyReverse.input('albumWallpaperArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#albumWallpaperArtistContribs', + }), + + withReverseReferenceList({ + reverse: soupyReverse.input('albumBannerArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#albumBannerArtistContribs', + }), + + { + dependencies: [ + '#trackCoverArtistContribs', + '#albumCoverArtistContribs', + '#albumWallpaperArtistContribs', + '#albumBannerArtistContribs', + ], + + compute: (continuation, { + ['#trackCoverArtistContribs']: trackCoverArtistContribs, + ['#albumCoverArtistContribs']: albumCoverArtistContribs, + ['#albumWallpaperArtistContribs']: albumWallpaperArtistContribs, + ['#albumBannerArtistContribs']: albumBannerArtistContribs, + }) => continuation({ + ['#contributions']: [ + ...trackCoverArtistContribs, + ...albumCoverArtistContribs, + ...albumWallpaperArtistContribs, + ...albumBannerArtistContribs, + ], + }), + }, + + { + dependencies: ['#contributions'], + compute: ({'#contributions': contributions}) => + sortContributionsChronologically( + contributions, + sortArtworksChronologically), + }, + ], + totalDuration: artistTotalDuration(), }); @@ -193,6 +319,17 @@ export class Artist extends Thing { 'URLs': {property: 'urls'}, 'Context Notes': {property: 'contextNotes'}, + // note: doesn't really work as an independent field yet + 'Avatar Artwork': { + property: 'avatarArtwork', + transform: + parseArtwork({ + single: true, + thingProperty: 'avatarArtwork', + fileExtensionFromThingProperty: 'avatarFileExtension', + }), + }, + 'Has Avatar': {property: 'hasAvatar'}, 'Avatar File Extension': {property: 'avatarFileExtension'}, @@ -238,7 +375,12 @@ export class Artist extends Thing { const artistData = [...artists, ...artistAliases]; - return {artistData}; + const artworkData = + artistData + .filter(artist => artist.hasAvatar) + .map(artist => artist.avatarArtwork); + + return {artistData, artworkData}; }, sort({artistData}) { @@ -257,7 +399,7 @@ export class Artist extends Thing { let aliasedArtist; try { aliasedArtist = this.aliasedArtist.name; - } catch (_error) { + } catch { aliasedArtist = CacheableObject.getUpdateValue(this, 'aliasedArtist'); } @@ -266,4 +408,12 @@ export class Artist extends Thing { return parts.join(''); } + + getOwnArtworkPath(artwork) { + return [ + 'media.artistAvatar', + this.directory, + artwork.fileExtension, + ]; + } } diff --git a/src/data/things/artwork.js b/src/data/things/artwork.js new file mode 100644 index 00000000..916aac0a --- /dev/null +++ b/src/data/things/artwork.js @@ -0,0 +1,512 @@ +import {inspect} from 'node:util'; + +import {colors} from '#cli'; +import {input} from '#composite'; +import find from '#find'; +import Thing from '#thing'; + +import { + isContentString, + isContributionList, + isDate, + isDimensions, + isFileExtension, + optional, + validateArrayItems, + validateProperties, + validateReference, + validateReferenceList, +} from '#validators'; + +import { + parseAnnotatedReferences, + parseContributors, + parseDate, + parseDimensions, +} from '#yaml'; + +import {withPropertyFromList, withPropertyFromObject} from '#composite/data'; + +import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + +import { + withRecontextualizedContributionList, + withResolvedAnnotatedReferenceList, + withResolvedContribs, +} from '#composite/wiki-data'; + +import { + contentString, + directory, + flag, + reverseReferenceList, + simpleString, + soupyFind, + soupyReverse, + thing, + wikiData, +} from '#composite/wiki-properties'; + +import { + withArtTags, + withAttachedArtwork, + withContainingArtworkList, + withContentWarningArtTags, + withContribsFromAttachedArtwork, + withDate, +} from '#composite/things/artwork'; + +export class Artwork extends Thing { + static [Thing.referenceType] = 'artwork'; + + static [Thing.getPropertyDescriptors] = ({ArtTag}) => ({ + // Update & expose + + unqualifiedDirectory: directory({ + name: input.value(null), + }), + + thing: thing(), + thingProperty: simpleString(), + + label: simpleString(), + source: contentString(), + originDetails: contentString(), + showFilename: simpleString(), + + dateFromThingProperty: simpleString(), + + date: [ + withDate({ + from: input.updateValue({validate: isDate}), + }), + + exposeDependency({dependency: '#date'}), + ], + + fileExtensionFromThingProperty: simpleString(), + + fileExtension: [ + { + compute: (continuation) => continuation({ + ['#default']: 'jpg', + }), + }, + + exposeUpdateValueOrContinue({ + validate: input.value(isFileExtension), + }), + + exitWithoutDependency({ + dependency: 'thing', + value: '#default', + }), + + exitWithoutDependency({ + dependency: 'fileExtensionFromThingProperty', + value: '#default', + }), + + withPropertyFromObject({ + object: 'thing', + property: 'fileExtensionFromThingProperty', + }), + + exposeDependencyOrContinue({ + dependency: '#value', + }), + + exposeDependency({ + dependency: '#default', + }), + ], + + dimensionsFromThingProperty: simpleString(), + + dimensions: [ + exposeUpdateValueOrContinue({ + validate: input.value(isDimensions), + }), + + exitWithoutDependency({ + dependency: 'dimensionsFromThingProperty', + value: input.value(null), + }), + + withPropertyFromObject({ + object: 'thing', + property: 'dimensionsFromThingProperty', + }).outputs({ + ['#value']: '#dimensionsFromThing', + }), + + exitWithoutDependency({ + dependency: 'dimensionsFromThingProperty', + value: input.value(null), + }), + + exposeDependencyOrContinue({ + dependency: '#dimensionsFromThing', + }), + + exposeConstant({ + value: input.value(null), + }), + ], + + attachAbove: flag(false), + + artistContribsFromThingProperty: simpleString(), + artistContribsArtistProperty: simpleString(), + + artistContribs: [ + withDate(), + + withResolvedContribs({ + from: input.updateValue({validate: isContributionList}), + date: '#date', + thingProperty: input.thisProperty(), + artistProperty: 'artistContribsArtistProperty', + }), + + exposeDependencyOrContinue({ + dependency: '#resolvedContribs', + mode: input.value('empty'), + }), + + withContribsFromAttachedArtwork(), + + exposeDependencyOrContinue({ + dependency: '#attachedArtwork.artistContribs', + }), + + exitWithoutDependency({ + dependency: 'artistContribsFromThingProperty', + value: input.value([]), + }), + + withPropertyFromObject({ + object: 'thing', + property: 'artistContribsFromThingProperty', + }).outputs({ + ['#value']: '#artistContribs', + }), + + withRecontextualizedContributionList({ + list: '#artistContribs', + }), + + exposeDependency({ + dependency: '#artistContribs', + }), + ], + + style: simpleString(), + + artTagsFromThingProperty: simpleString(), + + artTags: [ + withArtTags({ + from: input.updateValue({ + validate: + validateReferenceList(ArtTag[Thing.referenceType]), + }), + }), + + exposeDependency({ + dependency: '#artTags', + }), + ], + + referencedArtworksFromThingProperty: simpleString(), + + referencedArtworks: [ + { + compute: (continuation) => continuation({ + ['#find']: + find.mixed({ + track: find.trackPrimaryArtwork, + album: find.albumPrimaryArtwork, + }), + }), + }, + + withResolvedAnnotatedReferenceList({ + list: input.updateValue({ + validate: + // TODO: It's annoying to hardcode this when it's really the + // same behavior as through annotatedReferenceList and through + // referenceListUpdateDescription, the latter of which isn't + // available outside of #composite/wiki-data internals. + validateArrayItems( + validateProperties({ + reference: validateReference(['album', 'track']), + annotation: optional(isContentString), + })), + }), + + data: 'artworkData', + find: '#find', + + thing: input.value('artwork'), + }), + + exposeDependencyOrContinue({ + dependency: '#resolvedAnnotatedReferenceList', + mode: input.value('empty'), + }), + + exitWithoutDependency({ + dependency: 'referencedArtworksFromThingProperty', + value: input.value([]), + }), + + withPropertyFromObject({ + object: 'thing', + property: 'referencedArtworksFromThingProperty', + }).outputs({ + ['#value']: '#referencedArtworks', + }), + + exposeDependencyOrContinue({ + dependency: '#referencedArtworks', + }), + + exposeConstant({ + value: input.value([]), + }), + ], + + // Update only + + find: soupyFind(), + reverse: soupyReverse(), + + // used for referencedArtworks (mixedFind) + artworkData: wikiData({ + class: input.value(Artwork), + }), + + // Expose only + + isArtwork: [ + exposeConstant({ + value: input.value(true), + }), + ], + + referencedByArtworks: reverseReferenceList({ + reverse: soupyReverse.input('artworksWhichReference'), + }), + + isMainArtwork: [ + withContainingArtworkList(), + + exitWithoutDependency({ + dependency: '#containingArtworkList', + value: input.value(null), + }), + + { + dependencies: [input.myself(), '#containingArtworkList'], + compute: ({ + [input.myself()]: myself, + ['#containingArtworkList']: list, + }) => + list[0] === myself, + }, + ], + + mainArtwork: [ + withContainingArtworkList(), + + exitWithoutDependency({ + dependency: '#containingArtworkList', + value: input.value(null), + }), + + { + dependencies: ['#containingArtworkList'], + compute: ({'#containingArtworkList': list}) => + list[0], + }, + ], + + attachedArtwork: [ + withAttachedArtwork(), + + exposeDependency({ + dependency: '#attachedArtwork', + }), + ], + + attachingArtworks: reverseReferenceList({ + reverse: soupyReverse.input('artworksWhichAttach'), + }), + + groups: [ + withPropertyFromObject({ + object: 'thing', + property: input.value('groups'), + }), + + exposeDependencyOrContinue({ + dependency: '#thing.groups', + }), + + exposeConstant({ + value: input.value([]), + }), + ], + + contentWarningArtTags: [ + withContentWarningArtTags(), + + exposeDependency({ + dependency: '#contentWarningArtTags', + }), + ], + + contentWarnings: [ + withContentWarningArtTags(), + + withPropertyFromList({ + list: '#contentWarningArtTags', + property: input.value('name'), + }), + + exposeDependency({ + dependency: '#contentWarningArtTags.name', + }), + ], + }); + + static [Thing.yamlDocumentSpec] = { + fields: { + 'Directory': {property: 'unqualifiedDirectory'}, + 'File Extension': {property: 'fileExtension'}, + + 'Dimensions': { + property: 'dimensions', + transform: parseDimensions, + }, + + 'Label': {property: 'label'}, + 'Source': {property: 'source'}, + 'Origin Details': {property: 'originDetails'}, + 'Show Filename': {property: 'showFilename'}, + + 'Date': { + property: 'date', + transform: parseDate, + }, + + 'Attach Above': {property: 'attachAbove'}, + + 'Artists': { + property: 'artistContribs', + transform: parseContributors, + }, + + 'Style': {property: 'style'}, + + 'Tags': {property: 'artTags'}, + + 'Referenced Artworks': { + property: 'referencedArtworks', + transform: parseAnnotatedReferences, + }, + }, + }; + + static [Thing.reverseSpecs] = { + artworksWhichReference: { + bindTo: 'artworkData', + + referencing: referencingArtwork => + referencingArtwork.referencedArtworks + .map(({artwork: referencedArtwork, ...referenceDetails}) => ({ + referencingArtwork, + referencedArtwork, + referenceDetails, + })), + + referenced: ({referencedArtwork}) => [referencedArtwork], + + tidy: ({referencingArtwork, referenceDetails}) => ({ + artwork: referencingArtwork, + ...referenceDetails, + }), + + date: ({artwork}) => artwork.date, + }, + + artworksWhichAttach: { + bindTo: 'artworkData', + + referencing: referencingArtwork => + (referencingArtwork.attachAbove + ? [referencingArtwork] + : []), + + referenced: referencingArtwork => + [referencingArtwork.attachedArtwork], + }, + + artworksWhichFeature: { + bindTo: 'artworkData', + + referencing: artwork => [artwork], + referenced: artwork => artwork.artTags, + }, + }; + + get path() { + if (!this.thing) return null; + if (!this.thing.getOwnArtworkPath) return null; + + return this.thing.getOwnArtworkPath(this); + } + + countOwnContributionInContributionTotals(contrib) { + if (this.attachAbove) { + return false; + } + + if (contrib.annotation?.startsWith('edits for wiki')) { + return false; + } + + return true; + } + + [inspect.custom](depth, options, inspect) { + const parts = []; + + parts.push(Thing.prototype[inspect.custom].apply(this)); + + if (this.thing) { + if (depth >= 0) { + const newOptions = { + ...options, + depth: + (options.depth === null + ? null + : options.depth - 1), + }; + + parts.push(` for ${inspect(this.thing, newOptions)}`); + } else { + parts.push(` for ${colors.blue(Thing.getReference(this.thing))}`); + } + } + + return parts.join(''); + } +} diff --git a/src/data/things/content.js b/src/data/things/content.js new file mode 100644 index 00000000..a3dfc183 --- /dev/null +++ b/src/data/things/content.js @@ -0,0 +1,247 @@ +import {input} from '#composite'; +import Thing from '#thing'; +import {is, isDate} from '#validators'; +import {parseDate} from '#yaml'; + +import {contentString, simpleDate, soupyFind, thing} + from '#composite/wiki-properties'; + +import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, + withResultOfAvailabilityCheck, +} from '#composite/control-flow'; + +import { + contentArtists, + hasAnnotationPart, + withAnnotationParts, + withHasAnnotationPart, + withSourceText, + withSourceURLs, + withWebArchiveDate, +} from '#composite/things/content'; + +export class ContentEntry extends Thing { + static [Thing.getPropertyDescriptors] = () => ({ + // Update & expose + + thing: thing(), + + artists: contentArtists(), + + artistText: contentString(), + + annotation: contentString(), + + dateKind: { + flags: {update: true, expose: true}, + + update: { + validate: is(...[ + 'sometime', + 'throughout', + 'around', + ]), + }, + }, + + accessKind: [ + exitWithoutDependency({ + dependency: 'accessDate', + }), + + exposeUpdateValueOrContinue({ + validate: input.value( + is(...[ + 'captured', + 'accessed', + ])), + }), + + withWebArchiveDate(), + + withResultOfAvailabilityCheck({ + from: '#webArchiveDate', + }), + + { + dependencies: ['#availability'], + compute: (continuation, {['#availability']: availability}) => + (availability + ? continuation.exit('captured') + : continuation()), + }, + + exposeConstant({ + value: input.value('accessed'), + }), + ], + + date: simpleDate(), + + secondDate: simpleDate(), + + accessDate: [ + exposeUpdateValueOrContinue({ + validate: input.value(isDate), + }), + + withWebArchiveDate(), + + exposeDependencyOrContinue({ + dependency: '#webArchiveDate', + }), + + exposeConstant({ + value: input.value(null), + }), + ], + + body: contentString(), + + // Update only + + find: soupyFind(), + + // Expose only + + isContentEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + + annotationParts: [ + withAnnotationParts({ + mode: input.value('strings'), + }), + + exposeDependency({dependency: '#annotationParts'}), + ], + + sourceText: [ + withSourceText(), + exposeDependency({dependency: '#sourceText'}), + ], + + sourceURLs: [ + withSourceURLs(), + exposeDependency({dependency: '#sourceURLs'}), + ], + }); + + static [Thing.yamlDocumentSpec] = { + fields: { + 'Artists': {property: 'artists'}, + 'Artist Text': {property: 'artistText'}, + + 'Annotation': {property: 'annotation'}, + + 'Date Kind': {property: 'dateKind'}, + 'Access Kind': {property: 'accessKind'}, + + 'Date': {property: 'date', transform: parseDate}, + 'Second Date': {property: 'secondDate', transform: parseDate}, + 'Access Date': {property: 'accessDate', transform: parseDate}, + + 'Body': {property: 'body'}, + }, + }; +} + +export class CommentaryEntry extends ContentEntry { + static [Thing.getPropertyDescriptors] = () => ({ + // Expose only + + isCommentaryEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + + isWikiEditorCommentary: hasAnnotationPart({ + part: input.value('wiki editor'), + }), + }); +} + +export class LyricsEntry extends ContentEntry { + static [Thing.getPropertyDescriptors] = () => ({ + // Update & expose + + originDetails: contentString(), + + // Expose only + + isLyricsEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + + isWikiLyrics: hasAnnotationPart({ + part: input.value('wiki lyrics'), + }), + + helpNeeded: hasAnnotationPart({ + part: input.value('help needed'), + }), + + hasSquareBracketAnnotations: [ + withHasAnnotationPart({ + part: input.value('wiki lyrics'), + }), + + exitWithoutDependency({ + dependency: '#hasAnnotationPart', + mode: input.value('falsy'), + value: input.value(false), + }), + + exitWithoutDependency({ + dependency: 'body', + value: input.value(false), + }), + + { + dependencies: ['body'], + compute: ({body}) => + /\[.*\]/m.test(body), + }, + ], + }); + + static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(ContentEntry, { + fields: { + 'Origin Details': {property: 'originDetails'}, + }, + }); +} + +export class CreditingSourcesEntry extends ContentEntry { + static [Thing.getPropertyDescriptors] = () => ({ + // Expose only + + isCreditingSourcesEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + }); +} + +export class ReferencingSourcesEntry extends ContentEntry { + static [Thing.getPropertyDescriptors] = () => ({ + // Expose only + + isReferencingSourceEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + }); +} diff --git a/src/data/things/contribution.js b/src/data/things/contribution.js index c92fafb4..e1e248cb 100644 --- a/src/data/things/contribution.js +++ b/src/data/things/contribution.js @@ -5,10 +5,18 @@ import {colors} from '#cli'; import {input} from '#composite'; import {empty} from '#sugar'; import Thing from '#thing'; -import {isStringNonEmpty, isThing, validateReference} from '#validators'; +import {isBoolean, isStringNonEmpty, isThing, validateReference} + from '#validators'; -import {exitWithoutDependency, exposeDependency} from '#composite/control-flow'; -import {flag, simpleDate, soupyFind} from '#composite/wiki-properties'; +import {simpleDate, soupyFind} from '#composite/wiki-properties'; + +import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; import { withFilteredList, @@ -19,8 +27,6 @@ import { import { inheritFromContributionPresets, - thingPropertyMatches, - thingReferenceTypeMatches, withContainingReverseContributionList, withContributionArtist, withContributionContext, @@ -70,7 +76,26 @@ export class Contribution extends Thing { property: input.thisProperty(), }), - flag(true), + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + { + dependencies: ['thing', input.myself()], + compute: (continuation, { + ['thing']: thing, + [input.myself()]: contribution, + }) => + (thing.countOwnContributionInContributionTotals?.(contribution) + ? true + : thing.countOwnContributionInContributionTotals + ? false + : continuation()), + }, + + exposeConstant({ + value: input.value(true), + }), ], countInDurationTotals: [ @@ -78,7 +103,37 @@ export class Contribution extends Thing { property: input.thisProperty(), }), - flag(true), + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withPropertyFromObject({ + object: 'thing', + property: input.value('duration'), + }), + + exitWithoutDependency({ + dependency: '#thing.duration', + mode: input.value('falsy'), + value: input.value(false), + }), + + { + dependencies: ['thing', input.myself()], + compute: (continuation, { + ['thing']: thing, + [input.myself()]: contribution, + }) => + (thing.countOwnContributionInDurationTotals?.(contribution) + ? true + : thing.countOwnContributionInDurationTotals + ? false + : continuation()), + }, + + exposeConstant({ + value: input.value(true), + }), ], // Update only @@ -87,6 +142,12 @@ export class Contribution extends Thing { // Expose only + isContribution: [ + exposeConstant({ + value: input.value(true), + }), + ], + context: [ withContributionContext(), @@ -167,38 +228,6 @@ export class Contribution extends Thing { }), ], - isArtistContribution: thingPropertyMatches({ - value: input.value('artistContribs'), - }), - - isContributorContribution: thingPropertyMatches({ - value: input.value('contributorContribs'), - }), - - isCoverArtistContribution: thingPropertyMatches({ - value: input.value('coverArtistContribs'), - }), - - isBannerArtistContribution: thingPropertyMatches({ - value: input.value('bannerArtistContribs'), - }), - - isWallpaperArtistContribution: thingPropertyMatches({ - value: input.value('wallpaperArtistContribs'), - }), - - isForTrack: thingReferenceTypeMatches({ - value: input.value('track'), - }), - - isForAlbum: thingReferenceTypeMatches({ - value: input.value('album'), - }), - - isForFlash: thingReferenceTypeMatches({ - value: input.value('flash'), - }), - previousBySameArtist: [ withContainingReverseContributionList().outputs({ '#containingReverseContributionList': '#list', @@ -238,6 +267,21 @@ export class Contribution extends Thing { dependency: '#nearbyItem', }), ], + + groups: [ + withPropertyFromObject({ + object: 'thing', + property: input.value('groups'), + }), + + exposeDependencyOrContinue({ + dependency: '#thing.groups', + }), + + exposeConstant({ + value: input.value([]), + }), + ], }); [inspect.custom](depth, options, inspect) { @@ -259,7 +303,7 @@ export class Contribution extends Thing { let artist; try { artist = this.artist; - } catch (_error) { + } catch { // Computing artist might crash for any reason - don't distract from // other errors as a result of inspecting this contribution. } diff --git a/src/data/things/flash.js b/src/data/things/flash.js index fe1d17ff..73b22746 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -6,8 +6,16 @@ import {sortFlashesChronologically} from '#sort'; import Thing from '#thing'; import {anyOf, isColor, isContentString, isDirectory, isNumber, isString} from '#validators'; -import {parseAdditionalNames, parseContributors, parseDate, parseDimensions} - from '#yaml'; + +import { + parseArtwork, + parseAdditionalNames, + parseCommentary, + parseContributors, + parseCreditingSources, + parseDate, + parseDimensions, +} from '#yaml'; import {withPropertyFromObject} from '#composite/data'; @@ -19,10 +27,9 @@ import { } from '#composite/control-flow'; import { - additionalNameList, color, - commentary, commentatorArtists, + constitutibleArtwork, contentString, contributionList, dimensions, @@ -34,8 +41,8 @@ import { soupyFind, soupyReverse, thing, + thingList, urls, - wikiData, } from '#composite/wiki-properties'; import {withFlashAct} from '#composite/things/flash'; @@ -45,8 +52,10 @@ export class Flash extends Thing { static [Thing.referenceType] = 'flash'; static [Thing.getPropertyDescriptors] = ({ + AdditionalName, + CommentaryEntry, + CreditingSourcesEntry, Track, - FlashAct, WikiInfo, }) => ({ // Update & expose @@ -100,6 +109,10 @@ export class Flash extends Thing { coverArtDimensions: dimensions(), + coverArtwork: + constitutibleArtwork.fromYAMLFieldSpec + .call(this, 'Cover Artwork'), + contributorContribs: contributionList({ date: 'date', artistProperty: input.value('flashContributorContributions'), @@ -112,10 +125,17 @@ export class Flash extends Thing { urls: urls(), - additionalNames: additionalNameList(), + additionalNames: thingList({ + class: input.value(AdditionalName), + }), + + commentary: thingList({ + class: input.value(CommentaryEntry), + }), - commentary: commentary(), - creditSources: commentary(), + creditingSources: thingList({ + class: input.value(CreditingSourcesEntry), + }), // Update only @@ -129,6 +149,12 @@ export class Flash extends Thing { // Expose only + isFlash: [ + exposeConstant({ + value: input.value(true), + }), + ], + commentatorArtists: commentatorArtists(), act: [ @@ -205,6 +231,17 @@ export class Flash extends Thing { transform: parseAdditionalNames, }, + 'Cover Artwork': { + property: 'coverArtwork', + transform: + parseArtwork({ + single: true, + thingProperty: 'coverArtwork', + fileExtensionFromThingProperty: 'coverArtFileExtension', + dimensionsFromThingProperty: 'coverArtDimensions', + }), + }, + 'Cover Art File Extension': {property: 'coverArtFileExtension'}, 'Cover Art Dimensions': { @@ -219,12 +256,27 @@ export class Flash extends Thing { transform: parseContributors, }, - 'Commentary': {property: 'commentary'}, - 'Credit Sources': {property: 'creditSources'}, + 'Commentary': { + property: 'commentary', + transform: parseCommentary, + }, + + 'Crediting Sources': { + property: 'creditingSources', + transform: parseCreditingSources, + }, 'Review Points': {ignore: true}, }, }; + + getOwnArtworkPath(artwork) { + return [ + 'media.flashArt', + this.directory, + artwork.fileExtension, + ]; + } } export class FlashAct extends Thing { @@ -271,6 +323,12 @@ export class FlashAct extends Thing { // Expose only + isFlashAct: [ + exposeConstant({ + value: input.value(true), + }), + ], + side: [ withFlashSide(), exposeDependency({dependency: '#flashSide'}), @@ -326,6 +384,14 @@ export class FlashSide extends Thing { // Update only find: soupyFind(), + + // Expose only + + isFlashSide: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { @@ -411,7 +477,19 @@ export class FlashSide extends Thing { const flashActData = results.filter(x => x instanceof FlashAct); const flashSideData = results.filter(x => x instanceof FlashSide); - return {flashData, flashActData, flashSideData}; + const artworkData = flashData.map(flash => flash.coverArtwork); + const commentaryData = flashData.flatMap(flash => flash.commentary); + const creditingSourceData = flashData.flatMap(flash => flash.creditingSources); + + return { + flashData, + flashActData, + flashSideData, + + artworkData, + commentaryData, + creditingSourceData, + }; }, sort({flashData}) { diff --git a/src/data/things/group.js b/src/data/things/group.js index ed3c59bb..0935dc93 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -1,31 +1,73 @@ export const GROUP_DATA_FILE = 'groups.yaml'; +import {inspect} from 'node:util'; + +import {colors} from '#cli'; import {input} from '#composite'; import Thing from '#thing'; +import {is, isBoolean} from '#validators'; import {parseAnnotatedReferences, parseSerieses} from '#yaml'; +import {withPropertyFromObject} from '#composite/data'; +import {withUniqueReferencingThing} from '#composite/wiki-data'; + +import { + exposeConstant, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + import { annotatedReferenceList, color, contentString, directory, + flag, name, referenceList, - seriesList, soupyFind, + soupyReverse, + thing, + thingList, urls, - wikiData, } from '#composite/wiki-properties'; export class Group extends Thing { static [Thing.referenceType] = 'group'; - static [Thing.getPropertyDescriptors] = ({Album, Artist}) => ({ + static [Thing.getPropertyDescriptors] = ({Album, Artist, Series}) => ({ // Update & expose name: name('Unnamed Group'), directory: directory(), + excludeFromGalleryTabs: [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withUniqueReferencingThing({ + reverse: soupyReverse.input('groupCategoriesWhichInclude'), + }).outputs({ + '#uniqueReferencingThing': '#category', + }), + + withPropertyFromObject({ + object: '#category', + property: input.value('excludeGroupsFromGalleryTabs'), + }), + + exposeDependencyOrContinue({ + dependency: '#category.excludeGroupsFromGalleryTabs', + }), + + exposeConstant({ + value: input.value(false), + }), + ], + + divideAlbumsByStyle: flag(false), + description: contentString(), urls: urls(), @@ -34,8 +76,6 @@ export class Group extends Thing { class: input.value(Artist), find: soupyFind.input('artist'), - date: input.value(null), - reference: input.value('artist'), thing: input.value('artist'), }), @@ -45,17 +85,23 @@ export class Group extends Thing { find: soupyFind.input('album'), }), - serieses: seriesList({ - group: input.myself(), + serieses: thingList({ + class: input.value(Series), }), // Update only find: soupyFind(), - reverse: soupyFind(), + reverse: soupyReverse(), // Expose only + isGroup: [ + exposeConstant({ + value: input.value(true), + }), + ], + descriptionShort: { flags: {expose: true}, @@ -131,6 +177,10 @@ export class Group extends Thing { fields: { 'Group': {property: 'name'}, 'Directory': {property: 'directory'}, + + 'Exclude From Gallery Tabs': {property: 'excludeFromGalleryTabs'}, + 'Divide Albums By Style': {property: 'divideAlbumsByStyle'}, + 'Description': {property: 'description'}, 'URLs': {property: 'urls'}, @@ -194,8 +244,9 @@ export class Group extends Thing { const groupData = results.filter(x => x instanceof Group); const groupCategoryData = results.filter(x => x instanceof GroupCategory); + const seriesData = groupData.flatMap(group => group.serieses); - return {groupData, groupCategoryData}; + return {groupData, groupCategoryData, seriesData}; }, // Groups aren't sorted at all, always preserving the order in the data @@ -214,6 +265,8 @@ export class GroupCategory extends Thing { name: name('Unnamed Group Category'), directory: directory(), + excludeGroupsFromGalleryTabs: flag(false), + color: color(), groups: referenceList({ @@ -224,6 +277,14 @@ export class GroupCategory extends Thing { // Update only find: soupyFind(), + + // Expose only + + isGroupCategory: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.reverseSpecs] = { @@ -238,7 +299,82 @@ export class GroupCategory extends Thing { static [Thing.yamlDocumentSpec] = { fields: { 'Category': {property: 'name'}, + 'Color': {property: 'color'}, + + 'Exclude Groups From Gallery Tabs': { + property: 'excludeGroupsFromGalleryTabs', + }, }, }; } + +export class Series extends Thing { + static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({ + // Update & expose + + name: name('Unnamed Series'), + + showAlbumArtists: { + flags: {update: true, expose: true}, + update: { + validate: + is('all', 'differing', 'none'), + }, + }, + + description: contentString(), + + group: thing({ + class: input.value(Group), + }), + + albums: referenceList({ + class: input.value(Album), + find: soupyFind.input('album'), + }), + + // Update only + + find: soupyFind(), + }); + + static [Thing.yamlDocumentSpec] = { + fields: { + 'Name': {property: 'name'}, + + 'Description': {property: 'description'}, + + 'Show Album Artists': {property: 'showAlbumArtists'}, + + 'Albums': {property: 'albums'}, + }, + }; + + [inspect.custom](depth, options, inspect) { + const parts = []; + + parts.push(Thing.prototype[inspect.custom].apply(this)); + + if (depth >= 0) showGroup: { + let group = null; + try { + group = this.group; + } catch { + break showGroup; + } + + const groupName = group.name; + const groupIndex = group.serieses.indexOf(this); + + const num = + (groupIndex === -1 + ? 'indeterminate position' + : `#${groupIndex + 1}`); + + parts.push(` (${colors.yellow(num)} in ${colors.green(`"${groupName}"`)})`); + } + + return parts.join(''); + } +} diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 82bad2d3..2456ca95 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -17,7 +17,7 @@ import { validateReference, } from '#validators'; -import {exposeDependency} from '#composite/control-flow'; +import {exposeConstant, exposeDependency} from '#composite/control-flow'; import {withResolvedReference} from '#composite/wiki-data'; import { @@ -47,6 +47,14 @@ export class HomepageLayout extends Thing { sections: thingList({ class: input.value(HomepageLayoutSection), }), + + // Expose only + + isHomepageLayout: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { @@ -63,7 +71,6 @@ export class HomepageLayout extends Thing { thingConstructors: { HomepageLayout, HomepageLayoutSection, - HomepageLayoutAlbumsRow, }, }) => ({ title: `Process homepage layout file`, @@ -157,6 +164,14 @@ export class HomepageLayoutSection extends Thing { rows: thingList({ class: input.value(HomepageLayoutRow), }), + + // Expose only + + isHomepageLayoutSection: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { @@ -183,6 +198,12 @@ export class HomepageLayoutRow extends Thing { // Expose only + isHomepageLayoutRow: [ + exposeConstant({ + value: input.value(true), + }), + ], + type: { flags: {expose: true}, @@ -234,6 +255,12 @@ export class HomepageLayoutActionsRow extends HomepageLayoutRow { // Expose only + isHomepageLayoutActionsRow: [ + exposeConstant({ + value: input.value(true), + }), + ], + type: { flags: {expose: true}, expose: {compute: () => 'actions'}, @@ -250,7 +277,7 @@ export class HomepageLayoutActionsRow extends HomepageLayoutRow { export class HomepageLayoutAlbumCarouselRow extends HomepageLayoutRow { static [Thing.friendlyName] = `Homepage Album Carousel Row`; - static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({ + static [Thing.getPropertyDescriptors] = (opts, {Album} = opts) => ({ ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts), // Update & expose @@ -262,6 +289,12 @@ export class HomepageLayoutAlbumCarouselRow extends HomepageLayoutRow { // Expose only + isHomepageLayoutAlbumCarouselRow: [ + exposeConstant({ + value: input.value(true), + }), + ], + type: { flags: {expose: true}, expose: {compute: () => 'album carousel'}, @@ -322,6 +355,12 @@ export class HomepageLayoutAlbumGridRow extends HomepageLayoutRow { // Expose only + isHomepageLayoutAlbumGridRow: [ + exposeConstant({ + value: input.value(true), + }), + ], + type: { flags: {expose: true}, expose: {compute: () => 'album grid'}, diff --git a/src/data/things/index.js b/src/data/things/index.js index 17471f31..11307b50 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -9,9 +9,13 @@ import * as serialize from '#serialize'; import {withEntries} from '#sugar'; import Thing from '#thing'; +import * as additionalFileClasses from './additional-file.js'; +import * as additionalNameClasses from './additional-name.js'; import * as albumClasses from './album.js'; import * as artTagClasses from './art-tag.js'; import * as artistClasses from './artist.js'; +import * as artworkClasses from './artwork.js'; +import * as contentClasses from './content.js'; import * as contributionClasses from './contribution.js'; import * as flashClasses from './flash.js'; import * as groupClasses from './group.js'; @@ -24,9 +28,13 @@ import * as trackClasses from './track.js'; import * as wikiInfoClasses from './wiki-info.js'; const allClassLists = { + 'additional-file.js': additionalFileClasses, + 'additional-name.js': additionalNameClasses, 'album.js': albumClasses, 'art-tag.js': artTagClasses, 'artist.js': artistClasses, + 'artwork.js': artworkClasses, + 'content.js': contentClasses, 'contribution.js': contributionClasses, 'flash.js': flashClasses, 'group.js': groupClasses, diff --git a/src/data/things/language.js b/src/data/things/language.js index a3f861bd..08c52cb8 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,8 +1,9 @@ -import { Temporal, toTemporalInstant } from '@js-temporal/polyfill'; +import {Temporal, toTemporalInstant} from '@js-temporal/polyfill'; import {withAggregate} from '#aggregate'; import CacheableObject from '#cacheable-object'; import {logWarn} from '#cli'; +import {input} from '#composite'; import * as html from '#html'; import {empty} from '#sugar'; import {isLanguageCode} from '#validators'; @@ -16,6 +17,7 @@ import { isExternalLinkStyle, } from '#external-links'; +import {exposeConstant} from '#composite/control-flow'; import {externalFunction, flag, name} from '#composite/wiki-properties'; export const languageOptionRegex = /{(?<name>[A-Z0-9_]+)}/g; @@ -127,6 +129,12 @@ export class Language extends Thing { // Expose only + isLanguage: [ + exposeConstant({ + value: input.value(true), + }), + ], + onlyIfOptions: { flags: {expose: true}, expose: { @@ -135,6 +143,7 @@ export class Language extends Thing { }, intl_date: this.#intlHelper(Intl.DateTimeFormat, {full: true}), + intl_dateYear: this.#intlHelper(Intl.DateTimeFormat, {year: 'numeric'}), intl_number: this.#intlHelper(Intl.NumberFormat), intl_listConjunction: this.#intlHelper(Intl.ListFormat, {type: 'conjunction'}), intl_listDisjunction: this.#intlHelper(Intl.ListFormat, {type: 'disjunction'}), @@ -203,6 +212,10 @@ export class Language extends Thing { } formatString(...args) { + if (typeof args.at(-1) === 'function') { + throw new Error(`Passed function - did you mean language.encapsulate() instead?`); + } + const hasOptions = typeof args.at(-1) === 'object' && args.at(-1) !== null; @@ -309,7 +322,7 @@ export class Language extends Thing { return undefined; } - return optionValue; + return this.sanitize(optionValue); }, }); @@ -374,26 +387,16 @@ export class Language extends Thing { partInProgress += template.slice(lastIndex, match.index); - // Sanitize string arguments in particular. These are taken to come from - // (raw) data and may include special characters that aren't meant to be - // rendered as HTML markup. - const sanitizedInsertion = - this.#sanitizeValueForInsertion(insertion); - - if (typeof sanitizedInsertion === 'string') { - // Join consecutive strings together. - partInProgress += sanitizedInsertion; - } else if ( - sanitizedInsertion instanceof html.Tag && - sanitizedInsertion.contentOnly - ) { - // Collapse string-only tag contents onto the current string part. - partInProgress += sanitizedInsertion.toString(); - } else { - // Push the string part in progress, then the insertion as-is. - outputParts.push(partInProgress); - outputParts.push(sanitizedInsertion); - partInProgress = ''; + for (const insertionItem of html.smush(insertion).content) { + if (typeof insertionItem === 'string') { + // Join consecutive strings together. + partInProgress += insertionItem; + } else { + // Push the string part in progress, then the insertion as-is. + outputParts.push(partInProgress); + outputParts.push(insertionItem); + partInProgress = ''; + } } lastIndex = match.index + match[0].length; @@ -488,22 +491,44 @@ export class Language extends Thing { // or both are undefined, that's just blank content. const hasStart = startDate !== null && startDate !== undefined; const hasEnd = endDate !== null && endDate !== undefined; - if (!hasStart || !hasEnd) { - if (startDate === endDate) { - return html.blank(); - } else if (hasStart) { - throw new Error(`Expected both start and end of date range, got only start`); - } else if (hasEnd) { - throw new Error(`Expected both start and end of date range, got only end`); - } else { - throw new Error(`Got mismatched ${startDate}/${endDate} for start and end`); - } + if (!hasStart && !hasEnd) { + return html.blank(); + } else if (hasStart && !hasEnd) { + throw new Error(`Expected both start and end of date range, got only start`); + } else if (!hasStart && hasEnd) { + throw new Error(`Expected both start and end of date range, got only end`); } this.assertIntlAvailable('intl_date'); return this.intl_date.formatRange(startDate, endDate); } + formatYear(date) { + if (date === null || date === undefined) { + return html.blank(); + } + + this.assertIntlAvailable('intl_dateYear'); + return this.intl_dateYear.format(date); + } + + formatYearRange(startDate, endDate) { + // formatYearRange expects both values to be present, but if both are null + // or both are undefined, that's just blank content. + const hasStart = startDate !== null && startDate !== undefined; + const hasEnd = endDate !== null && endDate !== undefined; + if (!hasStart && !hasEnd) { + return html.blank(); + } else if (hasStart && !hasEnd) { + throw new Error(`Expected both start and end of date range, got only start`); + } else if (!hasStart && hasEnd) { + throw new Error(`Expected both start and end of date range, got only end`); + } + + this.assertIntlAvailable('intl_dateYear'); + return this.intl_dateYear.formatRange(startDate, endDate); + } + formatDateDuration({ years: numYears = 0, months: numMonths = 0, @@ -842,6 +867,18 @@ export class Language extends Thing { } } + typicallyLowerCase(string) { + // Utter nonsense implementation, so this only works on strings, + // not actual HTML content, and may rudely disrespect *intentful* + // capitalization of whatever goes into it. + + if (typeof string !== 'string') return string; + if (string.length <= 1) return string; + if (/^\S+?[A-Z]/.test(string)) return string; + + return string[0].toLowerCase() + string.slice(1); + } + // Utility function to quickly provide a useful string key // (generally a prefix) to stuff nested beneath it. encapsulate(...args) { @@ -900,7 +937,6 @@ Object.assign(Language.prototype, { countArtworks: countHelper('artworks'), countCommentaryEntries: countHelper('commentaryEntries', 'entries'), countContributions: countHelper('contributions'), - countCoverArts: countHelper('coverArts'), countDays: countHelper('days'), countFlashes: countHelper('flashes'), countMonths: countHelper('months'), diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index 43d1638e..28289f53 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -1,9 +1,11 @@ export const NEWS_DATA_FILE = 'news.yaml'; +import {input} from '#composite'; import {sortChronologically} from '#sort'; import Thing from '#thing'; import {parseDate} from '#yaml'; +import {exposeConstant} from '#composite/control-flow'; import {contentString, directory, name, simpleDate} from '#composite/wiki-properties'; @@ -22,6 +24,12 @@ export class NewsEntry extends Thing { // Expose only + isNewsEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + contentShort: { flags: {expose: true}, diff --git a/src/data/things/sorting-rule.js b/src/data/things/sorting-rule.js index b169a541..8ed3861a 100644 --- a/src/data/things/sorting-rule.js +++ b/src/data/things/sorting-rule.js @@ -22,6 +22,7 @@ import { reorderDocumentsInYAMLSourceText, } from '#yaml'; +import {exposeConstant} from '#composite/control-flow'; import {flag} from '#composite/wiki-properties'; function isSelectFollowingEntry(value) { @@ -47,6 +48,14 @@ export class SortingRule extends Thing { flags: {update: true, expose: true}, update: {validate: isStringNonEmpty}, }, + + // Expose only + + isSortingRule: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { @@ -119,6 +128,14 @@ export class ThingSortingRule extends SortingRule { validate: strictArrayOf(isStringNonEmpty), }, }, + + // Expose only + + isThingSortingRule: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(SortingRule, { @@ -129,7 +146,7 @@ export class ThingSortingRule extends SortingRule { sort(sortable) { if (this.properties) { - for (const property of this.properties.slice().reverse()) { + for (const property of this.properties.toReversed()) { const get = thing => thing[property]; const lc = property.toLowerCase(); @@ -218,6 +235,14 @@ export class DocumentSortingRule extends ThingSortingRule { flags: {update: true, expose: true}, update: {validate: isStringNonEmpty}, }, + + // Expose only + + isDocumentSortingRule: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(ThingSortingRule, { @@ -261,10 +286,8 @@ export class DocumentSortingRule extends ThingSortingRule { } static async* applyAll(rules, {wikiData, dataPath, dry}) { - rules = - rules - .slice() - .sort((a, b) => a.filename.localeCompare(b.filename, 'en')); + rules = rules + .toSorted((a, b) => a.filename.localeCompare(b.filename, 'en')); for (const {chunk, filename} of chunkByProperties(rules, ['filename'])) { const initialLayout = getThingLayoutForFilename(filename, wikiData); diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index 52a09c31..28167df2 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -2,11 +2,13 @@ export const DATA_STATIC_PAGE_DIRECTORY = 'static-page'; import * as path from 'node:path'; +import {input} from '#composite'; import {traverse} from '#node-utils'; import {sortAlphabetically} from '#sort'; import Thing from '#thing'; import {isName} from '#validators'; +import {exposeConstant} from '#composite/control-flow'; import {contentString, directory, flag, name, simpleString} from '#composite/wiki-properties'; @@ -36,6 +38,14 @@ export class StaticPage extends Thing { content: contentString(), absoluteLinks: flag(), + + // Expose only + + isStaticPage: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.findSpecs] = { diff --git a/src/data/things/track.js b/src/data/things/track.js index 69eb98a5..93193b6a 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -4,22 +4,36 @@ import CacheableObject from '#cacheable-object'; import {colors} from '#cli'; import {input} from '#composite'; import Thing from '#thing'; -import {isBoolean, isColor, isContributionList, isDate, isFileExtension} - from '#validators'; + +import { + isBoolean, + isColor, + isContentString, + isContributionList, + isDate, + isFileExtension, + validateReference, +} from '#validators'; import { parseAdditionalFiles, parseAdditionalNames, parseAnnotatedReferences, + parseArtwork, + parseCommentary, parseContributors, + parseCreditingSources, + parseReferencingSources, parseDate, parseDimensions, parseDuration, + parseLyrics, } from '#yaml'; -import {withPropertyFromObject} from '#composite/data'; +import {withPropertyFromList, withPropertyFromObject} from '#composite/data'; import { + exitWithoutDependency, exposeConstant, exposeDependency, exposeDependencyOrContinue, @@ -34,10 +48,8 @@ import { } from '#composite/wiki-data'; import { - additionalFiles, - additionalNameList, - commentary, commentatorArtists, + constitutibleArtworkList, contentString, contributionList, dimensions, @@ -50,45 +62,59 @@ import { reverseReferenceList, simpleDate, simpleString, - singleReference, soupyFind, soupyReverse, thing, + thingList, urls, wikiData, } from '#composite/wiki-properties'; import { + alwaysReferenceByDirectory, exitWithoutUniqueCoverArt, inheritContributionListFromMainRelease, inheritFromMainRelease, - withAlbum, withAllReleases, - withAlwaysReferenceByDirectory, withContainingTrackSection, + withCoverArtistContribs, withDate, withDirectorySuffix, withHasUniqueCoverArt, withMainRelease, + withMainReleaseTrack, withOtherReleases, withPropertyFromAlbum, withSuffixDirectoryFromAlbum, withTrackArtDate, + withTrackNumber, } from '#composite/things/track'; export class Track extends Thing { static [Thing.referenceType] = 'track'; static [Thing.getPropertyDescriptors] = ({ + AdditionalFile, + AdditionalName, Album, ArtTag, - Flash, - TrackSection, + Artwork, + CommentaryEntry, + CreditingSourcesEntry, + LyricsEntry, + ReferencingSourcesEntry, WikiInfo, }) => ({ - // Update & expose + // > Update & expose - Internal relationships + + album: thing({ + class: input.value(Album), + }), + + // > Update & expose - Identifying metadata name: name('Unnamed Track'), + nameText: contentString(), directory: [ withDirectorySuffix(), @@ -120,116 +146,49 @@ export class Track extends Thing { }) ], - additionalNames: additionalNameList(), - - bandcampTrackIdentifier: simpleString(), - bandcampArtworkIdentifier: simpleString(), - - duration: duration(), - urls: urls(), - dateFirstReleased: simpleDate(), + alwaysReferenceByDirectory: alwaysReferenceByDirectory(), - color: [ - exposeUpdateValueOrContinue({ - validate: input.value(isColor), - }), - - withContainingTrackSection(), - - withPropertyFromObject({ - object: '#trackSection', - property: input.value('color'), + // Album or track. The exposed value is really just what's provided here, + // whether or not a matching track is found on a provided album, for + // example. When presenting or processing, read `mainReleaseTrack`. + mainRelease: [ + withMainRelease({ + from: input.updateValue({ + validate: + validateReference(['album', 'track']), + }), }), - exposeDependencyOrContinue({dependency: '#trackSection.color'}), - - withPropertyFromAlbum({ - property: input.value('color'), + exposeDependency({ + dependency: '#mainRelease', }), - - exposeDependency({dependency: '#album.color'}), ], - alwaysReferenceByDirectory: [ - withAlwaysReferenceByDirectory(), - exposeDependency({dependency: '#alwaysReferenceByDirectory'}), - ], + bandcampTrackIdentifier: simpleString(), + bandcampArtworkIdentifier: simpleString(), - // Disables presenting the track as though it has its own unique artwork. - // This flag should only be used in select circumstances, i.e. to override - // an album's trackCoverArtists. This flag supercedes that property, as well - // as the track's own coverArtists. - disableUniqueCoverArt: flag(), + additionalNames: thingList({ + class: input.value(AdditionalName), + }), - // File extension for track's corresponding media file. This represents the - // track's unique cover artwork, if any, and does not inherit the extension - // of the album's main artwork. It does inherit trackCoverArtFileExtension, - // if present on the album. - coverArtFileExtension: [ - exitWithoutUniqueCoverArt(), + dateFirstReleased: simpleDate(), + // > Update & expose - Credits and contributors + + artistText: [ exposeUpdateValueOrContinue({ - validate: input.value(isFileExtension), + validate: input.value(isContentString), }), withPropertyFromAlbum({ - property: input.value('trackCoverArtFileExtension'), - }), - - exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), - - exposeConstant({ - value: input.value('jpg'), + property: input.value('trackArtistText'), }), - ], - coverArtDate: [ - withTrackArtDate({ - from: input.updateValue({ - validate: isDate, - }), - }), - - exposeDependency({dependency: '#trackArtDate'}), - ], - - coverArtDimensions: [ - exitWithoutUniqueCoverArt(), - - withPropertyFromAlbum({ - property: input.value('trackDimensions'), + exposeDependency({ + dependency: '#album.trackArtistText', }), - - exposeDependencyOrContinue({dependency: '#album.trackDimensions'}), - - dimensions(), - ], - - commentary: commentary(), - creditSources: commentary(), - - lyrics: [ - inheritFromMainRelease(), - contentString(), ], - additionalFiles: additionalFiles(), - sheetMusicFiles: additionalFiles(), - midiProjectFiles: additionalFiles(), - - mainReleaseTrack: singleReference({ - class: input.value(Track), - find: soupyFind.input('track'), - }), - - // Internal use only - for directly identifying an album inside a track's - // util.inspect display, if it isn't indirectly available (by way of being - // included in an album's track list). - dataSourceAlbum: singleReference({ - class: input.value(Album), - find: soupyFind.input('album'), - }), - artistContribs: [ inheritContributionListFromMainRelease(), @@ -250,20 +209,20 @@ export class Track extends Thing { }), withPropertyFromAlbum({ - property: input.value('artistContribs'), + property: input.value('trackArtistContribs'), }), withRecontextualizedContributionList({ - list: '#album.artistContribs', + list: '#album.trackArtistContribs', artistProperty: input.value('trackArtistContributions'), }), withRedatedContributionList({ - list: '#album.artistContribs', + list: '#album.trackArtistContribs', date: '#date', }), - exposeDependency({dependency: '#album.artistContribs'}), + exposeDependency({dependency: '#album.trackArtistContribs'}), ], contributorContribs: [ @@ -277,68 +236,142 @@ export class Track extends Thing { }), ], - coverArtistContribs: [ - exitWithoutUniqueCoverArt({ - value: input.value([]), + // > Update & expose - General configuration + + countInArtistTotals: [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), }), - withTrackArtDate({ - fallback: input.value(true), + withContainingTrackSection(), + + withPropertyFromObject({ + object: '#trackSection', + property: input.value('countTracksInArtistTotals'), }), - withResolvedContribs({ - from: input.updateValue({validate: isContributionList}), - thingProperty: input.thisProperty(), - artistProperty: input.value('trackCoverArtistContributions'), - date: '#trackArtDate', - }).outputs({ - '#resolvedContribs': '#coverArtistContribs', + exposeDependency({dependency: '#trackSection.countTracksInArtistTotals'}), + ], + + disableUniqueCoverArt: flag(), + disableDate: flag(), + + // > Update & expose - General metadata + + duration: duration(), + + color: [ + exposeUpdateValueOrContinue({ + validate: input.value(isColor), }), - exposeDependencyOrContinue({ - dependency: '#coverArtistContribs', - mode: input.value('empty'), + withContainingTrackSection(), + + withPropertyFromObject({ + object: '#trackSection', + property: input.value('color'), }), + exposeDependencyOrContinue({dependency: '#trackSection.color'}), + withPropertyFromAlbum({ - property: input.value('trackCoverArtistContribs'), + property: input.value('color'), }), - withRecontextualizedContributionList({ - list: '#album.trackCoverArtistContribs', - artistProperty: input.value('trackCoverArtistContributions'), + exposeDependency({dependency: '#album.color'}), + ], + + needsLyrics: [ + exposeUpdateValueOrContinue({ + mode: input.value('falsy'), + validate: input.value(isBoolean), }), - withRedatedContributionList({ - list: '#album.trackCoverArtistContribs', - date: '#trackArtDate', + exitWithoutDependency({ + dependency: 'lyrics', + mode: input.value('empty'), + value: input.value(false), + }), + + withPropertyFromList({ + list: 'lyrics', + property: input.value('helpNeeded'), }), - exposeDependency({dependency: '#album.trackCoverArtistContribs'}), + { + dependencies: ['#lyrics.helpNeeded'], + compute: ({ + ['#lyrics.helpNeeded']: helpNeeded, + }) => + helpNeeded.includes(true) + }, ], - referencedTracks: [ - inheritFromMainRelease({ - notFoundValue: input.value([]), + urls: urls(), + + // > Update & expose - Artworks + + trackArtworks: [ + exitWithoutUniqueCoverArt({ + value: input.value([]), }), - referenceList({ - class: input.value(Track), - find: soupyFind.input('track'), + constitutibleArtworkList.fromYAMLFieldSpec + .call(this, 'Track Artwork'), + ], + + coverArtistContribs: [ + withCoverArtistContribs({ + from: input.updateValue({ + validate: isContributionList, + }), }), + + exposeDependency({dependency: '#coverArtistContribs'}), ], - sampledTracks: [ - inheritFromMainRelease({ - notFoundValue: input.value([]), + coverArtDate: [ + withTrackArtDate({ + from: input.updateValue({ + validate: isDate, + }), }), - referenceList({ - class: input.value(Track), - find: soupyFind.input('track'), + exposeDependency({dependency: '#trackArtDate'}), + ], + + coverArtFileExtension: [ + exitWithoutUniqueCoverArt(), + + exposeUpdateValueOrContinue({ + validate: input.value(isFileExtension), + }), + + withPropertyFromAlbum({ + property: input.value('trackCoverArtFileExtension'), + }), + + exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), + + exposeConstant({ + value: input.value('jpg'), }), ], + coverArtDimensions: [ + exitWithoutUniqueCoverArt(), + + exposeUpdateValueOrContinue(), + + withPropertyFromAlbum({ + property: input.value('trackDimensions'), + }), + + exposeDependencyOrContinue({dependency: '#album.trackDimensions'}), + + dimensions(), + ], + artTags: [ exitWithoutUniqueCoverArt({ value: input.value([]), @@ -355,28 +388,80 @@ export class Track extends Thing { value: input.value([]), }), - withTrackArtDate({ - fallback: input.value(true), + referencedArtworkList(), + ], + + // > Update & expose - Referenced tracks + + referencedTracks: [ + inheritFromMainRelease({ + notFoundValue: input.value([]), }), - referencedArtworkList({ - date: '#trackArtDate', + referenceList({ + class: input.value(Track), + find: soupyFind.input('trackMainReleasesOnly'), }), ], - // Update only + sampledTracks: [ + inheritFromMainRelease({ + notFoundValue: input.value([]), + }), - find: soupyFind(), - reverse: soupyReverse(), + referenceList({ + class: input.value(Track), + find: soupyFind.input('trackMainReleasesOnly'), + }), + ], - // used for referencedArtworkList (mixedFind) - albumData: wikiData({ - class: input.value(Album), + // > Update & expose - Additional files + + additionalFiles: thingList({ + class: input.value(AdditionalFile), }), + sheetMusicFiles: thingList({ + class: input.value(AdditionalFile), + }), + + midiProjectFiles: thingList({ + class: input.value(AdditionalFile), + }), + + // > Update & expose - Content entries + + lyrics: [ + // TODO: Inherited lyrics are literally the same objects, so of course + // their .thing properties aren't going to point back to this one, and + // certainly couldn't be recontextualized... + inheritFromMainRelease(), + + thingList({ + class: input.value(LyricsEntry), + }), + ], + + commentary: thingList({ + class: input.value(CommentaryEntry), + }), + + creditingSources: thingList({ + class: input.value(CreditingSourcesEntry), + }), + + referencingSources: thingList({ + class: input.value(ReferencingSourcesEntry), + }), + + // > Update only + + find: soupyFind(), + reverse: soupyReverse(), + // used for referencedArtworkList (mixedFind) - trackData: wikiData({ - class: input.value(Track), + artworkData: wikiData({ + class: input.value(Artwork), }), // used for withMatchingContributionPresets (indirectly by Contribution) @@ -384,39 +469,53 @@ export class Track extends Thing { class: input.value(WikiInfo), }), - // Expose only + // > Expose only - commentatorArtists: commentatorArtists(), - - album: [ - withAlbum(), - exposeDependency({dependency: '#album'}), + isTrack: [ + exposeConstant({ + value: input.value(true), + }), ], + commentatorArtists: commentatorArtists(), + date: [ withDate(), exposeDependency({dependency: '#date'}), ], + trackNumber: [ + withTrackNumber(), + exposeDependency({dependency: '#trackNumber'}), + ], + hasUniqueCoverArt: [ withHasUniqueCoverArt(), exposeDependency({dependency: '#hasUniqueCoverArt'}), ], isMainRelease: [ - withMainRelease(), + withMainReleaseTrack(), exposeWhetherDependencyAvailable({ - dependency: '#mainRelease', + dependency: '#mainReleaseTrack', negate: input.value(true), }), ], isSecondaryRelease: [ - withMainRelease(), + withMainReleaseTrack(), exposeWhetherDependencyAvailable({ - dependency: '#mainRelease', + dependency: '#mainReleaseTrack', + }), + ], + + mainReleaseTrack: [ + withMainReleaseTrack(), + + exposeDependency({ + dependency: '#mainReleaseTrack', }), ], @@ -436,6 +535,34 @@ export class Track extends Thing { exposeDependency({dependency: '#otherReleases'}), ], + commentaryFromMainRelease: [ + withMainReleaseTrack(), + + exitWithoutDependency({ + dependency: '#mainReleaseTrack', + value: input.value([]), + }), + + withPropertyFromObject({ + object: '#mainReleaseTrack', + property: input.value('commentary'), + }), + + exposeDependency({ + dependency: '#mainReleaseTrack.commentary', + }), + ], + + groups: [ + withPropertyFromAlbum({ + property: input.value('groups'), + }), + + exposeDependency({ + dependency: '#album.groups', + }), + ], + referencedByTracks: reverseReferenceList({ reverse: soupyReverse.input('tracksWhichReference'), }), @@ -447,28 +574,18 @@ export class Track extends Thing { featuredInFlashes: reverseReferenceList({ reverse: soupyReverse.input('flashesWhichFeature'), }), - - referencedByArtworks: [ - exitWithoutUniqueCoverArt({ - value: input.value([]), - }), - - reverseReferenceList({ - reverse: soupyReverse.input('artworksWhichReference'), - }), - ], }); static [Thing.yamlDocumentSpec] = { fields: { + // Identifying metadata + 'Track': {property: 'name'}, + 'Track Text': {property: 'nameText'}, 'Directory': {property: 'directory'}, 'Suffix Directory': {property: 'suffixDirectoryFromAlbum'}, - - 'Additional Names': { - property: 'additionalNames', - transform: parseAdditionalNames, - }, + 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, + 'Main Release': {property: 'mainRelease'}, 'Bandcamp Track ID': { property: 'bandcampTrackIdentifier', @@ -480,17 +597,87 @@ export class Track extends Thing { transform: String, }, + 'Additional Names': { + property: 'additionalNames', + transform: parseAdditionalNames, + }, + + 'Date First Released': { + property: 'dateFirstReleased', + transform: parseDate, + }, + + // Credits and contributors + + 'Artist Text': { + property: 'artistText', + }, + + 'Artists': { + property: 'artistContribs', + transform: parseContributors, + }, + + 'Contributors': { + property: 'contributorContribs', + transform: parseContributors, + }, + + // General configuration + + 'Count In Artist Totals': {property: 'countInArtistTotals'}, + + 'Has Cover Art': { + property: 'disableUniqueCoverArt', + transform: value => + (typeof value === 'boolean' + ? !value + : value), + }, + + 'Has Date': { + property: 'disableDate', + transform: value => + (typeof value === 'boolean' + ? !value + : value), + }, + + // General metadata + 'Duration': { property: 'duration', transform: parseDuration, }, 'Color': {property: 'color'}, + + 'Needs Lyrics': { + property: 'needsLyrics', + }, + 'URLs': {property: 'urls'}, - 'Date First Released': { - property: 'dateFirstReleased', - transform: parseDate, + // Artworks + + 'Track Artwork': { + property: 'trackArtworks', + transform: + parseArtwork({ + thingProperty: 'trackArtworks', + dimensionsFromThingProperty: 'coverArtDimensions', + fileExtensionFromThingProperty: 'coverArtFileExtension', + dateFromThingProperty: 'coverArtDate', + artTagsFromThingProperty: 'artTags', + referencedArtworksFromThingProperty: 'referencedArtworks', + artistContribsFromThingProperty: 'coverArtistContribs', + artistContribsArtistProperty: 'trackCoverArtistContributions', + }), + }, + + 'Cover Artists': { + property: 'coverArtistContribs', + transform: parseContributors, }, 'Cover Art Date': { @@ -505,19 +692,19 @@ export class Track extends Thing { transform: parseDimensions, }, - 'Has Cover Art': { - property: 'disableUniqueCoverArt', - transform: value => - (typeof value === 'boolean' - ? !value - : value), + 'Art Tags': {property: 'artTags'}, + + 'Referenced Artworks': { + property: 'referencedArtworks', + transform: parseAnnotatedReferences, }, - 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, + // Referenced tracks - 'Lyrics': {property: 'lyrics'}, - 'Commentary': {property: 'commentary'}, - 'Credit Sources': {property: 'creditSources'}, + 'Referenced Tracks': {property: 'referencedTracks'}, + 'Sampled Tracks': {property: 'sampledTracks'}, + + // Additional files 'Additional Files': { property: 'additionalFiles', @@ -534,39 +721,41 @@ export class Track extends Thing { transform: parseAdditionalFiles, }, - 'Main Release': {property: 'mainReleaseTrack'}, - 'Referenced Tracks': {property: 'referencedTracks'}, - 'Sampled Tracks': {property: 'sampledTracks'}, + // Content entries - 'Referenced Artworks': { - property: 'referencedArtworks', - transform: parseAnnotatedReferences, + 'Lyrics': { + property: 'lyrics', + transform: parseLyrics, }, - 'Franchises': {ignore: true}, - 'Inherit Franchises': {ignore: true}, - - 'Artists': { - property: 'artistContribs', - transform: parseContributors, + 'Commentary': { + property: 'commentary', + transform: parseCommentary, }, - 'Contributors': { - property: 'contributorContribs', - transform: parseContributors, + 'Crediting Sources': { + property: 'creditingSources', + transform: parseCreditingSources, }, - 'Cover Artists': { - property: 'coverArtistContribs', - transform: parseContributors, + 'Referencing Sources': { + property: 'referencingSources', + transform: parseReferencingSources, }, - 'Art Tags': {property: 'artTags'}, + // Shenanigans + 'Franchises': {ignore: true}, + 'Inherit Franchises': {ignore: true}, 'Review Points': {ignore: true}, }, invalidFieldCombinations: [ + {message: `Secondary releases never count in artist totals`, fields: [ + 'Main Release', + 'Count In Artist Totals', + ]}, + {message: `Secondary releases inherit references from the main one`, fields: [ 'Main Release', 'Referenced Tracks', @@ -623,7 +812,7 @@ export class Track extends Thing { bindTo: 'trackData', include: track => - !CacheableObject.getUpdateValue(track, 'mainReleaseTrack'), + !CacheableObject.getUpdateValue(track, 'mainRelease'), // It's still necessary to check alwaysReferenceByDirectory here, since // it may be set manually (with `Always Reference By Directory: true`), @@ -652,6 +841,31 @@ export class Track extends Thing { ? [] : [track.name]), }, + + trackPrimaryArtwork: { + [Thing.findThisThingOnly]: false, + + referenceTypes: [ + 'track', + 'track-referencing-artworks', + 'track-referenced-artworks', + ], + + bindTo: 'artworkData', + + include: (artwork, {Artwork, Track}) => + artwork instanceof Artwork && + artwork.thing instanceof Track && + artwork === artwork.thing.trackArtworks[0], + + getMatchableNames: ({thing: track}) => + (track.alwaysReferenceByDirectory + ? [] + : [track.name]), + + getMatchableDirectories: ({thing: track}) => + [track.directory], + }, }; static [Thing.reverseSpecs] = { @@ -683,7 +897,7 @@ export class Track extends Thing { soupyReverse.contributionsBy('trackData', 'contributorContribs'), trackCoverArtistContributionsBy: - soupyReverse.contributionsBy('trackData', 'coverArtistContribs'), + soupyReverse.artworkContributionsBy('trackData', 'trackArtworks'), tracksWithCommentaryBy: { bindTo: 'trackData', @@ -703,27 +917,68 @@ export class Track extends Thing { // Track YAML loading is handled in album.js. static [Thing.getYamlLoadingSpec] = null; + getOwnAdditionalFilePath(_file, filename) { + if (!this.album) return null; + + return [ + 'media.albumAdditionalFile', + this.album.directory, + filename, + ]; + } + + getOwnArtworkPath(artwork) { + if (!this.album) return null; + + return [ + 'media.trackCover', + this.album.directory, + + (artwork.unqualifiedDirectory + ? this.directory + '-' + artwork.unqualifiedDirectory + : this.directory), + + artwork.fileExtension, + ]; + } + + countOwnContributionInContributionTotals(_contrib) { + if (!this.countInArtistTotals) { + return false; + } + + if (this.isSecondaryRelease) { + return false; + } + + return true; + } + + countOwnContributionInDurationTotals(_contrib) { + if (!this.countInArtistTotals) { + return false; + } + + if (this.isSecondaryRelease) { + return false; + } + + return true; + } + [inspect.custom](depth) { const parts = []; parts.push(Thing.prototype[inspect.custom].apply(this)); - if (CacheableObject.getUpdateValue(this, 'mainReleaseTrack')) { + if (CacheableObject.getUpdateValue(this, 'mainRelease')) { parts.unshift(`${colors.yellow('[secrelease]')} `); } let album; if (depth >= 0) { - try { - album = this.album; - } catch (_error) { - // Computing album might crash for any reason, which we don't want to - // distract from another error we might be trying to work out at the - // moment (for which debugging might involve inspecting this track!). - } - - album ??= this.dataSourceAlbum; + album = this.album; } if (album) { diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 590598be..7fb6a350 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -2,7 +2,7 @@ export const WIKI_INFO_FILE = 'wiki-info.yaml'; import {input} from '#composite'; import Thing from '#thing'; -import {parseContributionPresets} from '#yaml'; +import {parseContributionPresets, parseWallpaperParts} from '#yaml'; import { isBoolean, @@ -10,12 +10,21 @@ import { isContributionPresetList, isLanguageCode, isName, - isURL, } from '#validators'; -import {exitWithoutDependency} from '#composite/control-flow'; -import {contentString, flag, name, referenceList, soupyFind} - from '#composite/wiki-properties'; +import {exitWithoutDependency, exposeConstant} from '#composite/control-flow'; + +import { + canonicalBase, + contentString, + fileExtension, + flag, + name, + referenceList, + simpleString, + soupyFind, + wallpaperParts, +} from '#composite/wiki-properties'; export class WikiInfo extends Thing { static [Thing.friendlyName] = `Wiki Info`; @@ -55,18 +64,12 @@ export class WikiInfo extends Thing { update: {validate: isLanguageCode}, }, - canonicalBase: { - flags: {update: true, expose: true}, - update: {validate: isURL}, - expose: { - transform: (value) => - (value === null - ? null - : value.endsWith('/') - ? value - : value + '/'), - }, - }, + canonicalBase: canonicalBase(), + canonicalMediaBase: canonicalBase(), + + wikiWallpaperFileExtension: fileExtension('jpg'), + wikiWallpaperStyle: simpleString(), + wikiWallpaperParts: wallpaperParts(), divideTrackListsByGroups: referenceList({ class: input.value(Group), @@ -106,24 +109,49 @@ export class WikiInfo extends Thing { default: false, }, }, + + // Expose only + + isWikiInfo: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { fields: { 'Name': {property: 'name'}, 'Short Name': {property: 'nameShort'}, + 'Color': {property: 'color'}, + 'Description': {property: 'description'}, + 'Footer Content': {property: 'footerContent'}, + 'Default Language': {property: 'defaultLanguage'}, + 'Canonical Base': {property: 'canonicalBase'}, - 'Divide Track Lists By Groups': {property: 'divideTrackListsByGroups'}, + 'Canonical Media Base': {property: 'canonicalMediaBase'}, + + 'Wiki Wallpaper File Extension': {property: 'wikiWallpaperFileExtension'}, + + 'Wiki Wallpaper Style': {property: 'wikiWallpaperStyle'}, + + 'Wiki Wallpaper Parts': { + property: 'wikiWallpaperParts', + transform: parseWallpaperParts, + }, + 'Enable Flashes & Games': {property: 'enableFlashesAndGames'}, 'Enable Listings': {property: 'enableListings'}, 'Enable News': {property: 'enableNews'}, 'Enable Art Tag UI': {property: 'enableArtTagUI'}, 'Enable Group UI': {property: 'enableGroupUI'}, + 'Divide Track Lists By Groups': {property: 'divideTrackListsByGroups'}, + 'Contribution Presets': { property: 'contributionPresets', transform: parseContributionPresets, diff --git a/src/data/yaml.js b/src/data/yaml.js index a5614ea6..46cb4eda 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -8,11 +8,14 @@ import {inspect as nodeInspect} from 'node:util'; import yaml from 'js-yaml'; import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; +import {parseContentNodes, splitContentNodesAround} from '#replacer'; import {sortByName} from '#sort'; import Thing from '#thing'; import thingConstructors from '#things'; +import {matchContentEntries, multipleLyricsDetectionRegex} from '#wiki-data'; import { + aggregateThrows, annotateErrorWithFile, decorateErrorWithIndex, decorateErrorWithAnnotation, @@ -84,10 +87,14 @@ function makeProcessDocument(thingConstructor, { // ] // // ...means A can't coexist with B or C, B can't coexist with A or C, and - // C can't coexist iwth A, B, or D - but it's okay for D to coexist with + // C can't coexist with A, B, or D - but it's okay for D to coexist with // A or B. // invalidFieldCombinations = [], + + // Bouncing function used to process subdocuments: this is a function which + // in turn calls the appropriate *result of* makeProcessDocument. + processDocument: bouncer, }) { if (!thingConstructor) { throw new Error(`Missing Thing class`); @@ -97,6 +104,10 @@ function makeProcessDocument(thingConstructor, { throw new Error(`Expected fields to be provided`); } + if (!bouncer) { + throw new Error(`Missing processDocument bouncer`); + } + const knownFields = Object.keys(fieldSpecs); const ignoredFields = @@ -144,9 +155,12 @@ function makeProcessDocument(thingConstructor, { : `document`); const aggregate = openAggregate({ + ...aggregateThrows(ProcessDocumentError), message: `Errors processing ${constructorPart}` + namePart, }); + const thing = Reflect.construct(thingConstructor, []); + const documentEntries = Object.entries(document) .filter(([field]) => !ignoredFields.includes(field)); @@ -168,9 +182,22 @@ function makeProcessDocument(thingConstructor, { const fieldCombinationErrors = []; - for (const {message, fields} of invalidFieldCombinations) { + for (const {message, fields: fieldsSpec} of invalidFieldCombinations) { const fieldsPresent = - presentFields.filter(field => fields.includes(field)); + fieldsSpec.flatMap(fieldSpec => { + if (Array.isArray(fieldSpec)) { + const [field, match] = fieldSpec; + if (!presentFields.includes(field)) return []; + if (typeof match === 'function') { + return match(document[field]) ? [field] : []; + } else { + return document[field] === match ? [field] : []; + } + } + + const field = fieldSpec; + return presentFields.includes(field) ? [field] : []; + }); if (fieldsPresent.length >= 2) { const filteredDocument = @@ -180,7 +207,10 @@ function makeProcessDocument(thingConstructor, { {preserveOriginalOrder: true}); fieldCombinationErrors.push( - new FieldCombinationError(filteredDocument, message)); + new FieldCombinationError( + filteredDocument, + fieldsSpec, + message)); for (const field of Object.keys(filteredDocument)) { skippedFields.add(field); @@ -194,13 +224,52 @@ function makeProcessDocument(thingConstructor, { const fieldValues = {}; + const subdocSymbol = Symbol('subdoc'); + const subdocLayouts = {}; + + const isSubdocToken = value => + typeof value === 'object' && + value !== null && + Object.hasOwn(value, subdocSymbol); + + const transformUtilities = { + ...thingConstructors, + + subdoc(documentType, data, { + bindInto = null, + provide = null, + } = {}) { + if (!documentType) + throw new Error(`Expected document type, got ${typeAppearance(documentType)}`); + if (!data) + throw new Error(`Expected data, got ${typeAppearance(data)}`); + if (typeof data !== 'object' || data === null) + throw new Error(`Expected data to be an object, got ${typeAppearance(data)}`); + if (typeof bindInto !== 'string' && bindInto !== null) + throw new Error(`Expected bindInto to be a string, got ${typeAppearance(bindInto)}`); + if (typeof provide !== 'object' && provide !== null) + throw new Error(`Expected provide to be an object, got ${typeAppearance(provide)}`); + + return { + [subdocSymbol]: { + documentType, + data, + bindInto, + provide, + }, + }; + }, + }; + for (const [field, documentValue] of documentEntries) { if (skippedFields.has(field)) continue; // This variable would like to certify itself as "not into capitalism". let propertyValue = - (fieldSpecs[field].transform - ? fieldSpecs[field].transform(documentValue) + (documentValue === null + ? null + : fieldSpecs[field].transform + ? fieldSpecs[field].transform(documentValue, transformUtilities) : documentValue); // Completely blank items in a YAML list are read as null. @@ -223,10 +292,99 @@ function makeProcessDocument(thingConstructor, { } } + if (isSubdocToken(propertyValue)) { + subdocLayouts[field] = propertyValue[subdocSymbol]; + continue; + } + + if (Array.isArray(propertyValue) && propertyValue.every(isSubdocToken)) { + subdocLayouts[field] = + propertyValue + .map(token => token[subdocSymbol]); + continue; + } + fieldValues[field] = propertyValue; } - const thing = Reflect.construct(thingConstructor, []); + const subdocErrors = []; + + const followSubdocSetup = setup => { + let error = null; + + let subthing; + try { + const result = bouncer(setup.data, setup.documentType); + subthing = result.thing; + result.aggregate.close(); + } catch (caughtError) { + error = caughtError; + } + + if (subthing) { + if (setup.bindInto) { + subthing[setup.bindInto] = thing; + } + + if (setup.provide) { + Object.assign(subthing, setup.provide); + } + } + + return {error, subthing}; + }; + + for (const [field, layout] of Object.entries(subdocLayouts)) { + if (Array.isArray(layout)) { + const subthings = []; + let anySucceeded = false; + let anyFailed = false; + + for (const [index, setup] of layout.entries()) { + const {subthing, error} = followSubdocSetup(setup); + if (error) { + subdocErrors.push(new SubdocError( + {field, index}, + setup, + {cause: error})); + } + + if (subthing) { + subthings.push(subthing); + anySucceeded = true; + } else { + anyFailed = true; + } + } + + if (anySucceeded) { + fieldValues[field] = subthings; + } else if (anyFailed) { + skippedFields.add(field); + } + } else { + const setup = layout; + const {subthing, error} = followSubdocSetup(setup); + + if (error) { + subdocErrors.push(new SubdocError( + {field}, + setup, + {cause: error})); + } + + if (subthing) { + fieldValues[field] = subthing; + } else { + skippedFields.add(field); + } + } + } + + if (!empty(subdocErrors)) { + aggregate.push(new SubdocAggregateError( + subdocErrors, thingConstructor)); + } const fieldValueErrors = []; @@ -260,6 +418,8 @@ function makeProcessDocument(thingConstructor, { }); } +export class ProcessDocumentError extends AggregateError {} + export class UnknownFieldsError extends Error { constructor(fields) { super(`Unknown fields ignored: ${fields.map(field => colors.red(field)).join(', ')}`); @@ -274,19 +434,36 @@ export class FieldCombinationAggregateError extends AggregateError { } export class FieldCombinationError extends Error { - constructor(fields, message) { - const fieldNames = Object.keys(fields); + constructor(filteredDocument, fieldsSpec, message) { + const fieldNames = Object.keys(filteredDocument); const fieldNamesText = fieldNames - .map(field => colors.red(field)) + .map(field => { + if (fieldsSpec.includes(field)) { + return colors.red(field); + } + + const match = + fieldsSpec + .find(fieldSpec => + Array.isArray(fieldSpec) && + fieldSpec[0] === field) + .at(1); + + if (typeof match === 'function') { + return colors.red(`${field}: ${filteredDocument[field]}`); + } else { + return colors.red(`${field}: ${match}`); + } + }) .join(', '); const mainMessage = `Don't combine ${fieldNamesText}`; const causeMessage = (typeof message === 'function' - ? message(fields) + ? message(filteredDocument) : typeof message === 'string' ? message : null); @@ -298,7 +475,7 @@ export class FieldCombinationError extends Error { : null), }); - this.fields = fields; + this.fields = fieldNames; } } @@ -347,12 +524,46 @@ export class SkippedFieldsSummaryError extends Error { : `${entries.length} fields`); super( - colors.bright(colors.yellow(`Altogether, skipped ${numFieldsText}:\n`)) + + colors.bright(colors.yellow(`Altogether, skipped ${numFieldsText}:`)) + '\n' + lines.join('\n') + '\n' + colors.bright(colors.yellow(`See above errors for details.`))); } } +export class SubdocError extends Error { + constructor({field, index = null}, setup, options) { + const fieldText = + (index === null + ? colors.green(`"${field}"`) + : colors.yellow(`#${index + 1}`) + ' in ' + + colors.green(`"${field}"`)); + + const constructorText = + setup.documentType.name; + + if (options.cause instanceof ProcessDocumentError) { + options.cause[Symbol.for('hsmusic.aggregate.translucent')] = true; + } + + super( + `Errors processing ${constructorText} for ${fieldText} field`, + options); + } +} + +export class SubdocAggregateError extends AggregateError { + [Symbol.for('hsmusic.aggregate.translucent')] = true; + + constructor(errors, thingConstructor) { + const constructorText = + colors.green(thingConstructor.name); + + super( + errors, + `Errors processing subdocuments for ${constructorText}`); + } +} + export function parseDate(date) { return new Date(date); } @@ -435,49 +646,39 @@ export function parseContributors(entries) { }); } -export function parseAdditionalFiles(entries) { +export function parseAdditionalFiles(entries, {subdoc, AdditionalFile}) { return parseArrayEntries(entries, item => { if (typeof item !== 'object') return item; - return { - title: item['Title'], - description: item['Description'] ?? null, - files: item['Files'], - }; + return subdoc(AdditionalFile, item, {bindInto: 'thing'}); }); } -export function parseAdditionalNames(entries) { +export function parseAdditionalNames(entries, {subdoc, AdditionalName}) { return parseArrayEntries(entries, item => { - if (typeof item === 'object' && typeof item['Name'] === 'string') - return { - name: item['Name'], - annotation: item['Annotation'] ?? null, - }; + if (typeof item === 'object') { + return subdoc(AdditionalName, item, {bindInto: 'thing'}); + } if (typeof item !== 'string') return item; const match = item.match(extractAccentRegex); if (!match) return item; - return { - name: match.groups.main, - annotation: match.groups.accent ?? null, + const document = { + ['Name']: match.groups.main, + ['Annotation']: match.groups.accent ?? null, }; + + return subdoc(AdditionalName, document, {bindInto: 'thing'}); }); } -export function parseSerieses(entries) { +export function parseSerieses(entries, {subdoc, Series}) { return parseArrayEntries(entries, item => { if (typeof item !== 'object') return item; - return { - name: item['Name'], - description: item['Description'] ?? null, - albums: item['Albums'] ?? null, - - showAlbumArtists: item['Show Album Artists'] ?? null, - }; + return subdoc(Series, item, {bindInto: 'group'}); }); } @@ -615,6 +816,172 @@ export function parseAnnotatedReferences(entries, { }); } +export function parseArtwork({ + single = false, + thingProperty = null, + dimensionsFromThingProperty = null, + fileExtensionFromThingProperty = null, + dateFromThingProperty = null, + artistContribsFromThingProperty = null, + artistContribsArtistProperty = null, + artTagsFromThingProperty = null, + referencedArtworksFromThingProperty = null, +}) { + const provide = { + thingProperty, + dimensionsFromThingProperty, + fileExtensionFromThingProperty, + dateFromThingProperty, + artistContribsFromThingProperty, + artistContribsArtistProperty, + artTagsFromThingProperty, + referencedArtworksFromThingProperty, + }; + + const parseSingleEntry = (entry, {subdoc, Artwork}) => + subdoc(Artwork, entry, {bindInto: 'thing', provide}); + + const transform = (value, ...args) => + (Array.isArray(value) + ? value.map(entry => parseSingleEntry(entry, ...args)) + : single + ? parseSingleEntry(value, ...args) + : [parseSingleEntry(value, ...args)]); + + transform.provide = provide; + + return transform; +} + +export function parseContentEntriesFromSourceText(thingClass, sourceText, {subdoc}) { + function map(matchEntry) { + let artistText = null, artistReferences = null; + + const artistTextNodes = + Array.from( + splitContentNodesAround( + parseContentNodes(matchEntry.artistText), + /\|/g)); + + const separatorIndices = + artistTextNodes + .filter(node => node.type === 'separator') + .map(node => artistTextNodes.indexOf(node)); + + if (empty(separatorIndices)) { + if (artistTextNodes.length === 1 && artistTextNodes[0].type === 'text') { + artistReferences = matchEntry.artistText; + } else { + artistText = matchEntry.artistText; + } + } else { + const firstSeparatorIndex = + separatorIndices.at(0); + + const secondSeparatorIndex = + separatorIndices.at(1) ?? + artistTextNodes.length; + + artistReferences = + matchEntry.artistText.slice( + artistTextNodes.at(0).i, + artistTextNodes.at(firstSeparatorIndex - 1).iEnd); + + artistText = + matchEntry.artistText.slice( + artistTextNodes.at(firstSeparatorIndex).iEnd, + artistTextNodes.at(secondSeparatorIndex - 1).iEnd); + } + + if (artistReferences) { + artistReferences = + artistReferences + .split(',') + .map(ref => ref.trim()); + } + + return { + 'Artists': + artistReferences, + + 'Artist Text': + artistText, + + 'Annotation': + matchEntry.annotation, + + 'Date': + matchEntry.date, + + 'Second Date': + matchEntry.secondDate, + + 'Date Kind': + matchEntry.dateKind, + + 'Access Date': + matchEntry.accessDate, + + 'Access Kind': + matchEntry.accessKind, + + 'Body': + matchEntry.body, + }; + } + + const documents = + matchContentEntries(sourceText) + .map(matchEntry => + withEntries( + map(matchEntry), + entries => entries + .filter(([key, value]) => + value !== undefined && + value !== null))); + + const subdocs = + documents.map(document => + subdoc(thingClass, document, {bindInto: 'thing'})); + + return subdocs; +} + +export function parseContentEntries(thingClass, value, {subdoc}) { + if (typeof value === 'string') { + return parseContentEntriesFromSourceText(thingClass, value, {subdoc}); + } else if (Array.isArray(value)) { + return value.map(doc => subdoc(thingClass, doc, {bindInto: 'thing'})); + } else { + return value; + } +} + +export function parseCommentary(value, {subdoc, CommentaryEntry}) { + return parseContentEntries(CommentaryEntry, value, {subdoc}); +} + +export function parseCreditingSources(value, {subdoc, CreditingSourcesEntry}) { + return parseContentEntries(CreditingSourcesEntry, value, {subdoc}); +} + +export function parseReferencingSources(value, {subdoc, ReferencingSourcesEntry}) { + return parseContentEntries(ReferencingSourcesEntry, value, {subdoc}); +} + +export function parseLyrics(value, {subdoc, LyricsEntry}) { + if ( + typeof value === 'string' && + !multipleLyricsDetectionRegex.test(value) + ) { + const document = {'Body': value}; + + return [subdoc(LyricsEntry, document, {bindInto: 'thing'})]; + } + + return parseContentEntries(LyricsEntry, value, {subdoc}); +} + // documentModes: Symbols indicating sets of behavior for loading and processing // data files. export const documentModes = { @@ -644,6 +1011,12 @@ export const documentModes = { // array of processed documents (wiki objects). allInOne: Symbol('Document mode: allInOne'), + // allTogether: One or more documens, spread across any number of files. + // Expects files array (or function) and processDocument function. + // Calls save with an array of processed documents (wiki objects) - this is + // a flat array, *not* an array of the documents processed from *each* file. + allTogether: Symbol('Document mode: allTogether'), + // 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). @@ -690,7 +1063,7 @@ export const documentModes = { export function getAllDataSteps() { try { thingConstructors; - } catch (error) { + } catch { throw new Error(`Thing constructors aren't ready yet, can't get all data steps`); } @@ -754,6 +1127,7 @@ export async function getFilesFromDataStep(dataStep, {dataPath}) { } } + case documentModes.allTogether: case documentModes.headerAndEntries: case documentModes.onePerFile: { if (!dataStep.files) { @@ -899,7 +1273,7 @@ export function processThingsFromDataStep(documents, dataStep) { throw new Error(`Class "${thingClass.name}" doesn't specify Thing.yamlDocumentSpec`); } - fn = makeProcessDocument(thingClass, spec); + fn = makeProcessDocument(thingClass, {...spec, processDocument}); submap.set(thingClass, fn); } @@ -909,7 +1283,8 @@ export function processThingsFromDataStep(documents, dataStep) { const {documentMode} = dataStep; switch (documentMode) { - case documentModes.allInOne: { + case documentModes.allInOne: + case documentModes.allTogether: { const result = []; const aggregate = openAggregate({message: `Errors processing documents`}); @@ -1184,6 +1559,10 @@ export function saveThingsFromDataStep(thingLists, dataStep) { return dataStep.save(thing); } + case documentModes.allTogether: { + return dataStep.save(thingLists.flat()); + } + case documentModes.headerAndEntries: case documentModes.onePerFile: { return dataStep.save(thingLists); @@ -1280,8 +1659,7 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) { // link if the 'find' or 'reverse' properties will be implicitly linked ['albumData', [ - 'albumData', - 'trackData', + 'artworkData', 'wikiInfo', ]], @@ -1289,6 +1667,12 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) { ['artistData', [/* find, reverse */]], + ['artworkData', ['artworkData']], + + ['commentaryData', [/* find */]], + + ['creditingSourceData', [/* find */]], + ['flashData', [ 'wikiInfo', ]], @@ -1303,9 +1687,14 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) { ['homepageLayout.sections.rows', [/* find */]], + ['lyricsData', [/* find */]], + + ['referencingSourceData', [/* find */]], + + ['seriesData', [/* find */]], + ['trackData', [ - 'albumData', - 'trackData', + 'artworkData', 'wikiInfo', ]], @@ -1571,14 +1960,16 @@ export function flattenThingLayoutToDocumentOrder(layout) { } export function* splitDocumentsInYAMLSourceText(sourceText) { - const dividerRegex = /^-{3,}\n?/gm; + // Not multiline! + const dividerRegex = /(?:\r\n|\n|^)-{3,}(?:\r\n|\n|$)/g; + let previousDivider = ''; while (true) { const {lastIndex} = dividerRegex; const match = dividerRegex.exec(sourceText); if (match) { - const nextDivider = match[0].trim(); + const nextDivider = match[0]; yield { previousDivider, @@ -1589,11 +1980,12 @@ export function* splitDocumentsInYAMLSourceText(sourceText) { previousDivider = nextDivider; } else { const nextDivider = ''; + const lineBreak = previousDivider.match(/\r?\n/)?.[0] ?? ''; yield { previousDivider, nextDivider, - text: sourceText.slice(lastIndex).replace(/(?<!\n)$/, '\n'), + text: sourceText.slice(lastIndex).replace(/(?<!\n)$/, lineBreak), }; return; @@ -1619,7 +2011,7 @@ export function recombineDocumentsIntoYAMLSourceText(documents) { for (const document of documents) { if (sourceText) { - sourceText += divider + '\n'; + sourceText += divider; } sourceText += document.text; diff --git a/src/external-links.js b/src/external-links.js index 1055a391..06570067 100644 --- a/src/external-links.js +++ b/src/external-links.js @@ -4,11 +4,9 @@ import { anyOf, is, isBoolean, - isObject, isStringNonEmpty, looseArrayOf, optional, - validateAllPropertyValues, validateArrayItems, validateInstanceOf, validateProperties, @@ -32,6 +30,9 @@ export const externalLinkContexts = [ 'generic', 'group', 'track', + + 'composerRelease', + 'officialRelease', ]; export const isExternalLinkContext = @@ -257,6 +258,30 @@ export const externalLinkSpec = [ }, { + match: { + domain: '.bandcamp.com', + context: 'composerRelease', + }, + + platform: 'bandcamp.composerRelease', + handle: {domain: /^[^.]+/}, + + icon: 'bandcamp', + }, + + { + match: { + domain: '.bandcamp.com', + context: 'officialRelease', + }, + + platform: 'bandcamp.officialRelease', + handle: {domain: /^[^.]+/}, + + icon: 'bandcamp', + }, + + { match: {domain: '.bandcamp.com'}, platform: 'bandcamp', @@ -557,6 +582,17 @@ export const externalLinkSpec = [ }, { + match: {domains: ['reddit.com', 'old.reddit.com']}, + platform: 'reddit', + icon: 'globe', + + detail: { + substring: 'subreddit', + subreddit: {pathname: /^r\/[^/]+(?=\/)?/}, + }, + }, + + { match: {domain: 'soundcloud.com'}, platform: 'soundcloud', diff --git a/src/find-reverse.js b/src/find-reverse.js index 6a67ac0f..c99a4a71 100644 --- a/src/find-reverse.js +++ b/src/find-reverse.js @@ -11,7 +11,7 @@ export function getAllSpecs({ }) { try { thingConstructors; - } catch (error) { + } catch { throw new Error(`Thing constructors aren't ready yet, can't get all ${word} specs`); } @@ -52,7 +52,7 @@ export function findSpec(key, { try { thingConstructors; - } catch (error) { + } catch { throw new Error(`Thing constructors aren't ready yet, can't check if "${word}.${key}" available`); } diff --git a/src/find.js b/src/find.js index e590bc4f..7b605e97 100644 --- a/src/find.js +++ b/src/find.js @@ -4,6 +4,7 @@ import {colors, logWarn} from '#cli'; import {compareObjects, stitchArrays, typeAppearance} from '#sugar'; import thingConstructors from '#things'; import {isFunction, validateArrayItems} from '#validators'; +import {getCaseSensitiveKebabCase} from '#wiki-data'; import * as fr from './find-reverse.js'; @@ -30,7 +31,34 @@ function warnOrThrow(mode, message) { export const keyRefRegex = new RegExp(String.raw`^(?:(?<key>[a-z-]*):(?=\S))?(?<ref>.*)$`); -export function processAvailableMatchesByName(data, { +function getFuzzHash(fuzz = {}) { + if (!fuzz) { + return 0; + } + + return ( + fuzz.capitalization << 0 + + fuzz.kebab << 1 + ); +} + +export function fuzzName(name, fuzz = {}) { + if (!fuzz) { + return name; + } + + if (fuzz.capitalization) { + name = name.toLowerCase(); + } + + if (fuzz.kebab) { + name = getCaseSensitiveKebabCase(name); + } + + return name; +} + +export function processAvailableMatchesByName(data, fuzz, { include = _thing => true, getMatchableNames = thing => @@ -42,7 +70,7 @@ export function processAvailableMatchesByName(data, { multipleNameMatches = Object.create(null), }) { for (const thing of data) { - if (!include(thing)) continue; + if (!include(thing, thingConstructors)) continue; for (const name of getMatchableNames(thing)) { if (typeof name !== 'string') { @@ -50,17 +78,20 @@ export function processAvailableMatchesByName(data, { continue; } - const normalizedName = name.toLowerCase(); + const normalizedName = fuzzName(name, fuzz); if (normalizedName in results) { if (normalizedName in multipleNameMatches) { multipleNameMatches[normalizedName].push(thing); } else { - multipleNameMatches[normalizedName] = [results[normalizedName], thing]; + multipleNameMatches[normalizedName] = [ + results[normalizedName].thing, + thing, + ]; results[normalizedName] = null; } } else { - results[normalizedName] = thing; + results[normalizedName] = {thing, name}; } } } @@ -79,7 +110,7 @@ export function processAvailableMatchesByDirectory(data, { results = Object.create(null), }) { for (const thing of data) { - if (!include(thing)) continue; + if (!include(thing, thingConstructors)) continue; for (const directory of getMatchableDirectories(thing)) { if (typeof directory !== 'string') { @@ -87,16 +118,16 @@ export function processAvailableMatchesByDirectory(data, { continue; } - results[directory] = thing; + results[directory] = {thing, directory}; } } return {results}; } -export function processAllAvailableMatches(data, spec) { +export function processAllAvailableMatches(data, fuzz, spec) { const {results: byName, multipleNameMatches} = - processAvailableMatchesByName(data, spec); + processAvailableMatchesByName(data, fuzz, spec); const {results: byDirectory} = processAvailableMatchesByDirectory(data, spec); @@ -109,21 +140,69 @@ function oopsMultipleNameMatches(mode, { normalizedName, multipleNameMatches, }) { + try { + return warnOrThrow(mode, + `Multiple matches for reference "${name}". Please resolve:\n` + + multipleNameMatches[normalizedName] + .map(match => `- ${inspect(match)}\n`) + .join('') + + `Returning null for this reference.`); + } catch (caughtError) { + throw Object.assign(caughtError, { + [Symbol.for('hsmusic.find.multipleNameMatches')]: + multipleNameMatches[normalizedName], + }); + } +} + +function oopsNameCapitalizationMismatch(mode, { + matchingName, + matchedName, +}) { + if (matchingName.length === matchedName.length) { + let a = '', b = ''; + for (let i = 0; i < matchingName.length; i++) { + if ( + matchingName[i] === matchedName[i] || + matchingName[i].toLowerCase() !== matchingName[i].toLowerCase() + ) { + a += matchingName[i]; + b += matchedName[i]; + } else { + a += colors.bright(colors.red(matchingName[i])); + b += colors.bright(colors.green(matchedName[i])); + } + } + + matchingName = a; + matchedName = b; + } + return warnOrThrow(mode, - `Multiple matches for reference "${name}". Please resolve:\n` + - multipleNameMatches[normalizedName] - .map(match => `- ${inspect(match)}\n`) - .join('') + + `Provided capitalization differs from the matched name. Please resolve:\n` + + `- provided: ${matchingName}\n` + + `- should be: ${matchedName}\n` + `Returning null for this reference.`); } -export function prepareMatchByName(mode, {byName, multipleNameMatches}) { +export function prepareMatchByName(mode, fuzz, {byName, multipleNameMatches}) { return (name) => { - const normalizedName = name.toLowerCase(); + const normalizedName = fuzzName(name, fuzz); const match = byName[normalizedName]; if (match) { - return match; + if ( + !fuzz?.capitalization && + name !== match.name && + name.toLowerCase === match.name.toLowerCase() + ) { + return oopsNameCapitalizationMismatch(mode, { + matchingName: name, + matchedName: match.name, + }); + } else { + return match.thing; + } } else if (multipleNameMatches[normalizedName]) { return oopsMultipleNameMatches(mode, { name, @@ -154,7 +233,13 @@ export function prepareMatchByDirectory(mode, {referenceTypes, byDirectory}) { }); } - return byDirectory[directory]; + const match = byDirectory[directory]; + + if (match) { + return match.thing; + } else { + return null; + } }; } @@ -196,11 +281,18 @@ function findHelper({ // hasn't changed! const cache = new WeakMap(); - // The mode argument here may be 'warn', 'error', or 'quiet'. 'error' throws - // errors for null matches (with details about the error), while 'warn' and - // 'quiet' both return null, with 'warn' logging details directly to the - // console. - return (fullRef, data, {mode = 'warn'} = {}) => { + return (fullRef, data, { + // The mode argument here may be 'warn', 'error', or 'quiet'. 'error' throws + // errors for null matches (with details about the error), while 'warn' and + // 'quiet' both return null, with 'warn' logging details directly to the + // console. + mode = 'warn', + + fuzz = { + capitalization: false, + kebab: false, + }, + } = {}) => { if (!fullRef) return null; if (typeof fullRef !== 'string') { @@ -211,19 +303,23 @@ function findHelper({ throw new TypeError(`Expected data to be present`); } - let subcache = cache.get(data); - if (!subcache) { - subcache = - processAllAvailableMatches(data, { + let dataSubcache = cache.get(data); + if (!dataSubcache) { + cache.set(data, dataSubcache = new Map()); + } + + const fuzzHash = getFuzzHash(fuzz); + let fuzzSubcache = dataSubcache.get(fuzzHash); + if (!fuzzSubcache) { + dataSubcache.set(fuzzHash, fuzzSubcache = + processAllAvailableMatches(data, fuzz, { include, getMatchableNames, getMatchableDirectories, - }); - - cache.set(data, subcache); + })); } - const {byDirectory, byName, multipleNameMatches} = subcache; + const {byDirectory, byName, multipleNameMatches} = fuzzSubcache; return matchHelper(fullRef, mode, { matchByDirectory: @@ -233,7 +329,7 @@ function findHelper({ }), matchByName: - prepareMatchByName(mode, { + prepareMatchByName(mode, fuzz, { byName, multipleNameMatches, }), @@ -266,9 +362,9 @@ export function postprocessFindSpec(spec, {thingConstructor}) { if (spec[Symbol.for('Thing.findThisThingOnly')] !== false) { if (spec.include) { const oldInclude = spec.include; - newSpec.include = thing => + newSpec.include = (thing, ...args) => thing instanceof thingConstructor && - oldInclude(thing); + oldInclude(thing, ...args); } else { newSpec.include = thing => thing instanceof thingConstructor; @@ -312,7 +408,7 @@ function findMixedHelper(config) { const multipleNameMatches = Object.create(null); for (const spec of specs) { - processAvailableMatchesByName(data, { + processAvailableMatchesByName(data, null, { ...spec, results: byName, @@ -345,11 +441,17 @@ function findMixedHelper(config) { }); } - return byDirectory[referenceType][directory]; + const match = byDirectory[referenceType][directory]; + + if (match) { + return match.thing; + } else { + return null; + } }, matchByName: - prepareMatchByName(mode, { + prepareMatchByName(mode, null, { byName, multipleNameMatches, }), diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 3ccd8ce2..18f88ce6 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -162,10 +162,11 @@ import { import dimensionsOf from 'image-size'; -import CacheableObject from '#cacheable-object'; import {stringifyCache} from '#cli'; import {commandExists, isMain, promisifyProcess, traverse} from '#node-utils'; import {sortByName} from '#sort'; +import {delay, empty, filterMultipleArrays, queue, stitchArrays, unique} + from '#sugar'; import { colors, @@ -178,16 +179,6 @@ import { progressPromiseAll, } from '#cli'; -import { - delay, - empty, - chunkMultipleArrays, - filterMultipleArrays, - queue, - stitchArrays, - unique, -} from '#sugar'; - export const defaultMagickThreads = 8; function getSpecbustForCacheEntry(entry) { @@ -447,7 +438,7 @@ async function getImageMagickVersion(binary) { try { await promisifyProcess(proc, false); - } catch (error) { + } catch { return null; } @@ -556,42 +547,56 @@ async function determineThumbtacksNeededForFile({ return mismatchedWithinRightSize; } -async function generateImageThumbnail(imagePath, thumbtack, { +// Write all requested thumbtacks for a source image in one pass +// This saves a lot of disk reads which are probably the main bottleneck +function prepareConvertArgs(filePathInMedia, dirnameInCache, thumbtacks) { + const args = [filePathInMedia, '-strip']; + + const basename = + path.basename(filePathInMedia, path.extname(filePathInMedia)); + + // do larger sizes first + thumbtacks.sort((a, b) => thumbnailSpec[b].size - thumbnailSpec[a].size); + + for (const tack of thumbtacks) { + const {size, quality} = thumbnailSpec[tack]; + const filename = `${basename}.${tack}.jpg`; + const filePathInCache = path.join(dirnameInCache, filename); + args.push( + '(', '+clone', + '-resize', `${size}x${size}>`, + '-interlace', 'Plane', + '-quality', `${quality}%`, + '-write', filePathInCache, + '+delete', ')', + ); + } + + // throw away the (already written) image stream + args.push('null:'); + + return args; +} + +async function generateImageThumbnails(imagePath, thumbtacks, { mediaPath, mediaCachePath, spawnConvert, }) { + if (empty(thumbtacks)) return; + const filePathInMedia = path.join(mediaPath, imagePath); const dirnameInCache = path.join(mediaCachePath, path.dirname(imagePath)); - const filename = - path.basename(imagePath, path.extname(imagePath)) + - `.${thumbtack}.jpg`; - - const filePathInCache = - path.join(dirnameInCache, filename); - await mkdir(dirnameInCache, {recursive: true}); - const specEntry = thumbnailSpec[thumbtack]; - const {size, quality} = specEntry; - - const convertProcess = spawnConvert([ - filePathInMedia, - '-strip', - '-resize', - `${size}x${size}>`, - '-interlace', - 'Plane', - '-quality', - `${quality}%`, - filePathInCache, - ]); - - await promisifyProcess(convertProcess, false); + const convertArgs = + prepareConvertArgs(filePathInMedia, dirnameInCache, thumbtacks); + + await promisifyProcess(spawnConvert(convertArgs), false); } export async function determineMediaCachePath({ @@ -628,7 +633,7 @@ export async function determineMediaCachePath({ try { const files = await readdir(mediaPath); mediaIncludesThumbnailCache = files.includes(CACHE_FILE); - } catch (error) { + } catch { mediaIncludesThumbnailCache = false; } @@ -861,7 +866,7 @@ export async function migrateThumbsIntoDedicatedCacheDirectory({ path.join(mediaPath, CACHE_FILE), path.join(mediaCachePath, CACHE_FILE)); logInfo`Moved thumbnail cache file.`; - } catch (error) { + } catch { logWarn`Failed to move cache file. (${CACHE_FILE})`; logWarn`Check its permissions, or try copying/pasting.`; } @@ -1100,33 +1105,23 @@ export default async function genThumbs({ const writeMessageFn = () => `Writing image thumbnails. [failed: ${numFailed}]`; - const generateCallImageIndices = - imageThumbtacksNeeded - .flatMap(({length}, index) => - Array.from({length}, () => index)); - - const generateCallImagePaths = - generateCallImageIndices - .map(index => imagePaths[index]); - - const generateCallThumbtacks = - imageThumbtacksNeeded.flat(); - const generateCallFns = stitchArrays({ - imagePath: generateCallImagePaths, - thumbtack: generateCallThumbtacks, - }).map(({imagePath, thumbtack}) => () => - generateImageThumbnail(imagePath, thumbtack, { + imagePath: imagePaths, + thumbtacks: imageThumbtacksNeeded, + }).map(({imagePath, thumbtacks}) => () => + generateImageThumbnails(imagePath, thumbtacks, { mediaPath, mediaCachePath, spawnConvert, }).catch(error => { numFailed++; - return ({error}); + return {error}; })); - logInfo`Generating ${generateCallFns.length} thumbnails for ${imagePaths.length} media files.`; + const totalThumbs = imageThumbtacksNeeded.reduce((sum, tacks) => sum + tacks.length, 0); + + logInfo`Generating ${totalThumbs} thumbnails for ${imagePaths.length} media files.`; if (generateCallFns.length > 500) { logInfo`Go get a latte - this could take a while!`; } @@ -1135,37 +1130,30 @@ export default async function genThumbs({ await progressPromiseAll(writeMessageFn, queue(generateCallFns, magickThreads)); - let successfulIndices; + let successfulPaths; { - const erroredIndices = generateCallImageIndices.slice(); - const erroredPaths = generateCallImagePaths.slice(); - const erroredThumbtacks = generateCallThumbtacks.slice(); + const erroredPaths = imagePaths.slice(); const errors = generateCallResults.map(result => result?.error); const {removed} = filterMultipleArrays( - erroredIndices, erroredPaths, - erroredThumbtacks, errors, - (_index, _imagePath, _thumbtack, error) => error); + (_imagePath, error) => error); - successfulIndices = new Set(removed[0]); - - const chunks = - chunkMultipleArrays(erroredPaths, erroredThumbtacks, errors, - (imagePath, lastImagePath) => imagePath !== lastImagePath); + ([successfulPaths] = removed); // TODO: This should obviously be an aggregate error. // ...Just like every other error report here, and those dang aggregates // should be constructable from within the queue, rather than after. - for (const [[imagePath], thumbtacks, errors] of chunks) { - logError`Failed to generate thumbnails for ${imagePath}:`; - for (const {thumbtack, error} of stitchArrays({thumbtack: thumbtacks, error: errors})) { - logError`- ${thumbtack}: ${error}`; - } - } + stitchArrays({ + imagePath: erroredPaths, + error: errors, + }).forEach(({imagePath, error}) => { + logError`Failed to generate thumbnails for ${imagePath}:`; + logError`- ${error}`; + }); if (empty(errors)) { logInfo`All needed thumbnails generated successfully - nice!`; @@ -1179,8 +1167,8 @@ export default async function genThumbs({ imagePaths, imageThumbtacksNeeded, imageDimensions, - (_imagePath, _thumbtacksNeeded, _dimensions, index) => - successfulIndices.has(index)); + (imagePath, _thumbtacksNeeded, _dimensions) => + successfulPaths.includes(imagePath)); for (const { imagePath, @@ -1242,41 +1230,20 @@ export function getExpectedImagePaths(mediaPath, {urls, wikiData}) { const fromRoot = urls.from('media.root'); const paths = [ + wikiData.artworkData + .filter(artwork => artwork.path) + .map(artwork => fromRoot.to(...artwork.path)), + wikiData.albumData - .map(album => [ - album.hasCoverArt && [ - fromRoot.to('media.albumCover', album.directory, album.coverArtFileExtension), - ], - - !empty(CacheableObject.getUpdateValue(album, 'bannerArtistContribs')) && [ - fromRoot.to('media.albumBanner', album.directory, album.bannerFileExtension), - ], - - !empty(CacheableObject.getUpdateValue(album, 'wallpaperArtistContribs')) && - empty(album.wallpaperParts) && [ - fromRoot.to('media.albumWallpaper', album.directory, album.wallpaperFileExtension), - ], - - !empty(CacheableObject.getUpdateValue(album, 'wallpaperArtistContribs')) && - !empty(album.wallpaperParts) && - album.wallpaperParts.flatMap(part => [ - part.asset && - fromRoot.to('media.albumWallpaperPart', album.directory, part.asset), - ]), - ]) - .flat(2) - .filter(Boolean), - - wikiData.artistData - .filter(artist => artist.hasAvatar) - .map(artist => fromRoot.to('media.artistAvatar', artist.directory, artist.avatarFileExtension)), - - wikiData.flashData - .map(flash => fromRoot.to('media.flashArt', flash.directory, flash.coverArtFileExtension)), - - wikiData.trackData - .filter(track => track.hasUniqueCoverArt) - .map(track => fromRoot.to('media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension)), + .flatMap(album => album.wallpaperParts + .filter(part => part.asset) + .map(part => + fromRoot.to('media.albumWallpaperPart', album.directory, part.asset))), + + wikiData.wikiInfo.wikiWallpaperParts + .filter(part => part.asset) + .map(part => + fromRoot.to('media.path', part.asset)), ].flat(); sortByName(paths, {getName: path => path}); diff --git a/src/html.js b/src/html.js index 0fe424df..30b4d287 100644 --- a/src/html.js +++ b/src/html.js @@ -53,6 +53,17 @@ export const attributeSpec = { }, }; +let disabledSlotValidation = false; +let disabledTagTracing = false; + +export function disableSlotValidation() { + disabledSlotValidation = true; +} + +export function disableTagTracing() { + disabledTagTracing = true; +} + // Pass to tag() as an attributes key to make tag() return a 8lank tag if the // provided content is empty. Useful for when you'll only 8e showing an element // according to the presence of content that would 8elong there. @@ -223,7 +234,11 @@ export function isBlank(content) { // could include content. These need to be checked too. // Check each of the templates one at a time. for (const template of result) { - const content = template.content; + // Resolve the content all the way down to a tag - + // if it's a template that returns another template, + // that won't do, because we need to detect if its + // final content is a tag marked onlyIfSiblings. + const content = normalize(template); if (content instanceof Tag && content.onlyIfSiblings) { continue; @@ -271,7 +286,11 @@ export const validators = { }, }; -export function blank() { +export function blank(...args) { + if (args.length) { + throw new Error(`Passed arguments - did you mean isBlank() instead?`) + } + return []; } @@ -352,8 +371,10 @@ export class Tag { this.attributes = attributes; this.content = content; - this.#traceError = new Error(); - } + if (!disabledTagTracing) { + this.#traceError = new Error(); + } +} clone() { return Reflect.construct(this.constructor, [ @@ -512,6 +533,10 @@ export class Tag { } } + #getAttributeRaw(attribute) { + return this.attributes.get(attribute); + } + set onlyIfContent(value) { this.#setAttributeFlag(onlyIfContent, value); } @@ -579,7 +604,7 @@ export class Tag { try { this.content = this.content; - } catch (error) { + } catch { this.#setAttributeFlag(imaginarySibling, false); } } @@ -662,7 +687,7 @@ export class Tag { const chunkwrapSplitter = (this.chunkwrap - ? this.#getAttributeString('split') + ? this.#getAttributeRaw('split') : null); let seenChunkwrapSplitter = @@ -702,17 +727,19 @@ export class Tag { `of ${inspect(this, {compact: true})}`, {cause: caughtError}); - error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)] = true; - error[Symbol.for(`hsmusic.aggregate.traceFrom`)] = this.#traceError; + if (this.#traceError && !disabledTagTracing) { + error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)] = true; + error[Symbol.for(`hsmusic.aggregate.traceFrom`)] = this.#traceError; - error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)] = [ - /content-function\.js/, - /util\/html\.js/, - ]; + error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)] = [ + /content-function\.js/, + /util\/html\.js/, + ]; - error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)] = [ - /content\/dependencies\/(.*\.js:.*(?=\)))/, - ]; + error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)] = [ + /content\/dependencies\/(.*\.js:.*(?=\)))/, + ]; + } throw error; } @@ -727,7 +754,7 @@ export class Tag { const chunkwrapChunks = (typeof nonTemplateItem === 'string' && chunkwrapSplitter - ? itemContent.split(chunkwrapSplitter) + ? Array.from(getChunkwrapChunks(itemContent, chunkwrapSplitter)) : null); const itemIncludesChunkwrapSplit = @@ -773,7 +800,7 @@ export class Tag { appendItemContent: { if (itemIncludesChunkwrapSplit) { - for (const [index, chunk] of chunkwrapChunks.entries()) { + for (const [index, {chunk, following}] of chunkwrapChunks.entries()) { if (index === 0) { // The first chunk isn't actually a chunk all on its own, it's // text that should be appended to the previous chunk. We will @@ -781,12 +808,27 @@ export class Tag { // the next chunk. content += chunk; } else { - const whitespace = chunk.match(/^\s+/) ?? ''; - content += chunkwrapSplitter; + const followingWhitespace = following.match(/\s+$/) ?? ''; + const chunkWhitespace = chunk.match(/^\s+/) ?? ''; + + if (followingWhitespace) { + content += following.slice(0, -followingWhitespace.length); + } else { + content += following; + } + content += '</span>'; - content += whitespace; + + content += followingWhitespace; + content += chunkWhitespace; + content += '<span class="chunkwrap">'; - content += chunk.slice(whitespace.length); + + if (chunkWhitespace) { + content += chunk.slice(chunkWhitespace.length); + } else { + content += chunk; + } } } @@ -1009,6 +1051,49 @@ export class Tag { } } +export function* getChunkwrapChunks(content, splitter) { + const splitString = + (typeof splitter === 'string' + ? splitter + : null); + + if (splitString) { + let following = ''; + for (const chunk of content.split(splitString)) { + yield {chunk, following}; + following = splitString; + } + + return; + } + + const splitRegExp = + (splitter instanceof RegExp + ? new RegExp( + splitter.source, + (splitter.flags.includes('g') + ? splitter.flags + : splitter.flags + 'g')) + : null); + + if (splitRegExp) { + let following = ''; + let prevIndex = 0; + for (const match of content.matchAll(splitRegExp)) { + const chunk = content.slice(prevIndex, match.index); + yield {chunk, following}; + + following = match[0]; + prevIndex = match.index + match[0].length; + } + + const chunk = content.slice(prevIndex); + yield {chunk, following}; + + return; + } +} + export function attributes(attributes) { return new Attributes(attributes); } @@ -1069,6 +1154,34 @@ export class Attributes { } add(...args) { + // Very common case: add({class: 'foo', id: 'bar'})¡ + // The argument is a plain object (no Template, no Attributes, + // no blessAttributes symbol). We can skip the expensive + // isAttributesAdditionSinglet() validation and flatten/array handling. + if ( + args.length === 1 && + args[0] && + typeof args[0] === 'object' && + !Array.isArray(args[0]) && + !(args[0] instanceof Attributes) && + !(args[0] instanceof Template) && + !Object.hasOwn(args[0], blessAttributes) + ) { + const obj = args[0]; + + // Preserve existing merge semantics by funnelling each key through + // the internal #addOneAttribute helper (handles class/style union, + // unique merging, etc.) but avoid *per-object* validation overhead. + for (const key of Reflect.ownKeys(obj)) { + this.#addOneAttribute(key, obj[key]); + } + + // Match the original return style (list of results) so callers that + // inspect the return continue to work. + return obj; + } + + // Fall back to the original slow-but-thorough implementation switch (args.length) { case 1: isAttributesAdditionSinglet(args[0]); @@ -1080,10 +1193,11 @@ export class Attributes { default: throw new Error( - `Expected array or object, or attribute and value`); + 'Expected array or object, or attribute and value'); } } + with(...args) { const clone = this.clone(); clone.add(...args); @@ -1254,6 +1368,9 @@ export class Attributes { return value.some(Boolean); } else if (value === null) { return false; + } else if (value instanceof RegExp) { + // Oooooooo. + return true; } else { // Other objects are an error. break; @@ -1285,13 +1402,16 @@ export class Attributes { case 'number': return value.toString(); - // If it's a kept object, it's an array. case 'object': { - const joiner = - (descriptor?.arraylike && descriptor?.join) - ?? ' '; + if (Array.isArray(value)) { + const joiner = + (descriptor?.arraylike && descriptor?.join) + ?? ' '; - return value.filter(Boolean).join(joiner); + return value.filter(Boolean).join(joiner); + } else { + return value; + } } default: @@ -1671,6 +1791,10 @@ export class Template { } static validateSlotValueAgainstDescription(value, description) { + if (disabledSlotValidation) { + return true; + } + if (value === undefined) { throw new TypeError(`Specify value as null or don't specify at all`); } @@ -1848,17 +1972,11 @@ export class Template { return this.content.toString(); } - static resolve(tagOrTemplate) { + static resolve(content) { // Flattens contents of a template, recursively "resolving" until a // non-template is ready (or just returns a provided non-template // argument as-is). - if (!(tagOrTemplate instanceof Template)) { - return tagOrTemplate; - } - - let {content} = tagOrTemplate; - while (content instanceof Template) { content = content.content; } @@ -1866,7 +1984,7 @@ export class Template { return content; } - static resolveForSlots(tagOrTemplate, slots) { + static resolveForSlots(content, slots) { if (!slots || typeof slots !== 'object') { throw new Error( `Expected slots to be an object or array, ` + @@ -1874,24 +1992,24 @@ export class Template { } if (!Array.isArray(slots)) { - return Template.resolveForSlots(tagOrTemplate, Object.keys(slots)).slots(slots); + return Template.resolveForSlots(content, Object.keys(slots)).slots(slots); } - while (tagOrTemplate && tagOrTemplate instanceof Template) { + while (content instanceof Template) { try { for (const slot of slots) { - tagOrTemplate.getSlotDescription(slot); + content.getSlotDescription(slot); } - return tagOrTemplate; + return content; } catch { - tagOrTemplate = tagOrTemplate.content; + content = content.content; } } throw new Error( `Didn't find slots ${inspect(slots, {compact: true})} ` + - `resolving ${inspect(tagOrTemplate, {compact: true})}`); + `resolving ${inspect(content, {compact: true})}`); } [inspect.custom]() { @@ -1963,6 +2081,8 @@ export const isAttributeValue = anyOf( isString, isNumber, isBoolean, isArray, isTag, isTemplate, + // Evil. Ooooo + validateInstanceOf(RegExp), validateArrayItems(item => isAttributeValue(item))); export const isAttributesAdditionPair = pair => { diff --git a/src/listing-spec.js b/src/listing-spec.js index 142c5976..a301845b 100644 --- a/src/listing-spec.js +++ b/src/listing-spec.js @@ -178,6 +178,7 @@ listingSpec.push({ directory: 'tracks/with-lyrics', stringsKey: 'listTracks.withLyrics', contentFunction: 'listTracksWithLyrics', + seeAlso: ['tracks/needing-lyrics'], }); listingSpec.push({ @@ -195,6 +196,13 @@ listingSpec.push({ }); listingSpec.push({ + directory: 'tracks/needing-lyrics', + stringsKey: 'listTracks.needingLyrics', + contentFunction: 'listTracksNeedingLyrics', + seeAlso: ['tracks/with-lyrics'], +}); + +listingSpec.push({ directory: 'tags/by-name', stringsKey: 'listArtTags.byName', contentFunction: 'listArtTagsByName', @@ -291,7 +299,7 @@ for (const [index, listing] of listingSpec.entries()) { errors.push(new AggregateError(suberrors, `Errors matching "see also" listings for ${listing.directory}`)); } } else { - listing.seeAlso = null; + listing.seeAlso = []; } } diff --git a/src/page/album.js b/src/page/album.js index 8c08b960..e585618c 100644 --- a/src/page/album.js +++ b/src/page/album.js @@ -8,15 +8,22 @@ export function targets({wikiData}) { export function pathsForTarget(album) { return [ - { - type: 'page', - path: ['album', album.directory], - - contentFunction: { - name: 'generateAlbumInfoPage', - args: [album], - }, - }, + (album.style === 'single' + ? { + type: 'redirect', + fromPath: ['album', album.directory], + toPath: ['track', album.tracks[0].directory], + title: album.name, + } + : { + type: 'page', + path: ['album', album.directory], + + contentFunction: { + name: 'generateAlbumInfoPage', + args: [album], + }, + }), { type: 'page', @@ -43,7 +50,8 @@ export function pathsForTarget(album) { path: ['albumReferencedArtworks', album.directory], condition: () => - !empty(album.referencedArtworks), + album.hasCoverArt && + !empty(album.coverArtworks[0].referencedArtworks), contentFunction: { name: 'generateAlbumReferencedArtworksPage', @@ -56,7 +64,8 @@ export function pathsForTarget(album) { path: ['albumReferencingArtworks', album.directory], condition: () => - !empty(album.referencedByArtworks), + album.hasCoverArt && + !empty(album.coverArtworks[0].referencedByArtworks), contentFunction: { name: 'generateAlbumReferencingArtworksPage', diff --git a/src/page/artist.js b/src/page/artist.js index 257e060d..7cd50bb3 100644 --- a/src/page/artist.js +++ b/src/page/artist.js @@ -32,6 +32,23 @@ export function pathsForTarget(artist) { args: [artist], }, }, + + { + type: 'page', + path: ['artistRollingWindow', artist.directory], + + condition: () => + artist.musicContributions.some(contrib => contrib.date) || + artist.artworkContributions.some(contrib => + contrib.date && + contrib.thingProperty !== 'wallpaperArtistContribs' && + contrib.thingProperty !== 'bannerArtistContribs'), + + contentFunction: { + name: 'generateArtistRollingWindowPage', + args: [artist], + }, + }, ]; } diff --git a/src/page/track.js b/src/page/track.js index 301af991..95647334 100644 --- a/src/page/track.js +++ b/src/page/track.js @@ -25,7 +25,8 @@ export function pathsForTarget(track) { path: ['trackReferencedArtworks', track.directory], condition: () => - !empty(track.referencedArtworks), + track.hasUniqueCoverArt && + !empty(track.trackArtworks[0].referencedArtworks), contentFunction: { name: 'generateTrackReferencedArtworksPage', @@ -38,7 +39,8 @@ export function pathsForTarget(track) { path: ['trackReferencingArtworks', track.directory], condition: () => - !empty(track.referencedByArtworks), + track.hasUniqueCoverArt && + !empty(track.trackArtworks[0].referencedByArtworks), contentFunction: { name: 'generateTrackReferencingArtworksPage', diff --git a/src/replacer.js b/src/replacer.js index 32657a5a..8a929444 100644 --- a/src/replacer.js +++ b/src/replacer.js @@ -8,7 +8,8 @@ import * as marked from 'marked'; import * as html from '#html'; -import {escapeRegex, typeAppearance} from '#sugar'; +import {empty, escapeRegex, typeAppearance} from '#sugar'; +import {matchInlineLinks, matchMarkdownLinks} from '#wiki-data'; export const replacerSpec = { 'album': { @@ -174,6 +175,11 @@ export const replacerSpec = { find: 'trackWithArtwork', link: 'linkTrackReferencingArtworks', }, + + 'tooltip': { + value: (ref) => ref, + link: null, + } }; // Syntax literals. @@ -184,6 +190,9 @@ const tagHash = '#'; const tagArgument = '*'; const tagArgumentValue = '='; const tagLabel = '|'; +const tooltipBeginning = '<<'; +const tooltipEnding = '>>'; +const tooltipContent = ':'; const noPrecedingWhitespace = '(?<!\\s)'; @@ -202,6 +211,14 @@ const R_tagArgumentValue = escapeRegex(tagArgumentValue); const R_tagLabel = escapeRegex(tagLabel); +const R_tooltipBeginning = + '(?<=[^<]|^)' + escapeRegex(tooltipBeginning) + '(?!<)'; + +const R_tooltipEnding = + '(?<=[^>]|^)' + escapeRegex(tooltipEnding) + '(?!>)'; + +const R_tooltipContent = escapeRegex(tooltipContent); + const regexpCache = {}; const makeError = (i, message) => ({i, type: 'error', data: {message}}); @@ -241,9 +258,13 @@ function parseNodes(input, i, stopAt, textOnly) { } }; - const literalsToMatch = stopAt - ? stopAt.concat([R_tagBeginning]) - : [R_tagBeginning]; + const beginnings = [tagBeginning, tooltipBeginning]; + const R_beginnings = [R_tagBeginning, R_tooltipBeginning]; + + const literalsToMatch = + (stopAt + ? stopAt.concat(R_beginnings) + : R_beginnings); // The 8ackslash stuff here is to only match an even (or zero) num8er // of sequential 'slashes. Even amounts always cancel out! Odd amounts @@ -294,8 +315,10 @@ function parseNodes(input, i, stopAt, textOnly) { if (textOnly && closestMatch === tagBeginning) throw makeError(i, `Unexpected [[tag]] - expected only text here.`); + if (textOnly && closestMatch === tooltipBeginning) + throw makeError(i, `Unexpected <<tooltip>> - expected only text here.`); - const stopHere = closestMatch !== tagBeginning; + const stopHere = !beginnings.includes(closestMatch); iString = i; i = closestMatchIndex; @@ -447,6 +470,51 @@ function parseNodes(input, i, stopAt, textOnly) { continue; } + + if (closestMatch === tooltipBeginning) { + const iTooltip = closestMatchIndex; + + let N; + + // Label (hoverable text) + + let label; + + N = parseNodes(input, i, [R_tooltipContent, R_tooltipEnding]); + + if (!stopped) + throw endOfInput(i, `reading tooltip label`); + if (input.slice(i).startsWith(tooltipEnding)) + throw makeError(i, `Expected tooltip label and content.`); + if (!N.length) + throw makeError(i, `Expected tooltip label before content.`); + + label = N; + i = stop_iParse; + + // Content (tooltip text) + + let content; + + N = parseNodes(input, i, [R_tooltipEnding]); + + if (!stopped) + throw endOfInput(i, `reading tooltip content`); + if (!N.length) + throw makeError(i, `Expected tooltip content`); + + content = N; + i = stop_iParse; + + nodes.push({ + i: iTooltip, + iEnd: i, + type: 'tooltip', + data: {label, content}, + }); + + continue; + } } return nodes; @@ -458,7 +526,7 @@ export function squashBackslashes(text) { // a set of characters where the backslash carries meaning // into later formatting (i.e. markdown). Note that we do // NOT compress double backslashes into single backslashes. - return text.replace(/([^\\](?:\\{2})*)\\(?![\\*_~>-])/g, '$1'); + return text.replace(/([^\\](?:\\{2})*)\\(?![\\*_~>.-])/g, '$1'); } export function restoreRawHTMLTags(text) { @@ -520,6 +588,7 @@ export function postprocessComments(inputNodes) { function postprocessHTMLTags(inputNodes, tagName, callback) { const outputNodes = []; + const errors = []; const lastNode = inputNodes.at(-1); @@ -587,10 +656,16 @@ function postprocessHTMLTags(inputNodes, tagName, callback) { return false; })(); - outputNodes.push( - callback(attributes, { - inline, - })); + try { + outputNodes.push( + callback(attributes, { + inline, + })); + } catch (caughtError) { + errors.push(new Error( + `Failed to process ${match[0]}`, + {cause: caughtError})); + } // No longer at the start of a line after the tag - there will at // least be text with only '\n' before the next of this tag that's @@ -613,15 +688,33 @@ function postprocessHTMLTags(inputNodes, tagName, callback) { outputNodes.push(node); } + if (!empty(errors)) { + throw new AggregateError( + errors, + `Errors postprocessing <${tagName}> tags`); + } + return outputNodes; } +function complainAboutMediaSrc(src) { + if (!src) { + throw new Error(`Missing "src" attribute`); + } + + if (src.startsWith('/media/')) { + throw new Error(`Start "src" with "media/", not "/media/"`); + } +} + export function postprocessImages(inputNodes) { return postprocessHTMLTags(inputNodes, 'img', (attributes, {inline}) => { const node = {type: 'image'}; node.src = attributes.get('src'); + complainAboutMediaSrc(node.src); + node.inline = attributes.get('inline') ?? inline; if (attributes.get('link')) node.link = attributes.get('link'); @@ -642,13 +735,17 @@ export function postprocessImages(inputNodes) { export function postprocessVideos(inputNodes) { return postprocessHTMLTags(inputNodes, 'video', - attributes => { + (attributes, {inline}) => { const node = {type: 'video'}; node.src = attributes.get('src'); + complainAboutMediaSrc(node.src); + + node.inline = attributes.get('inline') ?? inline; if (attributes.get('width')) node.width = parseInt(attributes.get('width')); if (attributes.get('height')) node.height = parseInt(attributes.get('height')); + if (attributes.get('align')) node.align = attributes.get('align'); if (attributes.get('pixelate')) node.pixelate = true; return node; @@ -661,8 +758,13 @@ export function postprocessAudios(inputNodes) { const node = {type: 'audio'}; node.src = attributes.get('src'); + complainAboutMediaSrc(node.src); + node.inline = attributes.get('inline') ?? inline; + if (attributes.get('align')) node.align = attributes.get('align'); + if (attributes.get('nameless')) node.nameless = true; + return node; }); } @@ -754,7 +856,7 @@ export function postprocessSummaries(inputNodes) { } export function postprocessExternalLinks(inputNodes) { - const outputNodes = []; + let outputNodes = []; for (const node of inputNodes) { if (node.type !== 'text') { @@ -762,110 +864,292 @@ export function postprocessExternalLinks(inputNodes) { continue; } - const plausibleLinkRegexp = /\[.*?\)/g; + let textNode = { + i: node.i, + iEnd: null, + type: 'text', + data: '', + }; - let textContent = ''; + let parseFrom = 0; + for (const match of matchMarkdownLinks(node.data, {marked})) { + const {label, href, index, length} = match; - let plausibleMatch = null, parseFrom = 0; - while (plausibleMatch = plausibleLinkRegexp.exec(node.data)) { - textContent += node.data.slice(parseFrom, plausibleMatch.index); - - // Pedantic rules use more particular parentheses detection in link - // destinations - they allow one level of balanced parentheses, and - // otherwise, parentheses must be escaped. This allows for entire links - // to be wrapped in parentheses, e.g below: - // - // This is so cool. ([You know??](https://example.com)) - // - const definiteMatch = - marked.Lexer.rules.inline.pedantic.link - .exec(node.data.slice(plausibleMatch.index)); - - if (definiteMatch) { - const {1: label, 2: href} = definiteMatch; - - // Split the containing text node into two - the second of these will - // be added after iterating over matches, or by the next match. - if (textContent.length) { - outputNodes.push({type: 'text', data: textContent}); - textContent = ''; - } + textNode.data += node.data.slice(parseFrom, index); - const offset = plausibleMatch.index + definiteMatch.index; - const length = definiteMatch[0].length; + // Split the containing text node into two - the second of these will + // be filled in and pushed by the next match, or after iterating over + // all matches. + if (textNode.data) { + textNode.iEnd = textNode.i + textNode.data.length; + outputNodes.push(textNode); - outputNodes.push({ - i: node.i + offset, - iEnd: node.i + offset + length, - type: 'external-link', - data: {label, href}, - }); + textNode = { + i: node.i + index + length, + iEnd: null, + type: 'text', + data: '', + }; + } - parseFrom = offset + length; - } else { - parseFrom = plausibleMatch.index; + outputNodes.push({ + i: node.i + index, + iEnd: node.i + index + length, + type: 'external-link', + data: {label, href}, + }); + + parseFrom = index + length; + } + + if (parseFrom !== node.data.length) { + textNode.data += node.data.slice(parseFrom); + textNode.iEnd = node.iEnd; + } + + if (textNode.data) { + outputNodes.push(textNode); + } + } + + // Repeat everything, but for inline links, which are just a URL on its own, + // not formatted as a Markdown link. These don't have provided labels, and + // get labels automatically filled in by content code. + + inputNodes = outputNodes; + outputNodes = []; + + for (const node of inputNodes) { + if (node.type !== 'text') { + outputNodes.push(node); + continue; + } + + let textNode = { + i: node.i, + iEnd: null, + type: 'text', + data: '', + }; + + let parseFrom = 0; + for (const match of matchInlineLinks(node.data)) { + const {href, index, length} = match; + + textNode.data += node.data.slice(parseFrom, index); + + if (textNode.data) { + textNode.iEnd = textNode.i + textNode.data.length; + outputNodes.push(textNode); + + textNode = { + i: node.i + index + length, + iEnd: null, + type: 'text', + data: '', + }; } + + outputNodes.push({ + i: node.i + index, + iEnd: node.i + index + length, + type: 'external-link', + data: {label: null, href}, + }); + + parseFrom = index + length; } if (parseFrom !== node.data.length) { - textContent += node.data.slice(parseFrom); + textNode.data += node.data.slice(parseFrom); + textNode.iEnd = node.iEnd; } - if (textContent.length) { - outputNodes.push({type: 'text', data: textContent}); + if (textNode.data) { + outputNodes.push(textNode); } } return outputNodes; } -export function parseInput(input) { +export function parseContentNodes(input, { + errorMode = 'throw', +} = {}) { if (typeof input !== 'string') { throw new TypeError(`Expected input to be string, got ${typeAppearance(input)}`); } - try { - let output = parseNodes(input, 0); - output = postprocessComments(output); - output = postprocessImages(output); - output = postprocessVideos(output); - output = postprocessAudios(output); - output = postprocessHeadings(output); - output = postprocessSummaries(output); - output = postprocessExternalLinks(output); - return output; - } catch (errorNode) { - if (errorNode.type !== 'error') { - throw errorNode; - } - - const { - i, - data: {message}, - } = errorNode; - - let lineStart = input.slice(0, i).lastIndexOf('\n'); - if (lineStart >= 0) { - lineStart += 1; - } else { - lineStart = 0; + let result = null, error = null; + + process: { + try { + result = parseNodes(input, 0); + } catch (caughtError) { + if (caughtError.type === 'error') { + const {i, data: {message}} = caughtError; + + let lineStart = input.slice(0, i).lastIndexOf('\n'); + if (lineStart >= 0) { + lineStart += 1; + } else { + lineStart = 0; + } + + let lineEnd = input.slice(i).indexOf('\n'); + if (lineEnd >= 0) { + lineEnd += i; + } else { + lineEnd = input.length; + } + + const line = input.slice(lineStart, lineEnd); + + const cursor = i - lineStart; + + error = + new SyntaxError( + `Parse error (at pos ${i}): ${message}\n` + + line + `\n` + + '-'.repeat(cursor) + '^'); + } else { + error = caughtError; + } + + // A parse error means there's no output to continue with at all, + // so stop here. + break process; } - let lineEnd = input.slice(i).indexOf('\n'); - if (lineEnd >= 0) { - lineEnd += i; + const postprocessErrors = []; + + for (const postprocess of [ + postprocessComments, + postprocessImages, + postprocessVideos, + postprocessAudios, + postprocessHeadings, + postprocessSummaries, + postprocessExternalLinks, + ]) { + try { + result = postprocess(result); + } catch (caughtError) { + const error = + new Error( + `Error in step ${`"${postprocess.name}"`}`, + {cause: caughtError}); + + error[Symbol.for('hsmusic.aggregate.translucent')] = true; + + postprocessErrors.push(error); + } + } + + if (!empty(postprocessErrors)) { + error = + new AggregateError( + postprocessErrors, + `Errors postprocessing content text`); + + error[Symbol.for('hsmusic.aggregate.translucent')] = 'single'; + } + } + + if (errorMode === 'throw') { + if (error) { + throw error; } else { - lineEnd = input.length; + return result; + } + } else if (errorMode === 'return') { + if (!result) { + result = [{ + i: 0, + iEnd: input.length, + type: 'text', + data: input, + }]; + } + + return {error, result}; + } else { + throw new Error(`Unknown errorMode ${errorMode}`); + } +} + +export function* splitContentNodesAround(nodes, splitter) { + if (splitter instanceof RegExp) { + const regex = splitter; + + splitter = function*(text) { + for (const match of text.matchAll(regex)) { + yield { + index: match.index, + length: match[0].length, + }; + } + }; + } + + if (typeof splitter === 'string') { + throw new TypeError(`Expected generator or regular expression`); + } + + function* splitTextNode(node) { + let textNode = { + i: node.i, + iEnd: null, + type: 'text', + data: '', + }; + + let parseFrom = 0; + for (const match of splitter(node.data)) { + const {index, length} = match; + + textNode.data += node.data.slice(parseFrom, index); + + if (textNode.data) { + textNode.iEnd = textNode.i + textNode.data.length; + yield textNode; + } + + yield { + i: node.i + index, + iEnd: node.i + index + length, + type: 'separator', + data: { + text: node.data.slice(index, index + length), + match, + }, + }; + + textNode = { + i: node.i + index + length, + iEnd: null, + type: 'text', + data: '', + }; + + parseFrom = index + length; } - const line = input.slice(lineStart, lineEnd); + if (parseFrom !== node.data.length) { + textNode.data += node.data.slice(parseFrom); + textNode.iEnd = node.iEnd; + } - const cursor = i - lineStart; + if (textNode.data) { + yield textNode; + } + } - throw new SyntaxError([ - `Parse error (at pos ${i}): ${message}`, - line, - '-'.repeat(cursor) + '^', - ].join('\n')); + for (const node of nodes) { + if (node.type === 'text') { + yield* splitTextNode(node); + } else { + yield node; + } } } diff --git a/src/reverse.js b/src/reverse.js index 9ad5c8a7..b4b225f0 100644 --- a/src/reverse.js +++ b/src/reverse.js @@ -83,7 +83,9 @@ function reverseHelper(spec) { for (const referencedThing of allReferencedThings) { if (cacheRecord.has(referencedThing)) { const referencingThings = cacheRecord.get(referencedThing); - sortByDate(referencingThings); + sortByDate(referencingThings, { + getDate: spec.date ?? (thing => thing.date), + }); } } @@ -100,28 +102,7 @@ function reverseHelper(spec) { }; } -const hardcodedReverseSpecs = { - // Artworks aren't Thing objects. - // This spec operates on albums and tracks alike! - artworksWhichReference: { - bindTo: 'wikiData', - - referencing: ({albumData, trackData}) => - [...albumData, ...trackData] - .flatMap(referencingThing => - referencingThing.referencedArtworks - .map(({thing: referencedThing, ...referenceDetails}) => ({ - referencingThing, - referencedThing, - referenceDetails, - }))), - - referenced: ({referencedThing}) => [referencedThing], - - tidy: ({referencingThing, referenceDetails}) => - ({thing: referencingThing, ...referenceDetails}), - }, -}; +const hardcodedReverseSpecs = {}; const findReverseHelperConfig = { word: `reverse`, diff --git a/src/search-select.js b/src/search-select.js new file mode 100644 index 00000000..e9048a9e --- /dev/null +++ b/src/search-select.js @@ -0,0 +1,217 @@ +// Complements the specs in search-shape.js with the functions that actually +// process live wiki data into records that are appropriate for storage. +// These files totally go together, so read them side by side, okay? + +import baseSearchSpec from '#search-shape'; +import {getKebabCase} from '#wiki-data'; + +function prepareArtwork(artwork, thing, { + checkIfImagePathHasCachedThumbnails, + getThumbnailEqualOrSmaller, + urls, +}) { + if (!artwork) { + return undefined; + } + + const hasWarnings = + artwork.artTags?.some(artTag => artTag.isContentWarning); + + const artworkPath = + artwork.path; + + if (!artworkPath) { + return undefined; + } + + const mediaSrc = + urls + .from('media.root') + .to(...artworkPath); + + if (!checkIfImagePathHasCachedThumbnails(mediaSrc)) { + return undefined; + } + + const selectedSize = + getThumbnailEqualOrSmaller( + (hasWarnings ? 'mini' : 'adorb'), + mediaSrc); + + const mediaSrcJpeg = + mediaSrc.replace(/\.(png|jpg)$/, `.${selectedSize}.jpg`); + + const displaySrc = + urls + .from('thumb.root') + .to('thumb.path', mediaSrcJpeg); + + const serializeSrc = + displaySrc.replace(thing.directory, '<>'); + + return serializeSrc; +} + +function baselineProcess(thing, _opts) { + const fields = {}; + + fields.primaryName = + thing.name; + + fields.artwork = + null; + + fields.color = + thing.color; + + fields.disambiguator = + null; + + return fields; +} + +function genericSelect(wikiData) { + const groupOrder = + wikiData.wikiInfo.divideTrackListsByGroups; + + const getGroupRank = thing => { + const relevantRanks = + Array.from(groupOrder.entries()) + .filter(({1: group}) => thing.groups.includes(group)) + .map(({0: index}) => index); + + if (relevantRanks.length === 0) { + return Infinity; + } else if (relevantRanks.length === 1) { + return relevantRanks[0]; + } else { + return relevantRanks[0] + 0.5; + } + } + + const sortByGroupRank = things => + things.sort((a, b) => getGroupRank(a) - getGroupRank(b)); + + return [ + sortByGroupRank(wikiData.albumData.slice()), + + wikiData.artTagData, + + wikiData.artistData + .filter(artist => !artist.isAlias), + + wikiData.flashData, + + wikiData.groupData, + + sortByGroupRank( + wikiData.trackData + .filter(track => + track.isMainRelease || + (getKebabCase(track.name) !== + getKebabCase(track.mainReleaseTrack.name)))), + ].flat(); +} + +function genericProcess(thing, opts) { + const fields = baselineProcess(thing, opts); + + const kind = + thing.constructor[Symbol.for('Thing.referenceType')]; + + const boundPrepareArtwork = artwork => + prepareArtwork(artwork, thing, opts); + + fields.artwork = + (kind === 'track' && thing.hasUniqueCoverArt + ? boundPrepareArtwork(thing.trackArtworks[0]) + : kind === 'track' + ? boundPrepareArtwork(thing.album.coverArtworks[0]) + : kind === 'album' + ? boundPrepareArtwork(thing.coverArtworks[0]) + : kind === 'flash' + ? boundPrepareArtwork(thing.coverArtwork) + : null); + + fields.parentName = + (kind === 'track' + ? thing.album.name + : kind === 'group' + ? thing.category.name + : kind === 'flash' + ? thing.act.name + : null); + + fields.disambiguator = + fields.parentName; + + fields.artTags = + (Array.from(new Set( + (kind === 'track' + ? thing.trackArtworks.flatMap(artwork => artwork.artTags) + : kind === 'album' + ? thing.coverArtworks.flatMap(artwork => artwork.artTags) + : [])))) + + .map(artTag => artTag.nameShort); + + fields.additionalNames = + (thing.constructor.hasPropertyDescriptor('additionalNames') + ? thing.additionalNames.map(entry => entry.name) + : thing.constructor.hasPropertyDescriptor('aliasNames') + ? thing.aliasNames + : []); + + const contribKeys = [ + 'artistContribs', + 'contributorContribs', + ]; + + const contributions = + contribKeys + .filter(key => Object.hasOwn(thing, key)) + .flatMap(key => thing[key]); + + fields.contributors = + contributions + .flatMap(({artist}) => [ + artist.name, + ...artist.aliasNames, + ]); + + const groups = + (Object.hasOwn(thing, 'groups') + ? thing.groups + : Object.hasOwn(thing, 'album') + ? thing.album.groups + : []); + + const mainContributorNames = + contributions + .map(({artist}) => artist.name); + + fields.groups = + groups + .filter(group => !mainContributorNames.includes(group.name)) + .map(group => group.name); + + return fields; +} + +const spiffySearchSpec = { + generic: { + ...baseSearchSpec.generic, + + select: genericSelect, + process: genericProcess, + }, + + verbatim: { + ...baseSearchSpec.verbatim, + + select: genericSelect, + process: genericProcess, + }, +}; + +export default spiffySearchSpec; diff --git a/src/search.js b/src/search.js index a2dae9e1..138a2d2c 100644 --- a/src/search.js +++ b/src/search.js @@ -9,11 +9,53 @@ import FlexSearch from 'flexsearch'; import {pack} from 'msgpackr'; import {logWarn} from '#cli'; -import {makeSearchIndex, populateSearchIndex, searchSpec} from '#search-spec'; +import {makeSearchIndex} from '#search-shape'; +import searchSpec from '#search-select'; import {stitchArrays} from '#sugar'; import {checkIfImagePathHasCachedThumbnails, getThumbnailEqualOrSmaller} from '#thumbs'; +// TODO: This function basically mirrors bind-utilities.js, which isn't +// exactly robust, but... binding might need some more thought across the +// codebase in *general.* +function bindSearchUtilities({ + checkIfImagePathHasCachedThumbnails, + getThumbnailEqualOrSmaller, + thumbsCache, + urls, +}) { + const bound = { + urls, + }; + + bound.checkIfImagePathHasCachedThumbnails = + (imagePath) => + checkIfImagePathHasCachedThumbnails(imagePath, thumbsCache); + + bound.getThumbnailEqualOrSmaller = + (preferred, imagePath) => + getThumbnailEqualOrSmaller(preferred, imagePath, thumbsCache); + + return bound; +} + +function populateSearchIndex(index, descriptor, wikiData, utilities) { + for (const thing of descriptor.select(wikiData)) { + const reference = thing.constructor.getReference(thing); + + let processed; + try { + processed = descriptor.process(thing, utilities); + } catch (caughtError) { + throw new Error( + `Failed to process searchable thing ${reference}`, + {cause: caughtError}); + } + + index.add({reference, ...processed}); + } +} + async function serializeIndex(index) { const results = {}; @@ -60,17 +102,20 @@ export async function writeSearchData({ .map(descriptor => makeSearchIndex(descriptor, {FlexSearch})); + const utilities = + bindSearchUtilities({ + checkIfImagePathHasCachedThumbnails, + getThumbnailEqualOrSmaller, + thumbsCache, + urls, + wikiData, + }); + stitchArrays({ index: indexes, descriptor: descriptors, }).forEach(({index, descriptor}) => - populateSearchIndex(index, descriptor, { - checkIfImagePathHasCachedThumbnails, - getThumbnailEqualOrSmaller, - thumbsCache, - urls, - wikiData, - })); + populateSearchIndex(index, descriptor, wikiData, utilities)); const serializedIndexes = await Promise.all(indexes.map(serializeIndex)); diff --git a/src/static/css/site.css b/src/static/css/site.css index c92c65ad..61803c9d 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -61,7 +61,7 @@ body::before, .wallpaper-part { #page-container { max-width: 1100px; - margin: 0 auto 40px; + margin: 0 auto 38px; padding: 15px 0; } @@ -76,10 +76,25 @@ body::before, .wallpaper-part { height: unset; } +@property --banner-shine { + syntax: '<percentage>'; + initial-value: 0%; + inherits: false; +} + #banner { margin: 10px 0; width: 100%; position: relative; + + --banner-shine: 4%; + -webkit-box-reflect: below -12px linear-gradient(transparent, color-mix(in srgb, transparent, var(--banner-shine) white)); + transition: --banner-shine 0.8s; +} + +#banner:hover { + --banner-shine: 35%; + transition-delay: 0.3s; } #banner::after { @@ -161,10 +176,9 @@ body::before, .wallpaper-part { } .sidebar-column { - flex: 1 1 20%; + flex: 1 1 35%; min-width: 150px; max-width: 250px; - flex-basis: 250px; align-self: flex-start; } @@ -262,7 +276,11 @@ body::before, .wallpaper-part { #page-container { background-color: var(--bg-color, rgba(35, 35, 35, 0.8)); color: #ffffff; - box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); + border-bottom: 2px solid #fff1; + box-shadow: + 0 0 40px #0008, + 0 2px 15px -3px #2221, + 0 2px 6px 2px #1113; } #skippers > * { @@ -583,6 +601,15 @@ summary.underline-white > span:hover a:not(:hover) { border-bottom-left-radius: 0; } +.track-list-sidebar-box summary { + padding-left: 20px !important; + text-indent: -15px !important; +} + +.track-list-sidebar-box .track-section-range { + white-space: nowrap; +} + .wiki-search-sidebar-box { padding: 1px 0 0 0; @@ -734,6 +761,96 @@ summary.underline-white > span:hover a:not(:hover) { cursor: default; } +.wiki-search-filter-container { + padding: 4px; +} + +.wiki-search-filter-link { + display: inline-block; + margin: 2px; + padding: 2px 4px; + border: 2px solid transparent; + border-radius: 4px; +} + +.wiki-search-filter-link:where(.active.shown) { + animation: + 0.15s ease 0.00s forwards normal show-filter, + 0.60s linear 0.15s infinite alternate blink-filter; +} + +.wiki-search-filter-link:where(.active:not(.shown)) { + animation: + 0.00s linear 0.00s forwards normal show-filter, + 0.60s linear 0.00s infinite alternate blink-filter; +} + +.wiki-search-filter-link:where(:not(.active).hidden) { + /* We can't just reverse the show-filter animation, + * because that won't actually start it over again. + */ + animation: + 0.15s ease 0.00s forwards reverse show-filter-the-sequel; +} + +.wiki-search-filter-link.active-from-query { + background: var(--primary-color); + border-color: var(--primary-color); + color: #000a; + animation: none; +} + +.wiki-search-filter-link.active-from-query::after { + content: "I"; + color: black; + font-family: monospace; + font-weight: 800; + font-size: 1.2em; + margin-left: 0.5ch; + vertical-align: middle; + animation: 1s steps(2, jump-none) 0.6s infinite blink-caret; +} + +@keyframes show-filter { + from { + background: transparent; + border-color: transparent; + color: var(--primary-color); + } + + to { + background: var(--primary-color); + border-color: var(--primary-color); + color: black; + } +} + +/* Exactly the same as show-filter above. */ +@keyframes show-filter-the-sequel { + from { + background: transparent; + border-color: transparent; + color: var(--primary-color); + } + + to { + background: var(--primary-color); + border-color: var(--primary-color); + color: black; + } +} + +@keyframes blink-filter { + to { + background: color-mix(in srgb, var(--primary-color) 90%, transparent); + } +} + +@keyframes blink-caret { + from { opacity: 0; } + to { opacity: 1; } +} + .wiki-search-result { position: relative; display: flex; @@ -801,6 +918,11 @@ summary.underline-white > span:hover a:not(:hover) { display: inline-block; } +.wiki-search-result-disambiguator { + opacity: 0.9; + display: inline-block; +} + .wiki-search-result-image-container { align-self: flex-start; flex-shrink: 0; @@ -915,17 +1037,23 @@ a .normal-content { } .image-media-link::after { - content: ''; - display: inline-block; - width: 22px; - height: 1em; + /* Thanks to Jay Freestone for being awesome: + * https://www.jayfreestone.com/writing/wrapping-and-inline-pseudo-elements/ + */ + + pointer-events: none; + content: '\200b'; + padding-left: 22px; background-color: var(--primary-color); - mask-image: url(/static-4p1/misc/image.svg); + /* mask-image is set in content JavaScript, + * because we can't identify the correct nor + * absolute path to the file from CSS. + */ + mask-repeat: no-repeat; mask-position: calc(100% - 2px); - vertical-align: text-bottom; } .image-media-link:hover::after { @@ -950,33 +1078,59 @@ a .normal-content { font-weight: 800; } +.nav-links-hierarchical .nav-link + .nav-link::before, .nav-links-hierarchical .nav-link + .blockwrap .nav-link::before { content: "\0020/\0020"; } -.series-nav-link { +.series-nav-links { display: inline-block; } -.series-nav-link:not(:first-child)::before { +.series-nav-links:not(:first-child)::before { content: "\00a0»\00a0"; font-weight: normal; } -.series-nav-link:not(:last-child)::after { +.series-nav-links:not(:last-child)::after { content: ",\00a0"; } -.series-nav-link + .series-nav-link::before { +.series-nav-links + .series-nav-links::before { content: ""; } +.dot-switcher > span:not(:first-child) { + display: inline-block; + white-space: nowrap; +} + +/* Yeah, all this stuff only applies to elements of the dot switcher + * besides the first, which will necessarily have a bullet point at left. + */ +.dot-switcher *:where(.dot-switcher > span:not(:first-child) > *) { + display: inline-block; + white-space: wrap; + text-align: left; + vertical-align: top; +} + .dot-switcher > span:not(:first-child)::before { content: "\0020\00b7\0020"; + white-space: pre; font-weight: 800; } +.dot-switcher > span { + color: #ffffffcc; +} + .dot-switcher > span.current { + font-weight: normal; + color: white; +} + +.dot-switcher > span.current a { font-weight: 800; } @@ -990,6 +1144,15 @@ a .normal-content { text-decoration: none !important; } +label:hover span { + text-decoration: underline; + text-decoration-style: solid; +} + +label > input[type=checkbox]:not(:checked) + span { + opacity: 0.8; +} + #secondary-nav { text-align: center; @@ -999,7 +1162,7 @@ a .normal-content { display: block; } -#secondary-nav.album-secondary-nav.with-previous-next { +#secondary-nav.album-secondary-nav { display: flex; justify-content: space-around; padding-left: 7.5% !important; @@ -1017,7 +1180,8 @@ a .normal-content { margin-right: 5px; } -#secondary-nav.album-secondary-nav .dot-switcher { +#secondary-nav.album-secondary-nav .group-nav-links .dot-switcher, +#secondary-nav.album-secondary-nav .series-nav-links .dot-switcher { white-space: nowrap; } @@ -1069,7 +1233,22 @@ a .normal-content { text-decoration: none !important; } +.text-with-tooltip.wiki-edits > .hoverable { + white-space: nowrap; +} + +:where(.isolate-tooltip-z-indexing) { + position: relative; + z-index: 1; +} + +:where(.isolate-tooltip-z-indexing > *) { + position: relative; + z-index: -1; +} + .tooltip { + font-size: 1rem; position: absolute; z-index: 3; left: -10px; @@ -1077,7 +1256,12 @@ a .normal-content { display: none; } -li:not(:first-child:last-child) .tooltip, +.cover-artwork .tooltip, +#sidebar .tooltip { + font-size: 0.9rem; +} + +li:not(:first-child:last-child) .tooltip:where(:not(.cover-artwork .tooltip)), .offset-tooltips > :not(:first-child:last-child) .tooltip { left: 14px; } @@ -1111,7 +1295,8 @@ li:not(:first-child:last-child) .tooltip, .missing-duration-tooltip, .commentary-date-tooltip, .rerelease-tooltip, -.first-release-tooltip { +.first-release-tooltip, +.content-tooltip { padding: 3px 4px 2px 2px; left: -10px; } @@ -1119,18 +1304,23 @@ li:not(:first-child:last-child) .tooltip, .thing-name-tooltip, .wiki-edits-tooltip { padding: 3px 4px 2px 2px; - left: -6px !important; + left: -6px; } -.wiki-edits-tooltip { +.thing-name-tooltip .tooltip-content, +.wiki-edits-tooltip .tooltip-content { font-size: 0.85em; } -/* Terrifying? - * https://stackoverflow.com/a/64424759/4633828 - */ -.thing-name-tooltip { margin-right: -120px; } -.wiki-edits-tooltip { margin-right: -200px; } +.thing-name-tooltip .tooltip-content { + width: max-content; + max-width: 120px; +} + +.wiki-edits-tooltip .tooltip-content { + width: max-content; + max-width: 200px; +} .contribution-tooltip .tooltip-content { padding: 6px 2px 2px 2px; @@ -1155,6 +1345,16 @@ li:not(:first-child:last-child) .tooltip, height: 1.4em; } +.contribution-tooltip .chronology-heading { + grid-column-start: handle-start; + grid-column-end: platform-end; + min-width: 30ch; + + font-size: 0.85em; + font-style: oblique; + margin-bottom: 2px; +} + .contribution-tooltip .chronology-link { display: grid; grid-column-start: icon-start; @@ -1288,6 +1488,36 @@ li:not(:first-child:last-child) .tooltip, font-size: 0.9em; } +.content-tooltip-guy .hoverable a { + text-decoration-color: transparent; + text-decoration-style: dotted; +} + +.content-tooltip-guy { + display: inline-block; +} + +.content-tooltip-guy:not(.has-link) .hoverable { + cursor: default; +} + +.content-tooltip-guy.has-link .text-with-tooltip-interaction-cue { + text-decoration-color: var(--primary-color); +} + +.content-tooltip .tooltip-content { + padding: 3px 4.5px; + width: max-content; + max-width: 240px; +} + +.cover-artwork .content-tooltip { + font-size: 0.85rem; + padding: 2px 3px; + width: max-content; + max-width: 220px; +} + .external-icon { display: inline-block; padding: 0 3px; @@ -1318,6 +1548,12 @@ li:not(:first-child:last-child) .tooltip, color: var(--page-primary-color); } +.wiki-commentary s:not(.spoiler) { + text-decoration-color: #fff9; + text-decoration-thickness: 1.4px; + color: #fffb; +} + s.spoiler { display: inline-block; color: transparent; @@ -1338,6 +1574,42 @@ s.spoiler::-moz-selection { background: white; } +span.path, code.filename { + font-size: 0.95em; + font-family: "courier new", monospace; + font-weight: 800; + background: #ccc3; + + padding: 0.05em 0.5ch; + border: 1px solid #ccce; + border-radius: 2px; + line-height: 1.4; +} + +blockquote :is(span.path, code.filename) { + font-size: 0.9em; +} + +.image-details code.filename { + margin-left: -0.4ch; + opacity: 0.8; +} + +.image-details code.filename:hover { + opacity: 1; + cursor: text; +} + +span.path i { + display: inline-block; + font-style: normal; +} + +span.path i::before { + content: "\0020/\0020"; + color: #ccc; +} + progress { accent-color: var(--primary-color); } @@ -1366,12 +1638,9 @@ hr.cute, border-style: none none dotted none; } -#cover-art-container { +.cover-artwork { font-size: 0.8em; border: 2px solid var(--primary-color); - box-shadow: - 0 2px 14px -6px var(--primary-color), - 0 0 12px 12px #00000080; border-radius: 0 0 4px 4px; background: var(--bg-black-color); @@ -1380,37 +1649,70 @@ hr.cute, backdrop-filter: blur(3px); } -#cover-art-container:has(.image-details), -#cover-art-container.has-image-details { +.cover-artwork:has(.image-details), +.cover-artwork.has-image-details { border-radius: 0 0 6px 6px; } -#cover-art-container:not(:has(.image-details)), -#cover-art-container:not(.has-image-details) { +.cover-artwork:not(:has(.image-details)), +.cover-artwork:not(.has-image-details) { /* Hacky: `overflow: hidden` hides tag tooltips, so it can't be applied * if we've got tags/details visible. But it's okay, because we only * need to apply it if it *doesn't* - that's when the rounded border - * of #cover-art-container needs to cut off its child image-container + * of the .cover-artwork needs to cut off its child .image-container * (which has a background that otherwise causes sharp corners). */ overflow: hidden; } -#cover-art-container .image-container { - /* Border is handled on the cover-art-container. */ +#artwork-column .cover-artwork { + --normal-shadow: 0 0 12px 12px #00000080; + + box-shadow: + 0 2px 14px -6px var(--primary-color), + var(--normal-shadow); +} + +#artwork-column .cover-artwork:not(:first-child), +#artwork-column .cover-artwork-joiner { + margin-left: 30px; + margin-right: 5px; +} + +#artwork-column .cover-artwork:not(:first-child) { + --normal-shadow: 0 0 9px 9px #00000068; +} + +#artwork-column .cover-artwork:first-child + .cover-artwork-joiner, +#artwork-column .cover-artwork.attached-artwork-is-main-artwork, +#artwork-column .cover-artwork.attached-artwork-is-main-artwork + .cover-artwork-joiner { + margin-left: 17.5px; + margin-right: 17.5px; +} + +.cover-artwork:where(#artwork-column .cover-artwork:not(:first-child)) { + margin-top: 20px; +} + +#artwork-column .cover-artwork:last-child:not(:first-child) { + margin-bottom: 25px; +} + +.cover-artwork .image-container { + /* Border is handled on the .cover-artwork. */ border: none; - border-radius: 0; + border-radius: 0 !important; } -#cover-art-container .image-details { +.cover-artwork .image-details { border-top-color: var(--deep-color); } -#cover-art-container .image-details + .image-details { +.cover-artwork .image-details + .image-details { border-top-color: var(--primary-color); } -#cover-art-container .image { +.cover-artwork .image { display: block; width: 100%; height: 100%; @@ -1453,6 +1755,10 @@ hr.cute, margin-bottom: 2px; } +ul.image-details.art-tag-details { + padding-bottom: 0; +} + ul.image-details.art-tag-details li { display: inline-block; } @@ -1461,35 +1767,69 @@ ul.image-details.art-tag-details li:not(:last-child)::after { content: " \00b7 "; } -.image-details.non-unique-details { - font-style: oblique; -} - p.image-details.illustrator-details { text-align: center; font-style: oblique; } -p.content-heading:has(+ .commentary-entry-heading.dated) { - clear: right; +p.image-details.origin-details { + margin-bottom: 2px; } -.commentary-entry-heading { - display: flex; - flex-direction: row; +p.image-details.origin-details .origin-details-line { + display: block; + margin-top: 0.25em; +} - margin-left: 15px; - padding-left: 5px; - max-width: 625px; - padding-bottom: 0.2em; +p.image-details.origin-details .filename-line { + display: block; + margin-top: 0.25em; +} - border-bottom: 1px solid var(--dim-color); +.cover-artwork-joiner { + z-index: -2; } -.commentary-entry-heading-text { - flex-grow: 1; - padding-left: 1.25ch; +.cover-artwork-joiner::after { + content: ""; + display: block; + width: 0; + height: 15px; + margin-left: auto; + margin-right: auto; + border-right: 3px solid var(--primary-color); +} + +.cover-artwork-joiner + .cover-artwork { + margin-top: 0 !important; +} + +.album-art-info { + font-size: 0.8em; + border: 2px solid var(--deep-color); + + margin: 10px min(15px, 1vw) 15px; + + background: var(--bg-black-color); + padding: 6px; + border-radius: 5px; + + -webkit-backdrop-filter: blur(3px); + backdrop-filter: blur(3px); +} + +.album-art-info p { + margin: 0; +} + +.commentary-entry-heading { + margin-left: 15px; + padding-left: calc(5px + 1.25ch); text-indent: -1.25ch; + margin-right: min(calc(8vw - 35px), 45px); + padding-bottom: 0.2em; + + border-bottom: 1px solid var(--dim-color); } .commentary-entry-accent { @@ -1497,13 +1837,12 @@ p.content-heading:has(+ .commentary-entry-heading.dated) { } .commentary-entry-heading .commentary-date { - flex-shrink: 0; - - margin-left: 0.75ch; - align-self: flex-end; + display: inline-block; + text-indent: 0; +} - padding-left: 0.5ch; - padding-right: 0.25ch; +.commentary-entry-heading.dated .commentary-entry-heading-text { + margin-right: 0.75ch; } .commentary-entry-heading .hoverable { @@ -1518,6 +1857,16 @@ p.content-heading:has(+ .commentary-entry-heading.dated) { color: var(--primary-color); } +.inherited-commentary-section { + clear: right; + margin-top: 1em; + margin-bottom: 1.5em; + margin-right: min(4vw, 60px); + border: 2px solid var(--deep-color); + border-radius: 4px; + background: #ffffff07; +} + .commentary-art { float: right; width: 30%; @@ -1532,6 +1881,58 @@ p.content-heading:has(+ .commentary-entry-heading.dated) { box-shadow: 0 0 4px 5px rgba(0, 0, 0, 0.25) !important; } +.lyrics-switcher { + padding-left: 20px; +} + +.lyrics-switcher > span:not(:first-child)::before { + content: "\0020\00b7\0020"; + font-weight: 800; +} + +.lyrics-entry .lyrics-details, +.lyrics-entry .origin-details { + font-size: 0.9em; + font-style: oblique; +} + +.lyrics-entry .lyrics-details { + margin-bottom: 0; +} + +.lyrics-entry .origin-details { + margin-top: 0.25em; +} + +.lyrics-entry.long-lyrics { + clip-path: inset(-15px -20px); +} + +.lyrics-entry.long-lyrics::after { + content: ""; + pointer-events: none; + display: block; + + /* Slight stretching past the bottom of the screen seems + * to make resizing the window (and "revealing" that area) + * a bit smoother. + */ + position: fixed; + bottom: -20px; + left: 0; + right: 0; + + height: calc(20px + min(90px, 13.5vh)); + background: linear-gradient(to bottom, transparent, black 70%, black); + opacity: 0.6; +} + +.lyrics-entry sup { + vertical-align: text-top; + opacity: 0.8; + cursor: default; +} + .js-hide, .js-show-once-data, .js-hide-once-data { @@ -1539,25 +1940,32 @@ p.content-heading:has(+ .commentary-entry-heading.dated) { } .content-image-container, -.content-video-container { +.content-video-container, +.content-audio-container { margin-top: 1em; margin-bottom: 1em; } -.content-image-container.align-center, -.content-video-container.align-center, -.content-audio-container.align-center { +.content-image-container.align-center { text-align: center; margin-top: 1.5em; margin-bottom: 1.5em; } -a.align-center, img.align-center, audio.align-center { +.content-image-container.align-full { + width: 100%; +} + +a.align-center, img.align-center, audio.align-center, video.align-center { display: block; margin-left: auto; margin-right: auto; } +a.align-full, a.align-full img, img.align-full, video.align-full { + width: 100%; +} + center { margin-top: 1em; margin-bottom: 1em; @@ -1608,6 +2016,11 @@ h1 { white-space: nowrap; } +#content details { + margin-top: 0.25em; + margin-bottom: 0.25em; +} + #content.top-index h1, #content.flash-index h1 { text-align: center; @@ -1668,6 +2081,40 @@ ul.quick-info li:not(:last-child)::after { margin-top: 25px; } +.gallery-set-switcher { + text-align: center; +} + +.gallery-view-switcher, +.gallery-style-selector { + margin-left: auto; + margin-right: auto; + text-align: center; + line-height: 1.4; +} + +.gallery-style-selector .styles { + display: inline-flex; + justify-content: center; +} + +.gallery-style-selector .styles label:not(:last-child) { + margin-right: 1.25ch; +} + +.gallery-style-selector .count { + font-size: 0.85em; + + position: relative; + bottom: -0.25em; + + opacity: 0.9; +} + +#content.top-index section { + margin-bottom: 1.5em; +} + .quick-description:not(.has-external-links-only) { --clamped-padding-ratio: max(var(--responsive-padding-ratio), 0.06); margin-left: auto; @@ -1698,6 +2145,7 @@ ul.quick-info li:not(:last-child)::after { .quick-description > blockquote { margin-left: 0 !important; + margin-right: 0 !important; } .quick-description .description-content.long hr ~ p { @@ -1745,7 +2193,6 @@ ul.quick-info li:not(:last-child)::after { li .by { font-style: oblique; - max-width: 600px; } li .by a { @@ -1761,8 +2208,8 @@ p code { #content blockquote { margin-left: 40px; - max-width: 600px; - margin-right: 0; + margin-right: min(8vw, 75px); + width: auto; } #content blockquote blockquote { @@ -1809,7 +2256,6 @@ main.long-content > h1 { dl dt { padding-left: 40px; - max-width: 600px; } dl dt { @@ -1838,6 +2284,13 @@ ul > li.has-details { margin-left: -17px; } +li .origin-details { + display: block; + margin-left: 2ch; + font-size: 0.9em; + font-style: oblique; +} + .album-group-list dt, .group-series-list dt { font-style: oblique; @@ -1849,6 +2302,15 @@ ul > li.has-details { margin-left: 0; } +.album-group-list li { + padding-left: 1.5ch; + text-indent: -1.5ch; +} + +.album-group-list li > * { + text-indent: 0; +} + .album-group-list blockquote { max-width: 540px; margin-bottom: 9px; @@ -1879,31 +2341,54 @@ ul > li.has-details { #content hr { border: 1px inset #808080; - width: 100%; +} + +#content hr.split { + color: #808080; } #content hr.split::before { content: "(split)"; - color: #808080; } -#content hr.split { +#content hr.main-separator { + color: var(--dim-color); + clear: none; + margin-top: -0.25em; + margin-bottom: 1.75em; +} + +#content hr.main-separator::before { + content: "♦"; + font-size: 1.2em; +} + +#content hr.split, +#content hr.main-separator { position: relative; overflow: hidden; border: none; } -#content hr.split::after { +#content hr.split::after, +#content hr.main-separator::after { display: inline-block; content: ""; - border: 1px inset #808080; - width: 100%; + width: calc(100% - min(calc(8vw - 35px), 45px)); position: absolute; top: 50%; - margin-top: -2px; margin-left: 10px; } +#content hr.split::after { + border: 1px inset currentColor; + margin-top: -2px; +} + +#content hr.main-separator::after { + border-bottom: 1px solid currentColor; +} + li > ul { margin-top: 5px; } @@ -1983,6 +2468,65 @@ html[data-url-key="localized.albumCommentary"] p.track-info { margin-left: 20px; } +html[data-url-key="localized.artistRollingWindow"] #content p { + text-align: center; +} + +html[data-url-key="localized.artistRollingWindow"] #content input[type=number] { + width: 3em; + margin: 0 0.25em; + background: black; + color: white; + border: 1px dotted var(--primary-color); + padding: 4px; + border-radius: 3px; +} + +html[data-url-key="localized.artistRollingWindow"] #timeframe-selection-control a { + display: inline-block; + padding: 5px; + text-decoration: underline; + text-decoration-style: dotted; +} + +html[data-url-key="localized.artistRollingWindow"] #timeframe-selection-control a:not([href]) { + text-decoration: none; + opacity: 0.7; +} + +html[data-url-key="localized.artistRollingWindow"] #timeframe-source-area { + border: 1px dashed #ffffff42; + border-top-style: solid; + border-bottom-style: solid; + + display: flex; + flex-direction: column; + justify-content: center; + min-height: calc(100vh - 260px); +} + +html[data-url-key="localized.artistRollingWindow"] #timeframe-source-area .grid-listing { + width: 100%; +} + +html[data-url-key="localized.artistRollingWindow"] .grid-item.peeking { + opacity: 0.8; + background: #ffffff24; +} + +html[data-url-key="localized.artistRollingWindow"] .grid-item > span:not(:first-of-type) { + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; +} + +html[data-url-key="localized.artistRollingWindow"] .grid-item > span:not(:first-of-type) > *:not([style*="display: none"]) ~ *::before { + content: '\00b7'; + margin-left: 0.5ch; + margin-right: 0.5ch; +} + html[data-url-key="localized.groupInfo"] .by a { color: var(--page-primary-color); } @@ -2021,7 +2565,9 @@ h1 a[href="#additional-names-box"]:hover { max-width: min(60vw, 600px); padding: 15px 20px 10px 20px; +} +#additional-names-box:not(.always-visible) { display: none; } @@ -2217,7 +2763,33 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las linear-gradient(#000000bb, #000000bb), var(--primary-color); - box-shadow: 0 -2px 6px -1px var(--dim-color) inset; + --drop-shadow: 0 -2px 6px -1px var(--dim-color) inset; + box-shadow: var(--drop-shadow); +} + +.drop.shiny { + cursor: default; +} + +@supports (box-shadow: 1px 1px 1px color-mix(in srgb, blue, 40% red)) { + @property --drop-shine { + syntax: '<percentage>'; + initial-value: 0%; + inherits: false; + } + + .drop.shiny { + cursor: default; + transition: --drop-shine 0.2s; + } + + .drop.shiny:hover { + --drop-shine: 100%; + + box-shadow: + var(--drop-shadow), + 0 2px 4px -0.5px color-mix(in srgb, var(--primary-color), calc(100% - var(--drop-shine)) transparent); + } } .commentary-drop { @@ -2249,7 +2821,8 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las /* Videos and audios (in content) get a lite version of image-container. */ .content-video-container, .content-audio-container { - width: min-content; + width: fit-content; + max-width: 100%; background-color: var(--dark-color); border: 2px solid var(--primary-color); border-radius: 2.5px 2.5px 3px 3px; @@ -2259,6 +2832,30 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las .content-video-container video, .content-audio-container audio { display: block; + max-width: 100%; +} + +.content-video-container.align-center, +.content-audio-container.align-center { + margin-left: auto; + margin-right: auto; +} + +.content-video-container.align-full, +.content-audio-container.align-full { + width: 100%; +} + +.content-audio-container .filename { + color: white; + font-family: monospace; + display: block; + font-size: 0.9em; + padding-left: 1ch; + padding-right: 1ch; + padding-bottom: 0.25em; + margin-bottom: 0.5em; + border-bottom: 1px solid #fff4; } .image-text-area { @@ -2315,6 +2912,23 @@ img { object-fit: cover; } +.image { + --reveal-filter: ; + --shadow-filter: ; + + backdrop-filter: blur(0); + filter: + var(--reveal-filter) + var(--shadow-filter); +} + +p > img, li > img { + max-width: 100%; + object-fit: contain; + height: auto; + vertical-align: text-bottom; +} + .image-inner-area::after { content: ""; display: block; @@ -2374,9 +2988,9 @@ video.pixelate, .pixelate video { text-decoration-style: dotted; } -.reveal .image { +.reveal:not(.revealed) .image { opacity: 0.7; - filter: blur(20px) brightness(0.7); + --reveal-filter: blur(20px) brightness(0.7); } .reveal .image.reveal-thumbnail { @@ -2400,7 +3014,6 @@ video.pixelate, .pixelate video { } .reveal.revealed .image { - filter: none; opacity: 1; } @@ -2411,7 +3024,6 @@ video.pixelate, .pixelate video { .reveal:not(.revealed) .image-outer-area > * { --reveal-border-radius: 6px; position: relative; - overflow: hidden; border-radius: var(--reveal-border-radius); } @@ -2447,7 +3059,7 @@ video.pixelate, .pixelate video { } .reveal:not(.revealed) .image-outer-area > *:hover .image { - filter: blur(20px) brightness(0.6); + --reveal-filter: blur(20px) brightness(0.6); opacity: 0.6; } @@ -2473,20 +3085,88 @@ video.pixelate, .pixelate video { justify-content: center; align-items: flex-start; padding: 5px 15px; + box-sizing: border-box; +} + +.grid-listing:not(:has(.grid-item:not([class*="hidden-by-"]))) { + padding-bottom: 140px; + background: #cccccc07; + border-radius: 10px; + border: 1px dashed #fff3; +} + +.grid-listing .reveal-all-container { + flex-basis: 100%; +} + +.grid-listing:not(:has(.grid-item:not([class*="hidden-by-"]))) .reveal-all-container { + display: none; +} + +.grid-listing .reveal-all-container.has-nearby-tab { + margin-bottom: 0.6em; +} + +.grid-listing .reveal-all { + max-width: 400px; + margin: 0.20em auto 0; + text-align: center; +} + +.grid-listing .reveal-all .warnings:not(.reveal-all:hover *) { + opacity: 0.4; +} + +.grid-listing .reveal-all a { + display: inline-block; + margin-bottom: 0.15em; + + text-decoration: underline; + text-decoration-style: dotted; +} + +.grid-listing .reveal-all b { + white-space: nowrap; } .grid-item { + line-height: 1.2; font-size: 0.9em; } .grid-item { + --tab-pull: 0px; + --tabnt-offset: 0px; + display: inline-block; text-align: center; background-color: #111111; border: 1px dotted var(--primary-color); border-radius: 2px; padding: 5px; + margin: 10px; + margin-top: + calc( + 10px + - var(--tab-pull) + + var(--tabnt-offset)); +} + +.grid-item.has-tab { + border-radius: 8px 8px 3px 3px; +} + +.grid-item.has-tab:hover { + --tab-pull: 3px; +} + +.grid-item:not(.has-tab) { + --tabnt-offset: calc(1.2em - 4px); +} + +.grid-item[class*="hidden-by-"] { + display: none; } .grid-item .image-container { @@ -2503,10 +3183,16 @@ video.pixelate, .pixelate video { } .grid-item .image { + --shadow-filter: + drop-shadow(0 3px 2px #0004) + drop-shadow(0 1px 5px #0001) + drop-shadow(0 3px 4px #0001); + width: 100%; height: 100% !important; margin-top: auto; margin-bottom: auto; + object-fit: contain; } .grid-item:hover { @@ -2523,20 +3209,27 @@ video.pixelate, .pixelate video { hyphens: auto; } -.grid-item > span:not(:first-child) { - margin-top: 2px; -} +/* tab */ +.grid-item > span:first-child { + margin-bottom: calc(3px + var(--tab-pull)); -.grid-item > span:first-of-type { - margin-top: 6px; + font-style: oblique; } -.grid-item > span:not(:first-of-type) { +/* info */ +.grid-item > .image-container + span ~ span { + margin-top: 2px; + font-size: 0.9em; opacity: 0.8; } -.grid-item:hover > span:first-of-type { +/* title */ +.grid-item > .image-container + span { + margin-top: 6px; +} + +.grid-item:hover > .image-container + span { text-decoration: underline; } @@ -2545,6 +3238,10 @@ video.pixelate, .pixelate video { max-width: 200px; } +.grid-name-marker { + color: white; +} + .grid-actions { display: flex; flex-direction: row; @@ -2562,6 +3259,47 @@ video.pixelate, .pixelate video { --dim-color: inherit !important; } +.grid-caption { + flex-basis: 100%; + text-align: center; + line-height: 1.5; +} + +.grid-expando { + margin-top: 1em; + margin-bottom: 2em; + flex-basis: 100%; + + display: flex; + flex-direction: row; + justify-content: space-around; +} + +.grid-expando-content { + margin: 0; + text-align: center; + line-height: 1.5; +} + +.grid-expando-toggle { + text-decoration: underline; + text-decoration-style: dotted; +} + +.grid-item.shown-by-expandable-cut { + animation: expand-cover-grid 0.8s forwards; +} + +@keyframes expand-cover-grid { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + /* Carousel */ .carousel-container { @@ -2582,7 +3320,6 @@ video.pixelate, .pixelate video { left: 0; right: 0; bottom: 0; - z-index: -20; background-color: var(--dim-color); filter: brightness(0.6); } @@ -2867,6 +3604,48 @@ h3.content-heading { clear: both; } +summary.content-heading { + list-style-type: none; +} + +summary.content-heading .cue { + display: inline-flex; + color: var(--primary-color); +} + +summary.content-heading .cue::after { + content: ""; + padding-left: 0.5ch; + display: list-item; + list-style-type: disclosure-closed; + list-style-position: inside; +} + +details[open] > summary.content-heading .cue::after { + list-style-type: disclosure-open; +} + +summary.content-heading > span:hover { + text-decoration: none !important; +} + +summary.content-heading > span:hover .cue { + text-decoration: underline; + text-decoration-style: wavy; +} + +summary.content-heading .when-open { + display: none; +} + +details[open] > summary.content-heading .when-open { + display: unset; +} + +details[open] > summary.content-heading .when-collapsed { + display: none; +} + /* This animation's name is referenced in JavaScript */ @keyframes highlight-hash-link { 0% { @@ -2919,11 +3698,11 @@ h3.content-heading { top: 0; } -.content-sticky-heading-anchor:not(:matches(.content-sticky-heading-root[inert]) *) { +.content-sticky-heading-anchor:not(:where(.content-sticky-heading-root[inert]) *) { position: relative; } -.content-sticky-heading-container:not(:matches(.content-sticky-heading-root[inert]) *) { +.content-sticky-heading-container:not(:where(.content-sticky-heading-root[inert]) *) { position: absolute; } @@ -2972,15 +3751,11 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r grid-template-columns: 1fr min(40%, 90px); } -.content-sticky-heading-root.has-cover { - padding-right: min(40%, 400px); -} - .content-sticky-heading-row h1 { position: relative; margin: 0; padding-right: 20px; - line-height: 1.4; + overflow-x: hidden; } .content-sticky-heading-row h1 .reference-collapsed-heading { @@ -3049,7 +3824,7 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r transition: transform 0.35s, opacity 0.30s; } -.content-sticky-heading-cover .image-container { +.content-sticky-heading-cover .cover-artwork { border-width: 1px; border-radius: 1.25px; box-shadow: none; @@ -3120,7 +3895,9 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r } #content, .sidebar { - contain: paint; + /* In the year of our pizza 2025, we try commenting this out. + */ + /*contain: paint;*/ } /* Sticky sidebar */ @@ -3338,7 +4115,7 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r /* Layout - Wide (most computers) */ -@media (min-width: 900px) { +@media (min-width: 850px) { #page-container.showing-sidebar-left:not(.sidebars-in-content-column) #secondary-nav:not(.always-visible), #page-container.showing-sidebar-right:not(.sidebars-in-content-column) #secondary-nav:not(.always-visible) { display: none; @@ -3352,7 +4129,7 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r * if so desired. */ -@media (min-width: 600px) and (max-width: 899.98px) { +@media (min-width: 600px) and (max-width: 849.98px) { /* Medium layout is mainly defined (to the user) by hiding the sidebar, so * don't apply the similar layout change of widening the long-content area * if this page doesn't have a sidebar to hide in the first place. @@ -3380,10 +4157,11 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r /* Cover art floats to the right. It's positioned in HTML beneath the * heading, so pull it up a little to "float" on top. */ - #cover-art-container { + #artwork-column { float: right; width: 40%; - max-width: 400px; + min-width: 220px; + max-width: 280px; margin: -60px 0 10px 20px; position: relative; @@ -3393,18 +4171,18 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r /* ...Except on top-indexes, where cover art is displayed prominently * between the heading and subheading. */ - #content.top-index #cover-art-container { + #content.top-index #artwork-column { float: none; margin: 2em auto 2.5em auto; max-width: 375px; } - html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:not(:nth-child(n+10)) { + html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:not(:nth-child(n+7)) { flex-basis: 23%; margin: 15px; } - html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:nth-child(n+10) { + html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:nth-child(n+7) { flex-basis: 18%; margin: 10px; } @@ -3412,7 +4190,7 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r /* Layout - Medium or Thin */ -@media (max-width: 899.98px) { +@media (max-width: 849.98px) { .sidebar.collapsible, .sidebar-box-joiner.collapsible, .sidebar-column.all-boxes-collapsible { @@ -3500,12 +4278,22 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r --responsive-padding-ratio: 0.02; } - #cover-art-container { + #artwork-column { margin: 25px 0 5px 0; width: 100%; max-width: unset; } + #artwork-column .cover-artwork { + --normal-shadow: 0 0 transparent; + } + + #artwork-column .cover-artwork:not(:first-child), + #artwork-column .cover-artwork-joiner { + margin-left: 30px; + margin-right: 30px; + } + #additional-names-box { width: unset; max-width: unset; diff --git a/src/static/js/client-util.js b/src/static/js/client-util.js index 71112313..764c1480 100644 --- a/src/static/js/client-util.js +++ b/src/static/js/client-util.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - export function rebase(href, rebaseKey = 'rebaseLocalized') { let result = document.documentElement.dataset[rebaseKey] || './'; @@ -37,7 +35,7 @@ export function cssProp(el, ...args) { } } -export function templateContent(el) { +export function templateContent(el, slots = {}) { if (el === null) { return null; } @@ -46,7 +44,25 @@ export function templateContent(el) { throw new Error(`Expected a <template> element`); } - return el.content.cloneNode(true); + const content = el.content.cloneNode(true); + + for (const [key, value] of Object.entries(slots)) { + const slot = content.querySelector(`slot[name="${key}"]`); + + if (!slot) { + console.warn(`Slot ${key} missing in template:`, el); + continue; + } + + if (value === null || value === undefined) { + console.warn(`Valueless slot ${key} in template:`, el); + continue; + } + + slot.replaceWith(value); + } + + return content; } // Curry-style, so multiple points can more conveniently be tested at once. @@ -127,3 +143,10 @@ export function dispatchInternalEvent(event, eventName, ...args) { return results; } + +const languageCode = document.documentElement.getAttribute('lang'); + +export function formatDate(inputDate) { + const date = new Date(inputDate); + return date.toLocaleDateString(languageCode); +} diff --git a/src/static/js/client/additional-names-box.js b/src/static/js/client/additional-names-box.js index 3535a0e5..a6d9b098 100644 --- a/src/static/js/client/additional-names-box.js +++ b/src/static/js/client/additional-names-box.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {cssProp} from '../client-util.js'; import {info as hashLinkInfo} from './hash-link.js'; @@ -33,7 +31,7 @@ export function getPageReferences() { '.content-sticky-heading-container' + ' ' + 'a[href="#additional-names-box"]' + - ':not(:matches([inert] *))'); + ':not(:where([inert] *))'); info.contentContainer = document.querySelector('#content'); @@ -121,7 +119,7 @@ function handleAdditionalNamesBoxLinkClicked(domEvent) { ? top > 0.4 * window.innerHeight : top > 0.5 * window.innerHeight) || - (bottom && bottomFitsInFrame + (bottom && boxFitsInFrame ? bottom > window.innerHeight - 20 : false); diff --git a/src/static/js/client/album-commentary-sidebar.js b/src/static/js/client/album-commentary-sidebar.js index c5eaf81b..d7c4a591 100644 --- a/src/static/js/client/album-commentary-sidebar.js +++ b/src/static/js/client/album-commentary-sidebar.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {empty} from '../../shared-util/sugar.js'; import {info as hashLinkInfo} from './hash-link.js'; diff --git a/src/static/js/client/art-tag-gallery-filter.js b/src/static/js/client/art-tag-gallery-filter.js index fd40d1a2..f74f640e 100644 --- a/src/static/js/client/art-tag-gallery-filter.js +++ b/src/static/js/client/art-tag-gallery-filter.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - export const info = { id: 'artTagGalleryFilterInfo', diff --git a/src/static/js/client/art-tag-network.js b/src/static/js/client/art-tag-network.js index 44e10c11..d0576152 100644 --- a/src/static/js/client/art-tag-network.js +++ b/src/static/js/client/art-tag-network.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {cssProp} from '../client-util.js'; import {atOffset, stitchArrays} from '../../shared-util/sugar.js'; diff --git a/src/static/js/client/artist-external-link-tooltip.js b/src/static/js/client/artist-external-link-tooltip.js index 21ddfb91..2eadf916 100644 --- a/src/static/js/client/artist-external-link-tooltip.js +++ b/src/static/js/client/artist-external-link-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {accumulateSum, empty} from '../../shared-util/sugar.js'; import {info as hoverableTooltipInfo, repositionCurrentTooltip} diff --git a/src/static/js/client/artist-rolling-window.js b/src/static/js/client/artist-rolling-window.js new file mode 100644 index 00000000..b8ff7354 --- /dev/null +++ b/src/static/js/client/artist-rolling-window.js @@ -0,0 +1,571 @@ +import {cssProp, formatDate} from '../client-util.js'; + +import {sortByDate} from '../../shared-util/sort.js'; +import {chunkByConditions, chunkByProperties, empty, stitchArrays} + from '../../shared-util/sugar.js'; + +export const info = { + id: 'artistRollingWindowInfo', + + timeframeMonthsBefore: null, + timeframeMonthsAfter: null, + timeframeMonthsPeek: null, + + contributionKind: null, + contributionGroup: null, + + timeframeSelectionSomeLine: null, + timeframeSelectionNoneLine: null, + + timeframeSelectionContributionCount: null, + timeframeSelectionTimeframeCount: null, + timeframeSelectionFirstDate: null, + timeframeSelectionLastDate: null, + + timeframeSelectionControl: null, + timeframeSelectionMenu: null, + timeframeSelectionPrevious: null, + timeframeSelectionNext: null, + + timeframeEmptyLine: null, + + sourceArea: null, + sourceGrid: null, + sources: null, +}; + +export function getPageReferences() { + if (document.documentElement.dataset.urlKey !== 'localized.artistRollingWindow') { + return; + } + + info.timeframeMonthsBefore = + document.getElementById('timeframe-months-before'); + + info.timeframeMonthsAfter = + document.getElementById('timeframe-months-after'); + + info.timeframeMonthsPeek = + document.getElementById('timeframe-months-peek'); + + info.contributionKind = + document.getElementById('contribution-kind'); + + info.contributionGroup = + document.getElementById('contribution-group'); + + info.timeframeSelectionSomeLine = + document.getElementById('timeframe-selection-some'); + + info.timeframeSelectionNoneLine = + document.getElementById('timeframe-selection-none'); + + info.timeframeSelectionContributionCount = + document.getElementById('timeframe-selection-contribution-count'); + + info.timeframeSelectionTimeframeCount = + document.getElementById('timeframe-selection-timeframe-count'); + + info.timeframeSelectionFirstDate = + document.getElementById('timeframe-selection-first-date'); + + info.timeframeSelectionLastDate = + document.getElementById('timeframe-selection-last-date'); + + info.timeframeSelectionControl = + document.getElementById('timeframe-selection-control'); + + info.timeframeSelectionMenu = + document.getElementById('timeframe-selection-menu'); + + info.timeframeSelectionPrevious = + document.getElementById('timeframe-selection-previous'); + + info.timeframeSelectionNext = + document.getElementById('timeframe-selection-next'); + + info.timeframeEmptyLine = + document.getElementById('timeframe-empty'); + + info.sourceArea = + document.getElementById('timeframe-source-area'); + + info.sourceGrid = + info.sourceArea.querySelector('.grid-listing'); + + info.sources = + info.sourceGrid.getElementsByClassName('grid-item'); +} + +export function addPageListeners() { + if (!info.sourceArea) { + return; + } + + for (const input of [ + info.timeframeMonthsBefore, + info.timeframeMonthsAfter, + info.timeframeMonthsPeek, + info.contributionKind, + info.contributionGroup, + ]) { + input.addEventListener('change', () => { + updateArtistRollingWindow() + }); + } + + info.timeframeSelectionMenu.addEventListener('change', () => { + updateRollingWindowTimeframeSelection(); + }); + + const eatClicks = (element, callback) => { + element.addEventListener('click', domEvent => { + domEvent.preventDefault(); + callback(); + }); + + element.addEventListener('mousedown', domEvent => { + if (domEvent.detail > 1) { + domEvent.preventDefault(); + } + }); + }; + + eatClicks(info.timeframeSelectionNext, nextRollingTimeframeSelection); + eatClicks(info.timeframeSelectionPrevious, previousRollingTimeframeSelection); +} + +export function mutatePageContent() { + if (!info.sourceArea) { + return; + } + + updateArtistRollingWindow(); +} + +function previousRollingTimeframeSelection() { + const menu = info.timeframeSelectionMenu; + + if (menu.selectedIndex > 0) { + menu.selectedIndex--; + } + + updateRollingWindowTimeframeSelection(); +} + +function nextRollingTimeframeSelection() { + const menu = info.timeframeSelectionMenu; + + if (menu.selectedIndex < menu.length - 1) { + menu.selectedIndex++; + } + + updateRollingWindowTimeframeSelection(); +} + +function getArtistRollingWindowSourceInfo() { + const sourceElements = + Array.from(info.sources); + + const sourceTimeElements = + sourceElements + .map(el => Array.from(el.getElementsByTagName('time'))); + + const sourceTimeClasses = + sourceTimeElements + .map(times => times + .map(time => Array.from(time.classList))); + + const sourceKinds = + sourceTimeClasses + .map(times => times + .map(classes => classes + .find(cl => cl.endsWith('-contribution-date')) + .slice(0, -'-contribution-date'.length))); + + const sourceGroups = + sourceElements + .map(el => + Array.from(el.querySelectorAll('.contribution-group')) + .map(data => data.value)); + + const sourceDates = + sourceTimeElements + .map(times => times + .map(time => new Date(time.getAttribute('datetime')))); + + return stitchArrays({ + element: sourceElements, + kinds: sourceKinds, + groups: sourceGroups, + dates: sourceDates, + }); +} + +function getArtistRollingWindowTimeframeInfo() { + const contributionKind = + info.contributionKind.value; + + const contributionGroup = + info.contributionGroup.value; + + const sourceInfo = + getArtistRollingWindowSourceInfo(); + + const principalSources = + sourceInfo.filter(source => { + if (!source.kinds.includes(contributionKind)) { + return false; + } + + if (contributionGroup !== '-') { + if (!source.groups.includes(contributionGroup)) { + return false; + } + } + + return true; + }); + + const principalSourceDates = + principalSources.map(source => + stitchArrays({ + kind: source.kinds, + date: source.dates, + }).find(({kind}) => kind === contributionKind) + .date); + + const getPeekDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + - parseInt(info.timeframeMonthsBefore.value) + - parseInt(info.timeframeMonthsPeek.value))); + + return date; + }; + + const getEntranceDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + - parseInt(info.timeframeMonthsBefore.value))); + + return date; + }; + + const getExitDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + + parseInt(info.timeframeMonthsAfter.value))); + + return date; + }; + + const principalSourceIndices = + Array.from({length: principalSources.length}, (_, i) => i); + + const timeframeSourceChunks = + chunkByConditions(principalSourceIndices, [ + (previous, next) => + +principalSourceDates[previous] !== + +principalSourceDates[next], + ]); + + const timeframeSourceChunkDates = + timeframeSourceChunks + .map(indices => indices[0]) + .map(index => principalSourceDates[index]); + + const timeframeSourceChunkPeekDates = + timeframeSourceChunkDates + .map(getPeekDate); + + const timeframeSourceChunkEntranceDates = + timeframeSourceChunkDates + .map(getEntranceDate); + + const timeframeSourceChunkExitDates = + timeframeSourceChunkDates + .map(getExitDate); + + const peekDateInfo = + stitchArrays({ + peek: timeframeSourceChunkPeekDates, + indices: timeframeSourceChunks, + }).map(({peek, indices}) => ({ + date: peek, + peek: indices, + })); + + const entranceDateInfo = + stitchArrays({ + entrance: timeframeSourceChunkEntranceDates, + indices: timeframeSourceChunks, + }).map(({entrance, indices}) => ({ + date: entrance, + entrance: indices, + })); + + const exitDateInfo = + stitchArrays({ + exit: timeframeSourceChunkExitDates, + indices: timeframeSourceChunks, + }).map(({exit, indices}) => ({ + date: exit, + exit: indices, + })); + + const dateInfoChunks = + chunkByProperties( + sortByDate([ + ...peekDateInfo, + ...entranceDateInfo, + ...exitDateInfo, + ]), + ['date']); + + const dateInfo = + dateInfoChunks + .map(({chunk}) => + Object.assign({ + peek: null, + entrance: null, + exit: null, + }, ...chunk)); + + const timeframeInfo = + dateInfo.reduce( + (accumulator, {date, peek, entrance, exit}) => { + const previous = accumulator.at(-1); + + // These mustn't be mutated! + let peeking = (previous ? previous.peeking : []); + let tracking = (previous ? previous.tracking : []); + + if (peek) { + peeking = + peeking.concat(peek); + } + + if (entrance) { + peeking = + peeking.filter(index => !entrance.includes(index)); + + tracking = + tracking.concat(entrance); + } + + if (exit) { + tracking = + tracking.filter(index => !exit.includes(index)); + } + + return [...accumulator, { + date, + peeking, + tracking, + peek, + entrance, + exit, + }]; + }, + []); + + const indicesToSources = indices => + (indices + ? indices.map(index => principalSources[index]) + : null); + + const finalizedTimeframeInfo = + timeframeInfo.map(({ + date, + peeking, + tracking, + peek, + entrance, + exit, + }) => ({ + date, + peeking: indicesToSources(peeking), + tracking: indicesToSources(tracking), + peek: indicesToSources(peek), + entrance: indicesToSources(entrance), + exit: indicesToSources(exit), + })); + + return finalizedTimeframeInfo; +} + +function updateArtistRollingWindow() { + const timeframeInfo = + getArtistRollingWindowTimeframeInfo(); + + if (empty(timeframeInfo)) { + cssProp(info.timeframeSelectionControl, 'display', 'none'); + cssProp(info.timeframeSelectionSomeLine, 'display', 'none'); + cssProp(info.timeframeSelectionNoneLine, 'display', null); + + updateRollingWindowTimeframeSelection(timeframeInfo); + + return; + } + + cssProp(info.timeframeSelectionControl, 'display', null); + cssProp(info.timeframeSelectionSomeLine, 'display', null); + cssProp(info.timeframeSelectionNoneLine, 'display', 'none'); + + // The last timeframe is just the exit of the final tracked sources, + // so we aren't going to display a menu option for it, and will just use + // it as the end of the final option's date range. + + const usedTimeframes = timeframeInfo.slice(0, -1); + const firstTimeframe = timeframeInfo.at(0); + const lastTimeframe = timeframeInfo.at(-1); + + const sourceCount = + timeframeInfo + .flatMap(({entrance}) => entrance ?? []) + .length; + + const timeframeCount = + usedTimeframes.length; + + info.timeframeSelectionContributionCount.innerText = sourceCount; + info.timeframeSelectionTimeframeCount.innerText = timeframeCount; + + const firstDate = firstTimeframe.date; + const lastDate = lastTimeframe.date; + + info.timeframeSelectionFirstDate.innerText = formatDate(firstDate); + info.timeframeSelectionLastDate.innerText = formatDate(lastDate); + + while (info.timeframeSelectionMenu.firstChild) { + info.timeframeSelectionMenu.firstChild.remove(); + } + + for (const [index, timeframe] of usedTimeframes.entries()) { + const nextTimeframe = timeframeInfo[index + 1]; + + const option = document.createElement('option'); + + option.appendChild(document.createTextNode( + `${formatDate(timeframe.date)} – ${formatDate(nextTimeframe.date)}`)); + + info.timeframeSelectionMenu.appendChild(option); + } + + updateRollingWindowTimeframeSelection(timeframeInfo); +} + +function updateRollingWindowTimeframeSelection(timeframeInfo) { + timeframeInfo ??= getArtistRollingWindowTimeframeInfo(); + + updateRollingWindowTimeframeSelectionControls(timeframeInfo); + updateRollingWindowTimeframeSelectionSources(timeframeInfo); +} + +function updateRollingWindowTimeframeSelectionControls(timeframeInfo) { + const currentIndex = + info.timeframeSelectionMenu.selectedIndex; + + const atFirstTimeframe = + currentIndex === 0; + + // The last actual timeframe is empty and not displayed as a menu option. + const atLastTimeframe = + currentIndex === timeframeInfo.length - 2; + + if (atFirstTimeframe) { + info.timeframeSelectionPrevious.removeAttribute('href'); + } else { + info.timeframeSelectionPrevious.setAttribute('href', '#'); + } + + if (atLastTimeframe) { + info.timeframeSelectionNext.removeAttribute('href'); + } else { + info.timeframeSelectionNext.setAttribute('href', '#'); + } +} + +function updateRollingWindowTimeframeSelectionSources(timeframeInfo) { + const currentIndex = + info.timeframeSelectionMenu.selectedIndex; + + const contributionGroup = + info.contributionGroup.value; + + cssProp(info.sourceGrid, 'display', null); + + const {peeking: peekingSources, tracking: trackingSources} = + (empty(timeframeInfo) + ? {peeking: [], tracking: []} + : timeframeInfo[currentIndex]); + + const peekingElements = + peekingSources.map(source => source.element); + + const trackingElements = + trackingSources.map(source => source.element); + + const showingElements = + [...trackingElements, ...peekingElements]; + + const hidingElements = + Array.from(info.sources) + .filter(element => + !peekingElements.includes(element) && + !trackingElements.includes(element)); + + for (const element of peekingElements) { + element.classList.add('peeking'); + element.classList.remove('tracking'); + } + + for (const element of trackingElements) { + element.classList.remove('peeking'); + element.classList.add('tracking'); + } + + for (const element of hidingElements) { + element.classList.remove('peeking'); + element.classList.remove('tracking'); + cssProp(element, 'display', 'none'); + } + + for (const element of showingElements) { + cssProp(element, 'display', null); + + for (const time of element.getElementsByTagName('time')) { + for (const className of time.classList) { + if (!className.endsWith('-contribution-date')) continue; + + const kind = className.slice(0, -'-contribution-date'.length); + if (kind === info.contributionKind.value) { + cssProp(time, 'display', null); + } else { + cssProp(time, 'display', 'none'); + } + } + } + + for (const data of element.getElementsByClassName('contribution-group')) { + if (contributionGroup === '-' || data.value !== contributionGroup) { + cssProp(data, 'display', null); + } else { + cssProp(data, 'display', 'none'); + } + } + } + + if (empty(peekingElements) && empty(trackingElements)) { + cssProp(info.timeframeEmptyLine, 'display', null); + } else { + cssProp(info.timeframeEmptyLine, 'display', 'none'); + } +} diff --git a/src/static/js/client/css-compatibility-assistant.js b/src/static/js/client/css-compatibility-assistant.js index 6e7b15b5..37b0645a 100644 --- a/src/static/js/client/css-compatibility-assistant.js +++ b/src/static/js/client/css-compatibility-assistant.js @@ -1,22 +1,28 @@ -/* eslint-env browser */ +import {stitchArrays} from '../../shared-util/sugar.js'; export const info = { id: 'cssCompatibilityAssistantInfo', - coverArtContainer: null, - coverArtImageDetails: null, + coverArtworks: null, + coverArtworkImageDetails: null, }; export function getPageReferences() { - info.coverArtContainer = - document.getElementById('cover-art-container'); + info.coverArtworks = + Array.from(document.querySelectorAll('.cover-artwork')); - info.coverArtImageDetails = - info.coverArtContainer?.querySelector('.image-details'); + info.coverArtworkImageDetails = + info.coverArtworks + .map(artwork => artwork.querySelector('.image-details')); } export function mutatePageContent() { - if (info.coverArtImageDetails) { - info.coverArtContainer.classList.add('has-image-details'); - } + stitchArrays({ + coverArtwork: info.coverArtworks, + imageDetails: info.coverArtworkImageDetails, + }).forEach(({coverArtwork, imageDetails}) => { + if (imageDetails) { + coverArtwork.classList.add('has-image-details'); + } + }); } diff --git a/src/static/js/client/datetimestamp-tooltip.js b/src/static/js/client/datetimestamp-tooltip.js index 46d1cd5b..00530484 100644 --- a/src/static/js/client/datetimestamp-tooltip.js +++ b/src/static/js/client/datetimestamp-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // TODO: Maybe datetimestamps can just be incorporated into text-with-tooltip? import {stitchArrays} from '../../shared-util/sugar.js'; diff --git a/src/static/js/client/dragged-link.js b/src/static/js/client/dragged-link.js index 56021e7f..3a4ee314 100644 --- a/src/static/js/client/dragged-link.js +++ b/src/static/js/client/dragged-link.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - export const info = { id: `draggedLinkInfo`, diff --git a/src/static/js/client/expandable-grid-section.js b/src/static/js/client/expandable-grid-section.js new file mode 100644 index 00000000..4d6e0058 --- /dev/null +++ b/src/static/js/client/expandable-grid-section.js @@ -0,0 +1,83 @@ +import {cssProp} from '../client-util.js'; + +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'expandableGallerySectionInfo', + + items: null, + toggles: null, + expandCues: null, + collapseCues: null, +}; + +export function getPageReferences() { + const expandos = + Array.from(document.querySelectorAll('.grid-expando')); + + const grids = + expandos + .map(expando => expando.closest('.grid-listing')); + + info.items = + grids + .map(grid => grid.querySelectorAll('.grid-item')) + .map(items => Array.from(items)); + + info.toggles = + expandos + .map(expando => expando.querySelector('.grid-expando-toggle')); + + info.expandCues = + info.toggles + .map(toggle => toggle.querySelector('.grid-expand-cue')); + + info.collapseCues = + info.toggles + .map(toggle => toggle.querySelector('.grid-collapse-cue')); +} + +export function addPageListeners() { + stitchArrays({ + items: info.items, + toggle: info.toggles, + expandCue: info.expandCues, + collapseCue: info.collapseCues, + }).forEach(({ + items, + toggle, + expandCue, + collapseCue, + }) => { + toggle.addEventListener('click', domEvent => { + domEvent.preventDefault(); + + const collapsed = + items.some(item => + item.classList.contains('hidden-by-expandable-cut')); + + for (const item of items) { + if ( + !item.classList.contains('hidden-by-expandable-cut') && + !item.classList.contains('shown-by-expandable-cut') + ) continue; + + if (collapsed) { + item.classList.remove('hidden-by-expandable-cut'); + item.classList.add('shown-by-expandable-cut'); + } else { + item.classList.add('hidden-by-expandable-cut'); + item.classList.remove('shown-by-expandable-cut'); + } + } + + if (collapsed) { + cssProp(expandCue, 'display', 'none'); + cssProp(collapseCue, 'display', null); + } else { + cssProp(expandCue, 'display', null); + cssProp(collapseCue, 'display', 'none'); + } + }); + }); +} diff --git a/src/static/js/client/gallery-style-selector.js b/src/static/js/client/gallery-style-selector.js new file mode 100644 index 00000000..44f98ac3 --- /dev/null +++ b/src/static/js/client/gallery-style-selector.js @@ -0,0 +1,121 @@ +import {cssProp} from '../client-util.js'; + +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'galleryStyleSelectorInfo', + + selectors: null, + sections: null, + + selectorStyleInputs: null, + selectorStyleInputStyles: null, + + selectorReleaseItems: null, + selectorReleaseItemStyles: null, + + selectorCountAll: null, + selectorCountFiltered: null, + selectorCountFilteredCount: null, + selectorCountNone: null, +}; + +export function getPageReferences() { + info.selectors = + Array.from(document.querySelectorAll('.gallery-style-selector')); + + info.sections = + info.selectors + .map(selector => selector.closest('section')); + + info.selectorStyleInputs = + info.selectors + .map(selector => selector.querySelectorAll('.styles input')) + .map(inputs => Array.from(inputs)); + + info.selectorStyleInputStyles = + info.selectorStyleInputs + .map(inputs => inputs + .map(input => input.closest('label').dataset.style)); + + info.selectorReleaseItems = + info.sections + .map(section => section.querySelectorAll('.grid-item')) + .map(items => Array.from(items)); + + info.selectorReleaseItemStyles = + info.selectorReleaseItems + .map(items => items + .map(item => item.dataset.style)); + + info.selectorCountAll = + info.selectors + .map(selector => selector.querySelector('.count.all')); + + info.selectorCountFiltered = + info.selectors + .map(selector => selector.querySelector('.count.filtered')); + + info.selectorCountFilteredCount = + info.selectorCountFiltered + .map(selector => selector.querySelector('span')); + + info.selectorCountNone = + info.selectors + .map(selector => selector.querySelector('.count.none')); +} + +export function addPageListeners() { + for (const index of info.selectors.keys()) { + for (const input of info.selectorStyleInputs[index]) { + input.addEventListener('input', () => updateVisibleReleases(index)); + } + } +} + +function updateVisibleReleases(index) { + const inputs = info.selectorStyleInputs[index]; + const inputStyles = info.selectorStyleInputStyles[index]; + + const selectedStyles = + stitchArrays({input: inputs, style: inputStyles}) + .filter(({input}) => input.checked) + .map(({style}) => style); + + const releases = info.selectorReleaseItems[index]; + const releaseStyles = info.selectorReleaseItemStyles[index]; + + let visible = 0; + + stitchArrays({ + release: releases, + style: releaseStyles, + }).forEach(({release, style}) => { + if (selectedStyles.includes(style)) { + release.classList.remove('hidden-by-style-mismatch'); + visible++; + } else { + release.classList.add('hidden-by-style-mismatch'); + } + }); + + const countAll = info.selectorCountAll[index]; + const countFiltered = info.selectorCountFiltered[index]; + const countFilteredCount = info.selectorCountFilteredCount[index]; + const countNone = info.selectorCountNone[index]; + + if (visible === releases.length) { + cssProp(countAll, 'display', null); + cssProp(countFiltered, 'display', 'none'); + cssProp(countNone, 'display', 'none'); + } else if (visible === 0) { + cssProp(countAll, 'display', 'none'); + cssProp(countFiltered, 'display', 'none'); + cssProp(countNone, 'display', null); + } else { + cssProp(countAll, 'display', 'none'); + cssProp(countFiltered, 'display', null); + cssProp(countNone, 'display', 'none'); + countFilteredCount.innerHTML = visible; + } +} diff --git a/src/static/js/client/hash-link.js b/src/static/js/client/hash-link.js index 27035e29..02ffdc23 100644 --- a/src/static/js/client/hash-link.js +++ b/src/static/js/client/hash-link.js @@ -1,6 +1,5 @@ -/* eslint-env browser */ - -import {filterMultipleArrays, stitchArrays} from '../../shared-util/sugar.js'; +import {filterMultipleArrays, stitchArrays, unique} + from '../../shared-util/sugar.js'; import {dispatchInternalEvent} from '../client-util.js'; @@ -11,6 +10,9 @@ export const info = { hrefs: null, targets: null, + details: null, + detailsIDs: null, + state: { highlightedTarget: null, scrollingAfterClick: false, @@ -40,6 +42,19 @@ export function getPageReferences() { info.hrefs, info.targets, (_link, _href, target) => target); + + info.details = + unique([ + ...document.querySelectorAll('details[id]'), + ... + Array.from(document.querySelectorAll('summary[id]')) + .map(summary => summary.closest('details')), + ]); + + info.detailsIDs = + info.details.map(details => + details.id || + details.querySelector('summary').id); } function processScrollingAfterHashLinkClicked() { @@ -60,6 +75,15 @@ function processScrollingAfterHashLinkClicked() { }, 200); } +export function mutatePageContent() { + if (location.hash.length > 1) { + const target = document.getElementById(location.hash.slice(1)); + if (target) { + expandDetails(target); + } + } +} + export function addPageListeners() { // Instead of defining a scroll offset (to account for the sticky heading) // in JavaScript, we interface with the CSS property 'scroll-margin-top'. @@ -94,6 +118,8 @@ export function addPageListeners() { return; } + expandDetails(target); + // Hide skipper box right away, so the layout is updated on time for the // math operations coming up next. const skipper = document.getElementById('skippers'); @@ -143,4 +169,32 @@ export function addPageListeners() { state.highlightedTarget = null; }); } + + stitchArrays({ + details: info.details, + id: info.detailsIDs, + }).forEach(({details, id}) => { + details.addEventListener('toggle', () => { + if (!details.open) { + detractHash(id); + } + }); + }); +} + +function expandDetails(target) { + if (target.nodeName === 'SUMMARY') { + const details = target.closest('details'); + if (details) { + details.open = true; + } + } else if (target.nodeName === 'DETAILS') { + target.open = true; + } +} + +function detractHash(id) { + if (location.hash === '#' + id) { + history.pushState({}, undefined, location.href.replace(/#.*$/, '')); + } } diff --git a/src/static/js/client/hoverable-tooltip.js b/src/static/js/client/hoverable-tooltip.js index 484f2ab0..22b9471c 100644 --- a/src/static/js/client/hoverable-tooltip.js +++ b/src/static/js/client/hoverable-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {empty, filterMultipleArrays} from '../../shared-util/sugar.js'; import {WikiRect} from '../rectangles.js'; @@ -118,17 +116,17 @@ export function registerTooltipElement(tooltip) { handleTooltipMouseLeft(tooltip); }); - tooltip.addEventListener('focusin', event => { - handleTooltipReceivedFocus(tooltip, event.relatedTarget); + tooltip.addEventListener('focusin', domEvent => { + handleTooltipReceivedFocus(tooltip, domEvent.relatedTarget); }); - tooltip.addEventListener('focusout', event => { + tooltip.addEventListener('focusout', domEvent => { // This event gets activated for tabbing *between* links inside the // tooltip, which is no good and certainly doesn't represent the focus // leaving the tooltip. - if (currentlyShownTooltipHasFocus(event.relatedTarget)) return; + if (currentlyShownTooltipHasFocus(domEvent.relatedTarget)) return; - handleTooltipLostFocus(tooltip, event.relatedTarget); + handleTooltipLostFocus(tooltip, domEvent.relatedTarget); }); } @@ -158,20 +156,20 @@ export function registerTooltipHoverableElement(hoverable, tooltip) { handleTooltipHoverableMouseLeft(hoverable); }); - hoverable.addEventListener('focusin', event => { - handleTooltipHoverableReceivedFocus(hoverable, event); + hoverable.addEventListener('focusin', domEvent => { + handleTooltipHoverableReceivedFocus(hoverable, domEvent); }); - hoverable.addEventListener('focusout', event => { - handleTooltipHoverableLostFocus(hoverable, event); + hoverable.addEventListener('focusout', domEvent => { + handleTooltipHoverableLostFocus(hoverable, domEvent); }); - hoverable.addEventListener('touchend', event => { - handleTooltipHoverableTouchEnded(hoverable, event); + hoverable.addEventListener('touchend', domEvent => { + handleTooltipHoverableTouchEnded(hoverable, domEvent); }); - hoverable.addEventListener('click', event => { - handleTooltipHoverableClicked(hoverable, event); + hoverable.addEventListener('click', domEvent => { + handleTooltipHoverableClicked(hoverable, domEvent); }); } @@ -416,7 +414,7 @@ function handleTooltipHoverableTouchEnded(hoverable, domEvent) { }, 1200); } -function handleTooltipHoverableClicked(hoverable) { +function handleTooltipHoverableClicked(hoverable, domEvent) { const {state} = info; // Don't navigate away from the page if the this hoverable was recently @@ -426,7 +424,7 @@ function handleTooltipHoverableClicked(hoverable) { state.currentlyActiveHoverable === hoverable && state.hoverableWasRecentlyTouched ) { - event.preventDefault(); + domEvent.preventDefault(); } } @@ -576,6 +574,17 @@ export function showTooltipFromHoverable(hoverable) { hoverable.classList.add('has-visible-tooltip'); + const isolator = + hoverable.closest('.isolate-tooltip-z-indexing > *'); + + if (isolator) { + for (const child of isolator.parentElement.children) { + cssProp(child, 'z-index', null); + } + + cssProp(isolator, 'z-index', '1'); + } + positionTooltipFromHoverableWithBrains(hoverable); cssProp(tooltip, 'display', 'block'); @@ -667,12 +676,12 @@ export function positionTooltipFromHoverableWithBrains(hoverable) { for (let i = 0; i < numBaselineRects; i++) { for (const [dir1, dir2] of [ + ['down', 'right'], + ['down', 'left'], ['right', 'down'], ['left', 'down'], ['right', 'up'], ['left', 'up'], - ['down', 'right'], - ['down', 'left'], ['up', 'right'], ['up', 'left'], ]) { @@ -995,6 +1004,14 @@ export function getTooltipBaselineOpportunityAreas(tooltip) { return results; } +export function mutatePageContent() { + for (const isolatorRoot of document.querySelectorAll('.isolate-tooltip-z-indexing')) { + if (isolatorRoot.firstElementChild) { + cssProp(isolatorRoot.firstElementChild, 'z-index', '1'); + } + } +} + export function addPageListeners() { const {state} = info; diff --git a/src/static/js/client/image-overlay.js b/src/static/js/client/image-overlay.js index da192178..6809726a 100644 --- a/src/static/js/client/image-overlay.js +++ b/src/static/js/client/image-overlay.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {getColors} from '../../shared-util/colors.js'; import {cssProp} from '../client-util.js'; @@ -96,7 +94,10 @@ function handleContainerClicked(evt) { // If you clicked anything near the action bar, don't hide the // image overlay. const rect = info.actionContainer.getBoundingClientRect(); - if (evt.clientY >= rect.top - 40 && evt.clientY <= rect.bottom + 40) { + if ( + evt.clientY >= rect.top - 40 && evt.clientY <= rect.bottom + 40 && + evt.clientX >= rect.left + 20 && evt.clientX <= rect.right - 20 + ) { return; } diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js index 81ea3415..3357837e 100644 --- a/src/static/js/client/index.js +++ b/src/static/js/client/index.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import '../group-contributions-table.js'; import * as additionalNamesBoxModule from './additional-names-box.js'; @@ -7,15 +5,20 @@ import * as albumCommentarySidebarModule from './album-commentary-sidebar.js'; import * as artTagGalleryFilterModule from './art-tag-gallery-filter.js'; import * as artTagNetworkModule from './art-tag-network.js'; import * as artistExternalLinkTooltipModule from './artist-external-link-tooltip.js'; +import * as artistRollingWindowModule from './artist-rolling-window.js'; import * as cssCompatibilityAssistantModule from './css-compatibility-assistant.js'; import * as datetimestampTooltipModule from './datetimestamp-tooltip.js'; import * as draggedLinkModule from './dragged-link.js'; +import * as expandableGridSectionModule from './expandable-grid-section.js'; +import * as galleryStyleSelectorModule from './gallery-style-selector.js'; import * as hashLinkModule from './hash-link.js'; import * as hoverableTooltipModule from './hoverable-tooltip.js'; import * as imageOverlayModule from './image-overlay.js'; import * as intrapageDotSwitcherModule from './intrapage-dot-switcher.js'; import * as liveMousePositionModule from './live-mouse-position.js'; +import * as memorableDetailsModule from './memorable-details.js'; import * as quickDescriptionModule from './quick-description.js'; +import * as revealAllGridControlModule from './reveal-all-grid-control.js'; import * as scriptedLinkModule from './scripted-link.js'; import * as sidebarSearchModule from './sidebar-search.js'; import * as stickyHeadingModule from './sticky-heading.js'; @@ -29,15 +32,20 @@ export const modules = [ artTagGalleryFilterModule, artTagNetworkModule, artistExternalLinkTooltipModule, + artistRollingWindowModule, cssCompatibilityAssistantModule, datetimestampTooltipModule, draggedLinkModule, + expandableGridSectionModule, + galleryStyleSelectorModule, hashLinkModule, hoverableTooltipModule, imageOverlayModule, intrapageDotSwitcherModule, liveMousePositionModule, + memorableDetailsModule, quickDescriptionModule, + revealAllGridControlModule, scriptedLinkModule, sidebarSearchModule, stickyHeadingModule, diff --git a/src/static/js/client/intrapage-dot-switcher.js b/src/static/js/client/intrapage-dot-switcher.js index d06bc5a6..b9a27a9b 100644 --- a/src/static/js/client/intrapage-dot-switcher.js +++ b/src/static/js/client/intrapage-dot-switcher.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {stitchArrays} from '../../shared-util/sugar.js'; import {cssProp} from '../client-util.js'; diff --git a/src/static/js/client/live-mouse-position.js b/src/static/js/client/live-mouse-position.js index 36a28429..32fc5bf4 100644 --- a/src/static/js/client/live-mouse-position.js +++ b/src/static/js/client/live-mouse-position.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - export const info = { id: 'liveMousePositionInfo', diff --git a/src/static/js/client/memorable-details.js b/src/static/js/client/memorable-details.js new file mode 100644 index 00000000..57d9fde8 --- /dev/null +++ b/src/static/js/client/memorable-details.js @@ -0,0 +1,62 @@ +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'memorableDetailsInfo', + + details: null, + ids: null, + + session: { + openDetails: { + type: 'json', + maxLength: settings => settings.maxOpenDetailsStorage, + }, + }, + + settings: { + maxOpenDetailsStorage: 1000, + }, +}; + +export function getPageReferences() { + info.details = + Array.from(document.querySelectorAll('details.memorable')); + + info.ids = + info.details.map(details => details.getAttribute('data-memorable-id')); +} + +export function mutatePageContent() { + stitchArrays({ + details: info.details, + id: info.ids, + }).forEach(({details, id}) => { + if (info.session.openDetails?.includes(id)) { + details.open = true; + } + }); +} + +export function addPageListeners() { + for (const [index, details] of info.details.entries()) { + details.addEventListener('toggle', () => { + handleDetailsToggled(index); + }); + } +} + +function handleDetailsToggled(index) { + const details = info.details[index]; + const id = info.ids[index]; + + if (details.open) { + if (info.session.openDetails) { + info.session.openDetails = [...info.session.openDetails, id]; + } else { + info.session.openDetails = [id]; + } + } else if (info.session.openDetails?.includes(id)) { + info.session.openDetails = + info.session.openDetails.filter(item => item !== id); + } +} diff --git a/src/static/js/client/quick-description.js b/src/static/js/client/quick-description.js index cff82252..9117d48c 100644 --- a/src/static/js/client/quick-description.js +++ b/src/static/js/client/quick-description.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {stitchArrays} from '../../shared-util/sugar.js'; export const info = { diff --git a/src/static/js/client/reveal-all-grid-control.js b/src/static/js/client/reveal-all-grid-control.js new file mode 100644 index 00000000..0572a190 --- /dev/null +++ b/src/static/js/client/reveal-all-grid-control.js @@ -0,0 +1,70 @@ +import {cssProp} from '../client-util.js'; + +export const info = { + id: 'revealAllGridControlInfo', + + revealAllLinks: null, + revealables: null, + + revealLabels: null, + concealLabels: null, +}; + +export function getPageReferences() { + info.revealAllLinks = + Array.from(document.querySelectorAll('.reveal-all a')); + + info.revealables = + info.revealAllLinks + .map(link => link.closest('.grid-listing')) + .map(listing => listing.querySelectorAll('.reveal')); + + info.revealLabels = + info.revealAllLinks + .map(link => link.querySelector('.reveal-label')); + + info.concealLabels = + info.revealAllLinks + .map(link => link.querySelector('.conceal-label')); +} + +export function addPageListeners() { + for (const [index, link] of info.revealAllLinks.entries()) { + link.addEventListener('click', domEvent => { + domEvent.preventDefault(); + handleRevealAllLinkClicked(index); + }); + } +} + +export function addInternalListeners() { + // Don't even think about it. "Reveal all artworks" is a stable control, + // meaning it only changes because the user interacted with it directly. +} + +function handleRevealAllLinkClicked(index) { + const revealables = info.revealables[index]; + const revealLabel = info.revealLabels[index]; + const concealLabel = info.concealLabels[index]; + + const shouldReveal = + (cssProp(revealLabel, 'display') === 'none' + ? false + : true); + + for (const revealable of revealables) { + if (shouldReveal) { + revealable.classList.add('revealed'); + } else { + revealable.classList.remove('revealed'); + } + } + + if (shouldReveal) { + cssProp(revealLabel, 'display', 'none'); + cssProp(concealLabel, 'display', null); + } else { + cssProp(revealLabel, 'display', null); + cssProp(concealLabel, 'display', 'none'); + } +} diff --git a/src/static/js/client/scripted-link.js b/src/static/js/client/scripted-link.js index 8b8d8a13..badc6ccb 100644 --- a/src/static/js/client/scripted-link.js +++ b/src/static/js/client/scripted-link.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {pick, stitchArrays} from '../../shared-util/sugar.js'; import { diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index fb902636..b4356a0f 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -1,7 +1,5 @@ -/* eslint-env browser */ - import {getColors} from '../../shared-util/colors.js'; -import {accumulateSum, empty} from '../../shared-util/sugar.js'; +import {accumulateSum, empty, unique} from '../../shared-util/sugar.js'; import { cssProp, @@ -41,6 +39,14 @@ export const info = { failedRule: null, failedContainer: null, + filterContainer: null, + albumFilterLink: null, + artistFilterLink: null, + flashFilterLink: null, + groupFilterLink: null, + tagFilterLink: null, + trackFilterLink: null, + resultsRule: null, resultsContainer: null, results: null, @@ -65,6 +71,17 @@ export const info = { groupResultKindString: null, tagResultKindString: null, + groupResultDisambiguatorString: null, + flashResultDisambiguatorString: null, + trackResultDisambiguatorString: null, + + albumResultFilterString: null, + artistResultFilterString: null, + flashResultFilterString: null, + groupResultFilterString: null, + tagResultFilterString: null, + trackResultFilterString: null, + state: { sidebarColumnShownForSearch: null, @@ -97,6 +114,10 @@ export const info = { maxLength: settings => settings.maxActiveResultsStorage, }, + activeFilterType: { + type: 'string', + }, + repeatQueryOnReload: { type: 'boolean', default: false, @@ -176,6 +197,33 @@ export function getPageReferences() { info.tagResultKindString = findString('tag-result-kind'); + + info.groupResultDisambiguatorString = + findString('group-result-disambiguator'); + + info.flashResultDisambiguatorString = + findString('flash-result-disambiguator'); + + info.trackResultDisambiguatorString = + findString('track-result-disambiguator'); + + info.albumResultFilterString = + findString('album-result-filter'); + + info.artistResultFilterString = + findString('artist-result-filter'); + + info.flashResultFilterString = + findString('flash-result-filter'); + + info.groupResultFilterString = + findString('group-result-filter'); + + info.tagResultFilterString = + findString('tag-result-filter'); + + info.trackResultFilterString = + findString('track-result-filter'); } export function addInternalListeners() { @@ -265,6 +313,38 @@ export function mutatePageContent() { info.searchBox.appendChild(info.failedRule); info.searchBox.appendChild(info.failedContainer); + // Filter section + + info.filterContainer = + document.createElement('div'); + + info.filterContainer.classList.add('wiki-search-filter-container'); + + cssProp(info.filterContainer, 'display', 'none'); + + forEachFilter((type, _filterLink) => { + // TODO: It's probably a sin to access `session` during this step LOL + const {session} = info; + + const filterLink = document.createElement('a'); + + filterLink.href = '#'; + filterLink.classList.add('wiki-search-filter-link'); + + if (session.activeFilterType === type) { + filterLink.classList.add('active'); + } + + const string = info[type + 'ResultFilterString']; + filterLink.appendChild(templateContent(string)); + + info[type + 'FilterLink'] = filterLink; + + info.filterContainer.appendChild(filterLink); + }); + + info.searchBox.appendChild(info.filterContainer); + // Results section info.resultsRule = @@ -371,7 +451,7 @@ export function addPageListeners() { const {settings, state} = info; if (!info.searchInput.value) { - clearSidebarSearch(); + clearSidebarSearch(); // ...but don't clear filter return; } @@ -433,10 +513,18 @@ export function addPageListeners() { info.endSearchLink.addEventListener('click', domEvent => { domEvent.preventDefault(); clearSidebarSearch(); + clearSidebarFilter(); possiblyHideSearchSidebarColumn(); restoreSidebarSearchColumn(); }); + forEachFilter((type, filterLink) => { + filterLink.addEventListener('click', domEvent => { + domEvent.preventDefault(); + toggleSidebarSearchFilter(type); + }); + }); + info.resultsContainer.addEventListener('scroll', () => { const {settings, state} = info; @@ -518,6 +606,21 @@ function trackSidebarSearchDownloadEnds(event) { } } +function forEachFilter(callback) { + const filterOrder = [ + 'track', + 'album', + 'artist', + 'group', + 'flash', + 'tag', + ]; + + for (const type of filterOrder) { + callback(type, info[type + 'FilterLink']); + } +} + async function activateSidebarSearch(query) { const {session, state} = info; @@ -584,6 +687,16 @@ function clearSidebarSearch() { hideSidebarSearchResults(); } +function clearSidebarFilter() { + const {session} = info; + + toggleSidebarSearchFilter(session.activeFilterType); + + forEachFilter((_type, filterLink) => { + filterLink.classList.remove('shown', 'hidden'); + }); +} + function updateSidebarSearchStatus() { const {state} = info; @@ -670,63 +783,131 @@ function showSidebarSearchFailed() { } function showSidebarSearchResults(results) { - console.debug(`Showing search results:`, results); + const {session} = info; - showSearchSidebarColumn(); + console.debug(`Showing search results:`, tidyResults(results)); - const flatResults = - Object.entries(results) - .filter(([index]) => index === 'generic') - .flatMap(([index, results]) => results - .flatMap(({doc, id}) => ({ - index, - reference: id ?? null, - referenceType: (id ? id.split(':')[0] : null), - directory: (id ? id.split(':')[1] : null), - data: doc, - }))); + showSearchSidebarColumn(); info.searchBox.classList.add('showing-results'); info.searchSidebarColumn.classList.add('search-showing-results'); - while (info.results.firstChild) { - info.results.firstChild.remove(); + let filterType = session.activeFilterType; + let shownAnyResults = + fillResultElements(results, {filterType: session.activeFilterType}); + + showFilterElements(results); + + if (!shownAnyResults) { + shownAnyResults = toggleSidebarSearchFilter(filterType); + filterType = null; } - cssProp(info.resultsRule, 'display', 'block'); - cssProp(info.resultsContainer, 'display', 'block'); + if (shownAnyResults) { + cssProp(info.endSearchRule, 'display', 'block'); + cssProp(info.endSearchLine, 'display', 'block'); - if (empty(flatResults)) { + tidySidebarSearchColumn(); + } else { const p = document.createElement('p'); p.classList.add('wiki-search-no-results'); p.appendChild(templateContent(info.noResultsString)); info.results.appendChild(p); } - for (const result of flatResults) { - const el = generateSidebarSearchResult(result); + restoreSidebarSearchResultsScrollOffset(); +} + +function tidyResults(results) { + const tidiedResults = + results.results.map(({doc, id}) => ({ + reference: id ?? null, + referenceType: (id ? id.split(':')[0] : null), + directory: (id ? id.split(':')[1] : null), + data: doc, + })); + + return tidiedResults; +} + +function fillResultElements(results, { + filterType = null, +} = {}) { + const tidiedResults = tidyResults(results); + + const filteredResults = + (filterType + ? tidiedResults.filter(result => result.referenceType === filterType) + : tidiedResults); + + while (info.results.firstChild) { + info.results.firstChild.remove(); + } + + cssProp(info.resultsRule, 'display', 'block'); + cssProp(info.resultsContainer, 'display', 'block'); + + if (empty(filteredResults)) { + return false; + } + + for (const result of filteredResults) { + const el = generateSidebarSearchResult(result, filteredResults); if (!el) continue; info.results.appendChild(el); } - if (!empty(flatResults)) { - cssProp(info.endSearchRule, 'display', 'block'); - cssProp(info.endSearchLine, 'display', 'block'); + return true; +} - tidySidebarSearchColumn(); - } +function showFilterElements(results) { + const {queriedKind} = results; - restoreSidebarSearchResultsScrollOffset(); + const tidiedResults = tidyResults(results); + + const allReferenceTypes = + unique(tidiedResults.map(result => result.referenceType)); + + let shownAny = false; + + forEachFilter((type, filterLink) => { + filterLink.classList.remove('shown', 'hidden'); + + if (allReferenceTypes.includes(type)) { + shownAny = true; + cssProp(filterLink, 'display', null); + + if (queriedKind) { + filterLink.setAttribute('inert', 'inert'); + } else { + filterLink.removeAttribute('inert'); + } + + if (type === queriedKind) { + filterLink.classList.add('active-from-query'); + } else { + filterLink.classList.remove('active-from-query'); + } + } else { + cssProp(filterLink, 'display', 'none'); + } + }); + + if (shownAny) { + cssProp(info.filterContainer, 'display', null); + } else { + cssProp(info.filterContainer, 'display', 'none'); + } } -function generateSidebarSearchResult(result) { +function generateSidebarSearchResult(result, results) { const preparedSlots = { color: result.data.color ?? null, name: - result.data.name ?? result.data.primaryName ?? null, + getSearchResultName(result), imageSource: getSearchResultImageSource(result), @@ -791,9 +972,37 @@ function generateSidebarSearchResult(result) { return null; } + const compareReferenceType = otherResult => + otherResult.referenceType === result.referenceType; + + const compareName = otherResult => + getSearchResultName(otherResult) === getSearchResultName(result); + + const ambiguous = + results.some(otherResult => + otherResult !== result && + compareReferenceType(otherResult) && + compareName(otherResult)); + + if (ambiguous) { + preparedSlots.disambiguate = + result.data.disambiguator; + + preparedSlots.disambiguatorString = + info[result.referenceType + 'ResultDisambiguatorString']; + } + return generateSidebarSearchResultTemplate(preparedSlots); } +function getSearchResultName(result) { + return ( + result.data.name ?? + result.data.primaryName ?? + null + ); +} + function getSearchResultImageSource(result) { const {artwork} = result.data; if (!artwork) return null; @@ -869,6 +1078,15 @@ function generateSidebarSearchResultTemplate(slots) { } } + if (!accentSpan && slots.disambiguate) { + accentSpan = document.createElement('span'); + accentSpan.classList.add('wiki-search-result-disambiguator'); + accentSpan.appendChild( + templateContent(slots.disambiguatorString, { + disambiguator: slots.disambiguate, + })); + } + if (!accentSpan && slots.kindString) { accentSpan = document.createElement('span'); accentSpan.classList.add('wiki-search-result-kind'); @@ -908,6 +1126,8 @@ function generateSidebarSearchResultTemplate(slots) { } function hideSidebarSearchResults() { + cssProp(info.filterContainer, 'display', 'none'); + cssProp(info.resultsRule, 'display', 'none'); cssProp(info.resultsContainer, 'display', 'none'); @@ -1040,6 +1260,36 @@ function tidySidebarSearchColumn() { } } +function toggleSidebarSearchFilter(toggleType) { + const {session} = info; + + if (!toggleType) return null; + + let shownAnyResults = null; + + forEachFilter((type, filterLink) => { + if (type === toggleType) { + const filterActive = filterLink.classList.toggle('active'); + const filterType = (filterActive ? type : null); + + if (cssProp(filterLink, 'display') !== 'none') { + filterLink.classList.add(filterActive ? 'shown' : 'hidden'); + } + + if (session.activeQueryResults) { + shownAnyResults = + fillResultElements(session.activeQueryResults, {filterType}); + } + + session.activeFilterType = filterType; + } else { + filterLink.classList.remove('active'); + } + }); + + return shownAnyResults; +} + function restoreSidebarSearchColumn() { const {state} = info; @@ -1073,6 +1323,8 @@ function forgetRecentSidebarSearch() { session.activeQuery = null; session.activeQueryResults = null; + + clearSidebarFilter(); } async function handleDroppedIntoSearchInput(domEvent) { @@ -1101,7 +1353,7 @@ async function handleDroppedIntoSearchInput(domEvent) { let droppedURL; try { droppedURL = new URL(droppedText); - } catch (error) { + } catch { droppedURL = null; } diff --git a/src/static/js/client/sticky-heading.js b/src/static/js/client/sticky-heading.js index fba05b84..c69e137f 100644 --- a/src/static/js/client/sticky-heading.js +++ b/src/static/js/client/sticky-heading.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {filterMultipleArrays, stitchArrays} from '../../shared-util/sugar.js'; import {cssProp, dispatchInternalEvent, templateContent} from '../client-util.js'; @@ -23,6 +21,7 @@ export const info = { contentContainers: null, contentHeadings: null, + contentCoverColumns: null, contentCovers: null, contentCoversReveal: null, @@ -82,9 +81,13 @@ export function getPageReferences() { info.stickyContainers .map(el => el.closest('.content-sticky-heading-root').parentElement); - info.contentCovers = + info.contentCoverColumns = info.contentContainers - .map(el => el.querySelector('#cover-art-container')); + .map(el => el.querySelector('#artwork-column')); + + info.contentCovers = + info.contentCoverColumns + .map(el => el ? el.querySelector('.cover-artwork') : null); info.contentCoversReveal = info.contentCovers @@ -212,10 +215,10 @@ function updateCollapseStatus(index) { function updateStickyCoverVisibility(index) { const stickyCoverContainer = info.stickyCoverContainers[index]; const stickyContainer = info.stickyContainers[index]; - const contentCover = info.contentCovers[index]; + const contentCoverColumn = info.contentCoverColumns[index]; - if (contentCover && stickyCoverContainer) { - if (contentCover.getBoundingClientRect().bottom < 4) { + if (contentCoverColumn && stickyCoverContainer) { + if (contentCoverColumn.getBoundingClientRect().bottom < 4) { stickyCoverContainer.classList.add('visible'); stickyContainer.classList.add('cover-visible'); } else { @@ -250,7 +253,11 @@ function getContentHeadingClosestToStickySubheading(index) { // Iterate from bottom to top of the content area. const contentHeadings = info.contentHeadings[index]; - for (const heading of contentHeadings.slice().reverse()) { + for (const heading of contentHeadings.toReversed()) { + if (heading.nodeName === 'SUMMARY' && !heading.closest('details').open) { + continue; + } + const headingRect = heading.getBoundingClientRect(); if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 40) { return heading; diff --git a/src/static/js/client/summary-nested-link.js b/src/static/js/client/summary-nested-link.js index 23857fa5..1c4e7e4b 100644 --- a/src/static/js/client/summary-nested-link.js +++ b/src/static/js/client/summary-nested-link.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import { empty, filterMultipleArrays, diff --git a/src/static/js/client/text-with-tooltip.js b/src/static/js/client/text-with-tooltip.js index dd207e04..2b855756 100644 --- a/src/static/js/client/text-with-tooltip.js +++ b/src/static/js/client/text-with-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {stitchArrays} from '../../shared-util/sugar.js'; import {registerTooltipElement, registerTooltipHoverableElement} diff --git a/src/static/js/client/wiki-search.js b/src/static/js/client/wiki-search.js index 2446c172..9a6e29c1 100644 --- a/src/static/js/client/wiki-search.js +++ b/src/static/js/client/wiki-search.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {promiseWithResolvers} from '../../shared-util/sugar.js'; import {dispatchInternalEvent} from '../client-util.js'; diff --git a/src/static/js/group-contributions-table.js b/src/static/js/group-contributions-table.js index 72ad2327..bef85fad 100644 --- a/src/static/js/group-contributions-table.js +++ b/src/static/js/group-contributions-table.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // TODO: Update to clientSteps style. const groupContributionsTableInfo = diff --git a/src/static/js/info-card.js b/src/static/js/info-card.js index 1d9f7c86..05d5d801 100644 --- a/src/static/js/info-card.js +++ b/src/static/js/info-card.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // Note: This is a super ancient chunk of code which isn't actually in use, // so it's just commented out here. diff --git a/src/static/js/lazy-loading.js b/src/static/js/lazy-loading.js index 1df56f08..0c8aef31 100644 --- a/src/static/js/lazy-loading.js +++ b/src/static/js/lazy-loading.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // Lazy loading! Roll your own. Woot. // This file includes a 8unch of fall8acks and stuff like that, and is written // with fairly Olden JavaScript(TM), so as to work on pretty much any 8rowser diff --git a/src/static/js/rectangles.js b/src/static/js/rectangles.js index cdab2cb8..24382ef8 100644 --- a/src/static/js/rectangles.js +++ b/src/static/js/rectangles.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {info as liveMousePositionInfo} from './client/live-mouse-position.js'; export class WikiRect extends DOMRect { @@ -510,4 +508,46 @@ export class WikiRect extends DOMRect { height: this.height, }); } + + // Other utilities + + #display = null; + + display() { + if (!this.#display) { + this.#display = document.createElement('div'); + document.body.appendChild(this.#display); + } + + Object.assign(this.#display.style, { + position: 'fixed', + background: '#000c', + border: '3px solid var(--primary-color)', + borderRadius: '4px', + top: this.top + 'px', + left: this.left + 'px', + width: this.width + 'px', + height: this.height + 'px', + pointerEvents: 'none', + }); + + let i = 0; + const int = setInterval(() => { + i++; + if (i >= 3) clearInterval(int); + if (!this.#display) return; + + this.#display.style.display = 'none'; + setTimeout(() => { + this.#display.style.display = ''; + }, 200); + }, 600); + } + + hide() { + if (this.#display) { + this.#display.remove(); + this.#display = null; + } + } } diff --git a/src/static/js/search-worker.js b/src/static/js/search-worker.js index 1b4684ad..96ad8a0b 100644 --- a/src/static/js/search-worker.js +++ b/src/static/js/search-worker.js @@ -1,8 +1,7 @@ -/* eslint-env worker */ - import FlexSearch from '../lib/flexsearch/flexsearch.bundle.module.min.js'; -import {makeSearchIndex, searchSpec} from '../shared-util/search-spec.js'; +import {default as searchSpec, makeSearchIndex} + from '../shared-util/search-shape.js'; import { empty, @@ -130,7 +129,7 @@ async function loadDatabase() { try { idb = await promisifyIDBRequest(request); - } catch (error) { + } catch { console.warn(`Couldn't load search IndexedDB - won't use an internal cache.`); console.warn(request.error); idb = null; @@ -371,58 +370,76 @@ function postActionResult(id, status, value) { } function performSearchAction({query, options}) { - const {generic, ...otherIndexes} = indexes; + const {queriedKind} = processTerms(query); + const genericResults = queryGenericIndex(query, options); + const verbatimResults = queryVerbatimIndex(query, options); - const genericResults = - queryGenericIndex(generic, query, options); + const verbatimIDs = + new Set(verbatimResults?.map(result => result.id)); - const otherResults = - withEntries(otherIndexes, entries => entries - .map(([indexName, index]) => [ - indexName, - index.search(query, options), - ])); + const commonResults = + (verbatimResults && genericResults + ? genericResults + .filter(({id}) => verbatimIDs.has(id)) + : verbatimResults ?? genericResults); return { - generic: genericResults, - ...otherResults, + results: commonResults, + queriedKind, }; } -function queryGenericIndex(index, query, options) { - const interestingFieldCombinations = [ - ['primaryName', 'parentName', 'groups'], - ['primaryName', 'parentName'], - ['primaryName', 'groups', 'contributors'], - ['primaryName', 'groups', 'artTags'], - ['primaryName', 'groups'], - ['primaryName', 'contributors'], - ['primaryName', 'artTags'], - ['parentName', 'groups', 'artTags'], - ['parentName', 'artTags'], - ['groups', 'contributors'], - ['groups', 'artTags'], - - // This prevents just matching *everything* tagged "john" if you - // only search "john", but it actually supports matching more than - // *two* tags at once: "john rose lowas" works! This is thanks to - // flexsearch matching multiple field values in a single query. - ['artTags', 'artTags'], - - ['contributors', 'parentName'], - ['contributors', 'groups'], - ['primaryName', 'contributors'], - ['primaryName'], - ]; +const interestingFieldCombinations = [ + ['primaryName'], + + ['primaryName', 'parentName', 'groups'], + ['primaryName', 'parentName'], + ['primaryName', 'groups', 'contributors'], + ['primaryName', 'groups', 'artTags'], + ['primaryName', 'groups'], + ['primaryName', 'contributors'], + ['primaryName', 'artTags'], + ['parentName', 'groups', 'artTags'], + ['parentName', 'artTags'], + ['groups', 'contributors'], + ['groups', 'artTags'], + + // This prevents just matching *everything* tagged "john" if you + // only search "john", but it actually supports matching more than + // *two* tags at once: "john rose lowas" works! This is thanks to + // flexsearch matching multiple field values in a single query. + ['artTags', 'artTags'], + + ['contributors', 'parentName'], + ['contributors', 'groups'], + ['primaryName', 'contributors'], +]; + +function queryGenericIndex(query, options) { + return queryIndex({ + indexKey: 'generic', + termsKey: 'genericTerms', + }, query, options); +} + +function queryVerbatimIndex(query, options) { + return queryIndex({ + indexKey: 'verbatim', + termsKey: 'verbatimTerms', + }, query, options); +} +function queryIndex({termsKey, indexKey}, query, options) { const interestingFields = unique(interestingFieldCombinations.flat()); - const {genericTerms, queriedKind} = + const {[termsKey]: terms, queriedKind} = processTerms(query); + if (empty(terms)) return null; + const particles = - particulate(genericTerms); + particulate(terms); const groupedParticles = groupArray(particles, ({length}) => length); @@ -437,7 +454,7 @@ function queryGenericIndex(index, query, options) { query: values, })); - const boilerplate = queryBoilerplate(index); + const boilerplate = queryBoilerplate(indexes[indexKey]); const particleResults = Object.fromEntries( @@ -459,62 +476,73 @@ function queryGenericIndex(index, query, options) { ])), ])); - const results = new Set(); + let matchedResults = new Set(); for (const interestingFieldCombination of interestingFieldCombinations) { for (const query of queriesBy(interestingFieldCombination)) { - const idToMatchingFieldsMap = new Map(); - for (const {field, query: fieldQuery} of query) { - for (const id of particleResults[field][fieldQuery]) { - if (idToMatchingFieldsMap.has(id)) { - idToMatchingFieldsMap.get(id).push(field); - } else { - idToMatchingFieldsMap.set(id, [field]); - } - } - } + const [firstQueryFieldLine, ...restQueryFieldLines] = query; const commonAcrossFields = - Array.from(idToMatchingFieldsMap.entries()) - .filter(([id, matchingFields]) => - matchingFields.length === interestingFieldCombination.length) - .map(([id]) => id); + new Set( + particleResults + [firstQueryFieldLine.field] + [firstQueryFieldLine.query]); + + for (const currQueryFieldLine of restQueryFieldLines) { + const tossResults = new Set(commonAcrossFields); + + const keepResults = + particleResults + [currQueryFieldLine.field] + [currQueryFieldLine.query]; + + for (const result of keepResults) { + tossResults.delete(result); + } + + for (const result of tossResults) { + commonAcrossFields.delete(result); + } + } for (const result of commonAcrossFields) { - results.add(result); + matchedResults.add(result); } } } - const constituted = - boilerplate.constitute(results); + matchedResults = Array.from(matchedResults); + + const filteredResults = + (queriedKind + ? matchedResults.filter(id => id.split(':')[0] === queriedKind) + : matchedResults); - const constitutedAndFiltered = - constituted - .filter(({id}) => - (queriedKind - ? id.split(':')[0] === queriedKind - : true)); + const constitutedResults = + boilerplate.constitute(filteredResults); - return constitutedAndFiltered; + return constitutedResults; } function processTerms(query) { const kindTermSpec = [ - {kind: 'album', terms: ['album']}, - {kind: 'artist', terms: ['artist']}, - {kind: 'flash', terms: ['flash']}, - {kind: 'group', terms: ['group']}, - {kind: 'tag', terms: ['art tag', 'tag']}, - {kind: 'track', terms: ['track']}, + {kind: 'album', terms: ['album', 'albums']}, + {kind: 'artist', terms: ['artist', 'artists']}, + {kind: 'flash', terms: ['flash', 'flashes']}, + {kind: 'group', terms: ['group', 'groups']}, + {kind: 'tag', terms: ['art tag', 'art tags', 'tag', 'tags']}, + {kind: 'track', terms: ['track', 'tracks']}, ]; const genericTerms = []; + const verbatimTerms = []; let queriedKind = null; const termRegexp = new RegExp( - String.raw`(?<kind>${kindTermSpec.flatMap(spec => spec.terms).join('|')})` + + String.raw`(?<kind>(?<=^|\s)(?:${kindTermSpec.flatMap(spec => spec.terms).join('|')})(?=$|\s))` + + String.raw`|(?<=^|\s)(?<quote>["'])(?<regularVerbatim>.+?)\k<quote>(?=$|\s)` + + String.raw`|(?<=^|\s)[“”‘’](?<curlyVerbatim>.+?)[“”‘’](?=$|\s)` + String.raw`|[^\s\-]+`, 'gi'); @@ -530,10 +558,16 @@ function processTerms(query) { continue; } + const verbatim = groups.regularVerbatim || groups.curlyVerbatim; + if (verbatim) { + verbatimTerms.push(verbatim); + continue; + } + genericTerms.push(match[0]); } - return {genericTerms, queriedKind}; + return {genericTerms, verbatimTerms, queriedKind}; } function particulate(terms) { diff --git a/src/static/js/xhr-util.js b/src/static/js/xhr-util.js index 8a43072c..bc0698da 100644 --- a/src/static/js/xhr-util.js +++ b/src/static/js/xhr-util.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - /** * This fetch function is adapted from a `loadImage` function * credited to Parziphal, Feb 13, 2017. diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 259e01bb..5bbecbf3 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -97,16 +97,6 @@ count: many: "" other: "{CONTRIBUTIONS} contributions" - coverArts: - _: "{COVER_ARTS}" - withUnit: - zero: "" - one: "{COVER_ARTS} cover art" - two: "" - few: "" - many: "" - other: "{COVER_ARTS} cover arts" - flashes: _: "{FLASHES}" withUnit: @@ -269,29 +259,47 @@ releaseInfo: # Descriptions - by: - _: "By {ARTISTS}." - featuring: "By {ARTISTS}, featuring {FEATURING}." + by: >- + By {ARTISTS}. + by.featuring: >- + By {ARTISTS}, featuring {FEATURING}. + by.withAlbum: >- + From {ALBUM}, by {ARTISTS}. + by.featuring.withAlbum: >- + From {ALBUM}, by {ARTISTS} feat. {FEATURING}. from: "From {ALBUM}." - coverArtBy: "Cover art by {ARTISTS}." - wallpaperArtBy: "Wallpaper art by {ARTISTS}." - bannerArtBy: "Banner art by {ARTISTS}." + wallpaperArtBy: "Wallpaper by {ARTISTS}" + bannerArtBy: "Banner by {ARTISTS}" released: "Released {DATE}." albumReleased: "Album released {DATE}." - artReleased: "Art released {DATE}." trackReleased: "Track released {DATE}." addedToWiki: "Added to wiki {DATE}." duration: "Duration: {DURATION}." contributors: "Contributors:" - lyrics: "Lyrics:" + + lyrics: + _: "Lyrics:" + + switcher: "({ENTRIES})" + note: "Context notes:" - alsoReleasedOn: "Also released on {ALBUMS}." + alsoReleased: + onAlbums: >- + Also released on {ALBUMS}. + + asSingle: >- + Also released {SINGLE}. + + onAlbums.asSingle: >- + Also released on {ALBUMS}, and {SINGLE}. + + single: "as a single" tracksReferenced: _: "Tracks that {TRACK} references:" @@ -365,12 +373,20 @@ releaseInfo: readCommentary: _: "Read {LINK}." - link: "artist commentary" - readCreditSources: + link: + _: "artist commentary" + withWikiCommentary: "artist and wiki commentary" + onlyWikiCommentary: "wiki commentary" + + readCreditingSources: _: "Read {LINK}." link: "crediting sources" + readReferencingSources: + _: "Read {LINK}." + link: "referencing sources" + additionalFiles: heading: "View or download additional files:" @@ -485,7 +501,16 @@ misc: # artistCommentary: artistCommentary: - _: "Artist commentary:" + _: "Artist commentary for {THING}:" + sticky: "Artist commentary:" + + withWikiCommentary: + _: "Artist and wiki commentary for {THING}:" + sticky: "Artist and wiki commentary:" + + onlyWikiCommentary: + _: "Wiki commentary for {THING}:" + sticky: "Wiki commentary:" entry: title: @@ -512,13 +537,10 @@ misc: info: fromMainRelease: >- - This commentary is properly placed on this track's main release, {ALBUM}. + The following commentary is properly placed on this track's main release, {ALBUM}. fromMainRelease.namedDifferently: >- - This commentary is properly placed on this track's main release, {ALBUM}, where it's named {NAME}. - - releaseSpecific: >- - This commentary is specific to this release, {ALBUM}. + The following commentary is properly placed on this track's main release, {ALBUM}, where it's named {NAME}. seeSpecificReleases: >- For release-specific commentary, check out: {ALBUMS}. @@ -559,6 +581,10 @@ misc: noExternalLinkPlatformName: "Other" chronology: + heading: + artistReleases: "Releases by {ARTIST}:" + artistTracks: "Tracks by {ARTIST}:" + previous: symbol: "←" info: @@ -576,47 +602,17 @@ misc: bannerArt: "banner art" coverArt: "cover art" flash: "flash" + release: "release" track: "track" trackArt: "track art" trackContribution: "track contribution" wallpaperArt: "wallpaper art" - # chronology: - # - # "Chronology links" are a section that appear in the nav bar for - # most things with individual contributors across the wiki! These - # allow for quick navigation between older and newer releases of - # a given artist, or seeing at a glance how many contributions an - # artist made before the one you're currently viewing. - # - # Chronology information is described for each artist and shows - # the kind of thing which is being contributed to, since all of - # the entries are displayed together in one list. - # - - chronology: - - # seeArtistPages: - # If the thing you're viewing has a lot of contributors, their - # chronology info will be exempt from the nav bar, which'll - # show this message instead. - - seeArtistPages: "(See artist pages for chronology info!)" - - # withNavigation: - # Navigation refers to previous/next links. - - withNavigation: "{HEADING} ({NAVIGATION})" - - heading: - coverArt: "{INDEX} cover art by {ARTIST}" - flash: "{INDEX} flash/game by {ARTIST}" - track: "{INDEX} track by {ARTIST}" - trackArt: "{INDEX} track art by {ARTIST}" - onlyIndex: "Only" - - creditSources: - _: "Crediting sources:" + creditingSources: + _: "{CUE} for {THING}:" + collapsed: "{CUE} for {THING}…" + cue: "Crediting sources" + sticky: "Crediting sources:" # external: # Links which will generally bring you somewhere off of the wiki. @@ -644,7 +640,12 @@ misc: amazonMusic: "Amazon Music" appleMusic: "Apple Music" artstation: "ArtStation" - bandcamp: "Bandcamp" + + bandcamp: + _: "Bandcamp" + + composerRelease: "Bandcamp (composer's release)" + officialRelease: "Bandcamp (official release)" bgreco: _: "bgreco.net" @@ -691,6 +692,11 @@ misc: nintendoMusic: "Nintendo Music" patreon: "Patreon" poetryFoundation: "Poetry Foundation" + + reddit: + _: "Reddit" + subreddit: "Reddit ({SUBREDDIT})" + soundcloud: "SoundCloud" spotify: "Spotify" steam: "Steam" @@ -708,6 +714,18 @@ misc: playlist: "YouTube (playlist)" fullAlbum: "YouTube (full album)" + # lyrics: + + lyrics: + source: >- + Via {SOURCE} + + contributors: >- + Contributions from {CONTRIBUTORS} + + squareBracketAnnotations: >- + Mind parts marked in [square brackets] + # missingImage: # Fallback text displayed in an image when it's sourced to a file # that isn't available under the wiki's media directory. While it @@ -766,8 +784,10 @@ misc: nav: previous: "Previous" next: "Next" + info: "Info" gallery: "Gallery" + rollingWindow: "Rolling Window" # pageTitle: # Title set under the page's <title> HTML element, which is @@ -815,6 +835,25 @@ misc: artist: "(artist)" group: "(group)" + resultDisambiguator: + group: "({DISAMBIGUATOR})" + flash: "(in {DISAMBIGUATOR})" + track: "(from {DISAMBIGUATOR})" + + resultFilter: + album: "Albums" + artTag: "Art Tags" + artist: "Artists" + flash: "Flashes" + group: "Groups" + track: "Tracks" + + referencingSources: + _: "{CUE} for {THING}:" + collapsed: "{CUE} for {THING}…" + cue: "Referencing sources" + sticky: "Referencing sources:" + # skippers: # # These are navigational links that only show up when you're @@ -842,7 +881,7 @@ misc: # Displayed on various info pages. artistCommentary: "Artist commentary" - creditSources: "Crediting sources" + creditingSources: "Crediting sources" # Displayed on artist info page. @@ -863,6 +902,7 @@ misc: sampledBy: "Sampled by..." features: "Features..." featuredIn: "Featured in..." + referencingSources: "Referencing sources" lyrics: "Lyrics" @@ -880,8 +920,6 @@ misc: socialEmbed: heading: "{WIKI_NAME} | {HEADING}" - trackArtFromAlbum: "Album cover for {ALBUM}" - # jumpTo: # Generic action displayed at the top of some longer pages, for # quickly scrolling down to a particular section. @@ -899,6 +937,48 @@ misc: warnings: "{WARNINGS}" reveal: "click to show" + # coverArtwork: + # Generic or particular strings for artworks outside a grid + # context, when just one cover is being spotlighted. + + coverArtwork: + artworkBy: >- + Artwork by {ARTISTS} + + artworkBy.customLabel: >- + {LABEL} by {ARTISTS} + + artworkBy.withYear: >- + Artwork ({YEAR}) by {ARTISTS} + + artworkBy.customLabel.withYear: >- + {LABEL} ({YEAR}) by {ARTISTS} + + source: >- + Via {SOURCE} + + source.customLabel: >- + {LABEL} via {SOURCE} + + source.withYear: >- + Via {SOURCE} ({YEAR}) + + source.customLabel.withYear: >- + {LABEL} ({YEAR}) via {SOURCE} + + customLabel: >- + {LABEL} + + customLabel.withYear: >- + {LABEL} ({YEAR}) + + year: >- + Released {YEAR} + + trackArtFromAlbum: "Album cover for {ALBUM}" + + sameTagsAsMainArtwork: "Same tags as main artwork" + # coverGrid: # Generic strings for various sorts of gallery grids, displayed # on the homepage, album galleries, artist artwork galleries, and @@ -907,13 +987,34 @@ misc: # that thing. coverGrid: + revealAll: + reveal: "Reveal all artworks" + conceal: "Conceal all artworks" + warnings: "In this gallery: {WARNINGS}" + + expandCollapseCue: "({CUE})" + expand: "Show the rest!" + collapse: "Collapse these" + noCoverArt: "{ALBUM}" + tab: + groups: "{GROUPS}" + artists: "{ARTISTS}" + artists.featuring: "{ARTISTS} feat. {FEATURING}" + details: + notFromThisGroup: "{NAME}{MARKER}" + notFromThisGroup.marker: "*" + accent: "({DETAILS})" albumLength: "{TRACKS}, {TIME}" + albumLength.single: "single, {TIME}" + coverArtists: "Artwork by {ARTISTS}" + coverArtists.customLabel: "{LABEL} by {ARTISTS}" + otherCoverArtists: "With {ARTISTS}" albumGalleryGrid: @@ -972,7 +1073,9 @@ albumSidebar: group: _: "{GROUP}" - withRange: "{GROUP} ({RANGE})" + + withRange: "{GROUP} {RANGE_PART}" + withRange.rangePart: "({RANGE})" # groupBox: # This is the box for groups. Apart from the next and previous @@ -1073,6 +1176,9 @@ albumGalleryPage: statsLine.withDate: >- {TRACKS} totaling {DURATION}. Released {DATE}. + statsLine.withDate.noDuration: >- + Released {DATE}. + # coverArtistsLine: # This is displayed if every track (which has artwork at all) # has the same illustration credits. @@ -1089,6 +1195,15 @@ albumGalleryPage: noTrackArtworksLine: >- This album doesn't have any track artwork. + # setSwitcher: + # This is displayed if multiple sets of artwork are available + # across the album. + + setSwitcher: + _: "({SETS})" + + unlabeledSet: "Main album art" + # # albumCommentaryPage: # The album commentary page is a more minimal layout that brings @@ -1240,6 +1355,16 @@ artistPage: flash: _: "{FLASH}" + artwork.accent: + withLabel: >- + {LABEL} + + withAnnotation: >- + {ANNOTATION} + + withLabel.withAnnotation: >- + {LABEL}: {ANNOTATION} + # contributedDurationLine: # This is shown at the top of the artist's track list, provided # any of their tracks have durations at all. @@ -1312,6 +1437,41 @@ artistGalleryPage: infoLine: >- Contributed to {COVER_ARTS}. +artistRollingWindowPage: + title: "{ARTIST} - Rolling Window" + + windowConfigurationLine: >- + With a rolling window of {TIME_BEFORE} before a given date, and {TIME_AFTER} after, peeking ahead {PEEK}... + + contributionConfigurationLine: >- + Selecting {KIND} contributions from group {GROUP}... + + timeframeSelectionLine: + _: >- + There are {CONTRIBUTIONS} contributions, making {TIMEFRAMES} timeframes between {FIRST_DATE} and {LAST_DATE}. + none: >- + There aren't any matching contributions, or those which do aren't dated, so there are no timeframes. + + emptyTimeframeLine: >- + This timeframe is empty, since no contributions are in range. + + timeframeSelectionControl: + _: "{PREVIOUS} {TIMEFRAMES} {NEXT}" + previous: "← Previous" + next: "Next →" + + contributionKind: + artwork: "Artwork" + music: "Music" + flash: "Flash" + + contributionGroup: + all: "All groups" + group: "{GROUP}" + + timeframe: + months: "{INPUT} months" + # # artTagPage: # Stuff that's common between art tag pages. @@ -1588,6 +1748,42 @@ groupGalleryPage: infoLine: >- {TRACKS} across {ALBUMS}, totaling {TIME}. + albumViewSwitcher: + _: "Showing albums:" + + bySeries: "By series" + byDate: "By date" + + albumStyleSwitcher: + _: "Showing these releases:" + + album: "Albums" + single: "Singles" + + count: + all: "all {TOTAL}" + filtered: "{COUNT} of {TOTAL}" + none: "none at all" + + albumsByDate: + title: "All albums" + + albumSection: + caption: >- + {TRACKS} across {ALBUMS}. + + caption.withDate: >- + {TRACKS} across {ALBUMS}, released {DATE}. + + caption.withYear: >- + {TRACKS} across {ALBUMS}, released during {YEAR}. + + caption.withYearRange: >- + {TRACKS} across {ALBUMS}, released {YEAR_RANGE}. + + caption.seriesAlbumsNotFromGroup: >- + Albums marked {MARKER} are part of {SERIES}, but not from {GROUP}. + # # listingIndex: # The listing index page shows all available listings on the wiki, @@ -1679,6 +1875,7 @@ listingPage: title: "Albums - by Tracks" title.short: "...by Tracks" item: "{ALBUM} ({TRACKS})" + item.single: "{ALBUM} ({TRACKS}—single)" # listAlbums.byDuration: # Lists albums by total duration of all tracks, longest to @@ -2115,6 +2312,23 @@ listingPage: title.withDate: "{ALBUM} ({DATE})" item: "{TRACK}" + # listTracks.needingLyrics: + # List tracks, chunked by album (which are sorted by date, + # falling back alphabetically) and in their usual track order, + # displaying only tracks which are marked as needing lyrics. + # The chunk titles also display the date each album was released, + # and tracks' own custom "Date First Released" fields are totally + # ignored. + + needingLyrics: + title: "Tracks - which need Lyrics" + title.short: "...which need Lyrics" + + chunk: + title: "{ALBUM}" + title.withDate: "{ALBUM} ({DATE})" + item: "{TRACK}" + other: # other.allSheetMusic: @@ -2326,15 +2540,14 @@ trackPage: backToTrack: "Return to track page" + singleAccent: "single" + track: _: "{TRACK}" withNumber: "{NUMBER}. {TRACK}" - chronology: - scope: - title: "Chronology links {SCOPE}" - wiki: "across this wiki" - album: "within this album" + needsLyrics: >- + This track has vocals, but there aren't lyrics available for it on this wiki, yet! socialEmbed: heading: "{ALBUM}" diff --git a/src/upd8.js b/src/upd8.js index a9929154..ae072d5a 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -42,8 +42,9 @@ import wrap from 'word-wrap'; import {mapAggregate, openAggregate, showAggregate} from '#aggregate'; import CacheableObject from '#cacheable-object'; -import {stringifyCache} from '#cli'; +import {formatDuration, stringifyCache} from '#cli'; import {displayCompositeCacheAnalysis} from '#composite'; +import * as html from '#html'; import find, {bindFind, getAllFindSpecs} from '#find'; import {processLanguageFile, watchLanguageFile, internalDefaultStringsFile} from '#language'; @@ -68,8 +69,9 @@ import { import { filterReferenceErrors, - reportDirectoryErrors, reportContentTextErrors, + reportDirectoryErrors, + reportOrphanedArtworks, } from '#data-checks'; import { @@ -116,7 +118,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); let COMMIT; try { COMMIT = execSync('git log --format="%h %B" -n 1 HEAD', {cwd: __dirname}).toString().trim(); -} catch (error) { +} catch { COMMIT = '(failed to detect)'; } @@ -175,6 +177,10 @@ async function main() { {...defaultStepStatus, name: `report directory errors`, for: ['verify']}, + reportOrphanedArtworks: + {...defaultStepStatus, name: `report orphaned artworks`, + for: ['verify']}, + filterReferenceErrors: {...defaultStepStatus, name: `filter reference errors`, for: ['verify']}, @@ -399,6 +405,11 @@ async function main() { type: 'flag', }, + 'skip-orphaned-artwork-validation': { + help: `Skips checking for internally orphaned artworks, which is a bad idea, unless you're debugging those in particular`, + type: 'flag', + }, + 'skip-reference-validation': { help: `Skips checking and reporting reference errors, which speeds up the build but may silently allow erroneous data to pass through`, type: 'flag', @@ -508,6 +519,11 @@ async function main() { type: 'flag', }, + 'skip-self-diagnosis': { + help: `Disable some runtime validation for the wiki's own code, which speeds up long builds, but may allow unpredicted corner cases to fail strangely and silently`, + type: 'flag', + }, + 'queue-size': { help: `Process more or fewer disk files at once to optimize performance or avoid I/O errors, unlimited if set to 0 (between 500 and 700 is usually a safe range for building HSMusic on Windows machines)\nDefaults to ${defaultQueueSize}`, type: 'value', @@ -646,7 +662,8 @@ async function main() { const thumbsOnly = cliOptions['thumbs-only'] ?? false; const noInput = cliOptions['no-input'] ?? false; - const showAggregateTraces = cliOptions['show-traces'] ?? false; + const skipSelfDiagnosis = cliOptions['skip-self-diagnosis'] ?? false; + const showTraces = cliOptions['show-traces'] ?? false; const precacheMode = cliOptions['precache-mode'] ?? 'common'; @@ -843,6 +860,16 @@ async function main() { }, }); + fallbackStep('reportOrphanedArtworks', { + default: 'perform', + cli: { + flag: 'skip-orphaned-artwork-validation', + negate: true, + warn: + `Skipping orphaned artwork validation. Hopefully you're debugging!`, + }, + }); + fallbackStep('filterReferenceErrors', { default: 'perform', cli: { @@ -1136,6 +1163,18 @@ async function main() { return false; } + if (skipSelfDiagnosis) { + logWarn`${'Skipping code self-diagnosis.'} (--skip-self-diagnosis provided)`; + logWarn`This build should run substantially faster, but corner cases`; + logWarn`not previously predicted may fail strangely and silently.`; + + html.disableSlotValidation(); + } + + if (!showTraces) { + html.disableTagTracing(); + } + Object.assign(stepStatusSummary.determineMediaCachePath, { status: STATUS_STARTED_NOT_DONE, timeStart: Date.now(), @@ -1314,7 +1353,7 @@ async function main() { const niceShowAggregate = (error, ...opts) => { showAggregate(error, { - showTraces: showAggregateTraces, + showTraces, pathToFileURL: (f) => path.relative(__dirname, fileURLToPath(f)), ...opts, }); @@ -1736,8 +1775,8 @@ async function main() { }); } - // Filter out any things with duplicate directories throughout the data, - // warning about them too. + // Check for things with duplicate directories throughout the data, + // and halt if any are found. if (stepStatusSummary.reportDirectoryErrors.status === STATUS_NOT_STARTED) { Object.assign(stepStatusSummary.reportDirectoryErrors, { @@ -1778,8 +1817,42 @@ async function main() { } } - // Filter out any reference errors throughout the data, warning about them - // too. + // Check for artwork objects which have been orphaned from their things, + // and halt if any are found. + + if (stepStatusSummary.reportOrphanedArtworks.status === STATUS_NOT_STARTED) { + Object.assign(stepStatusSummary.reportOrphanedArtworks, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); + + try { + reportOrphanedArtworks(wikiData, {getAllFindSpecs}); + + Object.assign(stepStatusSummary.reportOrphanedArtworks, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + memory: process.memoryUsage(), + }); + } catch (aggregate) { + if (!paragraph) console.log(''); + niceShowAggregate(aggregate); + + logError`Failed to initialize artwork data connections properly.`; + fileIssue(); + + Object.assign(stepStatusSummary.reportOrphanedArtworks, { + status: STATUS_FATAL_ERROR, + annotation: `orphaned artworks found`, + timeEnd: Date.now(), + memory: process.memoryUsage(), + }); + + return false; + } + } + + // Filter out any reference errors throughout the data, warning about these. if (stepStatusSummary.filterReferenceErrors.status === STATUS_NOT_STARTED) { Object.assign(stepStatusSummary.filterReferenceErrors, { @@ -2365,7 +2438,7 @@ async function main() { }); internalDefaultLanguage = internalDefaultLanguageWatcher.language; - } catch (_error) { + } catch { // No need to display the error here - it's already printed by // watchLanguageFile. errorLoadingInternalDefaultLanguage = true; @@ -3153,6 +3226,7 @@ async function main() { developersComment, languages, missingImagePaths, + niceShowAggregate, thumbsCache, urlSpec, urls, @@ -3215,7 +3289,7 @@ async function main() { } // TODO: isMain detection isn't consistent across platforms here -/* eslint-disable-next-line no-constant-condition */ +// eslint-disable-next-line no-constant-binary-expression if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmusic') { (async () => { let result; @@ -3309,23 +3383,6 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus })(); } -function formatDuration(timeDelta) { - const seconds = timeDelta / 1000; - - if (seconds > 90) { - const modSeconds = Math.floor(seconds % 60); - const minutes = Math.floor(seconds - seconds % 60) / 60; - return `${minutes}m${modSeconds}s`; - } - - if (seconds < 0.1) { - return 'instant'; - } - - const precision = (seconds > 1 ? 3 : 2); - return `${seconds.toPrecision(precision)}s`; -} - function showStepStatusSummary() { const longestNameLength = Math.max(... diff --git a/src/url-spec.js b/src/url-spec.js index 75cd8006..2e8b9fc1 100644 --- a/src/url-spec.js +++ b/src/url-spec.js @@ -1,6 +1,7 @@ // Exports defined here are re-exported through urls.js, // so they're generally imported from '#urls'. +import {readFileSync} from 'node:fs'; import {readFile} from 'node:fs/promises'; import * as path from 'node:path'; import {fileURLToPath} from 'node:url'; @@ -195,6 +196,24 @@ export async function processURLSpecFromFile(file) { error => annotateErrorWithFile(error, file)); } + return processURLSpecFromFileContents(file, contents); +} + +export function processURLSpecFromFileSync(file) { + let contents; + + try { + contents = readFileSync(file, 'utf-8'); + } catch (caughtError) { + throw annotateError( + new Error(`Failed to read URL spec file`, {cause: caughtError}), + error => annotateErrorWithFile(error, file)); + } + + return processURLSpecFromFileContents(file, contents); +} + +function processURLSpecFromFileContents(file, contents) { let sourceSpec; let parseLanguage; diff --git a/src/urls-default.yaml b/src/urls-default.yaml index c3bf89eb..667f7d8b 100644 --- a/src/urls-default.yaml +++ b/src/urls-default.yaml @@ -11,7 +11,7 @@ yamlAliases: # part of a build. This is so that multiple builds of a wiki can coexist # served from the same server / file system root: older builds' HTML files # refer to earlier values of STATIC_VERSION, avoiding name collisions. - - &staticVersion 4p1 + - &staticVersion 5p2 data: prefix: 'data/' @@ -41,6 +41,7 @@ localized: artist: 'artist/<>/' artistGallery: 'artist/<>/gallery/' + artistRollingWindow: 'artist/<>/rolling-window/' commentaryIndex: 'commentary/' diff --git a/src/urls.js b/src/urls.js index 5e334c1e..b51ea459 100644 --- a/src/urls.js +++ b/src/urls.js @@ -129,6 +129,9 @@ export function generateURLs(urlSpec) { if (template === null) { // Self-diagnose, brutally. + // TODO: This variable isn't used, and has never been used, + // but it might have been *meant* to be used? + // eslint-disable-next-line no-unused-vars const otherTemplateKey = (device ? 'posix' : 'device'); const {value: {[templateKey]: otherTemplate}} = @@ -283,9 +286,14 @@ export function getURLsFrom({ to = targetFullKey; } - return ( - subdirectoryPrefix + - urls.from(from).to(to, ...args)); + const toResult = + urls.from(from).to(to, ...args); + + if (getOrigin(toResult)) { + return toResult; + } else { + return subdirectoryPrefix + toResult; + } }; } diff --git a/src/validators.js b/src/validators.js index 3b23e8f6..63268ded 100644 --- a/src/validators.js +++ b/src/validators.js @@ -3,8 +3,12 @@ import {inspect as nodeInspect} from 'node:util'; import {openAggregate, withAggregate} from '#aggregate'; import {colors, ENABLE_COLOR} from '#cli'; import {cut, empty, matchMultiline, typeAppearance} from '#sugar'; -import {commentaryRegexCaseInsensitive, commentaryRegexCaseSensitiveOneShot} - from '#wiki-data'; + +import { + commentaryRegexCaseInsensitive, + commentaryRegexCaseSensitiveOneShot, + multipleLyricsDetectionRegex, +} from '#wiki-data'; function inspect(value) { return nodeInspect(value, {colors: ENABLE_COLOR}); @@ -288,69 +292,108 @@ export function isColor(color) { throw new TypeError(`Unknown color format`); } -export function isCommentary(commentaryText) { - isContentString(commentaryText); - - const rawMatches = - Array.from(commentaryText.matchAll(commentaryRegexCaseInsensitive)); +export function validateContentEntries({ + headingPhrase, + entryPhrase, - if (empty(rawMatches)) { - throw new TypeError(`Expected at least one commentary heading`); - } + caseInsensitiveRegex, + caseSensitiveOneShotRegex, +}) { + return content => { + isContentString(content); - const niceMatches = - rawMatches.map(match => ({ - position: match.index, - length: match[0].length, - })); + const rawMatches = + Array.from(content.matchAll(caseInsensitiveRegex)); - validateArrayItems(({position, length}, index) => { - if (index === 0 && position > 0) { - throw new TypeError(`Expected first commentary heading to be at top`); + if (empty(rawMatches)) { + throw new TypeError(`Expected at least one ${headingPhrase}`); } - const ownInput = commentaryText.slice(position, position + length); - const restOfInput = commentaryText.slice(position + length); + const niceMatches = + rawMatches.map(match => ({ + position: match.index, + length: match[0].length, + })); - const upToNextLineBreak = - (restOfInput.includes('\n') - ? restOfInput.slice(0, restOfInput.indexOf('\n')) - : restOfInput); + validateArrayItems(({position, length}, index) => { + if (index === 0 && position > 0) { + throw new TypeError(`Expected first ${headingPhrase} to be at top`); + } - if (/\S/.test(upToNextLineBreak)) { - throw new TypeError( - `Expected commentary heading to occupy entire line, got extra text:\n` + - `${colors.green(`"${cut(ownInput, 40)}"`)} (<- heading)\n` + - `(extra on same line ->) ${colors.red(`"${cut(upToNextLineBreak, 30)}"`)}\n` + - `(Check for missing "|-" in YAML, or a misshapen annotation)`); - } + const ownInput = content.slice(position, position + length); + const restOfInput = content.slice(position + length); - if (!commentaryRegexCaseSensitiveOneShot.test(ownInput)) { - throw new TypeError( - `Miscapitalization in commentary heading:\n` + - `${colors.red(`"${cut(ownInput, 60)}"`)}\n` + - `(Check for ${colors.red(`"<I>"`)} instead of ${colors.green(`"<i>"`)})`); - } + const upToNextLineBreak = + (restOfInput.includes('\n') + ? restOfInput.slice(0, restOfInput.indexOf('\n')) + : restOfInput); - const nextHeading = - (index === niceMatches.length - 1 - ? commentaryText.length - : niceMatches[index + 1].position); + if (/\S/.test(upToNextLineBreak)) { + throw new TypeError( + `Expected ${headingPhrase} to occupy entire line, got extra text:\n` + + `${colors.green(`"${cut(ownInput, 40)}"`)} (<- heading)\n` + + `(extra on same line ->) ${colors.red(`"${cut(upToNextLineBreak, 30)}"`)}\n` + + `(Check for missing "|-" in YAML, or a misshapen annotation)`); + } - const upToNextHeading = - commentaryText.slice(position + length, nextHeading); + if (!caseSensitiveOneShotRegex.test(ownInput)) { + throw new TypeError( + `Miscapitalization in ${headingPhrase}:\n` + + `${colors.red(`"${cut(ownInput, 60)}"`)}\n` + + `(Check for ${colors.red(`"<I>"`)} instead of ${colors.green(`"<i>"`)})`); + } - if (!/\S/.test(upToNextHeading)) { - throw new TypeError( - `Expected commentary entry to have body text, only got a heading`); - } + const nextHeading = + (index === niceMatches.length - 1 + ? content.length + : niceMatches[index + 1].position); + + const upToNextHeading = + content.slice(position + length, nextHeading); + + if (!/\S/.test(upToNextHeading)) { + throw new TypeError( + `Expected ${entryPhrase} to have body text, only got a heading`); + } + + return true; + })(niceMatches); return true; - })(niceMatches); + }; +} + +export const isCommentary = + validateContentEntries({ + headingPhrase: `commentary heading`, + entryPhrase: `commentary entry`, + + caseInsensitiveRegex: commentaryRegexCaseInsensitive, + caseSensitiveOneShotRegex: commentaryRegexCaseSensitiveOneShot, + }); + +export function isOldStyleLyrics(content) { + isContentString(content); + + if (multipleLyricsDetectionRegex.test(content)) { + throw new TypeError( + `Expected old-style lyrics block not to include "<i> ... :</i>" at start of any line`); + } return true; } +export const isLyrics = + anyOf( + isOldStyleLyrics, + validateContentEntries({ + headingPhrase: `lyrics heading`, + entryPhrase: `lyrics entry`, + + caseInsensitiveRegex: commentaryRegexCaseInsensitive, + caseSensitiveOneShotRegex: commentaryRegexCaseSensitiveOneShot, + })); + const isArtistRef = validateReference('artist'); export function validateProperties(spec) { @@ -695,14 +738,6 @@ export const isContributionPreset = validateProperties({ export const isContributionPresetList = validateArrayItems(isContributionPreset); -export const isAdditionalFile = validateProperties({ - title: isName, - description: optional(isContentString), - files: optional(validateArrayItems(isString)), -}); - -export const isAdditionalFileList = validateArrayItems(isAdditionalFile); - export const isTrackSection = validateProperties({ name: optional(isName), color: optional(isColor), @@ -713,17 +748,6 @@ export const isTrackSection = validateProperties({ export const isTrackSectionList = validateArrayItems(isTrackSection); -export const isSeries = validateProperties({ - name: isName, - description: optional(isContentString), - albums: optional(validateReferenceList('album')), - - showAlbumArtists: - optional(is('all', 'differing', 'none')), -}); - -export const isSeriesList = validateArrayItems(isSeries); - export const isWallpaperPart = validateProperties({ asset: optional(isString), style: optional(isString), @@ -818,9 +842,11 @@ export function validateReference(type) { type.map(type => `"${type}:"`).join(', ') + `, got "${typePart}:"`); } - } else if (typePart !== type) { - throw new TypeError( - `Expected ref to begin with "${type}:", got "${typePart}:"`); + } else if (type) { + if (typePart !== type) { + throw new TypeError( + `Expected ref to begin with "${type}:", got "${typePart}:"`); + } } isDirectory(directoryPart); @@ -944,13 +970,6 @@ export function validateWikiData({ }; } -export const isAdditionalName = validateProperties({ - name: isContentString, - annotation: optional(isContentString), -}); - -export const isAdditionalNameList = validateArrayItems(isAdditionalName); - // Compositional utilities export function anyOf(...validators) { diff --git a/src/web-routes.js b/src/web-routes.js index b93607d6..a7115bbd 100644 --- a/src/web-routes.js +++ b/src/web-routes.js @@ -1,11 +1,13 @@ -import {readdir} from 'node:fs/promises'; +import {readdir, stat} from 'node:fs/promises'; import * as path from 'node:path'; import {fileURLToPath} from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +/* eslint-disable no-unused-vars */ const codeSrcPath = __dirname; const codeRootPath = path.resolve(codeSrcPath, '..'); +/* eslint-enable no-unused-vars */ function getNodeDependencyRootPath(dependencyName) { return ( @@ -106,6 +108,21 @@ export async function identifyDynamicWebRoutes({ ]), () => { + const from = + path.resolve(path.join(mediaPath, 'favicon.ico')); + + return stat(from).then( + // {statically: 'copy'} is not workable for individual files + // at the moment, so this remains a symlink. + () => [{ + from, + to: ['shared.path', 'favicon.ico'], + statically: 'symlink', + }], + () => []); + }, + + () => { if (!wikiCachePath) return []; const from = diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index d55ab215..afbf8b2f 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -24,6 +24,7 @@ export function bindUtilities({ language, languages, missingImagePaths, + niceShowAggregate, pagePath, pagePathStringFromRoot, thumbsCache, @@ -42,6 +43,7 @@ export function bindUtilities({ language, languages, missingImagePaths, + niceShowAggregate, pagePath, pagePathStringFromRoot, thumb, diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index ecb9df21..5dece8d0 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -253,7 +253,7 @@ export async function go({ let url; try { url = new URL(request.url, `http://${request.headers.host}`); - } catch (error) { + } catch { response.writeHead(500, contentTypePlain); response.end('Failed to parse request URL\n'); return; @@ -300,7 +300,7 @@ export async function go({ let filePath; try { filePath = path.resolve(localDirectory, decodeURI(safePath.split('/').join(path.sep))); - } catch (error) { + } catch { response.writeHead(404, contentTypePlain); response.end(`File not found for: ${safePath}`); console.log(`${requestHead} [404] ${pathname}`); diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index 2baed816..89450fc2 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -1,14 +1,7 @@ import * as path from 'node:path'; -import { - copyFile, - cp, - mkdir, - stat, - symlink, - writeFile, - unlink, -} from 'node:fs/promises'; +import {cp, mkdir, readFile, stat, symlink, writeFile, unlink} + from 'node:fs/promises'; import {rimraf} from 'rimraf'; @@ -95,6 +88,11 @@ export function getCLIOptions() { type: 'value', }, + 'paths': { + help: `Skip rest and build only pages matching paths in this text file`, + type: 'value', + }, + // NOT for neatly ena8ling or disa8ling specific features of the site! // This is only in charge of what general groups of files to write. // They're here to make development quicker when you're only working @@ -115,8 +113,6 @@ export async function go({ universalUtilities, - mediaPath, - defaultLanguage, languages, urls, @@ -128,6 +124,7 @@ export async function go({ const outputPath = cliOptions['out-path'] || process.env.HSMUSIC_OUT; const appendIndexHTML = cliOptions['append-index-html'] ?? false; const writeOneLanguage = cliOptions['lang'] ?? null; + const pathsFromFile = cliOptions['paths'] ?? null; if (!outputPath) { logError`Expected ${'--out-path'} option or ${'HSMUSIC_OUT'} to be set`; @@ -147,6 +144,36 @@ export async function go({ logInfo`Writing all languages.`; } + let filterPaths = null; + if (pathsFromFile) { + let pathsText; + try { + pathsText = await readFile(pathsFromFile, 'utf8'); + } catch (error) { + logError`Failed to read file specified in ${'--paths'}:`; + logError`${error.code}: ${pathsFromFile}`; + return false; + } + + filterPaths = pathsText.split('\n').filter(Boolean); + + if (empty(filterPaths)) { + logWarn`Specified to build only paths in file ${'--paths'}:`; + logWarn`${pathsFromFile}`; + logWarn`But this file is totally empty...`; + } + + if (filterPaths.some(path => !path.startsWith('/'))) { + logError`All lines in ${'--paths'} file should start with slash ('${'/'}')`; + logError`These lines don't:`; + console.error(filterPaths.filter(path => !path.startsWith('/')).join('\n')); + logError`Please check file contents, or specified path, and try again.`; + return false; + } + + logInfo`Writing ${filterPaths.length} paths specified in: ${pathsFromFile} (${'--paths'})`; + } + const selectedPageFlags = Object.keys(cliOptions) .filter(key => pageFlags.includes(key)); @@ -166,11 +193,6 @@ export async function go({ }); if (writeAll) { - await writeFavicon({ - mediaPath, - outputPath, - }); - await writeSharedFilesAndPages({ outputPath, randomLinkDataJSON: generateRandomLinkDataJSON({wikiData}), @@ -225,6 +247,27 @@ export async function go({ // TODO: Validate each pathsForTargets entry } + if (!empty(filterPaths)) { + paths = + paths.filter(path => + filterPaths.includes( + (path.type === 'page' + ? '/' + + getPagePathname({ + baseDirectory: '', + pagePath: path.path, + urls, + }) + : path.type === 'redirect' + ? '/' + + getPagePathname({ + baseDirectory: '', + pagePath: path.fromPath, + urls, + }) + : null))); + } + paths = paths.filter(path => path.condition?.() ?? true); @@ -595,30 +638,6 @@ async function writeWebRouteCopies({ } } -async function writeFavicon({ - mediaPath, - outputPath, -}) { - const faviconFile = 'favicon.ico'; - - try { - await stat(path.join(mediaPath, faviconFile)); - } catch (error) { - return; - } - - try { - await copyFile( - path.join(mediaPath, faviconFile), - path.join(outputPath, faviconFile)); - } catch (error) { - logWarn`Failed to copy favicon! ${error.message}`; - return; - } - - logInfo`Copied favicon to site root.`; -} - async function writeSharedFilesAndPages({ outputPath, randomLinkDataJSON, diff --git a/tap-snapshots/test/snapshot/generateAlbumAdditionalFilesList.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumAdditionalFilesList.js.test.cjs deleted file mode 100644 index 4f09569d..00000000 --- a/tap-snapshots/test/snapshot/generateAlbumAdditionalFilesList.js.test.cjs +++ /dev/null @@ -1,56 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/generateAlbumAdditionalFilesList.js > TAP > generateAlbumAdditionalFilesList (snapshot) > basic behavior 1`] = ` -<ul class="additional-files-list"> - <li> - <details> - <summary><span><span class="group-name">SBURB Wallpaper</span></span></summary> - <ul> - <li><a href="media/album-additional/exciting-album/sburbwp_1280x1024.jpg">sburbwp_1280x1024.jpg</a></li> - <li><a href="media/album-additional/exciting-album/sburbwp_1440x900.jpg">sburbwp_1440x900.jpg</a></li> - <li><a href="media/album-additional/exciting-album/sburbwp_1920x1080.jpg">sburbwp_1920x1080.jpg</a></li> - </ul> - </details> - </li> - <li> - <details> - <summary><span><span class="group-name">Fake Section</span></span></summary> - <ul> - <li class="entry-description">No sizes for these files</li> - <li><a href="media/album-additional/exciting-album/oops.mp3">oops.mp3</a></li> - <li><a href="media/album-additional/exciting-album/Internet%20Explorer.gif">Internet Explorer.gif</a></li> - <li><a href="media/album-additional/exciting-album/daisy.mp3">daisy.mp3</a></li> - </ul> - </details> - </li> - <li> - <details open> - <summary><span><span class="group-name">Empty Section</span></span></summary> - <ul> - <li class="entry-description">These files haven't been made available.</li> - <li>There are no files available or listed for this entry.</li> - </ul> - </details> - </li> - <li> - <details> - <summary><span><span class="group-name">Alternate Covers</span></span></summary> - <ul> - <li class="entry-description">This is just an example description.</li> - <li><a href="media/album-additional/exciting-album/Homestuck_Vol4_alt1.jpg">Homestuck_Vol4_alt1.jpg</a></li> - <li><a href="media/album-additional/exciting-album/Homestuck_Vol4_alt2.jpg">Homestuck_Vol4_alt2.jpg</a></li> - <li><a href="media/album-additional/exciting-album/Homestuck_Vol4_alt3.jpg">Homestuck_Vol4_alt3.jpg</a></li> - </ul> - </details> - </li> -</ul> -` - -exports[`test/snapshot/generateAlbumAdditionalFilesList.js > TAP > generateAlbumAdditionalFilesList (snapshot) > no additional files 1`] = ` - -` diff --git a/tap-snapshots/test/snapshot/generateAlbumBanner.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumBanner.js.test.cjs deleted file mode 100644 index b23df5c5..00000000 --- a/tap-snapshots/test/snapshot/generateAlbumBanner.js.test.cjs +++ /dev/null @@ -1,18 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/generateAlbumBanner.js > TAP > generateAlbumBanner (snapshot) > basic behavior 1`] = ` -<div id="banner"><img src="media/album-art/cool-album/banner.png" width="800" height="200" alt="album banner"></div> -` - -exports[`test/snapshot/generateAlbumBanner.js > TAP > generateAlbumBanner (snapshot) > no banner 1`] = ` - -` - -exports[`test/snapshot/generateAlbumBanner.js > TAP > generateAlbumBanner (snapshot) > no dimensions 1`] = ` -<div id="banner"><img src="media/album-art/cool-album/banner.png" width="1100" height="200" alt="album banner"></div> -` diff --git a/tap-snapshots/test/snapshot/generateAlbumReleaseInfo.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumReleaseInfo.js.test.cjs deleted file mode 100644 index 14cce64e..00000000 --- a/tap-snapshots/test/snapshot/generateAlbumReleaseInfo.js.test.cjs +++ /dev/null @@ -1,41 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/generateAlbumReleaseInfo.js > TAP > generateAlbumReleaseInfo (snapshot) > basic behavior 1`] = ` -<p> - By <span class="contribution nowrap"><a href="artist/toby-fox/">Toby Fox</a> (music probably)</span> and <span class="contribution nowrap"><span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="artist/tensei/">Tensei</a></span><span class="tooltip contribution-tooltip"><span class="tooltip-content"><a class="external-link" href="https://tenseimusic.bandcamp.com/"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-bandcamp"></use></svg></span> - <span class="external-handle">tenseimusic</span> - </a> - <span class="external-platform">Bandcamp</span></span></span></span> (hot jams)</span>. - <br> - Cover art by <span class="contribution nowrap"><a href="artist/hb/">Hanni Brosh</a></span>. - <br> - Wallpaper art by <span class="contribution nowrap"><a href="artist/hb/">Hanni Brosh</a></span> and <span class="contribution nowrap"><a href="artist/niklink/">Niklink</a> (edits)</span>. - <br> - Banner art by <span class="contribution nowrap"><a href="artist/hb/">Hanni Brosh</a></span> and <span class="contribution nowrap"><a href="artist/niklink/">Niklink</a> (edits)</span>. - <br> - Released 3/14/2011. - <br> - Art released 4/1/1991. - <br> - Duration: ~10:25. -</p> -<p>Listen on <a class="external-link" href="https://homestuck.bandcamp.com/album/alterniabound-with-alternia">Bandcamp</a>, <a class="external-link" href="https://www.youtube.com/playlist?list=PLnVpmehyaOFZWO9QOZmD6A3TIK0wZ6xE2">YouTube (playlist)</a>, or <a class="external-link" href="https://www.youtube.com/watch?v=HO5V2uogkYc">YouTube (full album)</a>.</p> -` - -exports[`test/snapshot/generateAlbumReleaseInfo.js > TAP > generateAlbumReleaseInfo (snapshot) > equal cover art date 1`] = ` -<p>Released 4/12/2020.</p> -` - -exports[`test/snapshot/generateAlbumReleaseInfo.js > TAP > generateAlbumReleaseInfo (snapshot) > reduced details 1`] = ` - -` - -exports[`test/snapshot/generateAlbumReleaseInfo.js > TAP > generateAlbumReleaseInfo (snapshot) > URLs only 1`] = ` -<p>Listen on <a class="external-link" href="https://homestuck.bandcamp.com/foo">Bandcamp</a> or <a class="external-link" href="https://soundcloud.com/bar">SoundCloud</a>.</p> -` diff --git a/tap-snapshots/test/snapshot/generateAlbumSecondaryNav.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumSecondaryNav.js.test.cjs deleted file mode 100644 index 145dd0f7..00000000 --- a/tap-snapshots/test/snapshot/generateAlbumSecondaryNav.js.test.cjs +++ /dev/null @@ -1,34 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/generateAlbumSecondaryNav.js > TAP > generateAlbumSecondaryNav (snapshot) > basic behavior, mode: album 1`] = ` -<nav id="secondary-nav" class="album-secondary-nav with-previous-next"> - <span class="group-with-series"> - <span style="--primary-color: #abcdef"><a href="group/vcg/">VCG</a> (<span class="dot-switcher interpage"><span><a title="First" href="album/first/">Previous</a></span><span><a title="Last" href="album/last/">Next</a></span></span>)</span> - <span class="series-nav-link" style="--primary-color: #abcdef"><a class="series" href="group/vcg/">Series</a> (<span class="dot-switcher interpage"><span><a class="inert-previous-next-link">Previous</a></span><span><a title="Last" href="album/last/">Next</a></span></span>)</span> - </span> - <span style="--primary-color: #123456"><a href="group/bepis/">Bepis</a> (<span class="dot-switcher interpage"><span><a class="inert-previous-next-link">Previous</a></span><span><a title="Second" href="album/second/">Next</a></span></span>)</span> -</nav> -` - -exports[`test/snapshot/generateAlbumSecondaryNav.js > TAP > generateAlbumSecondaryNav (snapshot) > basic behavior, mode: track 1`] = ` -<nav id="secondary-nav" class="album-secondary-nav"> - <span class="dot-switcher"><span> - <span class="group-with-series"> - <span style="--primary-color: #abcdef"><a href="group/vcg/">VCG</a></span> - <span class="series-nav-link" style="--primary-color: #abcdef"><a class="series" href="group/vcg/">Series</a></span> - </span> - </span><span><span style="--primary-color: #123456"><a href="group/bepis/">Bepis</a></span></span></span> -</nav> -` - -exports[`test/snapshot/generateAlbumSecondaryNav.js > TAP > generateAlbumSecondaryNav (snapshot) > dateless album in mixed group 1`] = ` -<nav id="secondary-nav" class="album-secondary-nav with-previous-next"> - <span style="--primary-color: #abcdef"><a href="group/vcg/">VCG</a></span> - <span style="--primary-color: #123456"><a href="group/bepis/">Bepis</a></span> -</nav> -` diff --git a/tap-snapshots/test/snapshot/generateAlbumSidebarGroupBox.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumSidebarGroupBox.js.test.cjs deleted file mode 100644 index 6502f719..00000000 --- a/tap-snapshots/test/snapshot/generateAlbumSidebarGroupBox.js.test.cjs +++ /dev/null @@ -1,31 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/generateAlbumSidebarGroupBox.js > TAP > generateAlbumSidebarGroupBox (snapshot) > basic behavior, mode: album 1`] = ` -<div class="sidebar collapsible individual-group-sidebar-box"> - <h1><a href="group/vcg/">VCG</a></h1> - Very cool group. - <p>Visit on <a class="external-link" href="https://vcg.bandcamp.com/">Bandcamp</a> or <a class="external-link" href="https://youtube.com/@vcg">YouTube</a>.</p> - <p class="group-chronology-link">Next: <a href="album/last/">Last</a></p> - <p class="group-chronology-link">Previous: <a href="album/first/">First</a></p> -</div> -` - -exports[`test/snapshot/generateAlbumSidebarGroupBox.js > TAP > generateAlbumSidebarGroupBox (snapshot) > basic behavior, mode: track 1`] = ` -<div class="sidebar collapsible individual-group-sidebar-box"> - <h1><a href="group/vcg/">VCG</a></h1> - <p>Visit on <a class="external-link" href="https://vcg.bandcamp.com/">Bandcamp</a> or <a class="external-link" href="https://youtube.com/@vcg">YouTube</a>.</p> -</div> -` - -exports[`test/snapshot/generateAlbumSidebarGroupBox.js > TAP > generateAlbumSidebarGroupBox (snapshot) > dateless album in mixed group 1`] = ` -<div class="sidebar collapsible individual-group-sidebar-box"> - <h1><a href="group/vcg/">VCG</a></h1> - Very cool group. - <p>Visit on <a class="external-link" href="https://vcg.bandcamp.com/">Bandcamp</a> or <a class="external-link" href="https://youtube.com/@vcg">YouTube</a>.</p> -</div> -` diff --git a/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs deleted file mode 100644 index 40a8017d..00000000 --- a/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs +++ /dev/null @@ -1,185 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > basic behavior, default track section 1`] = ` -<ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li>[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - <li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li> -</ul> -` - -exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > basic behavior, with descriptions 1`] = ` -<dl class="album-group-list"> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">First section: (~1:00)</span> - <template class="content-heading-sticky-title">First section:</template> - </dt> - <dd> - <blockquote><p>Why yes!</p></blockquote> - <ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li>[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - </ul> - </dd> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">Second section:</span> - <template class="content-heading-sticky-title">Second section:</template> - </dt> - <dd> - <blockquote><p>How <em>contentful,</em> this is.</p></blockquote> - <ul><li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li></ul> - </dd> -</dl> -` - -exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > basic behavior, with track sections 1`] = ` -<dl class="album-group-list"> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">First section: (~1:00)</span> - <template class="content-heading-sticky-title">First section:</template> - </dt> - <dd> - <ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li>[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - </ul> - </dd> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">Second section:</span> - <template class="content-heading-sticky-title">Second section:</template> - </dt> - <dd><ul><li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li></ul></dd> -</dl> -` - -exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > collapseDurationScope: album 1`] = ` -<dl class="album-group-list"> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">First section: (~1:00)</span> - <template class="content-heading-sticky-title">First section:</template> - </dt> - <dd> - <ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li>[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - </ul> - </dd> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">Second section:</span> - <template class="content-heading-sticky-title">Second section:</template> - </dt> - <dd><ul><li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li></ul></dd> -</dl> -<ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li>[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - <li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li> -</ul> -<ul> - <li><a href="track/t2/">Track 2</a></li> - <li style="--primary-color: #ea2e83"><a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li> -</ul> -` - -exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > collapseDurationScope: never 1`] = ` -<dl class="album-group-list"> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">First section: (~1:00)</span> - <template class="content-heading-sticky-title">First section:</template> - </dt> - <dd> - <ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li>[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - </ul> - </dd> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">Second section:</span> - <template class="content-heading-sticky-title">Second section:</template> - </dt> - <dd><ul><li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li></ul></dd> -</dl> -<ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li>[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - <li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li> -</ul> -<ul> - <li>[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t2/">Track 2</a></li> - <li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li> -</ul> -` - -exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > collapseDurationScope: section 1`] = ` -<dl class="album-group-list"> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">First section: (~1:00)</span> - <template class="content-heading-sticky-title">First section:</template> - </dt> - <dd> - <ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li>[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - </ul> - </dd> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">Second section:</span> - <template class="content-heading-sticky-title">Second section:</template> - </dt> - <dd><ul><li style="--primary-color: #ea2e83"><a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li></ul></dd> -</dl> -<ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li>[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - <li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li> -</ul> -<ul> - <li><a href="track/t2/">Track 2</a></li> - <li style="--primary-color: #ea2e83"><a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li> -</ul> -` - -exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > collapseDurationScope: track 1`] = ` -<dl class="album-group-list"> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">First section: (~1:00)</span> - <template class="content-heading-sticky-title">First section:</template> - </dt> - <dd> - <ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li><a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - </ul> - </dd> - <dt class="content-heading" tabindex="0"> - <span class="content-heading-main-title">Second section:</span> - <template class="content-heading-sticky-title">Second section:</template> - </dt> - <dd><ul><li style="--primary-color: #ea2e83"><a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li></ul></dd> -</dl> -<ul> - <li>(0:20) <a href="track/t1/">Track 1</a></li> - <li><a href="track/t2/">Track 2</a></li> - <li>(0:40) <a href="track/t3/">Track 3</a></li> - <li style="--primary-color: #ea2e83"><a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li> -</ul> -<ul> - <li><a href="track/t2/">Track 2</a></li> - <li style="--primary-color: #ea2e83"><a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <span class="contribution nowrap"><a href="artist/apricot/">Apricot</a></span>,</span> <span class="chunkwrap"><span class="contribution nowrap"><a href="artist/peach/">Peach</a></span>,</span> <span class="chunkwrap">and <span class="contribution nowrap"><a href="artist/cerise/">Cerise</a></span></span></span></li> -</ul> -` diff --git a/tap-snapshots/test/snapshot/generateBanner.js.test.cjs b/tap-snapshots/test/snapshot/generateBanner.js.test.cjs deleted file mode 100644 index 870097c8..00000000 --- a/tap-snapshots/test/snapshot/generateBanner.js.test.cjs +++ /dev/null @@ -1,14 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/generateBanner.js > TAP > generateBanner (snapshot) > basic behavior 1`] = ` -<div id="banner"><img src="media/album-art/cool-album/banner.png" width="800" height="200" alt="Very cool banner art."></div> -` - -exports[`test/snapshot/generateBanner.js > TAP > generateBanner (snapshot) > no dimensions 1`] = ` -<div id="banner"><img src="media/album-art/cool-album/banner.png" width="1100" height="200"></div> -` diff --git a/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs b/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs deleted file mode 100644 index 098fe145..00000000 --- a/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs +++ /dev/null @@ -1,36 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/generateTrackReleaseInfo.js > TAP > generateTrackReleaseInfo (snapshot) > basic behavior 1`] = ` -<p> - By <span class="contribution nowrap"><a href="artist/toby-fox/">Toby Fox</a></span>. - <br> - Released 11/29/2011. - <br> - Duration: 0:58. -</p> -<p>Listen on <a class="external-link" href="https://soundcloud.com/foo">SoundCloud</a> or <a class="external-link" href="https://youtube.com/watch?v=bar">YouTube</a>.</p> -` - -exports[`test/snapshot/generateTrackReleaseInfo.js > TAP > generateTrackReleaseInfo (snapshot) > cover artist contribs, non-unique 1`] = ` -<p>By <span class="contribution nowrap"><a href="artist/toby-fox/">Toby Fox</a></span>.</p> -<p>This wiki doesn't have any listening links for <i>Suspicious Track</i>.</p> -` - -exports[`test/snapshot/generateTrackReleaseInfo.js > TAP > generateTrackReleaseInfo (snapshot) > cover artist contribs, unique 1`] = ` -<p> - By <span class="contribution nowrap"><a href="artist/toby-fox/">Toby Fox</a></span>. - <br> - Cover art by <span class="contribution nowrap"><a href="artist/alpaca/">Alpaca</a> (🔥)</span>. -</p> -<p>This wiki doesn't have any listening links for <i>Suspicious Track</i>.</p> -` - -exports[`test/snapshot/generateTrackReleaseInfo.js > TAP > generateTrackReleaseInfo (snapshot) > reduced details 1`] = ` -<p>By <span class="contribution nowrap"><a href="artist/toby-fox/">Toby Fox</a></span>.</p> -<p>This wiki doesn't have any listening links for <i>Suspicious Track</i>.</p> -` diff --git a/tap-snapshots/test/snapshot/image.js.test.cjs b/tap-snapshots/test/snapshot/image.js.test.cjs deleted file mode 100644 index 283f4352..00000000 --- a/tap-snapshots/test/snapshot/image.js.test.cjs +++ /dev/null @@ -1,74 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/image.js > TAP > image (snapshot) > content warnings via tags 1`] = ` -<div class="image-container reveal"> - <div class="image-outer-area"> - <div class="image-inner-area"> - <img class="image" src="media/album-art/beyond-canon/cover.png"> - <span class="reveal-text-container"> - <span class="reveal-text"> - <img class="reveal-symbol" src="static/misc/warning.svg"> - <br> - <span class="reveal-warnings">too cool for school</span> - <br> - <span class="reveal-interaction">click to show</span> - </span> - </span> - </div> - </div> -</div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > dimensions 1`] = ` -<div class="image-container"><div class="image-outer-area"><div class="image-inner-area"><img class="image" width="600" height="400" src="foobar"></div></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > dimensions with square 1`] = ` -<div class="image-container square"><div class="image-outer-area square-content"><div class="image-inner-area"><img class="image" width="600" height="400" src="foobar"></div></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > lazy with square 1`] = ` -<noscript><div class="image-container square"><div class="image-outer-area square-content"><div class="image-inner-area"><img class="image" src="foobar"></div></div></div></noscript> -<div class="image-container square js-hide"><div class="image-outer-area square-content"><div class="image-inner-area"><img class="image lazy" data-original="foobar"></div></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > link with file size 1`] = ` -<div class="image-container has-link"><div class="image-outer-area"><a class="image-link" href="media/album-art/pingas/cover.png"><div class="image-inner-area"><img class="image" src="media/album-art/pingas/cover.png"></div></a></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > missing image path 1`] = ` -<div class="image-container placeholder-image"><div class="image-outer-area"><div class="image-inner-area"><div class="image-text-area">(This image file is missing)</div></div></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > missing image path w/ missingSourceContent 1`] = ` -<div class="image-container placeholder-image"><div class="image-outer-area"><div class="image-inner-area"><div class="image-text-area">Cover's missing, whoops</div></div></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > source missing 1`] = ` -<div class="image-container placeholder-image"><div class="image-outer-area"><div class="image-inner-area"><div class="image-text-area">Example of missing source message.</div></div></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > source via path 1`] = ` -<div class="image-container"><div class="image-outer-area"><div class="image-inner-area"><img class="image" src="media/album-art/beyond-canon/cover.png"></div></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > source via src 1`] = ` -<div class="image-container"><div class="image-outer-area"><div class="image-inner-area"><img class="image" src="https://example.com/bananas.gif"></div></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > square 1`] = ` -<div class="image-container square"><div class="image-outer-area square-content"><div class="image-inner-area"><img class="image" src="foobar"></div></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > thumb requested but source is gif 1`] = ` -<div class="image-container"><div class="image-outer-area"><div class="image-inner-area"><img class="image" src="media/flash-art/5426.gif"></div></div></div> -` - -exports[`test/snapshot/image.js > TAP > image (snapshot) > thumbnail details 1`] = ` -<div class="image-container"><div class="image-outer-area"><div class="image-inner-area"><img class="image" data-original-length="1200" data-thumbs="voluminous:1200 middling:900 petite:20" src="thumb/album-art/beyond-canon/cover.voluminous.jpg"></div></div></div> -` diff --git a/tap-snapshots/test/snapshot/linkArtist.js.test.cjs b/tap-snapshots/test/snapshot/linkArtist.js.test.cjs deleted file mode 100644 index ae3677c3..00000000 --- a/tap-snapshots/test/snapshot/linkArtist.js.test.cjs +++ /dev/null @@ -1,14 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/linkArtist.js > TAP > linkArtist (snapshot) > basic behavior 1`] = ` -<a href="artist/toby-fox/">Toby Fox</a> -` - -exports[`test/snapshot/linkArtist.js > TAP > linkArtist (snapshot) > prefer short name 1`] = ` -<span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="artist/55gore/">55gore</a></span><span class="tooltip thing-name-tooltip"><span class="tooltip-content">ICCTTCMDMIROTMCWMWFTPFTDDOTARHPOESWGBTWEATFCWSEBTSSFOFG</span></span></span> -` diff --git a/tap-snapshots/test/snapshot/linkContribution.js.test.cjs b/tap-snapshots/test/snapshot/linkContribution.js.test.cjs deleted file mode 100644 index 0c44a85c..00000000 --- a/tap-snapshots/test/snapshot/linkContribution.js.test.cjs +++ /dev/null @@ -1,118 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > loads of links 1`] = ` -<span class="contribution nowrap"><span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="artist/lorem-ipsum-lover/">Lorem Ipsum Lover</a></span><span class="tooltip contribution-tooltip"><span class="tooltip-content"><a class="external-link" href="https://loremipsum.io"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">loremipsum.io</span> - </a> - <span class="external-platform">Other</span> - <a class="external-link" href="https://loremipsum.io/generator/"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">loremipsum.io</span> - </a> - <span class="external-platform">Other</span> - <a class="external-link" href="https://loremipsum.io/#meaning"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">loremipsum.io</span> - </a> - <span class="external-platform">Other</span> - <a class="external-link" href="https://loremipsum.io/#usage-and-examples"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">loremipsum.io</span> - </a> - <span class="external-platform">Other</span> - <a class="external-link" href="https://loremipsum.io/#controversy"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">loremipsum.io</span> - </a> - <span class="external-platform">Other</span> - <a class="external-link" href="https://loremipsum.io/#when-to-use-lorem-ipsum"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">loremipsum.io</span> - </a> - <span class="external-platform">Other</span> - <a class="external-link" href="https://loremipsum.io/#lorem-ipsum-all-the-things"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">loremipsum.io</span> - </a> - <span class="external-platform">Other</span> - <a class="external-link" href="https://loremipsum.io/#original-source"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">loremipsum.io</span> - </a> - <span class="external-platform">Other</span></span></span></span></span> -` - -exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > no accents 1`] = ` -<span class="contribution nowrap"><a href="artist/clark-powell/">Clark Powell</a></span> -<span class="contribution nowrap"><a href="artist/the-big-baddies/">Grounder & Scratch</a></span> -<span class="contribution nowrap"><a href="artist/toby-fox/">Toby Fox</a></span> -` - -exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > no preventWrapping 1`] = ` -<span class="contribution"><span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="artist/clark-powell/">Clark Powell</a></span><span class="tooltip contribution-tooltip"><span class="tooltip-content"><a class="external-link" href="https://soundcloud.com/plazmataz"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-soundcloud"></use></svg></span> - <span class="external-handle">plazmataz</span> - </a> - <span class="external-platform">SoundCloud</span></span></span></span></span> -<span class="contribution"><a href="artist/the-big-baddies/">Grounder & Scratch</a> (Snooping)</span> -<span class="contribution"><span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="artist/toby-fox/">Toby Fox</a></span><span class="tooltip contribution-tooltip"><span class="tooltip-content"><a class="external-link" href="https://tobyfox.bandcamp.com/"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-bandcamp"></use></svg></span> - <span class="external-handle">tobyfox</span> - </a> - <span class="external-platform">Bandcamp</span> - <a class="external-link" href="https://toby.fox/"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">toby.fox</span> - </a> - <span class="external-platform">Other</span></span></span></span> (Arrangement)</span> -` - -exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > only showAnnotation 1`] = ` -<span class="contribution nowrap"><a href="artist/clark-powell/">Clark Powell</a></span> -<span class="contribution nowrap"><a href="artist/the-big-baddies/">Grounder & Scratch</a> (Snooping)</span> -<span class="contribution nowrap"><a href="artist/toby-fox/">Toby Fox</a> (Arrangement)</span> -` - -exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > only showExternalLinks 1`] = ` -<span class="contribution nowrap"><span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="artist/clark-powell/">Clark Powell</a></span><span class="tooltip contribution-tooltip"><span class="tooltip-content"><a class="external-link" href="https://soundcloud.com/plazmataz"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-soundcloud"></use></svg></span> - <span class="external-handle">plazmataz</span> - </a> - <span class="external-platform">SoundCloud</span></span></span></span></span> -<span class="contribution nowrap"><a href="artist/the-big-baddies/">Grounder & Scratch</a></span> -<span class="contribution nowrap"><span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="artist/toby-fox/">Toby Fox</a></span><span class="tooltip contribution-tooltip"><span class="tooltip-content"><a class="external-link" href="https://tobyfox.bandcamp.com/"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-bandcamp"></use></svg></span> - <span class="external-handle">tobyfox</span> - </a> - <span class="external-platform">Bandcamp</span> - <a class="external-link" href="https://toby.fox/"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">toby.fox</span> - </a> - <span class="external-platform">Other</span></span></span></span></span> -` - -exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > showAnnotation & showExternalLinks 1`] = ` -<span class="contribution nowrap"><span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="artist/clark-powell/">Clark Powell</a></span><span class="tooltip contribution-tooltip"><span class="tooltip-content"><a class="external-link" href="https://soundcloud.com/plazmataz"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-soundcloud"></use></svg></span> - <span class="external-handle">plazmataz</span> - </a> - <span class="external-platform">SoundCloud</span></span></span></span></span> -<span class="contribution nowrap"><a href="artist/the-big-baddies/">Grounder & Scratch</a> (Snooping)</span> -<span class="contribution nowrap"><span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="artist/toby-fox/">Toby Fox</a></span><span class="tooltip contribution-tooltip"><span class="tooltip-content"><a class="external-link" href="https://tobyfox.bandcamp.com/"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-bandcamp"></use></svg></span> - <span class="external-handle">tobyfox</span> - </a> - <span class="external-platform">Bandcamp</span> - <a class="external-link" href="https://toby.fox/"> - <span class="external-icon"><svg><use href="static/misc/icons.svg#icon-globe"></use></svg></span> - <span class="external-handle">toby.fox</span> - </a> - <span class="external-platform">Other</span></span></span></span> (Arrangement)</span> -` diff --git a/tap-snapshots/test/snapshot/linkExternal.js.test.cjs b/tap-snapshots/test/snapshot/linkExternal.js.test.cjs deleted file mode 100644 index 03192e82..00000000 --- a/tap-snapshots/test/snapshot/linkExternal.js.test.cjs +++ /dev/null @@ -1,228 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: album, style: handle 1`] = ` -<a class="external-link" href="https://youtu.be/abc">YouTube</a> -<a class="external-link" href="https://youtube.com/watch?v=abc">YouTube</a> -<a class="external-link" href="https://youtube.com/Playlist?list=kweh">YouTube</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: album, style: platform 1`] = ` -<a class="external-link" href="https://youtu.be/abc">YouTube</a> -<a class="external-link" href="https://youtube.com/watch?v=abc">YouTube</a> -<a class="external-link" href="https://youtube.com/Playlist?list=kweh">YouTube</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: albumMultipleTracks, style: handle 1`] = ` -<a class="external-link" href="https://youtu.be/abc">YouTube (full album)</a> -<a class="external-link" href="https://youtube.com/watch?v=abc">YouTube (full album)</a> -<a class="external-link" href="https://youtube.com/Playlist?list=kweh">YouTube</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: albumMultipleTracks, style: platform 1`] = ` -<a class="external-link" href="https://youtu.be/abc">YouTube (full album)</a> -<a class="external-link" href="https://youtube.com/watch?v=abc">YouTube (full album)</a> -<a class="external-link" href="https://youtube.com/Playlist?list=kweh">YouTube</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: albumNoTracks, style: handle 1`] = ` -<a class="external-link" href="https://youtu.be/abc">YouTube</a> -<a class="external-link" href="https://youtube.com/watch?v=abc">YouTube</a> -<a class="external-link" href="https://youtube.com/Playlist?list=kweh">YouTube</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: albumNoTracks, style: platform 1`] = ` -<a class="external-link" href="https://youtu.be/abc">YouTube</a> -<a class="external-link" href="https://youtube.com/watch?v=abc">YouTube</a> -<a class="external-link" href="https://youtube.com/Playlist?list=kweh">YouTube</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: albumOneTrack, style: handle 1`] = ` -<a class="external-link" href="https://youtu.be/abc">YouTube</a> -<a class="external-link" href="https://youtube.com/watch?v=abc">YouTube</a> -<a class="external-link" href="https://youtube.com/Playlist?list=kweh">YouTube</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: albumOneTrack, style: platform 1`] = ` -<a class="external-link" href="https://youtu.be/abc">YouTube</a> -<a class="external-link" href="https://youtube.com/watch?v=abc">YouTube</a> -<a class="external-link" href="https://youtube.com/Playlist?list=kweh">YouTube</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: flash, style: handle 1`] = ` -<a class="external-link" href="https://www.bgreco.net/hsflash/002238.html">bgreco.net (high quality audio)</a> -<a class="external-link" href="https://homestuck.com/story/1234">Homestuck (page 1234)</a> -<a class="external-link" href="https://homestuck.com/story/pony">Homestuck (secret page)</a> -<a class="external-link" href="https://www.youtube.com/watch?v=wKgOp3Kg2wI">YouTube (on any device)</a> -<a class="external-link" href="https://youtu.be/IOcvkkklWmY">YouTube (on any device)</a> -<a class="external-link" href="https://some.external.site/foo/bar/">some.external.site</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: flash, style: platform 1`] = ` -<a class="external-link" href="https://www.bgreco.net/hsflash/002238.html">bgreco.net (high quality audio)</a> -<a class="external-link" href="https://homestuck.com/story/1234">Homestuck (page 1234)</a> -<a class="external-link" href="https://homestuck.com/story/pony">Homestuck (secret page)</a> -<a class="external-link" href="https://www.youtube.com/watch?v=wKgOp3Kg2wI">YouTube (on any device)</a> -<a class="external-link" href="https://youtu.be/IOcvkkklWmY">YouTube (on any device)</a> -<a class="external-link" href="https://some.external.site/foo/bar/">some.external.site</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: generic, style: handle 1`] = ` -<a class="external-link" href="https://music.apple.com/us/artist/system-of-a-down/462715">Apple Music</a> -<a class="external-link" href="https://www.artstation.com/eevaningtea">eevaningtea</a> -<a class="external-link" href="https://witnesstheabsurd.artstation.com/">witnesstheabsurd</a> -<a class="external-link" href="https://music.solatrus.com/">music.solatrus.com</a> -<a class="external-link" href="https://homestuck.bandcamp.com/">homestuck</a> -<a class="external-link" href="https://bsky.app/profile/jacobtheloofah.bsky.social">jacobtheloofah</a> -<a class="external-link" href="https://aliceflare.carrd.co">aliceflare</a> -<a class="external-link" href="https://bigchaslappa.carrd.co/">bigchaslappa</a> -<a class="external-link" href="https://cohost.org/cosmoptera">cosmoptera</a> -<a class="external-link" href="https://music.deconreconstruction.com/albums/catch-322">MUSIC@DCRC</a> -<a class="external-link" href="https://music.deconreconstruction.com/albums/catch-322?track=arcjecs-theme">MUSIC@DCRC</a> -<a class="external-link" href="https://www.deconreconstruction.com/">Deconreconstruction</a> -<a class="external-link" href="https://culdhira.deviantart.com">culdhira</a> -<a class="external-link" href="https://www.deviantart.com/chesswanderlust-sama">chesswanderlust-sama</a> -<a class="external-link" href="https://www.deviantart.com/shilloshilloh/art/Homestuck-Jake-English-268874606">DeviantArt</a> -<a class="external-link" href="https://www.facebook.com/DoomedCloud/">DoomedCloud</a> -<a class="external-link" href="https://www.facebook.com/pages/WoodenToaster/280642235307371">WoodenToaster</a> -<a class="external-link" href="https://www.facebook.com/Svixy/posts/400018786702633">Facebook</a> -<a class="external-link" href="https://mspaintadventures.fandom.com/wiki/Draconian_Dignitary">MSPA Wiki (Draconian Dignitary)</a> -<a class="external-link" href="https://mspaintadventures.fandom.com/wiki/">MSPA Wiki</a> -<a class="external-link" href="https://mspaintadventures.fandom.com/">MSPA Wiki</a> -<a class="external-link" href="https://community.fandom.com/">Fandom</a> -<a class="external-link" href="https://community.fandom.com/wiki/">Fandom</a> -<a class="external-link" href="https://community.fandom.com/wiki/Community_Central">Fandom</a> -<a class="external-link" href="https://gamebanana.com/members/2028092">GameBanana</a> -<a class="external-link" href="https://gamebanana.com/mods/459476">GameBanana</a> -<a class="external-link" href="https://homestuck.com/">Homestuck</a> -<a class="external-link" href="https://hsmusic.wiki/media/misc/archive/Firefly%20Cloud%20Remix.mp3">HSMusic (wiki archive)</a> -<a class="external-link" href="https://hsmusic.wiki/feedback/">HSMusic</a> -<a class="external-link" href="https://archive.org/details/a-life-well-lived">Internet Archive</a> -<a class="external-link" href="https://archive.org/details/VastError_Volume1/11+Renaissance.mp3">Internet Archive</a> -<a class="external-link" href="https://instagram.com/bass.and.noises">bass.and.noises</a> -<a class="external-link" href="https://www.instagram.com/levc_egm/">levc_egm</a> -<a class="external-link" href="https://tuyoki.itch.io/">tuyoki</a> -<a class="external-link" href="https://itch.io/profile/bravelittletoreador">bravelittletoreador</a> -<a class="external-link" href="https://ko-fi.com/gnaach">gnaach</a> -<a class="external-link" href="https://linktr.ee/bbpanzu">bbpanzu</a> -<a class="external-link" href="https://types.pl/">types.pl</a> -<a class="external-link" href="https://canwc.mspfa.com/">MSPFA</a> -<a class="external-link" href="https://mspfa.com/?s=12003&p=1045">MSPFA</a> -<a class="external-link" href="https://mspfa.com/user/?u=103334508819793669241">MSPFA</a> -<a class="external-link" href="https://wodaro.neocities.org">wodaro.neocities.org</a> -<a class="external-link" href="https://neomints.neocities.org/">neomints.neocities.org</a> -<a class="external-link" href="https://buzinkai.newgrounds.com/">buzinkai</a> -<a class="external-link" href="https://www.newgrounds.com/audio/listen/1256058">Newgrounds</a> -<a class="external-link" href="https://www.patreon.com/CecilyRenns">CecilyRenns</a> -<a class="external-link" href="https://www.poetryfoundation.org/poets/christina-rossetti">Poetry Foundation</a> -<a class="external-link" href="https://www.poetryfoundation.org/poems/45000/remember-56d224509b7ae">Poetry Foundation</a> -<a class="external-link" href="https://soundcloud.com/plazmataz">plazmataz</a> -<a class="external-link" href="https://soundcloud.com/worthikids/1-i-accidentally-broke-my">SoundCloud</a> -<a class="external-link" href="https://open.spotify.com/artist/63SNNpNOicDzG3LY82G4q3">Spotify</a> -<a class="external-link" href="https://open.spotify.com/album/0iHvPD8rM3hQa0qeVtPQ3t">Spotify</a> -<a class="external-link" href="https://open.spotify.com/track/6YEGQH32aAXb9vQQbBrPlw">Spotify</a> -<a class="external-link" href="https://www.tiktok.com/@richaadeb">richaadeb</a> -<a class="external-link" href="https://toyhou.se/ghastaboo">ghastaboo</a> -<a class="external-link" href="https://aeritus.tumblr.com/">aeritus</a> -<a class="external-link" href="https://vol5anthology.tumblr.com/post/159528808107/hey-everyone-its-413-and-that-means-we-have">vol5anthology</a> -<a class="external-link" href="https://www.tumblr.com/electricwestern">electricwestern</a> -<a class="external-link" href="https://www.tumblr.com/spellmynamewithabang/142767566733/happy-413-this-is-the-first-time-anyones-heard">Tumblr</a> -<a class="external-link" href="https://www.twitch.tv/ajhebard">ajhebard</a> -<a class="external-link" href="https://www.twitch.tv/vargskelethor/">vargskelethor/</a> -<a class="external-link" href="https://twitter.com/awkwarddoesart">awkwarddoesart</a> -<a class="external-link" href="https://twitter.com/purenonsens/">purenonsens</a> -<a class="external-link" href="https://twitter.com/circlejourney/status/1202265927183548416">Twitter</a> -<a class="external-link" href="https://web.archive.org/web/20120405160556/https://homestuck.bandcamp.com/album/colours-and-mayhem-universe-a">Wayback Machine</a> -<a class="external-link" href="https://web.archive.org/web/20160807111207/http://griffinspacejam.com:80/">Wayback Machine</a> -<a class="external-link" href="https://en.wikipedia.org/wiki/Haydn_Quartet_(vocal_ensemble)">Wikipedia</a> -<a class="external-link" href="https://youtube.com/@bani-chan8949">bani-chan8949</a> -<a class="external-link" href="https://www.youtube.com/@Razzie16">Razzie16</a> -<a class="external-link" href="https://www.youtube.com/channel/UCQXfvlKkpbOqEz4BepHqK7g">YouTube</a> -<a class="external-link" href="https://www.youtube.com/watch?v=6ekVnZm29kw">YouTube</a> -<a class="external-link" href="https://youtu.be/WBkC038wSio">YouTube</a> -<a class="external-link" href="https://www.youtube.com/playlist?list=PLy5UGIMKOXpONMExgI7lVYFwQa54QFp_H">YouTube</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > context: generic, style: platform 1`] = ` -<a class="external-link" href="https://music.apple.com/us/artist/system-of-a-down/462715">Apple Music</a> -<a class="external-link" href="https://www.artstation.com/eevaningtea">ArtStation</a> -<a class="external-link" href="https://witnesstheabsurd.artstation.com/">ArtStation</a> -<a class="external-link" href="https://music.solatrus.com/">Bandcamp (music.solatrus.com)</a> -<a class="external-link" href="https://homestuck.bandcamp.com/">Bandcamp</a> -<a class="external-link" href="https://bsky.app/profile/jacobtheloofah.bsky.social">Bluesky</a> -<a class="external-link" href="https://aliceflare.carrd.co">Carrd</a> -<a class="external-link" href="https://bigchaslappa.carrd.co/">Carrd</a> -<a class="external-link" href="https://cohost.org/cosmoptera">Cohost</a> -<a class="external-link" href="https://music.deconreconstruction.com/albums/catch-322">MUSIC@DCRC</a> -<a class="external-link" href="https://music.deconreconstruction.com/albums/catch-322?track=arcjecs-theme">MUSIC@DCRC</a> -<a class="external-link" href="https://www.deconreconstruction.com/">Deconreconstruction</a> -<a class="external-link" href="https://culdhira.deviantart.com">DeviantArt</a> -<a class="external-link" href="https://www.deviantart.com/chesswanderlust-sama">DeviantArt</a> -<a class="external-link" href="https://www.deviantart.com/shilloshilloh/art/Homestuck-Jake-English-268874606">DeviantArt</a> -<a class="external-link" href="https://www.facebook.com/DoomedCloud/">Facebook</a> -<a class="external-link" href="https://www.facebook.com/pages/WoodenToaster/280642235307371">Facebook</a> -<a class="external-link" href="https://www.facebook.com/Svixy/posts/400018786702633">Facebook</a> -<a class="external-link" href="https://mspaintadventures.fandom.com/wiki/Draconian_Dignitary">MSPA Wiki (Draconian Dignitary)</a> -<a class="external-link" href="https://mspaintadventures.fandom.com/wiki/">MSPA Wiki</a> -<a class="external-link" href="https://mspaintadventures.fandom.com/">MSPA Wiki</a> -<a class="external-link" href="https://community.fandom.com/">Fandom</a> -<a class="external-link" href="https://community.fandom.com/wiki/">Fandom</a> -<a class="external-link" href="https://community.fandom.com/wiki/Community_Central">Fandom</a> -<a class="external-link" href="https://gamebanana.com/members/2028092">GameBanana</a> -<a class="external-link" href="https://gamebanana.com/mods/459476">GameBanana</a> -<a class="external-link" href="https://homestuck.com/">Homestuck</a> -<a class="external-link" href="https://hsmusic.wiki/media/misc/archive/Firefly%20Cloud%20Remix.mp3">HSMusic (wiki archive)</a> -<a class="external-link" href="https://hsmusic.wiki/feedback/">HSMusic</a> -<a class="external-link" href="https://archive.org/details/a-life-well-lived">Internet Archive</a> -<a class="external-link" href="https://archive.org/details/VastError_Volume1/11+Renaissance.mp3">Internet Archive</a> -<a class="external-link" href="https://instagram.com/bass.and.noises">Instagram</a> -<a class="external-link" href="https://www.instagram.com/levc_egm/">Instagram</a> -<a class="external-link" href="https://tuyoki.itch.io/">itch.io</a> -<a class="external-link" href="https://itch.io/profile/bravelittletoreador">itch.io</a> -<a class="external-link" href="https://ko-fi.com/gnaach">Ko-fi</a> -<a class="external-link" href="https://linktr.ee/bbpanzu">Linktree</a> -<a class="external-link" href="https://types.pl/">Mastodon (types.pl)</a> -<a class="external-link" href="https://canwc.mspfa.com/">MSPFA</a> -<a class="external-link" href="https://mspfa.com/?s=12003&p=1045">MSPFA</a> -<a class="external-link" href="https://mspfa.com/user/?u=103334508819793669241">MSPFA</a> -<a class="external-link" href="https://wodaro.neocities.org">Neocities</a> -<a class="external-link" href="https://neomints.neocities.org/">Neocities</a> -<a class="external-link" href="https://buzinkai.newgrounds.com/">Newgrounds</a> -<a class="external-link" href="https://www.newgrounds.com/audio/listen/1256058">Newgrounds</a> -<a class="external-link" href="https://www.patreon.com/CecilyRenns">Patreon</a> -<a class="external-link" href="https://www.poetryfoundation.org/poets/christina-rossetti">Poetry Foundation</a> -<a class="external-link" href="https://www.poetryfoundation.org/poems/45000/remember-56d224509b7ae">Poetry Foundation</a> -<a class="external-link" href="https://soundcloud.com/plazmataz">SoundCloud</a> -<a class="external-link" href="https://soundcloud.com/worthikids/1-i-accidentally-broke-my">SoundCloud</a> -<a class="external-link" href="https://open.spotify.com/artist/63SNNpNOicDzG3LY82G4q3">Spotify</a> -<a class="external-link" href="https://open.spotify.com/album/0iHvPD8rM3hQa0qeVtPQ3t">Spotify</a> -<a class="external-link" href="https://open.spotify.com/track/6YEGQH32aAXb9vQQbBrPlw">Spotify</a> -<a class="external-link" href="https://www.tiktok.com/@richaadeb">TikTok</a> -<a class="external-link" href="https://toyhou.se/ghastaboo">Toyhouse</a> -<a class="external-link" href="https://aeritus.tumblr.com/">Tumblr</a> -<a class="external-link" href="https://vol5anthology.tumblr.com/post/159528808107/hey-everyone-its-413-and-that-means-we-have">Tumblr</a> -<a class="external-link" href="https://www.tumblr.com/electricwestern">Tumblr</a> -<a class="external-link" href="https://www.tumblr.com/spellmynamewithabang/142767566733/happy-413-this-is-the-first-time-anyones-heard">Tumblr</a> -<a class="external-link" href="https://www.twitch.tv/ajhebard">Twitch</a> -<a class="external-link" href="https://www.twitch.tv/vargskelethor/">Twitch</a> -<a class="external-link" href="https://twitter.com/awkwarddoesart">Twitter</a> -<a class="external-link" href="https://twitter.com/purenonsens/">Twitter</a> -<a class="external-link" href="https://twitter.com/circlejourney/status/1202265927183548416">Twitter</a> -<a class="external-link" href="https://web.archive.org/web/20120405160556/https://homestuck.bandcamp.com/album/colours-and-mayhem-universe-a">Wayback Machine</a> -<a class="external-link" href="https://web.archive.org/web/20160807111207/http://griffinspacejam.com:80/">Wayback Machine</a> -<a class="external-link" href="https://en.wikipedia.org/wiki/Haydn_Quartet_(vocal_ensemble)">Wikipedia</a> -<a class="external-link" href="https://youtube.com/@bani-chan8949">YouTube</a> -<a class="external-link" href="https://www.youtube.com/@Razzie16">YouTube</a> -<a class="external-link" href="https://www.youtube.com/channel/UCQXfvlKkpbOqEz4BepHqK7g">YouTube</a> -<a class="external-link" href="https://www.youtube.com/watch?v=6ekVnZm29kw">YouTube</a> -<a class="external-link" href="https://youtu.be/WBkC038wSio">YouTube</a> -<a class="external-link" href="https://www.youtube.com/playlist?list=PLy5UGIMKOXpONMExgI7lVYFwQa54QFp_H">YouTube</a> -` - -exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > unknown domain (arbitrary world wide web path) 1`] = ` -<a class="external-link" href="https://snoo.ping.as/usual/i/see/">snoo.ping.as</a> -` diff --git a/tap-snapshots/test/snapshot/linkTemplate.js.test.cjs b/tap-snapshots/test/snapshot/linkTemplate.js.test.cjs deleted file mode 100644 index b5acde91..00000000 --- a/tap-snapshots/test/snapshot/linkTemplate.js.test.cjs +++ /dev/null @@ -1,32 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/linkTemplate.js > TAP > linkTemplate (snapshot) > fill many slots 1`] = ` -<a class="dog" id="cat1" href="https://hsmusic.wiki/media/cool%20file.pdf#fooey">My Cool Link</a> -` - -exports[`test/snapshot/linkTemplate.js > TAP > linkTemplate (snapshot) > fill path slot & provide appendIndexHTML 1`] = ` -<a href="/c*lzone/myCoolPath/ham/pineapple/tomato/index.html">delish</a> -` - -exports[`test/snapshot/linkTemplate.js > TAP > linkTemplate (snapshot) > link in content 1`] = ` -<a href="#the-more-ye-know"> - Oh geez oh heck - There's a link in here!! - But here's <b>a normal tag.</b> - <div>Gotta keep them normal tags.</div> - <div>But not... NESTED LINKS, OOO.</div> -</a> -` - -exports[`test/snapshot/linkTemplate.js > TAP > linkTemplate (snapshot) > missing content 1`] = ` -<a href="banana">(Missing link content)</a> -` - -exports[`test/snapshot/linkTemplate.js > TAP > linkTemplate (snapshot) > special characters in path argument 1`] = ` -<a href="media/album-additional/homestuck-vol-1/Showtime%20(Piano%20Refrain)%20-%20%23xXxAwesomeSheetMusick%3FrxXx%23.pdf">Damn, that's some good sheet music</a> -` diff --git a/tap-snapshots/test/snapshot/linkThing.js.test.cjs b/tap-snapshots/test/snapshot/linkThing.js.test.cjs deleted file mode 100644 index 7dd8b003..00000000 --- a/tap-snapshots/test/snapshot/linkThing.js.test.cjs +++ /dev/null @@ -1,45 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > basic behavior 1`] = ` -<a style="--primary-color: #abcdef" href="track/foo/">Cool track!</a> -` - -exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > color 1`] = ` -<a href="track/showtime-piano-refrain/">Showtime (Piano Refrain)</a> -<a style="--primary-color: #38f43d" href="track/showtime-piano-refrain/">Showtime (Piano Refrain)</a> -<a style="--primary-color: #aaccff" href="track/showtime-piano-refrain/">Showtime (Piano Refrain)</a> -<span style="--primary-color: #aaccff" class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="track/showtime-piano-refrain/">Showtime (Piano Refrain)</a></span><span class="tooltip thing-name-tooltip"><span class="tooltip-content">Showtime (Piano Refrain)</span></span></span> -` - -exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > nested links in content stripped 1`] = ` -<a href="foo/"><b>Oooo! Very spooky.</b></a> -` - -exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > preferShortName 1`] = ` -<span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="tag/five-oceanfalls/">Five</a></span><span class="tooltip thing-name-tooltip"><span class="tooltip-content">Five (Oceanfalls)</span></span></span> -` - -exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > tags in name escaped 1`] = ` -<a href="track/foo/"><a href="SNOOPING">AS USUAL</a> I SEE</a> -<a href="track/bar/"><b>boldface</b></a> -<a href="album/exile/">>Exile<</a> -<a href="track/heart/"><3</a> -` - -exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > tooltip & content 1`] = ` -<a href="album/beyond-canon/">Beyond Canon</a> -<a title="Beyond Canon" href="album/beyond-canon/">Beyond Canon</a> -<a title="Beyond Canon" href="album/beyond-canon/">Next</a> -<a href="album/beyond-canon/">Beyond Canon</a> -<span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="album/beyond-canon/">BC</a></span><span class="tooltip thing-name-tooltip"><span class="tooltip-content">Beyond Canon</span></span></span> -<span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="album/beyond-canon/">Next</a></span><span class="tooltip thing-name-tooltip"><span class="tooltip-content">Beyond Canon</span></span></span> -<a href="album/beyond-canon/">Next</a> -<span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="album/beyond-canon/">Beyond Canon</a></span><span class="tooltip thing-name-tooltip"><span class="tooltip-content">Beyond Canon</span></span></span> -<span class="text-with-tooltip"><span class="hoverable"><a class="text-with-tooltip-interaction-cue" href="album/beyond-canon/">Next</a></span><span class="tooltip thing-name-tooltip"><span class="tooltip-content">Beyond Canon</span></span></span> -<a href="album/beyond-canon/">Banana</a> -` diff --git a/tap-snapshots/test/snapshot/transformContent.js.test.cjs b/tap-snapshots/test/snapshot/transformContent.js.test.cjs deleted file mode 100644 index ac23a1bf..00000000 --- a/tap-snapshots/test/snapshot/transformContent.js.test.cjs +++ /dev/null @@ -1,142 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > absorb punctuation 1`] = ` -<p>Don't you think this is an <a class="external-link from-content indicate-external" href="https://github.com/hsmusic/hsmusic-wiki/pull/567" title="github.com (opens in new tab)" target="_blank">interesting pull request<span class="normal-content">,</span></a> Steve?</p> -<p>Aren't you <a class="external-link from-content indicate-external" href="https://github.com/hsmusic/hsmusic-wiki/pull/567" title="github.com (opens in new tab)" target="_blank">interested<span class="normal-content">...</span></a> in <a class="external-link from-content indicate-external" href="https://github.com/hsmusic/hsmusic-wiki/pull/567" title="github.com (opens in new tab)" target="_blank">checking it out<span class="normal-content">?!!</span></a></p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > basic markdown 1`] = ` -<p>Hello <em>world!</em> This is <strong>SO COOL.</strong></p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > dates 1`] = ` -<p><time datetime="Thu, 13 Apr 2023 00:00:00 GMT">4/12/2023</time> Yep!</p> -<p>Very nice: <time datetime="Fri, 25 Oct 2413 03:00:00 GMT">10/25/2413</time></p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > emails 1`] = ` -<p>Email cute dogs to qznebula@protonmail.com please.</p> -<p>Just kidding... <a class="external-link from-content indicate-external" href="mailto:qznebula@protonmail.com" title="External (opens in new tab)" target="_blank">unless?</a></p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > escape end of tag 1`] = ` -<p>My favorite album is <a style="--primary-color: #123456" href="to-localized.album/cool-album">[Tactical Omission]</a>.</p> -<p>Your favorite album is <a style="--primary-color: #123456" href="to-localized.album/cool-album">[Tactical Wha-Huh-Now</a>].</p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > escape entire tag 1`] = ` -<p>[[album:cool-album|spooky]] <a style="--primary-color: #123456" href="to-localized.album/cool-album">scary</a></p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > escape markdown 1`] = ` -<p>What will it be, <em>ye fool?</em> *arr*</p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > hanging indent list 1`] = ` -<p>Hello!</p> -<ul> -<li><p>I am a list item and I -go on and on and on -and on and on and on.</p> -</li> -<li><p>I am another list item. -Yeah.</p> -</li> -</ul> -<p>In-between!</p> -<ul> -<li>Spooky, -spooky, I say!</li> -<li>Following list item. -No empty line around me.</li> -<li>Very cool. -So, so cool.</li> -</ul> -<p>Goodbye!</p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > indent on a directly following line 1`] = ` -<div> - <span>Wow!</span> -</div> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > indent on an indierctly following line 1`] = ` -<p>Some text.</p> -<p>Yes, some more text.</p> -<pre><code>I am hax0rz!! -All yor base r blong 2 us. -</code></pre> -<p>Aye.</p> -<p>Aye aye aye.</p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > inline images 1`] = ` -<p><img src="snooping.png"> as USUAL...</p> -<p>What do you know? <img src="cowabunga.png" width="24" height="32"></p> -<p><a style="--primary-color: #123456" href="to-localized.album/cool-album">I'm on the left.</a><img src="im-on-the-right.jpg"></p> -<p><img src="im-on-the-left.jpg"><a style="--primary-color: #123456" href="to-localized.album/cool-album">I'm on the right.</a></p> -<p>Media time! <img src="to-media.path/misc/interesting.png"> Oh yeah!</p> -<p><img src="must.png"><img src="stick.png"><img src="together.png"></p> -<p>And... all done! <img src="end-of-source.png"></p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > links to a thing 1`] = ` -<p>This is <a style="--primary-color: #123456" href="to-localized.album/cool-album">my favorite album</a>.</p> -<p>That's right, <a style="--primary-color: #123456" href="to-localized.album/cool-album">Cool Album</a>!</p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > lyrics - basic line breaks 1`] = ` -<p>Hey, ho<br> -And away we go<br> -Truly, music</p> -<p>(Oh yeah)<br> -(That's right)</p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > lyrics - line breaks around tags 1`] = ` -<p>The date be <time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br> -I say, the date be <time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br> -<time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br> -<time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br> -(Aye!)</p> -<p><time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br> -<time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br> -<time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br></p> -<p><time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br> -<time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time>, and don't ye forget it</p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > lyrics - repeated and edge line breaks 1`] = ` -<p>Well, you know<br> -How it goes</p> -<p>Yessiree</p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > non-inline image #1 1`] = ` -<div class="content-image-container">[mocked: image - slots: { src: 'spark.png', link: true, thumb: 'large', attributes: [ { class: 'content-image' }, undefined ] }]</div> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > non-inline image #2 1`] = ` -<p>Rad.</p> -<div class="content-image-container">[mocked: image - slots: { src: 'spark.png', link: true, thumb: 'large', attributes: [ { class: 'content-image' }, undefined ] }]</div> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > non-inline image #3 1`] = ` -<div class="content-image-container">[mocked: image - slots: { src: 'spark.png', link: true, thumb: 'large', attributes: [ { class: 'content-image' }, undefined ] }]</div> -<p>Baller.</p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > super basic string 1`] = ` -<p>Neat listing: Albums - by Date</p> -` - -exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > two text paragraphs 1`] = ` -<p>Hello, world!</p> -<p>Wow, this is very cool.</p> -` diff --git a/test/lib/content-function.js b/test/lib/content-function.js index a46d18c9..49fe5c95 100644 --- a/test/lib/content-function.js +++ b/test/lib/content-function.js @@ -11,16 +11,21 @@ import {quickEvaluate} from '#content-function'; import * as html from '#html'; import {internalDefaultStringsFile, processLanguageFile} from '#language'; import {empty} from '#sugar'; -import {generateURLs, thumb, urlSpec} from '#urls'; + +import { + applyLocalizedWithBaseDirectory, + generateURLs, + internalDefaultURLSpecFile, + processURLSpecFromFileSync, + thumb, +} from '#urls'; import mock from './generic-mock.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -function cleanURLSpec(reference) { - const prepared = structuredClone(reference); - - for (const spec of Object.values(prepared)) { +function cleanURLSpec(urlSpec) { + for (const spec of Object.values(urlSpec)) { if (spec.prefix) { // Strip out STATIC_VERSION. This updates fairly regularly and we // don't want it to affect snapshot tests. @@ -28,12 +33,23 @@ function cleanURLSpec(reference) { .replace(/static-\d+[a-z]\d+/i, 'static'); } } +} + +function urlsPlease() { + const {aggregate: urlsAggregate, result: urlSpec} = + processURLSpecFromFileSync(internalDefaultURLSpecFile); + + urlsAggregate.close(); + + applyLocalizedWithBaseDirectory(urlSpec); + + cleanURLSpec(urlSpec); - return prepared; + return generateURLs(urlSpec); } export function testContentFunctions(t, message, fn) { - const urls = generateURLs(cleanURLSpec(urlSpec)); + const urls = urlsPlease(); t.test(message, async t => { let loadedContentDependencies; diff --git a/test/lib/wiki-data.js b/test/lib/wiki-data.js index 7c3d2147..f0ee0ef5 100644 --- a/test/lib/wiki-data.js +++ b/test/lib/wiki-data.js @@ -1,5 +1,6 @@ import CacheableObject from '#cacheable-object'; -import find from '#find'; +import find, {bindFind} from '#find'; +import {bindReverse} from '#reverse'; import {withEntries} from '#sugar'; import Thing from '#thing'; import thingConstructors from '#things'; @@ -9,11 +10,13 @@ export function linkAndBindWikiData(wikiData, { inferAlbumsOwnTrackData = true, } = {}) { function customLinkWikiDataArrays(wikiData, options = {}) { - linkWikiDataArrays( - (options.XXX_decacheWikiData - ? withEntries(wikiData, entries => entries - .map(([key, value]) => [key, value.slice()])) - : wikiData)); + if (options.XXX_decacheWikiData) { + wikiData = + withEntries(wikiData, entries => entries + .map(([key, value]) => [key, value.slice()])); + } + + linkWikiDataArrays(wikiData, {bindFind, bindReverse}); } customLinkWikiDataArrays(wikiData); diff --git a/test/snapshot/generateAlbumAdditionalFilesList.js b/test/snapshot/generateAlbumAdditionalFilesList.js deleted file mode 100644 index c25e5682..00000000 --- a/test/snapshot/generateAlbumAdditionalFilesList.js +++ /dev/null @@ -1,84 +0,0 @@ -import t from 'tap'; - -import {testContentFunctions} from '#test-lib'; -import thingConstructors from '#things'; - -const {Album} = thingConstructors; - -testContentFunctions(t, 'generateAlbumAdditionalFilesList (snapshot)', async (t, evaluate) => { - const sizeMap = { - 'sburbwp_1280x1024.jpg': 2500, - 'sburbwp_1440x900.jpg': null, - 'sburbwp_1920x1080.jpg': null, - 'Internet Explorer.gif': 1, - 'Homestuck_Vol4_alt1.jpg': 1234567, - 'Homestuck_Vol4_alt2.jpg': 1234567, - 'Homestuck_Vol4_alt3.jpg': 1234567, - }; - - const extraDependencies = { - getSizeOfAdditionalFile: file => - Object.entries(sizeMap) - .find(key => file.includes(key)) - ?.at(1) ?? null, - }; - - await evaluate.load({ - mock: { - image: evaluate.stubContentFunction('image'), - }, - }); - - const album = new Album(); - album.directory = 'exciting-album'; - - evaluate.snapshot('no additional files', { - extraDependencies, - name: 'generateAlbumAdditionalFilesList', - args: [album, []], - }); - - try { - evaluate.snapshot('basic behavior', { - extraDependencies, - name: 'generateAlbumAdditionalFilesList', - args: [ - album, - [ - { - title: 'SBURB Wallpaper', - files: [ - 'sburbwp_1280x1024.jpg', - 'sburbwp_1440x900.jpg', - 'sburbwp_1920x1080.jpg', - ], - }, - { - title: 'Fake Section', - description: 'No sizes for these files', - files: [ - 'oops.mp3', - 'Internet Explorer.gif', - 'daisy.mp3', - ], - }, - { - title: `Empty Section`, - description: `These files haven't been made available.`, - }, - { - title: 'Alternate Covers', - description: 'This is just an example description.', - files: [ - 'Homestuck_Vol4_alt1.jpg', - 'Homestuck_Vol4_alt2.jpg', - 'Homestuck_Vol4_alt3.jpg', - ], - }, - ], - ], - }); - } catch (error) { - console.log(error); - } -}); diff --git a/test/snapshot/generateAlbumBanner.js b/test/snapshot/generateAlbumBanner.js deleted file mode 100644 index 8e63308f..00000000 --- a/test/snapshot/generateAlbumBanner.js +++ /dev/null @@ -1,34 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'generateAlbumBanner (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - evaluate.snapshot('basic behavior', { - name: 'generateAlbumBanner', - args: [{ - directory: 'cool-album', - hasBannerArt: true, - bannerDimensions: [800, 200], - bannerFileExtension: 'png', - }], - }); - - evaluate.snapshot('no dimensions', { - name: 'generateAlbumBanner', - args: [{ - directory: 'cool-album', - hasBannerArt: true, - bannerDimensions: null, - bannerFileExtension: 'png', - }], - }); - - evaluate.snapshot('no banner', { - name: 'generateAlbumBanner', - args: [{ - directory: 'cool-album', - hasBannerArt: false, - }], - }); -}); diff --git a/test/snapshot/generateAlbumReleaseInfo.js b/test/snapshot/generateAlbumReleaseInfo.js deleted file mode 100644 index f41e502d..00000000 --- a/test/snapshot/generateAlbumReleaseInfo.js +++ /dev/null @@ -1,74 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'generateAlbumReleaseInfo (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - evaluate.snapshot('basic behavior', { - name: 'generateAlbumReleaseInfo', - args: [{ - artistContribs: [ - {artist: {name: 'Toby Fox', directory: 'toby-fox', urls: []}, annotation: 'music probably'}, - {artist: {name: 'Tensei', directory: 'tensei', urls: ['https://tenseimusic.bandcamp.com/']}, annotation: 'hot jams'}, - ], - - coverArtistContribs: [ - {artist: {name: 'Hanni Brosh', directory: 'hb', urls: []}, annotation: null}, - ], - - wallpaperArtistContribs: [ - {artist: {name: 'Hanni Brosh', directory: 'hb', urls: []}, annotation: null}, - {artist: {name: 'Niklink', directory: 'niklink', urls: []}, annotation: 'edits'}, - ], - - bannerArtistContribs: [ - {artist: {name: 'Hanni Brosh', directory: 'hb', urls: []}, annotation: null}, - {artist: {name: 'Niklink', directory: 'niklink', urls: []}, annotation: 'edits'}, - ], - - name: 'AlterniaBound', - date: new Date('March 14, 2011'), - coverArtDate: new Date('April 1, 1991'), - urls: [ - 'https://homestuck.bandcamp.com/album/alterniabound-with-alternia', - 'https://www.youtube.com/playlist?list=PLnVpmehyaOFZWO9QOZmD6A3TIK0wZ6xE2', - 'https://www.youtube.com/watch?v=HO5V2uogkYc', - ], - - tracks: [{duration: 253}, {duration: 372}], - }], - }); - - const sparse = { - artistContribs: [], - coverArtistContribs: [], - wallpaperArtistContribs: [], - bannerArtistContribs: [], - - name: 'Suspicious Album', - urls: [], - tracks: [], - }; - - evaluate.snapshot('reduced details', { - name: 'generateAlbumReleaseInfo', - args: [sparse], - }); - - evaluate.snapshot('URLs only', { - name: 'generateAlbumReleaseInfo', - args: [{ - ...sparse, - urls: ['https://homestuck.bandcamp.com/foo', 'https://soundcloud.com/bar'], - }], - }); - - evaluate.snapshot('equal cover art date', { - name: 'generateAlbumReleaseInfo', - args: [{ - ...sparse, - date: new Date('2020-04-13'), - coverArtDate: new Date('2020-04-13'), - }], - }); -}); diff --git a/test/snapshot/generateAlbumSecondaryNav.js b/test/snapshot/generateAlbumSecondaryNav.js deleted file mode 100644 index 2495bc4a..00000000 --- a/test/snapshot/generateAlbumSecondaryNav.js +++ /dev/null @@ -1,90 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'generateAlbumSecondaryNav (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - let album, anotherAlbum, group1, group2; - - group1 = { - name: 'VCG', - directory: 'vcg', - color: '#abcdef', - serieses: [], - }; - - group2 = { - name: 'Bepis', - directory: 'bepis', - color: '#123456', - serieses: [], - }; - - album = { - name: 'Album', - directory: 'album', - date: new Date('2010-04-13'), - groups: [group1, group2], - }; - - anotherAlbum = { - name: 'Last', - directory: 'last', - date: new Date('2010-06-12'), - }; - - group1.albums = [ - { - name: 'First', - directory: 'first', - date: new Date('2010-04-10'), - }, - album, - anotherAlbum, - ]; - - group1.serieses = [ - { - name: 'Series', - albums: [album, anotherAlbum], - group: group1, - }, - ]; - - group2.albums = [ - album, - { - name: 'Second', - directory: 'second', - date: new Date('2011-04-13'), - }, - ]; - - evaluate.snapshot('basic behavior, mode: album', { - name: 'generateAlbumSecondaryNav', - args: [album], - slots: {mode: 'album'}, - }); - - evaluate.snapshot('basic behavior, mode: track', { - name: 'generateAlbumSecondaryNav', - args: [album], - slots: {mode: 'track'}, - }); - - album = { - date: null, - groups: [group1, group2], - }; - - group1.albums = [ - ...group1.albums, - album, - ]; - - evaluate.snapshot('dateless album in mixed group', { - name: 'generateAlbumSecondaryNav', - args: [album], - slots: {mode: 'album'}, - }); -}); diff --git a/test/snapshot/generateAlbumSidebarGroupBox.js b/test/snapshot/generateAlbumSidebarGroupBox.js deleted file mode 100644 index f920bd96..00000000 --- a/test/snapshot/generateAlbumSidebarGroupBox.js +++ /dev/null @@ -1,57 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'generateAlbumSidebarGroupBox (snapshot)', async (t, evaluate) => { - await evaluate.load({ - mock: { - ...evaluate.mock.transformContent, - }, - }); - - let album, group; - - album = { - name: 'Middle', - directory: 'middle', - date: new Date('2010-04-13'), - }; - - group = { - name: 'VCG', - directory: 'vcg', - descriptionShort: 'Very cool group.', - urls: ['https://vcg.bandcamp.com/', 'https://youtube.com/@vcg'], - albums: [ - {name: 'First', directory: 'first', date: new Date('2010-04-10')}, - album, - {name: 'Last', directory: 'last', date: new Date('2010-06-12')}, - ], - }; - - evaluate.snapshot('basic behavior, mode: album', { - name: 'generateAlbumSidebarGroupBox', - args: [album, group], - slots: {mode: 'album'}, - }); - - evaluate.snapshot('basic behavior, mode: track', { - name: 'generateAlbumSidebarGroupBox', - args: [album, group], - slots: {mode: 'track'}, - }); - - album = { - date: null, - }; - - group.albums = [ - ...group.albums, - album, - ]; - - evaluate.snapshot('dateless album in mixed group', { - name: 'generateAlbumSidebarGroupBox', - args: [album, group], - slots: {mode: 'album'}, - }); -}); diff --git a/test/snapshot/generateAlbumTrackList.js b/test/snapshot/generateAlbumTrackList.js deleted file mode 100644 index e09f1c4a..00000000 --- a/test/snapshot/generateAlbumTrackList.js +++ /dev/null @@ -1,123 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'generateAlbumTrackList (snapshot)', async (t, evaluate) => { - await evaluate.load({ - mock: { - generateAlbumTrackListMissingDuration: - evaluate.stubContentFunction('generateAlbumTrackListMissingDuration'), - - image: - evaluate.stubContentFunction('image'), - }, - }); - - const contribs1 = [ - {artist: {name: 'Apricot', directory: 'apricot', urls: []}}, - ]; - - const contribs2 = [ - {artist: {name: 'Apricot', directory: 'apricot', urls: []}}, - {artist: {name: 'Peach', directory: 'peach', urls: ['https://peach.bandcamp.com/']}}, - {artist: {name: 'Cerise', directory: 'cerise', urls: []}}, - ]; - - const color1 = '#fb07ff'; - const color2 = '#ea2e83'; - - const tracks = [ - {name: 'Track 1', directory: 't1', duration: 20, artistContribs: contribs1, color: color1}, - {name: 'Track 2', directory: 't2', duration: 0, artistContribs: contribs1, color: color1}, - {name: 'Track 3', directory: 't3', duration: 40, artistContribs: contribs1, color: color1}, - {name: 'Track 4', directory: 't4', duration: 0, artistContribs: contribs2, color: color2}, - ]; - - const albumWithTrackSections = { - color: color1, - artistContribs: contribs1, - trackSections: [ - {name: 'First section', tracks: tracks.slice(0, 3)}, - {name: 'Second section', tracks: tracks.slice(3)}, - ], - tracks, - }; - - const albumWithoutTrackSections = { - color: color1, - artistContribs: contribs1, - trackSections: [{isDefaultTrackSection: true, tracks}], - tracks, - }; - - const albumWithTrackSectionDescriptions = { - color: color1, - artistContribs: contribs1, - trackSections: [ - {name: 'First section', tracks: tracks.slice(0, 3), description: `Why yes!`}, - {name: 'Second section', tracks: tracks.slice(3), description: `How *contentful,* this is.`}, - ], - tracks, - }; - - const albumWithNoDuration = { - color: color1, - artistContribs: contribs1, - trackSections: [{isDefaultTrackSection: true, tracks: [tracks[1], tracks[3]]}], - tracks: [tracks[1], tracks[3]], - }; - - evaluate.snapshot(`basic behavior, with track sections`, { - name: 'generateAlbumTrackList', - args: [albumWithTrackSections], - }); - - evaluate.snapshot(`basic behavior, default track section`, { - name: 'generateAlbumTrackList', - args: [albumWithoutTrackSections], - }); - - evaluate.snapshot(`basic behavior, with descriptions`, { - name: 'generateAlbumTrackList', - args: [albumWithTrackSectionDescriptions], - }); - - evaluate.snapshot(`collapseDurationScope: never`, { - name: 'generateAlbumTrackList', - slots: {collapseDurationScope: 'never'}, - multiple: [ - {args: [albumWithTrackSections]}, - {args: [albumWithoutTrackSections]}, - {args: [albumWithNoDuration]}, - ], - }); - - evaluate.snapshot(`collapseDurationScope: track`, { - name: 'generateAlbumTrackList', - slots: {collapseDurationScope: 'track'}, - multiple: [ - {args: [albumWithTrackSections]}, - {args: [albumWithoutTrackSections]}, - {args: [albumWithNoDuration]}, - ], - }); - - evaluate.snapshot(`collapseDurationScope: section`, { - name: 'generateAlbumTrackList', - slots: {collapseDurationScope: 'section'}, - multiple: [ - {args: [albumWithTrackSections]}, - {args: [albumWithoutTrackSections]}, - {args: [albumWithNoDuration]}, - ], - }); - - evaluate.snapshot(`collapseDurationScope: album`, { - name: 'generateAlbumTrackList', - slots: {collapseDurationScope: 'album'}, - multiple: [ - {args: [albumWithTrackSections]}, - {args: [albumWithoutTrackSections]}, - {args: [albumWithNoDuration]}, - ], - }); -}); diff --git a/test/snapshot/generateBanner.js b/test/snapshot/generateBanner.js deleted file mode 100644 index ab57c3cc..00000000 --- a/test/snapshot/generateBanner.js +++ /dev/null @@ -1,22 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'generateBanner (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - evaluate.snapshot('basic behavior', { - name: 'generateBanner', - slots: { - path: ['media.albumBanner', 'cool-album', 'png'], - alt: 'Very cool banner art.', - dimensions: [800, 200], - }, - }); - - evaluate.snapshot('no dimensions', { - name: 'generateBanner', - slots: { - path: ['media.albumBanner', 'cool-album', 'png'], - }, - }); -}); diff --git a/test/snapshot/generateTrackReleaseInfo.js b/test/snapshot/generateTrackReleaseInfo.js deleted file mode 100644 index 931377c8..00000000 --- a/test/snapshot/generateTrackReleaseInfo.js +++ /dev/null @@ -1,51 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'generateTrackReleaseInfo (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - const artistContribs = [{artist: {name: 'Toby Fox', directory: 'toby-fox', urls: []}, annotation: null}]; - const coverArtistContribs = [{artist: {name: 'Alpaca', directory: 'alpaca', urls: []}, annotation: '🔥'}]; - - evaluate.snapshot('basic behavior', { - name: 'generateTrackReleaseInfo', - args: [{ - artistContribs, - name: 'An Apple Disaster!!', - date: new Date('2011-11-30'), - duration: 58, - urls: ['https://soundcloud.com/foo', 'https://youtube.com/watch?v=bar'], - }], - }); - - const sparse = { - artistContribs, - name: 'Suspicious Track', - date: null, - duration: null, - urls: [], - }; - - evaluate.snapshot('reduced details', { - name: 'generateTrackReleaseInfo', - args: [sparse], - }); - - evaluate.snapshot('cover artist contribs, non-unique', { - name: 'generateTrackReleaseInfo', - args: [{ - ...sparse, - coverArtistContribs, - hasUniqueCoverArt: false, - }], - }); - - evaluate.snapshot('cover artist contribs, unique', { - name: 'generateTrackReleaseInfo', - args: [{ - ...sparse, - coverArtistContribs, - hasUniqueCoverArt: true, - }], - }); -}); diff --git a/test/snapshot/image.js b/test/snapshot/image.js deleted file mode 100644 index 1985211f..00000000 --- a/test/snapshot/image.js +++ /dev/null @@ -1,132 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'image (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - const quickSnapshot = (message, {extraDependencies, ...opts}) => - evaluate.snapshot(message, { - name: 'image', - extraDependencies: { - checkIfImagePathHasCachedThumbnails: path => !path.endsWith('.gif'), - getSizeOfImagePath: () => 0, - getDimensionsOfImagePath: () => [600, 600], - getThumbnailEqualOrSmaller: () => 'medium', - getThumbnailsAvailableForDimensions: () => - [['large', 800], ['medium', 400], ['small', 250]], - missingImagePaths: ['album-art/missing/cover.png'], - ...extraDependencies, - }, - ...opts, - }); - - quickSnapshot('source via path', { - slots: { - path: ['media.albumCover', 'beyond-canon', 'png'], - }, - }); - - quickSnapshot('source via src', { - slots: { - src: 'https://example.com/bananas.gif', - }, - }); - - quickSnapshot('source missing', { - slots: { - missingSourceContent: 'Example of missing source message.', - }, - }); - - quickSnapshot('dimensions', { - slots: { - src: 'foobar', - dimensions: [600, 400], - }, - }); - - quickSnapshot('square', { - slots: { - src: 'foobar', - square: true, - }, - }); - - quickSnapshot('dimensions with square', { - slots: { - src: 'foobar', - dimensions: [600, 400], - square: true, - }, - }); - - quickSnapshot('lazy with square', { - slots: { - src: 'foobar', - lazy: true, - square: true, - }, - }); - - quickSnapshot('link with file size', { - extraDependencies: { - getSizeOfImagePath: () => 10 ** 6, - }, - slots: { - path: ['media.albumCover', 'pingas', 'png'], - link: true, - }, - }); - - quickSnapshot('content warnings via tags', { - args: [ - [ - {name: 'Dirk Strider', directory: 'dirk'}, - {name: 'too cool for school', isContentWarning: true}, - ], - ], - slots: { - path: ['media.albumCover', 'beyond-canon', 'png'], - }, - }); - - evaluate.snapshot('thumbnail details', { - name: 'image', - extraDependencies: { - checkIfImagePathHasCachedThumbnails: () => true, - getSizeOfImagePath: () => 0, - getDimensionsOfImagePath: () => [900, 1200], - getThumbnailsAvailableForDimensions: () => - [['voluminous', 1200], ['middling', 900], ['petite', 20]], - getThumbnailEqualOrSmaller: () => 'voluminous', - missingImagePaths: [], - }, - slots: { - thumb: 'gargantuan', - path: ['media.albumCover', 'beyond-canon', 'png'], - }, - }); - - quickSnapshot('thumb requested but source is gif', { - slots: { - thumb: 'medium', - path: ['media.flashArt', '5426', 'gif'], - }, - }); - - quickSnapshot('missing image path', { - slots: { - thumb: 'medium', - path: ['media.albumCover', 'missing', 'png'], - link: true, - }, - }); - - quickSnapshot('missing image path w/ missingSourceContent', { - slots: { - thumb: 'medium', - path: ['media.albumCover', 'missing', 'png'], - missingSourceContent: `Cover's missing, whoops`, - }, - }); -}); diff --git a/test/snapshot/linkArtist.js b/test/snapshot/linkArtist.js deleted file mode 100644 index 7b2114b5..00000000 --- a/test/snapshot/linkArtist.js +++ /dev/null @@ -1,30 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'linkArtist (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - evaluate.snapshot('basic behavior', { - name: 'linkArtist', - args: [ - { - name: `Toby Fox`, - directory: `toby-fox`, - } - ], - }); - - evaluate.snapshot('prefer short name', { - name: 'linkArtist', - args: [ - { - name: 'ICCTTCMDMIROTMCWMWFTPFTDDOTARHPOESWGBTWEATFCWSEBTSSFOFG', - nameShort: '55gore', - directory: '55gore', - }, - ], - slots: { - preferShortName: true, - }, - }); -}); diff --git a/test/snapshot/linkContribution.js b/test/snapshot/linkContribution.js deleted file mode 100644 index 47ef1503..00000000 --- a/test/snapshot/linkContribution.js +++ /dev/null @@ -1,73 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'linkContribution (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - const quickSnapshot = (message, slots) => - evaluate.snapshot(message, { - name: 'linkContribution', - multiple: [ - {args: [ - {artist: { - name: 'Clark Powell', - directory: 'clark-powell', - urls: ['https://soundcloud.com/plazmataz'], - }, annotation: null}, - ]}, - {args: [ - {artist: { - name: 'Grounder & Scratch', - directory: 'the-big-baddies', - urls: [], - }, annotation: 'Snooping'}, - ]}, - {args: [ - {artist: { - name: 'Toby Fox', - directory: 'toby-fox', - urls: ['https://tobyfox.bandcamp.com/', 'https://toby.fox/'], - }, annotation: 'Arrangement'}, - ]}, - ], - slots, - }); - - quickSnapshot('showAnnotation & showExternalLinks', { - showAnnotation: true, - showExternalLinks: true, - }); - - quickSnapshot('only showAnnotation', { - showAnnotation: true, - }); - - quickSnapshot('only showExternalLinks', { - showExternalLinks: true, - }); - - quickSnapshot('no accents', {}); - - evaluate.snapshot('loads of links', { - name: 'linkContribution', - args: [ - {artist: {name: 'Lorem Ipsum Lover', directory: 'lorem-ipsum-lover', urls: [ - 'https://loremipsum.io', - 'https://loremipsum.io/generator/', - 'https://loremipsum.io/#meaning', - 'https://loremipsum.io/#usage-and-examples', - 'https://loremipsum.io/#controversy', - 'https://loremipsum.io/#when-to-use-lorem-ipsum', - 'https://loremipsum.io/#lorem-ipsum-all-the-things', - 'https://loremipsum.io/#original-source', - ]}, annotation: null}, - ], - slots: {showExternalLinks: true}, - }); - - quickSnapshot('no preventWrapping', { - showAnnotation: true, - showExternalLinks: true, - preventWrapping: false, - }); -}); diff --git a/test/snapshot/linkExternal.js b/test/snapshot/linkExternal.js deleted file mode 100644 index 90c98f4b..00000000 --- a/test/snapshot/linkExternal.js +++ /dev/null @@ -1,225 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'linkExternal (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - evaluate.snapshot('unknown domain (arbitrary world wide web path)', { - name: 'linkExternal', - args: ['https://snoo.ping.as/usual/i/see/'], - }); - - const urlsToArgs = urls => - urls.map(url => ({args: [url]})); - - const quickSnapshot = (message, urls, slots) => - evaluate.snapshot(message, { - name: 'linkExternal', - slots, - multiple: urlsToArgs(urls), - }); - - const quickSnapshotAllStyles = (context, urls) => { - for (const style of ['platform', 'handle']) { - const message = `context: ${context}, style: ${style}`; - quickSnapshot(message, urls, {context, style}); - } - }; - - // Try to comprehensively test every regular expression - // (in `match` and extractions like `handle` or `details`). - - // Try to *also* represent a reasonable variety of what kinds - // of URLs appear throughout the wiki. (This should serve to - // identify areas which #external-links is expected to - // accommodate, regardless whether or not there is special - // attention given in the actual descriptors.) - - // For normal custom-domain matches (e.g. Mastodon), - // it's OK to just test one custom domain in the list. - - // Generally match the sorting order in externalLinkSpec, - // so corresponding and missing test cases are easy to locate. - - quickSnapshotAllStyles('generic', [ - // platform: appleMusic - 'https://music.apple.com/us/artist/system-of-a-down/462715', - - // platform: artstation - 'https://www.artstation.com/eevaningtea', - 'https://witnesstheabsurd.artstation.com/', - - // platform: bandcamp - 'https://music.solatrus.com/', - 'https://homestuck.bandcamp.com/', - - // platform: bluesky - 'https://bsky.app/profile/jacobtheloofah.bsky.social', - - // platform: carrd - 'https://aliceflare.carrd.co', - 'https://bigchaslappa.carrd.co/', - - // platform: cohost - 'https://cohost.org/cosmoptera', - - // platform: deconreconstruction.music - 'https://music.deconreconstruction.com/albums/catch-322', - 'https://music.deconreconstruction.com/albums/catch-322?track=arcjecs-theme', - - // platform: deconreconstruction - 'https://www.deconreconstruction.com/', - - // platform: deviantart - 'https://culdhira.deviantart.com', - 'https://www.deviantart.com/chesswanderlust-sama', - 'https://www.deviantart.com/shilloshilloh/art/Homestuck-Jake-English-268874606', - - // platform: facebook - 'https://www.facebook.com/DoomedCloud/', - 'https://www.facebook.com/pages/WoodenToaster/280642235307371', - 'https://www.facebook.com/Svixy/posts/400018786702633', - - // platform: fandom.mspaintadventures - 'https://mspaintadventures.fandom.com/wiki/Draconian_Dignitary', - 'https://mspaintadventures.fandom.com/wiki/', - 'https://mspaintadventures.fandom.com/', - - // platform: fandom - 'https://community.fandom.com/', - 'https://community.fandom.com/wiki/', - 'https://community.fandom.com/wiki/Community_Central', - - // platform: gamebanana - 'https://gamebanana.com/members/2028092', - 'https://gamebanana.com/mods/459476', - - // platform: homestuck - 'https://homestuck.com/', - - // platform: hsmusic.archive - 'https://hsmusic.wiki/media/misc/archive/Firefly%20Cloud%20Remix.mp3', - - // platform: hsmusic - 'https://hsmusic.wiki/feedback/', - - // platform: internetArchive - 'https://archive.org/details/a-life-well-lived', - 'https://archive.org/details/VastError_Volume1/11+Renaissance.mp3', - - // platform: instagram - 'https://instagram.com/bass.and.noises', - 'https://www.instagram.com/levc_egm/', - - // platform: itch - 'https://tuyoki.itch.io/', - 'https://itch.io/profile/bravelittletoreador', - - // platform: ko-fi - 'https://ko-fi.com/gnaach', - - // platform: linktree - 'https://linktr.ee/bbpanzu', - - // platform: mastodon - 'https://types.pl/', - - // platform: mspfa - 'https://canwc.mspfa.com/', - 'https://mspfa.com/?s=12003&p=1045', - 'https://mspfa.com/user/?u=103334508819793669241', - - // platform: neocities - 'https://wodaro.neocities.org', - 'https://neomints.neocities.org/', - - // platform: newgrounds - 'https://buzinkai.newgrounds.com/', - 'https://www.newgrounds.com/audio/listen/1256058', - - // platform: patreon - 'https://www.patreon.com/CecilyRenns', - - // platform: poetryFoundation - 'https://www.poetryfoundation.org/poets/christina-rossetti', - 'https://www.poetryfoundation.org/poems/45000/remember-56d224509b7ae', - - // platform: soundcloud - 'https://soundcloud.com/plazmataz', - 'https://soundcloud.com/worthikids/1-i-accidentally-broke-my', - - // platform: spotify - 'https://open.spotify.com/artist/63SNNpNOicDzG3LY82G4q3', - 'https://open.spotify.com/album/0iHvPD8rM3hQa0qeVtPQ3t', - 'https://open.spotify.com/track/6YEGQH32aAXb9vQQbBrPlw', - - // platform: tiktok - 'https://www.tiktok.com/@richaadeb', - - // platform: toyhouse - 'https://toyhou.se/ghastaboo', - - // platform: tumblr - 'https://aeritus.tumblr.com/', - 'https://vol5anthology.tumblr.com/post/159528808107/hey-everyone-its-413-and-that-means-we-have', - 'https://www.tumblr.com/electricwestern', - 'https://www.tumblr.com/spellmynamewithabang/142767566733/happy-413-this-is-the-first-time-anyones-heard', - - // platform: twitch - 'https://www.twitch.tv/ajhebard', - 'https://www.twitch.tv/vargskelethor/', - - // platform: twitter - 'https://twitter.com/awkwarddoesart', - 'https://twitter.com/purenonsens/', - 'https://twitter.com/circlejourney/status/1202265927183548416', - - // platform: waybackMachine - 'https://web.archive.org/web/20120405160556/https://homestuck.bandcamp.com/album/colours-and-mayhem-universe-a', - 'https://web.archive.org/web/20160807111207/http://griffinspacejam.com:80/', - - // platform: wikipedia - 'https://en.wikipedia.org/wiki/Haydn_Quartet_(vocal_ensemble)', - - // platform: youtube - 'https://youtube.com/@bani-chan8949', - 'https://www.youtube.com/@Razzie16', - 'https://www.youtube.com/channel/UCQXfvlKkpbOqEz4BepHqK7g', - 'https://www.youtube.com/watch?v=6ekVnZm29kw', - 'https://youtu.be/WBkC038wSio', - 'https://www.youtube.com/playlist?list=PLy5UGIMKOXpONMExgI7lVYFwQa54QFp_H', - ]); - - quickSnapshotAllStyles('album', [ - 'https://youtu.be/abc', - 'https://youtube.com/watch?v=abc', - 'https://youtube.com/Playlist?list=kweh', - ]); - - quickSnapshotAllStyles('albumNoTracks', [ - 'https://youtu.be/abc', - 'https://youtube.com/watch?v=abc', - 'https://youtube.com/Playlist?list=kweh', - ]); - - quickSnapshotAllStyles('albumOneTrack', [ - 'https://youtu.be/abc', - 'https://youtube.com/watch?v=abc', - 'https://youtube.com/Playlist?list=kweh', - ]); - - quickSnapshotAllStyles('albumMultipleTracks', [ - 'https://youtu.be/abc', - 'https://youtube.com/watch?v=abc', - 'https://youtube.com/Playlist?list=kweh', - ]); - - quickSnapshotAllStyles('flash', [ - 'https://www.bgreco.net/hsflash/002238.html', - 'https://homestuck.com/story/1234', - 'https://homestuck.com/story/pony', - 'https://www.youtube.com/watch?v=wKgOp3Kg2wI', - 'https://youtu.be/IOcvkkklWmY', - 'https://some.external.site/foo/bar/', - ]); -}); diff --git a/test/snapshot/linkTemplate.js b/test/snapshot/linkTemplate.js deleted file mode 100644 index 300065e2..00000000 --- a/test/snapshot/linkTemplate.js +++ /dev/null @@ -1,63 +0,0 @@ -import t from 'tap'; -import * as html from '#html'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'linkTemplate (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - evaluate.snapshot('fill many slots', { - name: 'linkTemplate', - - slots: { - 'href': 'https://hsmusic.wiki/media/cool file.pdf', - 'hash': 'fooey', - 'attributes': {class: 'dog', id: 'cat1'}, - 'content': 'My Cool Link', - }, - }); - - evaluate.snapshot('fill path slot & provide appendIndexHTML', { - name: 'linkTemplate', - - extraDependencies: { - to: (...path) => '/c*lzone/' + path.join('/') + '/', - appendIndexHTML: true, - }, - - slots: { - path: ['myCoolPath', 'ham', 'pineapple', 'tomato'], - content: 'delish', - }, - }); - - evaluate.snapshot('special characters in path argument', { - name: 'linkTemplate', - slots: { - path: [ - 'media.albumAdditionalFile', - 'homestuck-vol-1', - 'Showtime (Piano Refrain) - #xXxAwesomeSheetMusick?rxXx#.pdf', - ], - content: `Damn, that's some good sheet music`, - }, - }); - - evaluate.snapshot('missing content', { - name: 'linkTemplate', - slots: {href: 'banana'}, - }); - - evaluate.snapshot('link in content', { - name: 'linkTemplate', - slots: { - hash: 'the-more-ye-know', - content: [ - `Oh geez oh heck`, - html.tag('a', {href: 'dogs'}, `There's a link in here!!`), - `But here's <b>a normal tag.</b>`, - html.tag('div', `Gotta keep them normal tags.`), - html.tag('div', `But not... <a href="#">NESTED LINKS, OOO.</a>`), - ], - }, - }); -}); diff --git a/test/snapshot/linkThing.js b/test/snapshot/linkThing.js deleted file mode 100644 index 9b5cff33..00000000 --- a/test/snapshot/linkThing.js +++ /dev/null @@ -1,94 +0,0 @@ -import t from 'tap'; -import * as html from '#html'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'linkThing (snapshot)', async (t, evaluate) => { - await evaluate.load(); - - const quickSnapshot = (message, oneOrMultiple) => - evaluate.snapshot(message, - (Array.isArray(oneOrMultiple) - ? {name: 'linkThing', multiple: oneOrMultiple} - : {name: 'linkThing', ...oneOrMultiple})); - - quickSnapshot('basic behavior', { - args: ['localized.track', { - directory: 'foo', - color: '#abcdef', - name: `Cool track!`, - }], - }); - - quickSnapshot('preferShortName', { - args: ['localized.artTagGallery', { - directory: 'five-oceanfalls', - name: 'Five (Oceanfalls)', - nameShort: 'Five', - }], - slots: {preferShortName: true}, - }); - - quickSnapshot('tooltip & content', { - args: ['localized.album', { - directory: 'beyond-canon', - name: 'Beyond Canon', - nameShort: 'BC', - }], - multiple: [ - {slots: {tooltipStyle: 'none'}}, - {slots: {tooltipStyle: 'browser'}}, - {slots: {tooltipStyle: 'browser', content: 'Next'}}, - {slots: {tooltipStyle: 'auto'}}, - {slots: {tooltipStyle: 'auto', preferShortName: true}}, - {slots: {tooltipStyle: 'auto', preferShortName: true, content: 'Next'}}, - {slots: {tooltipStyle: 'auto', content: 'Next'}}, - {slots: {tooltipStyle: 'wiki'}}, - {slots: {tooltipStyle: 'wiki', content: 'Next'}}, - {slots: {content: 'Banana'}}, - ], - }); - - quickSnapshot('color', { - args: ['localized.track', { - directory: 'showtime-piano-refrain', - name: 'Showtime (Piano Refrain)', - color: '#38f43d', - }], - multiple: [ - {slots: {color: false}}, - {slots: {color: true}}, - {slots: {color: '#aaccff'}}, - {slots: {color: '#aaccff', tooltipStyle: 'wiki'}}, - ], - }); - - quickSnapshot('tags in name escaped', [ - {args: ['localized.track', { - directory: 'foo', - name: `<a href="SNOOPING">AS USUAL</a> I SEE`, - }]}, - {args: ['localized.track', { - directory: 'bar', - name: `<b>boldface</b>`, - }]}, - {args: ['localized.album', { - directory: 'exile', - name: '>Exile<', - }]}, - {args: ['localized.track', { - directory: 'heart', - name: '<3', - }]}, - ]); - - quickSnapshot('nested links in content stripped', { - args: ['localized.staticPage', {directory: 'foo', name: 'Foo'}], - slots: { - content: - html.tag('b', {[html.joinChildren]: ''}, [ - html.tag('a', {href: 'bar'}, `Oooo!`), - ` Very spooky.`, - ]), - }, - }); -}); diff --git a/test/snapshot/transformContent.js b/test/snapshot/transformContent.js deleted file mode 100644 index 63391681..00000000 --- a/test/snapshot/transformContent.js +++ /dev/null @@ -1,171 +0,0 @@ -import t from 'tap'; -import {testContentFunctions} from '#test-lib'; - -testContentFunctions(t, 'transformContent (snapshot)', async (t, evaluate) => { - await evaluate.load({ - mock: { - image: evaluate.stubContentFunction('image'), - }, - }); - - const extraDependencies = { - wikiData: { - albumData: [ - {directory: 'cool-album', name: 'Cool Album', color: '#123456'}, - ], - }, - - to: (key, ...args) => `to-${key}/${args.join('/')}`, - }; - - const quickSnapshot = (message, content, slots) => - evaluate.snapshot(message, { - name: 'transformContent', - args: [content], - extraDependencies, - slots, - }); - - quickSnapshot( - 'two text paragraphs', - `Hello, world!\n` + - `Wow, this is very cool.`); - - quickSnapshot( - 'links to a thing', - `This is [[album:cool-album|my favorite album]].\n` + - `That's right, [[album:cool-album]]!`); - - quickSnapshot( - 'indent on a directly following line', - `<div>\n` + - ` <span>Wow!</span>\n` + - `</div>`); - - quickSnapshot( - 'indent on an indierctly following line', - `Some text.\n` + - `Yes, some more text.\n` + - `\n` + - ` I am hax0rz!!\n` + - ` All yor base r blong 2 us.\n` + - `\n` + - `Aye.\n` + - `Aye aye aye.`); - - quickSnapshot( - 'hanging indent list', - `Hello!\n` + - `\n` + - `* I am a list item and I\n` + - ` go on and on and on\n` + - ` and on and on and on.\n` + - `\n` + - `* I am another list item.\n` + - ` Yeah.\n` + - `\n` + - `In-between!\n` + - `\n` + - `* Spooky,\n` + - ` spooky, I say!\n` + - `* Following list item.\n` + - ` No empty line around me.\n` + - `* Very cool.\n` + - ` So, so cool.\n` + - `\n` + - `Goodbye!`); - - quickSnapshot( - 'inline images', - `<img src="snooping.png"> as USUAL...\n` + - `What do you know? <img src="cowabunga.png" width="24" height="32">\n` + - `[[album:cool-album|I'm on the left.]]<img src="im-on-the-right.jpg">\n` + - `<img src="im-on-the-left.jpg">[[album:cool-album|I'm on the right.]]\n` + - `Media time! <img src="media/misc/interesting.png"> Oh yeah!\n` + - `<img src="must.png"><img src="stick.png"><img src="together.png">\n` + - `And... all done! <img src="end-of-source.png">`); - - quickSnapshot( - 'non-inline image #1', - `<img src="spark.png">`); - - quickSnapshot( - 'non-inline image #2', - `Rad.\n` + - `<img src="spark.png">`); - - quickSnapshot( - 'non-inline image #3', - `<img src="spark.png">\n` + - `Baller.`); - - quickSnapshot( - 'dates', - `[[date:2023-04-13]] Yep!\n` + - `Very nice: [[date:25 October 2413]]`); - - quickSnapshot( - 'super basic string', - `Neat listing: [[string:listingPage.listAlbums.byDate.title]]`); - - quickSnapshot( - 'basic markdown', - `Hello *world!* This is **SO COOL.**`); - - quickSnapshot( - 'escape entire tag', - `\\[[album:cool-album|spooky]] [[album:cool-album|scary]]`); - - quickSnapshot( - 'escape end of tag', - `My favorite album is [[album:cool-album|[Tactical Omission\\]]].\n` + - `Your favorite album is [[album:cool-album|[Tactical Wha-Huh-Now]]].`); - - quickSnapshot( - 'escape markdown', - `What will it be, *ye fool?* \\*arr*`); - - quickSnapshot( - 'lyrics - basic line breaks', - `Hey, ho\n` + - `And away we go\n` + - `Truly, music\n` + - `\n` + - `(Oh yeah)\n` + - `(That's right)`, - {mode: 'lyrics'}); - - quickSnapshot( - 'lyrics - repeated and edge line breaks', - `\n\nWell, you know\nHow it goes\n\n\nYessiree\n\n\n`, - {mode: 'lyrics'}); - - quickSnapshot( - 'lyrics - line breaks around tags', - `The date be [[date:13 April 2004]]\n` + - `I say, the date be [[date:13 April 2004]]\n` + - `[[date:13 April 2004]]\n` + - `[[date:13 April 2004]][[date:13 April 2004]][[date:13 April 2004]]\n` + - `(Aye!)\n` + - `\n` + - `[[date:13 April 2004]]\n` + - `[[date:13 April 2004]][[date:13 April 2004]]\n` + - `[[date:13 April 2004]]\n` + - `\n` + - `[[date:13 April 2004]]\n` + - `[[date:13 April 2004]], and don't ye forget it`, - {mode: 'lyrics'}); - - quickSnapshot( - 'emails', - `Email cute dogs to qznebula@protonmail.com please.\n` + - `Just kidding... [unless?](mailto:qznebula@protonmail.com)`); - - quickSnapshot( - `absorb punctuation`, - `Don't you think this is an [interesting pull request](https://github.com/hsmusic/hsmusic-wiki/pull/567), Steve?\n` + - `Aren't you [interested](https://github.com/hsmusic/hsmusic-wiki/pull/567)... in [checking it out](https://github.com/hsmusic/hsmusic-wiki/pull/567)?!!`); - - // TODO: Snapshots for mode: inline - // TODO: Snapshots for mode: single-link -}); diff --git a/test/unit/content/dependencies/linkContribution.js b/test/unit/content/dependencies/linkContribution.js index 3ffd71d2..1baa80f8 100644 --- a/test/unit/content/dependencies/linkContribution.js +++ b/test/unit/content/dependencies/linkContribution.js @@ -1,7 +1,7 @@ import t from 'tap'; import {testContentFunctions} from '#test-lib'; -t.test('generateContributionLinks (unit)', async t => { +t.test('linkContribution (unit)', async t => { const artist1 = { name: 'Clark Powell', directory: 'clark-powell', @@ -24,7 +24,15 @@ t.test('generateContributionLinks (unit)', async t => { const annotation2 = 'Snooping'; const annotation3 = 'Arrangement'; - await testContentFunctions(t, 'generateContributionLinks (unit 1)', async (t, evaluate) => { + const thing1 = {}; + const thing2 = {}; + const thing3 = {}; + + const contribution1 = {artist: artist1, annotation: annotation1, thing: thing1}; + const contribution2 = {artist: artist2, annotation: annotation2, thing: thing2}; + const contribution3 = {artist: artist3, annotation: annotation3, thing: thing3}; + + await testContentFunctions(t, 'linkContribution (unit 1)', async (t, evaluate) => { const slots = { showAnnotation: true, showExternalLinks: true, @@ -71,15 +79,15 @@ t.test('generateContributionLinks (unit)', async t => { evaluate({ name: 'linkContribution', multiple: [ - {args: [{artist: artist1, annotation: annotation1}]}, - {args: [{artist: artist2, annotation: annotation2}]}, - {args: [{artist: artist3, annotation: annotation3}]}, + {args: [contribution1]}, + {args: [contribution2]}, + {args: [contribution3]}, ], slots, }); }); - await testContentFunctions(t, 'generateContributionLinks (unit 2)', async (t, evaluate) => { + await testContentFunctions(t, 'linkContribution (unit 2)', async (t, evaluate) => { const slots = { showAnnotation: false, showExternalLinks: false, @@ -127,9 +135,9 @@ t.test('generateContributionLinks (unit)', async t => { evaluate({ name: 'linkContribution', multiple: [ - {args: [{artist: artist1, annotation: annotation1}]}, - {args: [{artist: artist2, annotation: annotation2}]}, - {args: [{artist: artist3, annotation: annotation3}]}, + {args: [contribution1]}, + {args: [contribution2]}, + {args: [contribution3]}, ], slots, }); diff --git a/test/unit/data/cacheable-object.js b/test/unit/data/cacheable-object.js index 4b927248..4be31788 100644 --- a/test/unit/data/cacheable-object.js +++ b/test/unit/data/cacheable-object.js @@ -2,10 +2,14 @@ import t from 'tap'; import CacheableObject from '#cacheable-object'; -function newCacheableObject(PD) { - return new (class extends CacheableObject { - static [CacheableObject.propertyDescriptors] = PD; - }); +function newCacheableObject(propertyDescriptors) { + const constructor = class extends CacheableObject { + static [CacheableObject.propertyDescriptors] = propertyDescriptors; + }; + + constructor.finalizeCacheableObjectPrototype(); + + return Reflect.construct(constructor, []); } t.test(`CacheableObject simple separate update & expose`, t => { diff --git a/test/unit/data/composite/data/withPropertyFromObject.js b/test/unit/data/composite/data/withPropertyFromObject.js index 912c924c..068932e2 100644 --- a/test/unit/data/composite/data/withPropertyFromObject.js +++ b/test/unit/data/composite/data/withPropertyFromObject.js @@ -61,7 +61,7 @@ t.test(`withPropertyFromObject: "internal" input`, t => { ], }); - const thing = new (class extends CacheableObject { + const constructor = class extends CacheableObject { static [CacheableObject.propertyDescriptors] = { foo: { flags: {update: true, expose: false}, @@ -78,7 +78,11 @@ t.test(`withPropertyFromObject: "internal" input`, t => { }, }, }; - }); + }; + + constructor.finalizeCacheableObjectPrototype(); + + const thing = Reflect.construct(constructor, []); thing.foo = 100; thing.bar = 200; diff --git a/test/unit/data/composite/things/track/withAlbum.js b/test/unit/data/composite/things/track/withAlbum.js deleted file mode 100644 index 6f50776b..00000000 --- a/test/unit/data/composite/things/track/withAlbum.js +++ /dev/null @@ -1,119 +0,0 @@ -import t from 'tap'; - -import '#import-heck'; - -import Thing from '#thing'; - -import {compositeFrom, input} from '#composite'; -import {exposeConstant, exposeDependency} from '#composite/control-flow'; -import {withAlbum} from '#composite/things/track'; - -t.test(`withAlbum: basic behavior`, t => { - t.plan(3); - - const composite = compositeFrom({ - compose: false, - steps: [ - withAlbum(), - exposeDependency({dependency: '#album'}), - ], - }); - - t.match(composite, { - expose: { - dependencies: ['albumData', 'this'], - }, - }); - - const fakeTrack1 = { - [Thing.isThing]: true, - directory: 'foo', - }; - - const fakeTrack2 = { - [Thing.isThing]: true, - directory: 'bar', - }; - - const fakeAlbum = { - [Thing.isThing]: true, - directory: 'baz', - tracks: [fakeTrack1], - }; - - t.equal( - composite.expose.compute({ - albumData: [fakeAlbum], - this: fakeTrack1, - }), - fakeAlbum); - - t.equal( - composite.expose.compute({ - albumData: [fakeAlbum], - this: fakeTrack2, - }), - null); -}); - -t.test(`withAlbum: early exit conditions`, t => { - t.plan(4); - - const composite = compositeFrom({ - compose: false, - steps: [ - withAlbum(), - exposeConstant({ - value: input.value('bimbam'), - }), - ], - }); - - const fakeTrack1 = { - [Thing.isThing]: true, - directory: 'foo', - }; - - const fakeTrack2 = { - [Thing.isThing]: true, - directory: 'bar', - }; - - const fakeAlbum = { - [Thing.isThing]: true, - directory: 'baz', - tracks: [fakeTrack1], - }; - - t.equal( - composite.expose.compute({ - albumData: [fakeAlbum], - this: fakeTrack1, - }), - 'bimbam', - `does not early exit if albumData is present and contains the track`); - - t.equal( - composite.expose.compute({ - albumData: [fakeAlbum], - this: fakeTrack2, - }), - 'bimbam', - `does not early exit if albumData is present and does not contain the track`); - - t.equal( - composite.expose.compute({ - albumData: [], - this: fakeTrack1, - }), - 'bimbam', - `does not early exit if albumData is empty array`); - - t.equal( - composite.expose.compute({ - albumData: null, - this: fakeTrack1, - }), - null, - `early exits if albumData is null`); -}); diff --git a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js deleted file mode 100644 index 411fd11d..00000000 --- a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js +++ /dev/null @@ -1,373 +0,0 @@ -import t from 'tap'; - -import {compositeFrom, input} from '#composite'; -import thingConstructors from '#things'; - -import {exposeDependency} from '#composite/control-flow'; -import {withParsedCommentaryEntries} from '#composite/wiki-data'; - -const {Artist} = thingConstructors; - -const composite = compositeFrom({ - compose: false, - - steps: [ - withParsedCommentaryEntries({ - from: 'from', - }), - - exposeDependency({dependency: '#parsedCommentaryEntries'}), - ], -}); - -function stubArtist(artistName = `Test Artist`) { - const artist = new Artist(); - artist.name = artistName; - - return artist; -} - -t.test(`withParsedCommentaryEntries: basic behavior`, t => { - t.plan(7); - - const artist1 = stubArtist(`Mobius Trip`); - const artist2 = stubArtist(`Hadron Kaleido`); - const artist3 = stubArtist('Homestuck'); - - const artistData = [artist1, artist2, artist3]; - - t.match(composite, { - expose: { - dependencies: ['from', 'artistData'], - }, - }); - - t.same(composite.expose.compute({ - artistData, - from: - `<i>Mobius Trip:</i>\n` + - `Some commentary.\n` + - `Very cool.\n`, - }), [ - { - artists: [artist1], - artistDisplayText: null, - annotation: null, - date: null, - accessDate: null, - accessKind: null, - secondDate: null, - dateKind: null, - body: `Some commentary.\nVery cool.`, - }, - ]); - - t.same(composite.expose.compute({ - artistData, - from: - `<i>Mobius Trip|Moo-bius Trip:</i> (music, art, 12 January 2015)\n` + - `First commentary entry.\n` + - `Very cool.\n` + - `<i>Hadron Kaleido|<b>[[artist:hadron-kaleido|The Ol' Hadron]]</b>:</i> (moral support, 4/4/2022)\n` + - `Second commentary entry. Yes. So cool.\n` + - `<i>Mystery Artist:</i> (pingas, August 25, 2023)\n` + - `Oh no.. Oh dear...\n` + - `<i>Mobius Trip, Hadron Kaleido:</i>\n` + - `And back around we go.`, - }), [ - { - artists: [artist1], - artistDisplayText: `Moo-bius Trip`, - annotation: `music, art`, - date: new Date('12 January 2015'), - body: `First commentary entry.\nVery cool.`, - secondDate: null, - dateKind: null, - accessDate: null, - accessKind: null, - }, - { - artists: [artist2], - artistDisplayText: `<b>[[artist:hadron-kaleido|The Ol' Hadron]]</b>`, - annotation: `moral support`, - date: new Date('4 April 2022'), - body: `Second commentary entry. Yes. So cool.`, - secondDate: null, - dateKind: null, - accessDate: null, - accessKind: null, - }, - { - artists: [], - artistDisplayText: null, - annotation: `pingas`, - date: new Date('25 August 2023'), - body: `Oh no.. Oh dear...`, - secondDate: null, - dateKind: null, - accessDate: null, - accessKind: null, - }, - { - artists: [artist1, artist2], - artistDisplayText: null, - annotation: null, - date: null, - body: `And back around we go.`, - secondDate: null, - dateKind: null, - accessDate: null, - accessKind: null, - }, - ]); - - t.same(composite.expose.compute({ - artistData, - from: - `<i>Homestuck:</i> ([Bandcamp credits blurb](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/track/sburban-countdown-3) on "Homestuck Vol. 1-4 (with Midnight Crew: Drawing Dead)", 10/25/2019)\n` + - `\n` + - `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` + - `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]\n` + - `\n` + - `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 captured 4/13/2024)\n` + - `This isn't real!\n` + - `\n` + - `<i>Homestuck:</i> ([fake](https://homestuck.com/fake), 10/25/2011 accessed 10/27/2011)\n` + - `This isn't real either!\n` + - `\n` + - `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 accessed 4/13/2024)\n` + - `Not this one, neither!\n` - }), [ - { - artists: [artist3], - artistDisplayText: null, - annotation: `[Bandcamp credits blurb](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/track/sburban-countdown-3) on "Homestuck Vol. 1-4 (with Midnight Crew: Drawing Dead)"`, - date: new Date('10/25/2019'), - body: - `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` + - `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]`, - secondDate: null, - dateKind: null, - accessDate: new Date('10/24/2020'), - accessKind: 'captured', - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`, - date: new Date('7/20/2019'), - body: `This isn't real!`, - secondDate: null, - dateKind: null, - accessDate: new Date('4/13/2024'), - accessKind: 'captured', - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: `[fake](https://homestuck.com/fake)`, - date: new Date('10/25/2011'), - body: `This isn't real either!`, - secondDate: null, - dateKind: null, - accessDate: new Date('10/27/2011'), - accessKind: 'accessed', - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`, - date: new Date('7/20/2019'), - body: `Not this one, neither!`, - secondDate: null, - dateKind: null, - accessDate: new Date('4/13/2024'), - accessKind: 'accessed', - }, - ]); - - t.same(composite.expose.compute({ - artistData, - from: - `<i>Homestuck:</i> ([MSPA sound credits](https://web.archive.org/web/20120805031705/http://www.mspaintadventures.com:80/soundcredits.html), sometime 6/21/2012 - 8/5/2012)\n` + - `\n` + - `[[flash:246|Page 2146]] - <b>"Sburban Countdown"</b><br>\n` + - `Available on Bandcamp in [[album:homestuck-vol-1-4|Homestuck Vol. 1-4]]<br>\n` + - `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` + - `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]\n` + - `\n` + - `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 - 7/20/2022 captured 4/13/2024)\n` + - `It's goin' once.\n` + - `\n` + - `<i>Homestuck:</i> (10/25/2011 - 10/28/2011 accessed 10/27/2011)\n` + - `It's goin' twice.\n` + - `\n` + - `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 - 7/20/2022 accessed 4/13/2024)\n` + - `It's goin' thrice!\n` - }), [ - { - artists: [artist3], - artistDisplayText: null, - annotation: `[MSPA sound credits](https://web.archive.org/web/20120805031705/http://www.mspaintadventures.com:80/soundcredits.html)`, - body: - `[[flash:246|Page 2146]] - <b>"Sburban Countdown"</b><br>\n` + - `Available on Bandcamp in [[album:homestuck-vol-1-4|Homestuck Vol. 1-4]]<br>\n` + - `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` + - `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]`, - date: new Date('6/21/2012'), - secondDate: new Date('8/5/2012'), - dateKind: 'sometime', - accessDate: new Date('8/5/2012'), - accessKind: 'captured', - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`, - body: `It's goin' once.`, - date: new Date('7/20/2019'), - secondDate: new Date('7/20/2022'), - dateKind: null, - accessDate: new Date('4/13/2024'), - accessKind: 'captured', - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: '', // TODO: This should be null, but the regex isn't structured for that, at the moment. - body: `It's goin' twice.`, - date: new Date('10/25/2011'), - secondDate: new Date('10/28/2011'), - dateKind: null, - accessDate: new Date('10/27/2011'), - accessKind: 'accessed', - - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`, - body: `It's goin' thrice!`, - date: new Date('7/20/2019'), - secondDate: new Date('7/20/2022'), - dateKind: null, - accessDate: new Date('4/13/2024'), - accessKind: 'accessed', - }, - ]); - - t.same(composite.expose.compute({ - artistData, - from: - `<i>Homestuck:</i> ([MSPA sound credits](https://web.archive.org/web/20120805031705/http://www.mspaintadventures.com:80/soundcredits.html), sometime 6/21/2012 - 8/5/2012)\n` + - `\n` + - `[[flash:246|Page 2146]] - <b>"Sburban Countdown"</b><br>\n` + - `Available on Bandcamp in [[album:homestuck-vol-1-4|Homestuck Vol. 1-4]]<br>\n` + - `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` + - `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]\n` + - `\n` + - `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 - 7/20/2022 captured 4/13/2024)\n` + - `It's goin' once.\n` + - `\n` + - `<i>Homestuck:</i> (10/25/2011 - 10/28/2011 accessed 10/27/2011)\n` + - `It's goin' twice.\n` + - `\n` + - `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 - 7/20/2022 accessed 4/13/2024)\n` + - `It's goin' thrice!\n` - }), [ - { - artists: [artist3], - artistDisplayText: null, - annotation: `[MSPA sound credits](https://web.archive.org/web/20120805031705/http://www.mspaintadventures.com:80/soundcredits.html)`, - body: - `[[flash:246|Page 2146]] - <b>"Sburban Countdown"</b><br>\n` + - `Available on Bandcamp in [[album:homestuck-vol-1-4|Homestuck Vol. 1-4]]<br>\n` + - `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` + - `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]`, - date: new Date('6/21/2012'), - secondDate: new Date('8/5/2012'), - dateKind: 'sometime', - accessDate: new Date('8/5/2012'), - accessKind: 'captured', - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`, - body: `It's goin' once.`, - date: new Date('7/20/2019'), - secondDate: new Date('7/20/2022'), - dateKind: null, - accessDate: new Date('4/13/2024'), - accessKind: 'captured', - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: '', // TODO: This should be null, but the regex isn't structured for that, at the moment. - body: `It's goin' twice.`, - date: new Date('10/25/2011'), - secondDate: new Date('10/28/2011'), - dateKind: null, - accessDate: new Date('10/27/2011'), - accessKind: 'accessed', - - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`, - body: `It's goin' thrice!`, - date: new Date('7/20/2019'), - secondDate: new Date('7/20/2022'), - dateKind: null, - accessDate: new Date('4/13/2024'), - accessKind: 'accessed', - }, - ]); - - t.same(composite.expose.compute({ - artistData, - from: - `<i>Homestuck:</i> ([Homestuck sound credits](https://web.archive.org/web/20180717171235/https://www.homestuck.com/credits/sound), excerpt, around 4/3/2018)\n` + - `blablabla\n` + - `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), around 7/20/2019 - 7/20/2022 captured 4/13/2024)\n` + - `Snoopin', snoopin', snoo,\n` + - `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), throughout 7/20/2019 - 7/20/2022 accessed 4/13/2024)\n` + - `~ pingas ~\n` - }), [ - { - artists: [artist3], - artistDisplayText: null, - annotation: `[Homestuck sound credits](https://web.archive.org/web/20180717171235/https://www.homestuck.com/credits/sound), excerpt`, - body: `blablabla`, - date: new Date('4/3/2018'), - secondDate: null, - dateKind: 'around', - accessDate: new Date('7/17/2018'), - accessKind: 'captured', - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`, - body: `Snoopin', snoopin', snoo,`, - date: new Date('7/20/2019'), - secondDate: new Date('7/20/2022'), - dateKind: 'around', - accessDate: new Date('4/13/2024'), - accessKind: 'captured', - }, - { - artists: [artist3], - artistDisplayText: null, - annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`, - body: `~ pingas ~`, - date: new Date('7/20/2019'), - secondDate: new Date('7/20/2022'), - dateKind: 'throughout', - accessDate: new Date('4/13/2024'), - accessKind: 'accessed', - }, - ]); -}); diff --git a/test/unit/data/things/album.js b/test/unit/data/things/album.js deleted file mode 100644 index a64488f7..00000000 --- a/test/unit/data/things/album.js +++ /dev/null @@ -1,469 +0,0 @@ -import t from 'tap'; - -import thingConstructors from '#things'; - -import { - linkAndBindWikiData, - stubArtistAndContribs, - stubThing, - stubWikiData, -} from '#test-lib'; - -t.test(`Album.artTags`, t => { - const {Album, ArtTag} = thingConstructors; - - t.plan(3); - - const wikiData = stubWikiData(); - - const {contribs} = stubArtistAndContribs(wikiData); - const album = stubThing(wikiData, Album); - const tag1 = stubThing(wikiData, ArtTag, {name: `Tag 1`}); - const tag2 = stubThing(wikiData, ArtTag, {name: `Tag 2`}); - - linkAndBindWikiData(wikiData); - - t.same(album.artTags, [], - `artTags #1: defaults to empty array`); - - album.artTags = [`Tag 1`, `Tag 2`]; - - t.same(album.artTags, [], - `artTags #2: is empty if album doesn't have cover artists`); - - album.coverArtistContribs = contribs; - - t.same(album.artTags, [tag1, tag2], - `artTags #3: resolves if album has cover artists`); -}); - -t.test(`Album.bannerDimensions`, t => { - const {Album} = thingConstructors; - - t.plan(4); - - const wikiData = stubWikiData(); - - const album = stubThing(wikiData, Album); - const {contribs, badContribs} = stubArtistAndContribs(wikiData); - - linkAndBindWikiData(wikiData); - - t.equal(album.bannerDimensions, null, - `Album.bannerDimensions #1: defaults to null`); - - album.bannerDimensions = [1200, 275]; - - t.equal(album.bannerDimensions, null, - `Album.bannerDimensions #2: is null if bannerArtistContribs empty`); - - album.bannerArtistContribs = badContribs; - - t.equal(album.bannerDimensions, null, - `Album.bannerDimensions #3: is null if bannerArtistContribs resolves empty`); - - album.bannerArtistContribs = contribs; - - t.same(album.bannerDimensions, [1200, 275], - `Album.bannerDimensions #4: is own value`); -}); - -t.test(`Album.bannerFileExtension`, t => { - const {Album} = thingConstructors; - - t.plan(5); - - const wikiData = stubWikiData(); - - const album = stubThing(wikiData, Album); - const {contribs, badContribs} = stubArtistAndContribs(wikiData); - - linkAndBindWikiData(wikiData); - - t.equal(album.bannerFileExtension, null, - `Album.bannerFileExtension #1: defaults to null`); - - album.bannerFileExtension = 'png'; - - t.equal(album.bannerFileExtension, null, - `Album.bannerFileExtension #2: is null if bannerArtistContribs empty`); - - album.bannerArtistContribs = badContribs; - - t.equal(album.bannerFileExtension, null, - `Album.bannerFileExtension #3: is null if bannerArtistContribs resolves empty`); - - album.bannerArtistContribs = contribs; - - t.equal(album.bannerFileExtension, 'png', - `Album.bannerFileExtension #4: is own value`); - - album.bannerFileExtension = null; - - t.equal(album.bannerFileExtension, 'jpg', - `Album.bannerFileExtension #5: defaults to jpg`); -}); - -t.test(`Album.bannerStyle`, t => { - const {Album} = thingConstructors; - - t.plan(4); - - const wikiData = stubWikiData(); - - const album = stubThing(wikiData, Album); - const {contribs, badContribs} = stubArtistAndContribs(wikiData); - - linkAndBindWikiData(wikiData); - - t.equal(album.bannerStyle, null, - `Album.bannerStyle #1: defaults to null`); - - album.bannerStyle = `opacity: 0.5`; - - t.equal(album.bannerStyle, null, - `Album.bannerStyle #2: is null if bannerArtistContribs empty`); - - album.bannerArtistContribs = badContribs; - - t.equal(album.bannerStyle, null, - `Album.bannerStyle #3: is null if bannerArtistContribs resolves empty`); - - album.bannerArtistContribs = contribs; - - t.equal(album.bannerStyle, `opacity: 0.5`, - `Album.bannerStyle #4: is own value`); -}); - -t.test(`Album.coverArtDate`, t => { - const {Album} = thingConstructors; - - t.plan(6); - - const wikiData = stubWikiData(); - - const album = stubThing(wikiData, Album); - const {contribs, badContribs} = stubArtistAndContribs(wikiData); - - linkAndBindWikiData(wikiData); - - t.equal(album.coverArtDate, null, - `Album.coverArtDate #1: defaults to null`); - - album.date = new Date('2012-10-25'); - - t.equal(album.coverArtDate, null, - `Album.coverArtDate #2: is null if coverArtistContribs empty (1/2)`); - - album.coverArtDate = new Date('2011-04-13'); - - t.equal(album.coverArtDate, null, - `Album.coverArtDate #3: is null if coverArtistContribs empty (2/2)`); - - album.coverArtistContribs = contribs; - - t.same(album.coverArtDate, new Date('2011-04-13'), - `Album.coverArtDate #4: is own value`); - - album.coverArtDate = null; - - t.same(album.coverArtDate, new Date(`2012-10-25`), - `Album.coverArtDate #5: inherits album release date`); - - album.coverArtistContribs = badContribs; - - t.equal(album.coverArtDate, null, - `Album.coverArtDate #6: is null if coverArtistContribs resolves empty`); -}); - -t.test(`Album.coverArtFileExtension`, t => { - const {Album} = thingConstructors; - - t.plan(5); - - const wikiData = stubWikiData(); - - const album = stubThing(wikiData, Album); - const {contribs, badContribs} = stubArtistAndContribs(wikiData); - - linkAndBindWikiData(wikiData); - - t.equal(album.coverArtFileExtension, null, - `Album.coverArtFileExtension #1: is null if coverArtistContribs empty (1/2)`); - - album.coverArtFileExtension = 'png'; - - t.equal(album.coverArtFileExtension, null, - `Album.coverArtFileExtension #2: is null if coverArtistContribs empty (2/2)`); - - album.coverArtFileExtension = null; - album.coverArtistContribs = contribs; - - t.equal(album.coverArtFileExtension, 'jpg', - `Album.coverArtFileExtension #3: defaults to jpg`); - - album.coverArtFileExtension = 'png'; - - t.equal(album.coverArtFileExtension, 'png', - `Album.coverArtFileExtension #4: is own value`); - - album.coverArtistContribs = badContribs; - - t.equal(album.coverArtFileExtension, null, - `Album.coverArtFileExtension #5: is null if coverArtistContribs resolves empty`); -}); - -t.test(`Album.tracks`, t => { - const {Album, Track, TrackSection} = thingConstructors; - - t.plan(4); - - let wikiData = stubWikiData(); - - const album = stubThing(wikiData, Album); - album.directory = 'foo'; - - const track1 = stubThing(wikiData, Track, {directory: 'track1'}); - const track2 = stubThing(wikiData, Track, {directory: 'track2'}); - const track3 = stubThing(wikiData, Track, {directory: 'track3'}); - const tracks = [track1, track2, track3]; - - const section1 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section1'}); - const section2 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section2'}); - const section3 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section3'}); - const section4 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section4'}); - const section5 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section5'}); - const section6 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section6'}); - const sections = [section1, section2, section3, section4, section5, section6]; - - wikiData = null; - - for (const track of tracks) { - track.albumData = [album]; - } - - for (const section of sections) { - section.albumData = [album]; - } - - t.same(album.tracks, [], - `Album.tracks #1: defaults to empty array`); - - section1.tracks = [track1, track2, track3]; - - album.trackSections = [section1]; - - t.same(album.tracks, [track1, track2, track3], - `Album.tracks #2: pulls tracks from one track section`); - - section1.tracks = [track1]; - section2.tracks = [track2, track3]; - - album.trackSections = [section1, section2]; - - t.same(album.tracks, [track1, track2, track3], - `Album.tracks #3: pulls tracks from multiple track sections`); - - section1.tracks = [track1]; - section2.tracks = []; - section3.tracks = [track2]; - section4.tracks = []; - section5.tracks = []; - section6.tracks = [track3]; - - album.trackSections = [section1, section2, section3, section4, section5, section6]; - - t.same(album.tracks, [track1, track2, track3], - `Album.tracks #4: skips empty track sections`); -}); - -t.test(`Album.trackSections`, t => { - const {Album, Track, TrackSection} = thingConstructors; - - t.plan(7); - - let wikiData = stubWikiData(); - - const album = stubThing(wikiData, Album); - - const track1 = stubThing(wikiData, Track, {directory: 'track1'}); - const track2 = stubThing(wikiData, Track, {directory: 'track2'}); - const track3 = stubThing(wikiData, Track, {directory: 'track3'}); - const track4 = stubThing(wikiData, Track, {directory: 'track4'}); - const tracks = [track1, track2, track3, track4]; - - const section1 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section1'}); - const section2 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section2'}); - const section3 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section3'}); - const section4 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section4'}); - const section5 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section5'}); - const sections = [section1, section2, section3, section4, section5]; - - wikiData = null; - - for (const track of tracks) { - track.albumData = [album]; - } - - for (const section of sections) { - section.albumData = [album]; - } - - section1.tracks = [track1, track2]; - section2.tracks = [track3, track4]; - - album.trackSections = [section1, section2]; - - t.match(album.trackSections, [ - {tracks: [track1, track2]}, - {tracks: [track3, track4]}, - ], `Album.trackSections #1: exposes tracks`); - - t.match(album.trackSections, [ - {tracks: [track1, track2], startIndex: 0}, - {tracks: [track3, track4], startIndex: 2}, - ], `Album.trackSections #2: exposes startIndex`); - - section1.tracks = [track1]; - section2.tracks = [track2]; - section3.tracks = [track3]; - - section1.name = 'First section'; - section2.name = 'Second section'; - - album.trackSections = [section1, section2, section3]; - - t.match(album.trackSections, [ - {name: 'First section', tracks: [track1]}, - {name: 'Second section', tracks: [track2]}, - {name: 'Unnamed Track Section', tracks: [track3]}, - ], `Album.trackSections #3: exposes name, with fallback value`); - - album.color = '#123456'; - - section2.color = '#abcdef'; - - // XXX_decacheWikiData - album.trackSections = []; - album.trackSections = [section1, section2, section3]; - - t.match(album.trackSections, [ - {tracks: [track1], color: '#123456'}, - {tracks: [track2], color: '#abcdef'}, - {tracks: [track3], color: '#123456'}, - ], `Album.trackSections #4: exposes color, inherited from album`); - - section2.dateOriginallyReleased = new Date('2009-04-11'); - - // XXX_decacheWikiData - album.trackSections = []; - album.trackSections = [section1, section2, section3]; - - t.match(album.trackSections, [ - {tracks: [track1], dateOriginallyReleased: null}, - {tracks: [track2], dateOriginallyReleased: new Date('2009-04-11')}, - {tracks: [track3], dateOriginallyReleased: null}, - ], `Album.trackSections #5: exposes dateOriginallyReleased, if present`); - - section1.isDefaultTrackSection = true; - section2.isDefaultTrackSection = false; - - // XXX_decacheWikiData - album.trackSections = []; - album.trackSections = [section1, section2, section3]; - - t.match(album.trackSections, [ - {tracks: [track1], isDefaultTrackSection: true}, - {tracks: [track2], isDefaultTrackSection: false}, - {tracks: [track3], isDefaultTrackSection: false}, - ], `Album.trackSections #6: exposes isDefaultTrackSection, defaults to false`); - - section1.tracks = [track1, track2]; - section2.tracks = [track3]; - section3.tracks = []; - section4.tracks = []; - section5.tracks = [track4]; - - section1.color = '#112233'; - section2.color = '#334455'; - section3.color = '#bbbbba'; - section4.color = '#556677'; - section5.color = '#778899'; - - album.trackSections = [section1, section2, section3, section4, section5]; - - t.match(album.trackSections, [ - {tracks: [track1, track2], color: '#112233'}, - {tracks: [track3], color: '#334455'}, - {tracks: [], color: '#bbbbba'}, - {tracks: [], color: '#556677'}, - {tracks: [track4], color: '#778899'}, - ], `Album.trackSections #7: keeps empty sections`); -}); - -t.test(`Album.wallpaperFileExtension`, t => { - const {Album} = thingConstructors; - - t.plan(5); - - const wikiData = stubWikiData(); - - const album = stubThing(wikiData, Album); - const {contribs, badContribs} = stubArtistAndContribs(wikiData); - - linkAndBindWikiData(wikiData); - - t.equal(album.wallpaperFileExtension, null, - `Album.wallpaperFileExtension #1: defaults to null`); - - album.wallpaperFileExtension = 'png'; - - t.equal(album.wallpaperFileExtension, null, - `Album.wallpaperFileExtension #2: is null if wallpaperArtistContribs empty`); - - album.wallpaperArtistContribs = contribs; - - t.equal(album.wallpaperFileExtension, 'png', - `Album.wallpaperFileExtension #3: is own value`); - - album.wallpaperFileExtension = null; - - t.equal(album.wallpaperFileExtension, 'jpg', - `Album.wallpaperFileExtension #4: defaults to jpg`); - - album.wallpaperArtistContribs = badContribs; - - t.equal(album.wallpaperFileExtension, null, - `Album.wallpaperFileExtension #5: is null if wallpaperArtistContribs resolves empty`); -}); - -t.test(`Album.wallpaperStyle`, t => { - const {Album} = thingConstructors; - - t.plan(4); - - const wikiData = stubWikiData(); - - const album = stubThing(wikiData, Album); - const {contribs, badContribs} = stubArtistAndContribs(wikiData); - - linkAndBindWikiData(wikiData); - - t.equal(album.wallpaperStyle, null, - `Album.wallpaperStyle #1: defaults to null`); - - album.wallpaperStyle = `opacity: 0.5`; - - t.equal(album.wallpaperStyle, null, - `Album.wallpaperStyle #2: is null if wallpaperArtistContribs empty`); - - album.wallpaperArtistContribs = badContribs; - - t.equal(album.wallpaperStyle, null, - `Album.wallpaperStyle #3: is null if wallpaperArtistContribs resolves empty`); - - album.wallpaperArtistContribs = contribs; - - t.equal(album.wallpaperStyle, `opacity: 0.5`, - `Album.wallpaperStyle #4: is own value`); -}); diff --git a/test/unit/data/things/art-tag.js b/test/unit/data/things/art-tag.js deleted file mode 100644 index 015acd0e..00000000 --- a/test/unit/data/things/art-tag.js +++ /dev/null @@ -1,26 +0,0 @@ -import t from 'tap'; - -import thingConstructors from '#things'; - -t.test(`ArtTag.nameShort`, t => { - const {ArtTag} = thingConstructors; - - t.plan(3); - - const artTag = new ArtTag(); - - artTag.name = `Dave Strider`; - - t.equal(artTag.nameShort, `Dave Strider`, - `ArtTag #1: defaults to name`); - - artTag.name = `Dave Strider (Homestuck)`; - - t.equal(artTag.nameShort, `Dave Strider`, - `ArtTag #2: trims parenthical part at end`); - - artTag.name = `This (And) That (Then)`; - - t.equal(artTag.nameShort, `This (And) That`, - `ArtTag #2: doesn't trim midlde parenthical part`); -}); diff --git a/test/unit/data/things/flash.js b/test/unit/data/things/flash.js deleted file mode 100644 index de39e80d..00000000 --- a/test/unit/data/things/flash.js +++ /dev/null @@ -1,40 +0,0 @@ -import t from 'tap'; - -import thingConstructors from '#things'; - -import { - linkAndBindWikiData, - stubThing, - stubWikiData, -} from '#test-lib'; - -t.test(`Flash.color`, t => { - const {Flash, FlashAct} = thingConstructors; - - t.plan(4); - - const wikiData = stubWikiData(); - - const flash = stubThing(wikiData, Flash, {directory: 'my-flash'}); - const flashAct = stubThing(wikiData, FlashAct, {flashes: ['flash:my-flash']}); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.equal(flash.color, null, - `color #1: defaults to null`); - - flashAct.color = '#abcdef'; - XXX_decacheWikiData(); - - t.equal(flash.color, '#abcdef', - `color #2: inherits from flash act`); - - flash.color = '#123456'; - - t.equal(flash.color, '#123456', - `color #3: is own value`); - - t.throws(() => { flash.color = '#aeiouw'; }, - {cause: TypeError}, - `color #4: must be set to valid color`); -}); diff --git a/test/unit/data/things/track.js b/test/unit/data/things/track.js deleted file mode 100644 index 403dc064..00000000 --- a/test/unit/data/things/track.js +++ /dev/null @@ -1,752 +0,0 @@ -import t from 'tap'; - -import {bindFind} from '#find'; -import thingConstructors from '#things'; - -import { - linkAndBindWikiData, - stubArtistAndContribs, - stubFlashAndAct, - stubThing, - stubTrackAndAlbum, - stubWikiData, -} from '#test-lib'; - -t.test(`Track.album`, t => { - const {Album, Track, TrackSection} = thingConstructors; - - t.plan(6); - - // Note: These asserts use manual albumData/trackData relationships - // to illustrate more specifically the properties which are expected to - // be relevant for this case. Other properties use the same underlying - // get-album behavior as Track.album so aren't tested as aggressively. - - let wikiData = stubWikiData(); - - const track1 = stubThing(wikiData, Track, {directory: 'track1'}); - const track2 = stubThing(wikiData, Track, {directory: 'track2'}); - const album1 = stubThing(wikiData, Album); - const album2 = stubThing(wikiData, Album); - const section1 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section1'}); - const section2 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section2'}); - - wikiData = null; - - t.equal(track1.album, null, - `album #1: defaults to null`); - - track1.albumData = [album1, album2]; - track2.albumData = [album1, album2]; - section1.tracks = [track1]; - section2.tracks = [track2]; - section1.albumData = [album1]; - section2.albumData = [album2]; - album1.trackSections = [section1]; - album2.trackSections = [section2]; - - t.equal(track1.album, album1, - `album #2: is album when album's trackSections matches track`); - - track1.albumData = [album2, album1]; - - t.equal(track1.album, album1, - `album #3: is album when albumData is in different order`); - - track1.albumData = []; - - t.equal(track1.album, null, - `album #4: is null when track missing albumData`); - - section1.tracks = []; - - // XXX_decacheWikiData - album1.trackSections = []; - album1.trackSections = [section1]; - track1.albumData = []; - track1.albumData = [album2, album1]; - - t.equal(track1.album, null, - `album #5: is null when album track section missing tracks`); - - section1.tracks = [track2]; - - // XXX_decacheWikiData - album1.trackSections = []; - album1.trackSections = [section1]; - track1.albumData = []; - track1.albumData = [album2, album1]; - - t.equal(track1.album, null, - `album #6: is null when album track section doesn't match track`); -}); - -t.test(`Track.alwaysReferenceByDirectory`, t => { - t.plan(7); - - const wikiData = stubWikiData(); - - const {track: originalTrack} = - stubTrackAndAlbum(wikiData, 'original-track', 'original-album'); - - const {track: rereleaseTrack, album: rereleaseAlbum} = - stubTrackAndAlbum(wikiData, 'rerelease-track', 'rerelease-album'); - - originalTrack.name = 'Cowabunga'; - rereleaseTrack.name = 'Cowabunga'; - - originalTrack.dataSourceAlbum = 'album:original-album'; - rereleaseTrack.dataSourceAlbum = 'album:rerelease-album'; - - rereleaseTrack.originalReleaseTrack = 'track:original-track'; - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.equal(originalTrack.alwaysReferenceByDirectory, false, - `alwaysReferenceByDirectory #1: defaults to false`); - - t.equal(rereleaseTrack.alwaysReferenceByDirectory, true, - `alwaysReferenceByDirectory #2: is true if rerelease name matches original`); - - rereleaseTrack.name = 'Foo Dog!'; - - t.equal(rereleaseTrack.alwaysReferenceByDirectory, false, - `alwaysReferenceByDirectory #3: is false if rerelease name doesn't match original`); - - rereleaseTrack.name = `COWabunga`; - - t.equal(rereleaseTrack.alwaysReferenceByDirectory, false, - `alwaysReferenceByDirectory #4: is false if rerelease name doesn't match original exactly`); - - rereleaseAlbum.alwaysReferenceTracksByDirectory = true; - XXX_decacheWikiData(); - - t.equal(rereleaseTrack.alwaysReferenceByDirectory, true, - `alwaysReferenceByDirectory #5: is true if album's alwaysReferenceTracksByDirectory is true`); - - rereleaseTrack.alwaysReferenceByDirectory = false; - - t.equal(rereleaseTrack.alwaysReferenceByDirectory, false, - `alwaysReferenceByDirectory #6: doesn't inherit from album if set to false`); - - rereleaseTrack.name = 'Cowabunga'; - - t.equal(rereleaseTrack.alwaysReferenceByDirectory, false, - `alwaysReferenceByDirectory #7: doesn't compare original release name if set to false`); -}); - -t.test(`Track.artTags`, t => { - const {ArtTag} = thingConstructors; - - t.plan(6); - - const wikiData = stubWikiData(); - - const {track, album} = stubTrackAndAlbum(wikiData); - const {contribs} = stubArtistAndContribs(wikiData); - - const tag1 = - stubThing(wikiData, ArtTag, {name: `Tag 1`}); - - const tag2 = - stubThing(wikiData, ArtTag, {name: `Tag 2`}); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.same(track.artTags, [], - `artTags #1: defaults to empty array`); - - track.artTags = [`Tag 1`, `Tag 2`]; - - t.same(track.artTags, [], - `artTags #2: is empty if track doesn't have cover artists`); - - track.coverArtistContribs = contribs; - - t.same(track.artTags, [tag1, tag2], - `artTags #3: resolves if track has cover artists`); - - track.coverArtistContribs = null; - album.trackCoverArtistContribs = contribs; - - XXX_decacheWikiData(); - - t.same(track.artTags, [tag1, tag2], - `artTags #4: resolves if track inherits cover artists`); - - track.disableUniqueCoverArt = true; - - t.same(track.artTags, [], - `artTags #5: is empty if track disables unique cover artwork`); - - album.coverArtistContribs = contribs; - album.artTags = [`Tag 2`]; - - XXX_decacheWikiData(); - - t.notSame(track.artTags, [tag2], - `artTags #6: doesn't inherit from album's art tags`); -}); - -t.test(`Track.artistContribs`, t => { - const {Album, Artist, Track, TrackSection} = thingConstructors; - - t.plan(4); - - const wikiData = stubWikiData(); - - const track = - stubThing(wikiData, Track); - - const section = - stubThing(wikiData, TrackSection, {tracks: [track]}); - - const album = - stubThing(wikiData, Album, {trackSections: [section]}); - - const artist1 = - stubThing(wikiData, Artist, {name: `Artist 1`}); - - const artist2 = - stubThing(wikiData, Artist, {name: `Artist 2`}); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.same(track.artistContribs, [], - `artistContribs #1: defaults to empty array`); - - album.artistContribs = [ - {artist: `Artist 1`, annotation: `composition`}, - {artist: `Artist 2`, annotation: null}, - ]; - - XXX_decacheWikiData(); - - t.match(track.artistContribs, - [{artist: artist1, annotation: `composition`}, {artist: artist2, annotation: null}], - `artistContribs #2: inherits album artistContribs`); - - track.artistContribs = [ - {artist: `Artist 1`, annotation: `arrangement`}, - ]; - - t.match(track.artistContribs, [{artist: artist1, annotation: `arrangement`}], - `artistContribs #3: resolves from own value`); - - track.artistContribs = [ - {artist: `Artist 1`, annotation: `snooping`}, - {artist: `Artist 413`, annotation: `as`}, - {artist: `Artist 2`, annotation: `usual`}, - ]; - - t.match(track.artistContribs, - [{artist: artist1, annotation: `snooping`}, {artist: artist2, annotation: `usual`}], - `artistContribs #4: filters out names without matches`); -}); - -t.test(`Track.color`, t => { - t.plan(4); - - const wikiData = stubWikiData(); - - const {track, album, section} = stubTrackAndAlbum(wikiData); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.equal(track.color, null, - `color #1: defaults to null`); - - album.color = '#abcdef'; - section.color = '#beeeef'; - - album.trackSections = [section]; - - XXX_decacheWikiData(); - - t.equal(track.color, '#beeeef', - `color #2: inherits from track section before album`); - - track.color = '#123456'; - - t.equal(track.color, '#123456', - `color #3: is own value`); - - t.throws(() => { track.color = '#aeiouw'; }, - {cause: TypeError}, - `color #4: must be set to valid color`); -}); - -t.test(`Track.commentatorArtists`, t => { - const {Artist, Track} = thingConstructors; - - t.plan(8); - - const wikiData = stubWikiData(); - - const track = stubThing(wikiData, Track); - const artist1 = stubThing(wikiData, Artist, {name: `SnooPING`}); - const artist2 = stubThing(wikiData, Artist, {name: `ASUsual`}); - const artist3 = stubThing(wikiData, Artist, {name: `Icy`}); - - linkAndBindWikiData(wikiData); - - // Keep track of the last commentary string in a separate value, since - // the track.commentary property exposes as a completely different format - // (i.e. an array of objects, one for each entry), and so isn't compatible - // with the += operator on its own. - let commentary; - - track.commentary = commentary = - `<i>SnooPING:</i>\n` + - `Wow.\n`; - - t.same(track.commentatorArtists, [artist1], - `Track.commentatorArtists #1: works with one commentator`); - - track.commentary = commentary += - `<i>ASUsual:</i>\n` + - `Yes!\n`; - - t.same(track.commentatorArtists, [artist1, artist2], - `Track.commentatorArtists #2: works with two commentators`); - - track.commentary = commentary += - `<i>Icy|<b>Icy annotation You Did There</b>:</i>\n` + - `Incredible.\n`; - - t.same(track.commentatorArtists, [artist1, artist2, artist3], - `Track.commentatorArtists #3: works with custom artist text`); - - track.commentary = commentary = - `<i>Icy:</i> (project manager)\n` + - `Very good track.\n`; - - t.same(track.commentatorArtists, [artist3], - `Track.commentatorArtists #4: works with annotation`); - - track.commentary = commentary = - `<i>Icy:</i> (project manager, 08/15/2023)\n` + - `Very very good track.\n`; - - t.same(track.commentatorArtists, [artist3], - `Track.commentatorArtists #5: works with date`); - - track.commentary = commentary += - `<i>Ohohohoho:</i>\n` + - `OHOHOHOHOHOHO...\n`; - - t.same(track.commentatorArtists, [artist3], - `Track.commentatorArtists #6: ignores artist names not found`); - - track.commentary = commentary += - `<i>Icy:</i>\n` + - `I'm back!\n`; - - t.same(track.commentatorArtists, [artist3], - `Track.commentatorArtists #7: ignores duplicate artist`); - - track.commentary = commentary += - `<i>SNooPING, ASUsual, Icy:</i>\n` + - `WITH ALL THREE POWERS COMBINED...`; - - t.same(track.commentatorArtists, [artist3, artist1, artist2], - `Track.commentatorArtists #8: works with more than one artist in one entry`); -}); - -t.test(`Track.coverArtistContribs`, t => { - const {Artist} = thingConstructors; - - t.plan(5); - - const wikiData = stubWikiData(); - - const {track, album} = stubTrackAndAlbum(wikiData); - const artist1 = stubThing(wikiData, Artist, {name: `Artist 1`}); - const artist2 = stubThing(wikiData, Artist, {name: `Artist 2`}); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.same(track.coverArtistContribs, [], - `coverArtistContribs #1: defaults to empty array`); - - album.trackCoverArtistContribs = [ - {artist: `Artist 1`, annotation: `lines`}, - {artist: `Artist 2`, annotation: null}, - ]; - - XXX_decacheWikiData(); - - t.match(track.coverArtistContribs, - [{artist: artist1, annotation: `lines`}, {artist: artist2, annotation: null}], - `coverArtistContribs #2: inherits album trackCoverArtistContribs`); - - track.coverArtistContribs = [ - {artist: `Artist 1`, annotation: `collage`}, - ]; - - t.match(track.coverArtistContribs, [{artist: artist1, annotation: `collage`}], - `coverArtistContribs #3: resolves from own value`); - - track.coverArtistContribs = [ - {artist: `Artist 1`, annotation: `snooping`}, - {artist: `Artist 413`, annotation: `as`}, - {artist: `Artist 2`, annotation: `usual`}, - ]; - - t.match(track.coverArtistContribs, - [{artist: artist1, annotation: `snooping`}, {artist: artist2, annotation: `usual`}], - `coverArtistContribs #4: filters out names without matches`); - - track.disableUniqueCoverArt = true; - - t.same(track.coverArtistContribs, [], - `coverArtistContribs #5: is empty if track disables unique cover artwork`); -}); - -t.test(`Track.coverArtDate`, t => { - t.plan(8); - - const wikiData = stubWikiData(); - - const {track, album} = stubTrackAndAlbum(wikiData); - const {contribs, badContribs} = stubArtistAndContribs(wikiData); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - track.coverArtistContribs = contribs; - - t.equal(track.coverArtDate, null, - `coverArtDate #1: defaults to null`); - - album.trackArtDate = new Date('2012-12-12'); - - XXX_decacheWikiData(); - - t.same(track.coverArtDate, new Date('2012-12-12'), - `coverArtDate #2: inherits album trackArtDate`); - - track.coverArtDate = new Date('2009-09-09'); - - t.same(track.coverArtDate, new Date('2009-09-09'), - `coverArtDate #3: is own value`); - - track.coverArtistContribs = []; - - t.equal(track.coverArtDate, null, - `coverArtDate #4: is null if track coverArtistContribs empty`); - - album.trackCoverArtistContribs = contribs; - - XXX_decacheWikiData(); - - t.same(track.coverArtDate, new Date('2009-09-09'), - `coverArtDate #5: is not null if album trackCoverArtistContribs specified`); - - album.trackCoverArtistContribs = badContribs; - - XXX_decacheWikiData(); - - t.equal(track.coverArtDate, null, - `coverArtDate #6: is null if album trackCoverArtistContribs resolves empty`); - - track.coverArtistContribs = badContribs; - - t.equal(track.coverArtDate, null, - `coverArtDate #7: is null if track coverArtistContribs resolves empty`); - - track.coverArtistContribs = contribs; - track.disableUniqueCoverArt = true; - - t.equal(track.coverArtDate, null, - `coverArtDate #8: is null if track disables unique cover artwork`); -}); - -t.test(`Track.coverArtFileExtension`, t => { - t.plan(8); - - const wikiData = stubWikiData(); - - const {track, album} = stubTrackAndAlbum(wikiData); - const {contribs} = stubArtistAndContribs(wikiData); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.equal(track.coverArtFileExtension, null, - `coverArtFileExtension #1: defaults to null`); - - track.coverArtistContribs = contribs; - - t.equal(track.coverArtFileExtension, 'jpg', - `coverArtFileExtension #2: is jpg if has cover art and not further specified`); - - track.coverArtistContribs = []; - - album.coverArtistContribs = contribs; - XXX_decacheWikiData(); - - t.equal(track.coverArtFileExtension, null, - `coverArtFileExtension #3: only has value for unique cover art`); - - track.coverArtistContribs = contribs; - - album.trackCoverArtFileExtension = 'png'; - XXX_decacheWikiData(); - - t.equal(track.coverArtFileExtension, 'png', - `coverArtFileExtension #4: inherits album trackCoverArtFileExtension (1/2)`); - - track.coverArtFileExtension = 'gif'; - - t.equal(track.coverArtFileExtension, 'gif', - `coverArtFileExtension #5: is own value (1/2)`); - - track.coverArtistContribs = []; - - album.trackCoverArtistContribs = contribs; - XXX_decacheWikiData(); - - t.equal(track.coverArtFileExtension, 'gif', - `coverArtFileExtension #6: is own value (2/2)`); - - track.coverArtFileExtension = null; - - t.equal(track.coverArtFileExtension, 'png', - `coverArtFileExtension #7: inherits album trackCoverArtFileExtension (2/2)`); - - track.disableUniqueCoverArt = true; - - t.equal(track.coverArtFileExtension, null, - `coverArtFileExtension #8: is null if track disables unique cover art`); -}); - -t.test(`Track.date`, t => { - t.plan(3); - - const wikiData = stubWikiData(); - - const {track, album} = stubTrackAndAlbum(wikiData); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.equal(track.date, null, - `date #1: defaults to null`); - - album.date = new Date('2012-12-12'); - XXX_decacheWikiData(); - - t.same(track.date, album.date, - `date #2: inherits from album`); - - track.dateFirstReleased = new Date('2009-09-09'); - - t.same(track.date, new Date('2009-09-09'), - `date #3: is own dateFirstReleased`); -}); - -t.test(`Track.featuredInFlashes`, t => { - t.plan(2); - - const wikiData = stubWikiData(); - - const {track} = stubTrackAndAlbum(wikiData, 'track1'); - const {flash: flash1} = stubFlashAndAct(wikiData, 'flash1'); - const {flash: flash2} = stubFlashAndAct(wikiData, 'flash2'); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.same(track.featuredInFlashes, [], - `featuredInFlashes #1: defaults to empty array`); - - flash1.featuredTracks = ['track:track1']; - flash2.featuredTracks = ['track:track1']; - XXX_decacheWikiData(); - - t.same(track.featuredInFlashes, [flash1, flash2], - `featuredInFlashes #2: matches flashes' featuredTracks`); -}); - -t.test(`Track.hasUniqueCoverArt`, t => { - t.plan(7); - - const wikiData = stubWikiData(); - - const {track, album} = stubTrackAndAlbum(wikiData); - const {contribs, badContribs} = stubArtistAndContribs(wikiData); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.equal(track.hasUniqueCoverArt, false, - `hasUniqueCoverArt #1: defaults to false`); - - album.trackCoverArtistContribs = contribs; - XXX_decacheWikiData(); - - t.equal(track.hasUniqueCoverArt, true, - `hasUniqueCoverArt #2: is true if album specifies trackCoverArtistContribs`); - - track.disableUniqueCoverArt = true; - - t.equal(track.hasUniqueCoverArt, false, - `hasUniqueCoverArt #3: is false if disableUniqueCoverArt is true (1/2)`); - - track.disableUniqueCoverArt = false; - - album.trackCoverArtistContribs = badContribs; - XXX_decacheWikiData(); - - t.equal(track.hasUniqueCoverArt, false, - `hasUniqueCoverArt #4: is false if album's trackCoverArtistContribs resolve empty`); - - track.coverArtistContribs = contribs; - - t.equal(track.hasUniqueCoverArt, true, - `hasUniqueCoverArt #5: is true if track specifies coverArtistContribs`); - - track.disableUniqueCoverArt = true; - - t.equal(track.hasUniqueCoverArt, false, - `hasUniqueCoverArt #6: is false if disableUniqueCoverArt is true (2/2)`); - - track.disableUniqueCoverArt = false; - - track.coverArtistContribs = badContribs; - - t.equal(track.hasUniqueCoverArt, false, - `hasUniqueCoverArt #7: is false if track's coverArtistContribs resolve empty`); -}); - -t.test(`Track.originalReleaseTrack`, t => { - t.plan(3); - - const wikiData = stubWikiData(); - - const {track: track1} = stubTrackAndAlbum(wikiData, 'track1'); - const {track: track2} = stubTrackAndAlbum(wikiData, 'track2'); - - linkAndBindWikiData(wikiData); - - t.equal(track2.originalReleaseTrack, null, - `originalReleaseTrack #1: defaults to null`); - - track2.originalReleaseTrack = 'track:track1'; - - t.equal(track2.originalReleaseTrack, track1, - `originalReleaseTrack #2: is resolved from own value`); - - track2.trackData = []; - - t.equal(track2.originalReleaseTrack, null, - `originalReleaseTrack #3: is null when track missing trackData`); -}); - -t.test(`Track.otherReleases`, t => { - t.plan(6); - - const wikiData = stubWikiData(); - - const {track: track1} = stubTrackAndAlbum(wikiData, 'track1'); - const {track: track2} = stubTrackAndAlbum(wikiData, 'track2'); - const {track: track3} = stubTrackAndAlbum(wikiData, 'track3'); - const {track: track4} = stubTrackAndAlbum(wikiData, 'track4'); - - const {linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.same(track1.otherReleases, [], - `otherReleases #1: defaults to empty array`); - - track2.originalReleaseTrack = 'track:track1'; - track3.originalReleaseTrack = 'track:track1'; - track4.originalReleaseTrack = 'track:track1'; - XXX_decacheWikiData(); - - t.same(track1.otherReleases, [track2, track3, track4], - `otherReleases #2: otherReleases of original release are its rereleases`); - - wikiData.trackData = [track1, track3, track2, track4]; - linkWikiDataArrays({bindFind}); - - t.same(track1.otherReleases, [track3, track2, track4], - `otherReleases #3: otherReleases matches trackData order`); - - wikiData.trackData = [track3, track2, track1, track4]; - linkWikiDataArrays({bindFind}); - - t.same(track2.otherReleases, [track1, track3, track4], - `otherReleases #4: otherReleases of rerelease are original track then other rereleases (1/3)`); - - t.same(track3.otherReleases, [track1, track2, track4], - `otherReleases #5: otherReleases of rerelease are original track then other rereleases (2/3)`); - - t.same(track4.otherReleases, [track1, track3, track2], - `otherReleases #6: otherReleases of rerelease are original track then other rereleases (3/3)`); -}); - -t.test(`Track.referencedByTracks`, t => { - t.plan(4); - - const wikiData = stubWikiData(); - - const {track: track1} = stubTrackAndAlbum(wikiData, 'track1'); - const {track: track2} = stubTrackAndAlbum(wikiData, 'track2'); - const {track: track3} = stubTrackAndAlbum(wikiData, 'track3'); - const {track: track4} = stubTrackAndAlbum(wikiData, 'track4'); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.same(track1.referencedByTracks, [], - `referencedByTracks #1: defaults to empty array`); - - track2.referencedTracks = ['track:track1']; - track3.referencedTracks = ['track:track1']; - XXX_decacheWikiData(); - - t.same(track1.referencedByTracks, [track2, track3], - `referencedByTracks #2: matches tracks' referencedTracks`); - - track4.sampledTracks = ['track:track1']; - XXX_decacheWikiData(); - - t.same(track1.referencedByTracks, [track2, track3], - `referencedByTracks #3: doesn't match tracks' sampledTracks`); - - track3.originalReleaseTrack = 'track:track2'; - XXX_decacheWikiData(); - - t.same(track1.referencedByTracks, [track2], - `referencedByTracks #4: doesn't include rereleases`); -}); - -t.test(`Track.sampledByTracks`, t => { - t.plan(4); - - const wikiData = stubWikiData(); - - const {track: track1} = stubTrackAndAlbum(wikiData, 'track1'); - const {track: track2} = stubTrackAndAlbum(wikiData, 'track2'); - const {track: track3} = stubTrackAndAlbum(wikiData, 'track3'); - const {track: track4} = stubTrackAndAlbum(wikiData, 'track4'); - - const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData); - - t.same(track1.sampledByTracks, [], - `sampledByTracks #1: defaults to empty array`); - - track2.sampledTracks = ['track:track1']; - track3.sampledTracks = ['track:track1']; - XXX_decacheWikiData(); - - t.same(track1.sampledByTracks, [track2, track3], - `sampledByTracks #2: matches tracks' sampledTracks`); - - track4.referencedTracks = ['track:track1']; - XXX_decacheWikiData(); - - t.same(track1.sampledByTracks, [track2, track3], - `sampledByTracks #3: doesn't match tracks' referencedTracks`); - - track3.originalReleaseTrack = 'track:track2'; - XXX_decacheWikiData(); - - t.same(track1.sampledByTracks, [track2], - `sampledByTracks #4: doesn't include rereleases`); -}); diff --git a/test/unit/data/validators.js b/test/unit/data/validators.js index 3a217d6f..02f94866 100644 --- a/test/unit/data/validators.js +++ b/test/unit/data/validators.js @@ -339,7 +339,7 @@ t.test('isName', t => { t.plan(4); t.ok(isName('Dogz 2.0')); t.ok(isName('album:this-track-is-only-named-thusly-to-give-niklink-a-headache')); - t.throws(() => isName('')); + t.ok(() => isName('')); t.throws(() => isName(612)); }); @@ -374,8 +374,8 @@ test(t, 'validateReference', t => { t.ok(typeless('Hopes and Dreams')); t.ok(typeless('track:snowdin-town')); + t.ok(typeless('album:undertale-soundtrack')); t.throws(() => typeless(''), TypeError); - t.throws(() => typeless('album:undertale-soundtrack')); }); test(t, 'validateReferenceList', t => { diff --git a/test/unit/util/html.js b/test/unit/util/html.js index 1652aee2..126e36ff 100644 --- a/test/unit/util/html.js +++ b/test/unit/util/html.js @@ -545,7 +545,7 @@ t.test(`html.template`, t => { slots.slot1, slots.slot2, slots.slot3, - `(length: ${slots.slot4.length})`, + `(is null: ${slots.slot4 === null})`, ].join(' ')); }, }); @@ -557,7 +557,7 @@ t.test(`html.template`, t => { slot4: '', }); - t.equal(template3.toString(), `<span>123 0 false (length: 0)</span>`); + t.equal(template3.toString(), `<span>123 0 false (is null: true)</span>`); }); t.test(`Template - description errors`, t => { |